diff --git a/bindings/bind/compile.go b/bindings/bind/compile.go index cdcffa257..e6895faad 100644 --- a/bindings/bind/compile.go +++ b/bindings/bind/compile.go @@ -1,6 +1,7 @@ package bind import ( + "context" "embed" "encoding/base64" "encoding/hex" @@ -1094,7 +1095,7 @@ func getDynamicSuiRPC() (string, error) { return envRPC, nil } - cmd := exec.Command("docker", "ps", "--filter", "ancestor=mysten/sui-tools:mainnet-v1.72.5", "--format", "{{.Ports}}") + cmd := exec.CommandContext(context.Background(), "docker", "ps", "--filter", "ancestor=mysten/sui-tools:mainnet-v1.73.2", "--format", "{{.Ports}}") out, err := cmd.Output() if err != nil { return "", fmt.Errorf("docker ps failed: %w", err) diff --git a/deployment/adapters/mcmsreader.go b/deployment/adapters/mcmsreader.go index 0b84a56be..e812b9e7c 100644 --- a/deployment/adapters/mcmsreader.go +++ b/deployment/adapters/mcmsreader.go @@ -19,7 +19,7 @@ type MCMSReader struct{} // mcmsFieldsFromInput loads the on-chain state and selects the correct // MCMSStateFields (normal or fastcurse) based on the Qualifier in the input. -// A Qualifier value of "fastcurse" selects the fastcurse MCMS instance. +// A Qualifier value of "RMNMCMS" (fastCurseQualifier) selects the fastcurse MCMS instance. func mcmsFieldsFromInput(e cldf.Environment, chainSelector uint64, input mcms_utils.Input) (suideploy.MCMSStateFields, error) { stateMap, err := suideploy.LoadOnchainStatesui(e) if err != nil { diff --git a/deployment/go.mod b/deployment/go.mod index db6943014..a8c1840cf 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -14,10 +14,10 @@ require ( github.com/ethereum/go-ethereum v1.17.3 github.com/google/go-cmp v0.7.0 github.com/smartcontractkit/chain-selectors v1.0.102 - github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee + github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260624154507-ea7ff77a0ddb github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 github.com/smartcontractkit/chainlink-deployments-framework v0.109.1-0.20260604174622-e26b8cddfa0a - github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67 + github.com/smartcontractkit/chainlink-sui v0.0.0 github.com/smartcontractkit/mcms v0.45.2-0.20260604181544-da0bd7da623d github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.52.0 @@ -131,9 +131,9 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc // indirect - github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 // indirect + github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 // indirect github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect diff --git a/deployment/go.sum b/deployment/go.sum index e9f0856bc..fe3bae7b7 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -599,14 +599,14 @@ github.com/smartcontractkit/chain-selectors v1.0.102 h1:qYP4+72HfvogCHR5ymwRFee3 github.com/smartcontractkit/chain-selectors v1.0.102/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc h1:Um9FBcf0JNSFuGbxgccDG1vM3cNrMGy0SdJ7r6VbX0o= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= -github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee h1:YAE9gMuCsjp3toJXBQge7pvSZhsFCv9GakTEjjoiE50= -github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee/go.mod h1:2xVZJ3o9udYFeJhwyHXAMlNhptJ99uoiGjzfOicYNX8= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 h1:jkChf04hhdiMBApbb+lLDxHMY62Md6UeM7v++GSw3K8= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139/go.mod h1:wuhagkM/lU0GbV2YcrROOH0GlsfXJYwm6qmpa4CK70w= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 h1:tw3K4UkH5XfW5SoyYkvAlbzrccoGSLdz/XkxD6nyGC8= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139/go.mod h1:1WcontO9PeuKdUf5HXfs3nuICtzUvFNnyCmrHkTCF9Y= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee h1:ScVlPpWyoswUlu3izqOH7N0oW5UCrp7lPifkOZ2Mgew= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee/go.mod h1:xDXlDsou69NYOolOAj+KITRn9luER6Bg52NXelrLl+A= +github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb h1:uSXBvj/idCBg7hMLmek8YYNa0JKggqlTkHYdtIooeNM= +github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb/go.mod h1:2xVZJ3o9udYFeJhwyHXAMlNhptJ99uoiGjzfOicYNX8= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb h1:TZUVwF0ZexyJKqBjutCGjR/kG3LOa1krTQt/EXFgMYg= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb/go.mod h1:xu0Jum/nGRkjBwT/Vq7WCElWOTBBkFRwG0ZIaw9tF2I= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb h1:2p+8KYL0bhHblGcOJKRH84i9QduKGcY72NYcLJzNdNc= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= +github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260624154507-ea7ff77a0ddb h1:lG7cBn+mRgkdPpp1MSGK8pLq72g8LHa3aVPHZv/UReQ= +github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260624154507-ea7ff77a0ddb/go.mod h1:xDXlDsou69NYOolOAj+KITRn9luER6Bg52NXelrLl+A= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 h1:5z3LQ27MJmhiaeqp9S2TzbF5Wm4GGvUKAYOtE9AauR8= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89/go.mod h1:G2AII0QmWzXx8Ag9IKnGN3h/gwwNnhHUOCviJievdvo= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= diff --git a/deployment/lanes/adapter.go b/deployment/lanes/adapter.go new file mode 100644 index 000000000..18a641b2e --- /dev/null +++ b/deployment/lanes/adapter.go @@ -0,0 +1,86 @@ +package lanes + +import ( + "encoding/binary" + "math/big" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" +) + +// suiFamilySelector is bytes4(keccak256("CCIP ChainFamilySelector Sui")) = 0xc4e05953. +var suiFamilySelector = [4]byte{0xc4, 0xe0, 0x59, 0x53} + +// SuiAdapter implements laneapi.LaneAdapter for Sui CCIP 1.6.0 lanes. +// Package IDs are resolved from addresses.json via LoadOnchainStatesui; the ds +// parameter is unused. ConnectChains must run inside WithConnectChainsEnvironment. +type SuiAdapter struct{} + +func (a *SuiAdapter) GetOnRampAddress(_ datastore.DataStore, chainSelector uint64) ([]byte, error) { + e, err := connectChainsEnvironment() + if err != nil { + return nil, err + } + return resolveOnRampPackageID(e, chainSelector) +} + +func (a *SuiAdapter) GetOffRampAddress(_ datastore.DataStore, chainSelector uint64) ([]byte, error) { + e, err := connectChainsEnvironment() + if err != nil { + return nil, err + } + return resolveOffRampPackageID(e, chainSelector) +} + +func (a *SuiAdapter) GetFQAddress(_ datastore.DataStore, chainSelector uint64) ([]byte, error) { + e, err := connectChainsEnvironment() + if err != nil { + return nil, err + } + return resolveCCIPPackageID(e, chainSelector) +} + +func (a *SuiAdapter) GetRouterAddress(_ datastore.DataStore, chainSelector uint64) ([]byte, error) { + e, err := connectChainsEnvironment() + if err != nil { + return nil, err + } + return resolveRouterPackageID(e, chainSelector) +} + +// GetFeeQuoterDestChainConfig returns defaults applied on remote FeeQuoters when Sui is +// the destination chain. Values align with wired Sui testnet lanes (evm_feequoter_dest_configure) +// and Sui execution limits (MaxDataBytes 16_000). +func (a *SuiAdapter) GetFeeQuoterDestChainConfig() laneapi.FeeQuoterDestChainConfig { + return laneapi.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 16_000, + MaxPerMsgGasLimit: 3_000_000, + DestGasOverhead: 300_000, + DestGasPerPayloadByteBase: 16, + ChainFamilySelector: binary.BigEndian.Uint32(suiFamilySelector[:]), + DefaultTokenFeeUSDCents: 25, + DefaultTokenDestGasOverhead: 90_000, + DefaultTxGasLimit: 200_000, + NetworkFeeUSDCents: 10, + V1Params: &laneapi.FeeQuoterV1Params{ + MaxNumberOfTokensPerMsg: 10, + DestGasPerPayloadByteHigh: 40, + DestGasPerPayloadByteThreshold: 3_000, + DestDataAvailabilityOverheadGas: 100, + DestGasPerDataAvailabilityByte: 16, + DestDataAvailabilityMultiplierBps: 1, + EnforceOutOfOrder: true, + GasMultiplierWeiPerEth: 11e17, + }, + } +} + +func (a *SuiAdapter) GetDefaultGasPrice() *big.Int { + return big.NewInt(15e11) +} + +func (a *SuiAdapter) GetChainFamilySelector() [4]byte { + return suiFamilySelector +} diff --git a/deployment/lanes/adapter_test.go b/deployment/lanes/adapter_test.go new file mode 100644 index 000000000..a65f717be --- /dev/null +++ b/deployment/lanes/adapter_test.go @@ -0,0 +1,97 @@ +package lanes_test + +import ( + "encoding/binary" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" + + "github.com/smartcontractkit/chainlink-sui/deployment/lanes" +) + +func TestSuiAdapter_GetFeeQuoterDestChainConfig(t *testing.T) { + t.Parallel() + + cfg := (&lanes.SuiAdapter{}).GetFeeQuoterDestChainConfig() + + require.True(t, cfg.IsEnabled) + require.Equal(t, uint32(16_000), cfg.MaxDataBytes) + require.Equal(t, uint32(3_000_000), cfg.MaxPerMsgGasLimit) + require.Equal(t, uint32(300_000), cfg.DestGasOverhead) + require.Equal(t, uint8(16), cfg.DestGasPerPayloadByteBase) + require.Equal(t, uint32(0xc4e05953), cfg.ChainFamilySelector) + require.Equal(t, uint16(25), cfg.DefaultTokenFeeUSDCents) + require.Equal(t, uint32(90_000), cfg.DefaultTokenDestGasOverhead) + require.Equal(t, uint32(200_000), cfg.DefaultTxGasLimit) + require.Equal(t, uint16(10), cfg.NetworkFeeUSDCents) + require.NotNil(t, cfg.V1Params) + require.Equal(t, uint16(10), cfg.V1Params.MaxNumberOfTokensPerMsg) + require.True(t, cfg.V1Params.EnforceOutOfOrder) + require.Equal(t, uint64(11e17), cfg.V1Params.GasMultiplierWeiPerEth) +} + +func TestSuiAdapter_GetDefaultGasPrice(t *testing.T) { + t.Parallel() + + require.Equal(t, big.NewInt(15e11), (&lanes.SuiAdapter{}).GetDefaultGasPrice()) +} + +func TestSuiAdapter_GetChainFamilySelector(t *testing.T) { + t.Parallel() + + selector := (&lanes.SuiAdapter{}).GetChainFamilySelector() + require.Equal(t, [4]byte{0xc4, 0xe0, 0x59, 0x53}, selector) + require.Equal(t, uint32(0xc4e05953), binary.BigEndian.Uint32(selector[:])) +} + +func TestTranslateDestChainConfig(t *testing.T) { + t.Parallel() + + const destSel = uint64(945045181441419236) + cfg := laneapi.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 30_000, + MaxPerMsgGasLimit: 3_000_000, + DestGasOverhead: 300_000, + DestGasPerPayloadByteBase: 16, + ChainFamilySelector: 0x2812d52c, + DefaultTokenFeeUSDCents: 25, + DefaultTokenDestGasOverhead: 90_000, + DefaultTxGasLimit: 200_000, + NetworkFeeUSDCents: 10, + V1Params: &laneapi.FeeQuoterV1Params{ + MaxNumberOfTokensPerMsg: 1, + DestGasPerPayloadByteHigh: 40, + DestGasPerPayloadByteThreshold: 3_000, + DestDataAvailabilityOverheadGas: 100, + DestGasPerDataAvailabilityByte: 16, + DestDataAvailabilityMultiplierBps: 1, + EnforceOutOfOrder: true, + GasMultiplierWeiPerEth: 1e18, + GasPriceStalenessThreshold: 1_000_000, + }, + } + + got := lanes.TranslateDestChainConfig(cfg, destSel) + + require.Equal(t, destSel, got.DestChainSelector) + require.True(t, got.IsEnabled) + require.Equal(t, uint16(1), got.MaxNumberOfTokensPerMsg) + require.Equal(t, uint32(30_000), got.MaxDataBytes) + require.Equal(t, byte(16), got.DestGasPerPayloadByteBase) + require.Equal(t, []byte{0x28, 0x12, 0xd5, 0x2c}, got.ChainFamilySelector) + require.Equal(t, uint64(1e18), got.GasMultiplierWeiPerEth) + require.Equal(t, uint32(10), got.NetworkFeeUsdCents) +} + +func TestDefaultLinkTokenTransferFees(t *testing.T) { + t.Parallel() + + require.Equal(t, uint32(3000), lanes.DefaultLinkTokenTransferMinFeeUsdCents) + require.Equal(t, uint32(30000), lanes.DefaultLinkTokenTransferMaxFeeUsdCents) + require.Equal(t, uint16(1000), lanes.DefaultLinkTokenTransferDeciBps) + require.Equal(t, uint64(900_000_000_000_000_000), lanes.DefaultLinkPremiumMultiplierWeiPerEth) +} diff --git a/deployment/lanes/addresses.go b/deployment/lanes/addresses.go new file mode 100644 index 000000000..388c76fe1 --- /dev/null +++ b/deployment/lanes/addresses.go @@ -0,0 +1,91 @@ +package lanes + +import ( + "encoding/hex" + "fmt" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + suideploy "github.com/smartcontractkit/chainlink-sui/deployment" +) + +// packageIDToBytes encodes a Sui package ID as a 32-byte, left-padded address for +// cross-chain CCIP references (same convention as EVM offramp source onramp config). +func packageIDToBytes(address string) ([]byte, error) { + out, err := suideploy.StrTo32(address) + if err != nil { + return nil, fmt.Errorf("invalid Sui package id %q: %w", address, err) + } + return out, nil +} + +// remoteAddressBytesToHex encodes opaque remote address bytes from ConnectChains as a +// Sui address string. Bytes are left-padded to 32 bytes when shorter, matching EVM +// OffRamp source OnRamp encoding and deployment.StrTo32. +func remoteAddressBytesToHex(b []byte) (string, error) { + if len(b) == 0 { + return "", fmt.Errorf("empty address bytes") + } + if len(b) > 32 { + return "", fmt.Errorf("address longer than 32 bytes: %d", len(b)) + } + padded := make([]byte, 32) + copy(padded[32-len(b):], b) + return "0x" + hex.EncodeToString(padded), nil +} + +func loadChainState(env cldf.Environment, chainSelector uint64) (suideploy.CCIPChainState, error) { + stateMap, err := suideploy.LoadOnchainStatesui(env) + if err != nil { + return suideploy.CCIPChainState{}, fmt.Errorf("load sui onchain state: %w", err) + } + state, ok := stateMap[chainSelector] + if !ok { + return suideploy.CCIPChainState{}, fmt.Errorf("sui chain %d not found in address book state", chainSelector) + } + return state, nil +} + +func resolveOnRampPackageID(env cldf.Environment, chainSelector uint64) ([]byte, error) { + state, err := loadChainState(env, chainSelector) + if err != nil { + return nil, err + } + if state.OnRampAddress == "" { + return nil, fmt.Errorf("no SuiOnRamp package for chain %d in address book", chainSelector) + } + return packageIDToBytes(state.OnRampAddress) +} + +func resolveOffRampPackageID(env cldf.Environment, chainSelector uint64) ([]byte, error) { + state, err := loadChainState(env, chainSelector) + if err != nil { + return nil, err + } + if state.OffRampAddress == "" { + return nil, fmt.Errorf("no SuiOffRamp package for chain %d in address book", chainSelector) + } + return packageIDToBytes(state.OffRampAddress) +} + +func resolveCCIPPackageID(env cldf.Environment, chainSelector uint64) ([]byte, error) { + state, err := loadChainState(env, chainSelector) + if err != nil { + return nil, err + } + if state.CCIPAddress == "" { + return nil, fmt.Errorf("no SuiCCIP package for chain %d in address book", chainSelector) + } + return packageIDToBytes(state.CCIPAddress) +} + +func resolveRouterPackageID(env cldf.Environment, chainSelector uint64) ([]byte, error) { + state, err := loadChainState(env, chainSelector) + if err != nil { + return nil, err + } + if state.CCIPRouterAddress == "" { + return nil, fmt.Errorf("no SuiRouter package for chain %d in address book", chainSelector) + } + return packageIDToBytes(state.CCIPRouterAddress) +} diff --git a/deployment/lanes/addresses_test.go b/deployment/lanes/addresses_test.go new file mode 100644 index 000000000..51c7f8747 --- /dev/null +++ b/deployment/lanes/addresses_test.go @@ -0,0 +1,98 @@ +package lanes_test + +import ( + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + "github.com/smartcontractkit/chainlink-sui/deployment/lanes" +) + +const suiTestnetSelector = uint64(9762610643973837292) + +func testEnvWithAddressBook(t *testing.T) cldf.Environment { + t.Helper() + + return cldf.Environment{ + Name: "test", + ExistingAddresses: loadTestAddressBook(t), + BlockChains: chain.NewBlockChains(map[uint64]chain.BlockChain{ + suiTestnetSelector: sui.Chain{}, + }), + } +} + +func loadTestAddressBook(t *testing.T) cldf.AddressBook { + t.Helper() + + b, err := os.ReadFile("../testdata/addresses.json") + require.NoError(t, err) + + addrsByChain := make(map[uint64]map[string]cldf.TypeAndVersion) + require.NoError(t, json.Unmarshal(b, &addrsByChain)) + + return cldf.NewMemoryAddressBookFromMap(addrsByChain) +} + +func TestSuiAdapter_AddressGettersFromAddressBook(t *testing.T) { + env := testEnvWithAddressBook(t) + adapter := &lanes.SuiAdapter{} + + onRampPkg := "0xf87c6010be571a304f0d860857204bc66f037842156f0f6c9d80be265fd83752" + offRampPkg := "0x9438693fb18f5660aff9277240a2282be44dc01cdd7eed4e1d8de0591ad52c03" + ccipPkg := "0xece742a763bddf1e36629fa06b605497e413241afd14f05e558e80eef4f64e95" + routerPkg := "0xed4613bd35004954c07150c3e9b10230f5e23e3058bc2ca0e3e676cb43eb4dc1" + + err := lanes.WithConnectChainsEnvironment(env, func() error { + onRamp, err := adapter.GetOnRampAddress(nil, suiTestnetSelector) + require.NoError(t, err) + require.Equal(t, onRampPkg, "0x"+hex.EncodeToString(onRamp)) + + offRamp, err := adapter.GetOffRampAddress(nil, suiTestnetSelector) + require.NoError(t, err) + require.Equal(t, offRampPkg, "0x"+hex.EncodeToString(offRamp)) + + fq, err := adapter.GetFQAddress(nil, suiTestnetSelector) + require.NoError(t, err) + require.Equal(t, ccipPkg, "0x"+hex.EncodeToString(fq)) + + router, err := adapter.GetRouterAddress(nil, suiTestnetSelector) + require.NoError(t, err) + require.Equal(t, routerPkg, "0x"+hex.EncodeToString(router)) + + return nil + }) + require.NoError(t, err) +} + +func TestSuiAdapter_AddressGettersFromAddressBook_MissingRef(t *testing.T) { + adapter := &lanes.SuiAdapter{} + env := cldf.Environment{ + ExistingAddresses: cldf.NewMemoryAddressBook(), + BlockChains: chain.NewBlockChains(map[uint64]chain.BlockChain{ + suiTestnetSelector: sui.Chain{}, + }), + } + + err := lanes.WithConnectChainsEnvironment(env, func() error { + _, err := adapter.GetOnRampAddress(nil, suiTestnetSelector) + require.Error(t, err) + require.Contains(t, err.Error(), "SuiOnRamp") + return nil + }) + require.NoError(t, err) +} + +func TestSuiAdapter_AddressGetters_RequireConnectChainsScope(t *testing.T) { + adapter := &lanes.SuiAdapter{} + _, err := adapter.GetOnRampAddress(nil, suiTestnetSelector) + require.Error(t, err) + require.Contains(t, err.Error(), "WithConnectChainsEnvironment") +} diff --git a/deployment/lanes/batchop.go b/deployment/lanes/batchop.go new file mode 100644 index 000000000..78aab0168 --- /dev/null +++ b/deployment/lanes/batchop.go @@ -0,0 +1,36 @@ +package lanes + +import ( + "fmt" + + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + "github.com/smartcontractkit/chainlink-sui/deployment/utils" +) + +func appendMCMSBatchOpFromCall( + out *sequences.OnChainOutput, + chainSelector uint64, + call sui_ops.TransactionCall, + deps sui_ops.OpTxDeps, +) error { + if deps.Signer != nil { + return nil + } + if call.PackageID == "" { + return nil + } + + tx, err := utils.TransactionCallToMCMSTransaction(call) + if err != nil { + return fmt.Errorf("create MCMS transaction from op call: %w", err) + } + + out.BatchOps = append(out.BatchOps, mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(chainSelector), + Transactions: []mcmstypes.Transaction{tx}, + }) + return nil +} diff --git a/deployment/lanes/batchop_test.go b/deployment/lanes/batchop_test.go new file mode 100644 index 000000000..fdc25361c --- /dev/null +++ b/deployment/lanes/batchop_test.go @@ -0,0 +1,60 @@ +package lanes + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + mcmstypes "github.com/smartcontractkit/mcms/types" + + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + "github.com/smartcontractkit/chainlink-sui/relayer/signer" +) + +type stubSuiSigner struct{} + +func (stubSuiSigner) Sign(_ []byte) ([]string, error) { return nil, nil } + +func (stubSuiSigner) GetAddress() (string, error) { return "0x1", nil } + +var _ signer.SuiSigner = stubSuiSigner{} + +func TestAppendMCMSBatchOpFromCall(t *testing.T) { + t.Parallel() + + const chainSelector uint64 = 9762610643973837292 + + call := sui_ops.TransactionCall{ + PackageID: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Module: "offramp", + Function: "apply_source_chain_config_updates", + Data: []byte{0x01}, + StateObjID: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + } + + t.Run("appends batch op when signer is nil", func(t *testing.T) { + var out sequences.OnChainOutput + err := appendMCMSBatchOpFromCall(&out, chainSelector, call, sui_ops.OpTxDeps{}) + require.NoError(t, err) + require.Len(t, out.BatchOps, 1) + require.Equal(t, mcmstypes.ChainSelector(chainSelector), out.BatchOps[0].ChainSelector) + require.Len(t, out.BatchOps[0].Transactions, 1) + }) + + t.Run("skips batch op when signer is set", func(t *testing.T) { + var out sequences.OnChainOutput + err := appendMCMSBatchOpFromCall(&out, chainSelector, call, sui_ops.OpTxDeps{ + Signer: stubSuiSigner{}, + }) + require.NoError(t, err) + require.Empty(t, out.BatchOps) + }) + + t.Run("skips empty package id", func(t *testing.T) { + var out sequences.OnChainOutput + err := appendMCMSBatchOpFromCall(&out, chainSelector, sui_ops.TransactionCall{}, sui_ops.OpTxDeps{}) + require.NoError(t, err) + require.Empty(t, out.BatchOps) + }) +} diff --git a/deployment/lanes/connect_chains_dest.go b/deployment/lanes/connect_chains_dest.go new file mode 100644 index 000000000..31de4e0e9 --- /dev/null +++ b/deployment/lanes/connect_chains_dest.go @@ -0,0 +1,77 @@ +package lanes + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + ccip_offramp_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip_offramp" +) + +// ConfigureLaneLegAsDest wires a remote source chain into the Sui OffRamp. +// Sui has no router offramp step for inbound lanes (unlike Solana). +var ConfigureLaneLegAsDest = cldf_ops.NewSequence( + "ConfigureLaneLegAsDest", + semver.MustParse("1.6.0"), + "Configures lane leg as destination on CCIP 1.6.0 for Sui", + func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input laneapi.UpdateLanesInput) (sequences.OnChainOutput, error) { + if input.Source == nil || input.Dest == nil { + return sequences.OnChainOutput{}, fmt.Errorf("ConfigureLaneLegAsDest requires Source and Dest chain definitions") + } + if len(input.Source.OnRamp) == 0 { + return sequences.OnChainOutput{}, fmt.Errorf("source OnRamp address required to configure Sui as destination for chain %d", input.Source.Selector) + } + + env, err := connectChainsEnvironment() + if err != nil { + return sequences.OnChainOutput{}, err + } + + state, err := loadOffRampState(env, input.Dest.Selector) + if err != nil { + return sequences.OnChainOutput{}, err + } + + deps, err := opTxDepsForChain(chains, input.Dest.Selector) + if err != nil { + return sequences.OnChainOutput{}, err + } + + latestIDs := resolveLatestPackageIDs(input.Dest.Selector) + + opInput := ccip_offramp_ops.ApplySourceChainConfigUpdateInput{ + CCIPObjectRef: state.CCIPObjectRef, + OffRampPackageId: state.OffRampAddress, + LatestPackageId: latestIDs.OffRamp, + OffRampStateId: state.OffRampStateObjectId, + OwnerCapObjectId: state.OffRampOwnerCapId, + SourceChainsSelectors: []uint64{input.Source.Selector}, + SourceChainsIsEnabled: []bool{!input.IsDisabled}, + SourceChainsIsRMNVerificationDisabled: []bool{!input.Source.RMNVerificationEnabled}, + SourceChainsOnRamp: [][]byte{append([]byte(nil), input.Source.OnRamp...)}, + } + + report, err := cldf_ops.ExecuteOperation(b, ccip_offramp_ops.ApplySourceChainConfigUpdatesOp, deps, opInput) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf( + "failed to apply source chain config on Sui OffRamp for source %d: %w", + input.Source.Selector, err, + ) + } + + var out sequences.OnChainOutput + if err := appendMCMSBatchOpFromCall(&out, input.Dest.Selector, report.Output.Call, deps); err != nil { + return sequences.OnChainOutput{}, err + } + return out, nil + }, +) + +func (a *SuiAdapter) ConfigureLaneLegAsDest() *cldf_ops.Sequence[laneapi.UpdateLanesInput, sequences.OnChainOutput, cldf_chain.BlockChains] { + return ConfigureLaneLegAsDest +} diff --git a/deployment/lanes/connect_chains_layer2_test.go b/deployment/lanes/connect_chains_layer2_test.go new file mode 100644 index 000000000..3b66caa00 --- /dev/null +++ b/deployment/lanes/connect_chains_layer2_test.go @@ -0,0 +1,440 @@ +package lanes_test + +import ( + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + module_fee_quoter "github.com/smartcontractkit/chainlink-sui/bindings/generated/ccip/ccip/fee_quoter" + module_offramp "github.com/smartcontractkit/chainlink-sui/bindings/generated/ccip/ccip_offramp/offramp" + module_onramp "github.com/smartcontractkit/chainlink-sui/bindings/generated/ccip/ccip_onramp/onramp" + module_router "github.com/smartcontractkit/chainlink-sui/bindings/generated/ccip/ccip_router" + suideploy "github.com/smartcontractkit/chainlink-sui/deployment" + "github.com/smartcontractkit/chainlink-sui/deployment/lanes" + "github.com/smartcontractkit/chainlink-sui/deployment/ops/mcmstest" + "github.com/smartcontractkit/chainlink-sui/deployment/utils" +) + +const ( + testOnRampPackageID = "0xf87c6010be571a304f0d860857204bc66f037842156f0f6c9d80be265fd83752" + testOnRampStateObjectID = "0x75ec1e10b4302f7c69476eb196c88a0aa43a4d509bbbe5cc1feb213e4b6dd58b" + testOnRampOwnerCapID = "0x8101795ff02d4935a05fb519e1b21b83855a970639cf0c28fa7a51f7d2e689ae" + testRouterPackageID = "0xed4613bd35004954c07150c3e9b10230f5e23e3058bc2ca0e3e676cb43eb4dc1" + testRouterStateObjectID = "0xbb2486d233b0d358f82fb8c4c5c75881e65069ea8ebe5ab692a636c9e0eff7cd" + testRouterOwnerCapID = "0xb000000000000000000000000000000000000000000000000000000000000001" + testOffRampStateObjectID = "0x4fdacea0d627df26a6f34ac62952ed8d3c32ea70aacb90a7f2134b39e36a79cd" + testOffRampOwnerCapID = "0x8f7eb5b7879449519b39db447b94c87b6bb88f0a1deff40b3e0ffef3d1058f69" +) + +func TestConfigureLaneLegAsSource_MCMSBatchOp_EncoderParity(t *testing.T) { + env := testEnvWithAddressBook(t) + chains := testSuiChains() + evmRouter := common.HexToAddress(evmRouterAddress).Bytes() + input := sourceLegInput(evmRouter) + destRouterHex := leftPaddedAddressHex(evmRouter) + + report, err := runSourceLeg(t, env, chains, input) + require.NoError(t, err) + require.Len(t, report.Output.BatchOps, 5) + + feeQuoter, err := module_fee_quoter.NewFeeQuoter(testCCIPPackageID, nil) + require.NoError(t, err) + onRamp, err := module_onramp.NewOnramp(testOnRampPackageID, nil) + require.NoError(t, err) + router, err := module_router.NewRouter(testRouterPackageID, nil) + require.NoError(t, err) + + t.Run("apply_token_transfer_fee_config_updates", func(t *testing.T) { + encoded, err := feeQuoter.Encoder().ApplyTokenTransferFeeConfigUpdates( + bind.Object{Id: testCCIPObjectRef}, + bind.Object{Id: testCCIPOwnerCapID}, + evmSepoliaSelector, + []string{testLinkCoinMetadata}, + []uint32{lanes.DefaultLinkTokenTransferMinFeeUsdCents}, + []uint32{lanes.DefaultLinkTokenTransferMaxFeeUsdCents}, + []uint16{lanes.DefaultLinkTokenTransferDeciBps}, + []uint32{lanes.DefaultLinkTokenTransferDestGasOverhead}, + []uint32{lanes.DefaultLinkTokenTransferDestBytesOverhead}, + []bool{true}, + []string{}, + ) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches(t, report.Output.BatchOps[0].Transactions[0].Data, encoded, testCCIPObjectRef, nil) + }) + + t.Run("apply_dest_chain_config_updates", func(t *testing.T) { + cfg := input.Dest.FeeQuoterDestChainConfig + translated := lanes.TranslateDestChainConfig(cfg, input.Dest.Selector) + encoded, err := feeQuoter.Encoder().ApplyDestChainConfigUpdates( + bind.Object{Id: testCCIPObjectRef}, + bind.Object{Id: testCCIPOwnerCapID}, + translated.DestChainSelector, + translated.IsEnabled, + translated.MaxNumberOfTokensPerMsg, + translated.MaxDataBytes, + translated.MaxPerMsgGasLimit, + translated.DestGasOverhead, + translated.DestGasPerPayloadByteBase, + translated.DestGasPerPayloadByteHigh, + translated.DestGasPerPayloadByteThreshold, + translated.DestDataAvailabilityOverheadGas, + translated.DestGasPerDataAvailabilityByte, + translated.DestDataAvailabilityMultiplierBps, + translated.ChainFamilySelector, + translated.EnforceOutOfOrder, + translated.DefaultTokenFeeUsdCents, + translated.DefaultTokenDestGasOverhead, + translated.DefaultTxGasLimit, + translated.GasMultiplierWeiPerEth, + translated.GasPriceStalenessThreshold, + translated.NetworkFeeUsdCents, + ) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches(t, report.Output.BatchOps[1].Transactions[0].Data, encoded, testCCIPObjectRef, nil) + }) + + t.Run("apply_premium_multiplier_wei_per_eth_updates", func(t *testing.T) { + encoded, err := feeQuoter.Encoder().ApplyPremiumMultiplierWeiPerEthUpdates( + bind.Object{Id: testCCIPObjectRef}, + bind.Object{Id: testCCIPOwnerCapID}, + []string{testLinkCoinMetadata}, + []uint64{lanes.DefaultLinkPremiumMultiplierWeiPerEth}, + ) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches(t, report.Output.BatchOps[2].Transactions[0].Data, encoded, testCCIPObjectRef, nil) + }) + + t.Run("onramp_apply_dest_chain_config_updates", func(t *testing.T) { + encoded, err := onRamp.Encoder().ApplyDestChainConfigUpdates( + bind.Object{Id: testCCIPObjectRef}, + bind.Object{Id: testOnRampStateObjectID}, + bind.Object{Id: testOnRampOwnerCapID}, + []uint64{evmSepoliaSelector}, + []bool{input.Source.AllowListEnabled}, + []string{destRouterHex}, + ) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches(t, report.Output.BatchOps[3].Transactions[0].Data, encoded, testOnRampStateObjectID, nil) + }) + + t.Run("router_set_on_ramps", func(t *testing.T) { + encoded, err := router.Encoder().SetOnRamps( + bind.Object{Id: testRouterOwnerCapID}, + bind.Object{Id: testRouterStateObjectID}, + []uint64{evmSepoliaSelector}, + []string{testOnRampPackageID}, + ) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches(t, report.Output.BatchOps[4].Transactions[0].Data, encoded, testRouterStateObjectID, nil) + }) +} + +func TestConfigureLaneLegAsSource_OnRampEncoderParity_20And32ByteRouterEquivalent(t *testing.T) { + env := testEnvWithAddressBook(t) + chains := testSuiChains() + evmRouter := common.HexToAddress(evmRouterAddress).Bytes() + + report20, err := runSourceLeg(t, env, chains, sourceLegInput(evmRouter)) + require.NoError(t, err) + + padded := make([]byte, 32) + copy(padded[12:], evmRouter) + report32, err := runSourceLeg(t, env, chains, sourceLegInput(padded)) + require.NoError(t, err) + + require.Equal( + t, + report20.Output.BatchOps[3].Transactions[0].Data, + report32.Output.BatchOps[3].Transactions[0].Data, + "OnRamp dest router encoding should match for 20-byte and left-padded 32-byte EVM routers", + ) +} + +func TestConfigureLaneLegAsDest_MCMSBatchOp_EncoderParity(t *testing.T) { + env := testEnvWithAddressBook(t) + input := destLegInput() + + var report cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput] + err := lanes.WithConnectChainsEnvironment(env, func() error { + var execErr error + report, execErr = cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + testSuiChains(), + input, + ) + return execErr + }) + require.NoError(t, err) + require.Len(t, report.Output.BatchOps, 1) + + offRamp, err := module_offramp.NewOfframp(testOffRampPackageID, nil) + require.NoError(t, err) + encoded, err := offRamp.Encoder().ApplySourceChainConfigUpdates( + bind.Object{Id: testCCIPObjectRef}, + bind.Object{Id: testOffRampStateObjectID}, + bind.Object{Id: testOffRampOwnerCapID}, + []uint64{evmSepoliaSelector}, + []bool{true}, + []bool{false}, + [][]byte{input.Source.OnRamp}, + ) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches( + t, + report.Output.BatchOps[0].Transactions[0].Data, + encoded, + testOffRampStateObjectID, + nil, + ) +} + +func TestConfigureLaneLegAsDest_Flags(t *testing.T) { + env := testEnvWithAddressBook(t) + chains := testSuiChains() + + tests := []struct { + name string + isDisabled bool + rmnEnabled bool + wantEnabled bool + wantRMNDisabled bool + }{ + {name: "enabled with RMN verification", isDisabled: false, rmnEnabled: true, wantEnabled: true, wantRMNDisabled: false}, + {name: "enabled without RMN verification", isDisabled: false, rmnEnabled: false, wantEnabled: true, wantRMNDisabled: true}, + {name: "disabled lane", isDisabled: true, rmnEnabled: true, wantEnabled: false, wantRMNDisabled: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + input := destLegInput() + input.IsDisabled = tc.isDisabled + input.Source.RMNVerificationEnabled = tc.rmnEnabled + + var report cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput] + err := lanes.WithConnectChainsEnvironment(env, func() error { + var execErr error + report, execErr = cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + chains, + input, + ) + return execErr + }) + require.NoError(t, err) + + offRamp, err := module_offramp.NewOfframp(testOffRampPackageID, nil) + require.NoError(t, err) + encoded, err := offRamp.Encoder().ApplySourceChainConfigUpdates( + bind.Object{Id: testCCIPObjectRef}, + bind.Object{Id: testOffRampStateObjectID}, + bind.Object{Id: testOffRampOwnerCapID}, + []uint64{evmSepoliaSelector}, + []bool{tc.wantEnabled}, + []bool{tc.wantRMNDisabled}, + [][]byte{input.Source.OnRamp}, + ) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches( + t, + report.Output.BatchOps[0].Transactions[0].Data, + encoded, + testOffRampStateObjectID, + nil, + ) + }) + } +} + +func TestConfigureLaneLegAsDest_PartialLatestPackageIDs(t *testing.T) { + env := testEnvWithAddressBook(t) + const latestOffRampPackageID = "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + + var report cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput] + err := lanes.RunConnectChainsWithSuiScopes(env, map[uint64]lanes.LatestPackageIDsConfig{ + suiTestnetSelector: {OffRamp: latestOffRampPackageID}, + }, func() error { + var execErr error + report, execErr = cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + testSuiChains(), + destLegInput(), + ) + return execErr + }) + require.NoError(t, err) + tx := report.Output.BatchOps[0].Transactions[0] + require.Equal(t, testOffRampPackageID, tx.To) + latest, err := utils.TransactionLatestPackageID(tx) + require.NoError(t, err) + require.Equal(t, latestOffRampPackageID, latest) +} + +func TestRunConnectChainsWithSuiScopes(t *testing.T) { + env := testEnvWithAddressBook(t) + adapter := &lanes.SuiAdapter{} + const latestOffRamp = "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + + err := lanes.RunConnectChainsWithSuiScopes(env, map[uint64]lanes.LatestPackageIDsConfig{ + suiTestnetSelector: {OffRamp: latestOffRamp}, + }, func() error { + onRamp, err := adapter.GetOnRampAddress(nil, suiTestnetSelector) + require.NoError(t, err) + require.Equal(t, testOnRampPackageID, "0x"+hex.EncodeToString(onRamp)) + + var report cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput] + report, err = cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + testSuiChains(), + destLegInput(), + ) + require.NoError(t, err) + latest, err := utils.TransactionLatestPackageID(report.Output.BatchOps[0].Transactions[0]) + require.NoError(t, err) + require.Equal(t, latestOffRamp, latest) + return nil + }) + require.NoError(t, err) +} + +func TestConfigureLaneLegAsSource_MissingAddressBookFields(t *testing.T) { + chains := testSuiChains() + router := common.HexToAddress(evmRouterAddress).Bytes() + + t.Run("missing link token metadata", func(t *testing.T) { + env := testEnvOmittingAddressTypes(t, string(suideploy.SuiLinkTokenObjectMetadataID)) + err := lanes.WithConnectChainsEnvironment(env, func() error { + _, execErr := cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsSource, + chains, + sourceLegInput(router), + ) + return execErr + }) + require.Error(t, err) + require.Contains(t, err.Error(), "SuiLinkTokenObjectMetadataID") + }) +} + +func TestConfigureLaneLegAsDest_MissingAddressBookFields(t *testing.T) { + chains := testSuiChains() + + t.Run("missing offramp owner cap", func(t *testing.T) { + env := testEnvOmittingAddressTypes(t, string(suideploy.SuiOffRampOwnerCapObjectIDType)) + err := lanes.WithConnectChainsEnvironment(env, func() error { + _, execErr := cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + chains, + destLegInput(), + ) + return execErr + }) + require.Error(t, err) + require.Contains(t, err.Error(), "SuiOffRampOwnerCapObjectID") + }) +} + +func TestDisableRemoteChain_NoOp(t *testing.T) { + report, err := cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.DisableRemoteChain, + testSuiChains(), + laneapi.DisableRemoteChainInput{ + LocalChainSelector: suiTestnetSelector, + RemoteChainSelector: evmSepoliaSelector, + }, + ) + require.NoError(t, err) + require.Empty(t, report.Output.BatchOps) +} + +func TestSuiLaneAdapter_RegisteredInRegistry(t *testing.T) { + version := semver.MustParse("1.6.0") + adapter, ok := laneapi.GetLaneAdapterRegistry().GetLaneAdapter(chain_selectors.FamilySui, version) + require.True(t, ok) + require.IsType(t, &lanes.SuiAdapter{}, adapter) + + selector := adapter.(laneapi.ChainMetadataProvider).GetChainFamilySelector() + require.Equal(t, [4]byte{0xc4, 0xe0, 0x59, 0x53}, selector) +} + +func TestConfigureLaneLegAsSource_AllowListEnabled(t *testing.T) { + env := testEnvWithAddressBook(t) + chains := testSuiChains() + input := sourceLegInput(common.HexToAddress(evmRouterAddress).Bytes()) + input.Source.AllowListEnabled = true + + report, err := runSourceLeg(t, env, chains, input) + require.NoError(t, err) + + onRamp, err := module_onramp.NewOnramp(testOnRampPackageID, nil) + require.NoError(t, err) + encoded, err := onRamp.Encoder().ApplyDestChainConfigUpdates( + bind.Object{Id: testCCIPObjectRef}, + bind.Object{Id: testOnRampStateObjectID}, + bind.Object{Id: testOnRampOwnerCapID}, + []uint64{evmSepoliaSelector}, + []bool{true}, + []string{leftPaddedAddressHex(common.HexToAddress(evmRouterAddress).Bytes())}, + ) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches(t, report.Output.BatchOps[3].Transactions[0].Data, encoded, testOnRampStateObjectID, nil) +} + +func leftPaddedAddressHex(b []byte) string { + padded := make([]byte, 32) + copy(padded[32-len(b):], b) + return "0x" + hex.EncodeToString(padded) +} + +func testEnvOmittingAddressTypes(t *testing.T, omitTypes ...string) cldf.Environment { + t.Helper() + + b, err := os.ReadFile("../testdata/addresses.json") + require.NoError(t, err) + + addrsByChain := make(map[uint64]map[string]cldf.TypeAndVersion) + require.NoError(t, json.Unmarshal(b, &addrsByChain)) + + omit := make(map[string]struct{}, len(omitTypes)) + for _, typ := range omitTypes { + omit[typ] = struct{}{} + } + + chainAddrs := addrsByChain[suiTestnetSelector] + filtered := make(map[string]cldf.TypeAndVersion, len(chainAddrs)) + for addr, tv := range chainAddrs { + if _, skip := omit[string(tv.Type)]; skip { + continue + } + filtered[addr] = tv + } + addrsByChain[suiTestnetSelector] = filtered + + return cldf.Environment{ + Name: "test", + ExistingAddresses: cldf.NewMemoryAddressBookFromMap(addrsByChain), + BlockChains: chain.NewBlockChains(map[uint64]chain.BlockChain{ + suiTestnetSelector: sui.Chain{}, + }), + } +} diff --git a/deployment/lanes/connect_chains_source.go b/deployment/lanes/connect_chains_source.go new file mode 100644 index 000000000..21e85ee1a --- /dev/null +++ b/deployment/lanes/connect_chains_source.go @@ -0,0 +1,143 @@ +package lanes + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + ccip_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip" + ccip_onramp_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip_onramp" + ccip_router_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip_router" +) + +// ConfigureLaneLegAsSource wires FeeQuoter, OnRamp, and Router on Sui for outbound messages +// to a remote destination chain. Op order matches connect_sui_to_evm. +var ConfigureLaneLegAsSource = cldf_ops.NewSequence( + "ConfigureLaneLegAsSource", + semver.MustParse("1.6.0"), + "Configures lane leg as source on CCIP 1.6.0 for Sui", + func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input laneapi.UpdateLanesInput) (sequences.OnChainOutput, error) { + if input.Source == nil || input.Dest == nil { + return sequences.OnChainOutput{}, fmt.Errorf("ConfigureLaneLegAsSource requires Source and Dest chain definitions") + } + if len(input.Dest.Router) == 0 { + return sequences.OnChainOutput{}, fmt.Errorf("dest Router address required to configure Sui as source for chain %d", input.Dest.Selector) + } + + env, err := connectChainsEnvironment() + if err != nil { + return sequences.OnChainOutput{}, err + } + + state, err := loadSourceChainState(env, input.Source.Selector) + if err != nil { + return sequences.OnChainOutput{}, err + } + + deps, err := opTxDepsForChain(chains, input.Source.Selector) + if err != nil { + return sequences.OnChainOutput{}, err + } + + destRouter, err := remoteAddressBytesToHex(input.Dest.Router) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("encode dest router for chain %d: %w", input.Dest.Selector, err) + } + + latestIDs := resolveLatestPackageIDs(input.Source.Selector) + + var out sequences.OnChainOutput + + tokenFeeReport, err := cldf_ops.ExecuteOperation(b, ccip_ops.FeeQuoterApplyTokenTransferFeeConfigUpdatesOp, deps, ccip_ops.FeeQuoterApplyTokenTransferFeeConfigUpdatesInput{ + CCIPPackageId: state.CCIPAddress, + LatestPackageId: latestIDs.CCIP, + StateObjectId: state.CCIPObjectRef, + OwnerCapObjectId: state.CCIPOwnerCapObjectId, + DestChainSelector: input.Dest.Selector, + AddTokens: []string{state.LinkTokenCoinMetadataId}, + AddMinFeeUsdCents: []uint32{DefaultLinkTokenTransferMinFeeUsdCents}, + AddMaxFeeUsdCents: []uint32{DefaultLinkTokenTransferMaxFeeUsdCents}, + AddDeciBps: []uint16{DefaultLinkTokenTransferDeciBps}, + AddDestGasOverhead: []uint32{DefaultLinkTokenTransferDestGasOverhead}, + AddDestBytesOverhead: []uint32{DefaultLinkTokenTransferDestBytesOverhead}, + AddIsEnabled: []bool{true}, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("apply token transfer fee config on Sui for dest %d: %w", input.Dest.Selector, err) + } + if err := appendMCMSBatchOpFromCall(&out, input.Source.Selector, tokenFeeReport.Output.Call, deps); err != nil { + return sequences.OnChainOutput{}, err + } + + destCfgInput := TranslateDestChainConfig(input.Dest.FeeQuoterDestChainConfig, input.Dest.Selector) + destCfgInput.CCIPPackageId = state.CCIPAddress + destCfgInput.LatestPackageId = latestIDs.CCIP + destCfgInput.StateObjectId = state.CCIPObjectRef + destCfgInput.OwnerCapObjectId = state.CCIPOwnerCapObjectId + destCfgReport, err := cldf_ops.ExecuteOperation(b, ccip_ops.FeeQuoterApplyDestChainConfigUpdatesOp, deps, destCfgInput) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("apply dest chain config on Sui FeeQuoter for dest %d: %w", input.Dest.Selector, err) + } + if err := appendMCMSBatchOpFromCall(&out, input.Source.Selector, destCfgReport.Output.Call, deps); err != nil { + return sequences.OnChainOutput{}, err + } + + premiumReport, err := cldf_ops.ExecuteOperation(b, ccip_ops.FeeQuoterApplyPremiumMultiplierWeiPerEthUpdatesOp, deps, ccip_ops.FeeQuoterApplyPremiumMultiplierWeiPerEthUpdatesInput{ + CCIPPackageId: state.CCIPAddress, + LatestPackageId: latestIDs.CCIP, + StateObjectId: state.CCIPObjectRef, + OwnerCapObjectId: state.CCIPOwnerCapObjectId, + Tokens: []string{state.LinkTokenCoinMetadataId}, + PremiumMultiplierWeiPerEth: []uint64{DefaultLinkPremiumMultiplierWeiPerEth}, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("apply premium multiplier on Sui FeeQuoter for dest %d: %w", input.Dest.Selector, err) + } + if err := appendMCMSBatchOpFromCall(&out, input.Source.Selector, premiumReport.Output.Call, deps); err != nil { + return sequences.OnChainOutput{}, err + } + + onRampReport, err := cldf_ops.ExecuteOperation(b, ccip_onramp_ops.ApplyDestChainConfigUpdateOp, deps, ccip_onramp_ops.ApplyDestChainConfigureOnRampInput{ + OnRampPackageId: state.OnRampAddress, + LatestPackageId: latestIDs.OnRamp, + CCIPObjectRefId: state.CCIPObjectRef, + OwnerCapObjectId: state.OnRampOwnerCapObjectId, + StateObjectId: state.OnRampStateObjectId, + DestChainSelector: []uint64{input.Dest.Selector}, + DestChainAllowListEnabled: []bool{input.Source.AllowListEnabled}, + DestChainRouters: []string{destRouter}, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("apply dest chain config on Sui OnRamp for dest %d: %w", input.Dest.Selector, err) + } + if err := appendMCMSBatchOpFromCall(&out, input.Source.Selector, onRampReport.Output.Call, deps); err != nil { + return sequences.OnChainOutput{}, err + } + + routerReport, err := cldf_ops.ExecuteOperation(b, ccip_router_ops.SetOnRampsOp, deps, ccip_router_ops.SetOnRampsInput{ + RouterPackageId: state.CCIPRouterAddress, + LatestPackageId: latestIDs.Router, + RouterStateObjectId: state.CCIPRouterStateObjectID, + OwnerCapObjectId: state.CCIPRouterOwnerCapObjectId, + DestChainSelectors: []uint64{input.Dest.Selector}, + OnRampAddresses: []string{state.OnRampAddress}, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("set on-ramps on Sui Router for dest %d: %w", input.Dest.Selector, err) + } + if err := appendMCMSBatchOpFromCall(&out, input.Source.Selector, routerReport.Output.Call, deps); err != nil { + return sequences.OnChainOutput{}, err + } + + return out, nil + }, +) + +func (a *SuiAdapter) ConfigureLaneLegAsSource() *cldf_ops.Sequence[laneapi.UpdateLanesInput, sequences.OnChainOutput, cldf_chain.BlockChains] { + return ConfigureLaneLegAsSource +} diff --git a/deployment/lanes/connect_chains_test.go b/deployment/lanes/connect_chains_test.go new file mode 100644 index 000000000..530d6e8f4 --- /dev/null +++ b/deployment/lanes/connect_chains_test.go @@ -0,0 +1,295 @@ +package lanes_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/deployment/lanes" + "github.com/smartcontractkit/chainlink-sui/deployment/ops/mcmstest" + "github.com/smartcontractkit/chainlink-sui/deployment/utils" +) + +const ( + evmSepoliaSelector = uint64(16015286601757825753) + evmRouterAddress = "0xd3E190f381f06DC0d289590fd452C42Fa2DAC586" + + testCCIPPackageID = "0xece742a763bddf1e36629fa06b605497e413241afd14f05e558e80eef4f64e95" + testOffRampPackageID = "0x9438693fb18f5660aff9277240a2282be44dc01cdd7eed4e1d8de0591ad52c03" + testCCIPObjectRef = "0xbeace36c3c1e1f37c5806c4954140d15cbc7b0002ef7ccb490de26e82f5ec4ca" + testCCIPOwnerCapID = "0x874447a3ac6ae37bf545c6d71b0fa4a3d0af56a3f339faf4a4bab16ca4956ce7" + testLinkCoinMetadata = "0x8afb916ec72b91d28f519539659ebb1200b1824ff1f8d4c8f433acbb03017f2f" +) + +func testSuiChains() chain.BlockChains { + return chain.NewBlockChains(map[uint64]chain.BlockChain{ + suiTestnetSelector: sui.Chain{}, + }) +} + +func destLegInput() laneapi.UpdateLanesInput { + return laneapi.UpdateLanesInput{ + Source: &laneapi.ChainDefinition{ + Selector: evmSepoliaSelector, + OnRamp: make([]byte, 32), + RMNVerificationEnabled: true, + }, + Dest: &laneapi.ChainDefinition{ + Selector: suiTestnetSelector, + }, + } +} + +func sourceLegInput(destRouter []byte) laneapi.UpdateLanesInput { + return laneapi.UpdateLanesInput{ + Source: &laneapi.ChainDefinition{ + Selector: suiTestnetSelector, + }, + Dest: &laneapi.ChainDefinition{ + Selector: evmSepoliaSelector, + Router: destRouter, + FeeQuoterDestChainConfig: laneapi.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 30_000, + MaxPerMsgGasLimit: 3_000_000, + ChainFamilySelector: 0x2812d52c, + DefaultTokenFeeUSDCents: 25, + NetworkFeeUSDCents: 10, + }, + }, + } +} + +func TestConfigureLaneLegAsDest_MCMSBatchOp(t *testing.T) { + env := testEnvWithAddressBook(t) + input := destLegInput() + + var report cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput] + err := lanes.WithConnectChainsEnvironment(env, func() error { + var execErr error + report, execErr = cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + testSuiChains(), + input, + ) + return execErr + }) + require.NoError(t, err) + require.Len(t, report.Output.BatchOps, 1) + require.Equal(t, suiTestnetSelector, uint64(report.Output.BatchOps[0].ChainSelector)) + require.Len(t, report.Output.BatchOps[0].Transactions, 1) + + tx := report.Output.BatchOps[0].Transactions[0] + require.NotEmpty(t, tx.Data) + require.Equal(t, testOffRampPackageID, tx.To) + latestPackageID, err := utils.TransactionLatestPackageID(tx) + require.NoError(t, err) + require.Empty(t, latestPackageID) +} + +func TestConfigureLaneLegAsDest_MCMSBatchOp_WithLatestPackageID(t *testing.T) { + env := testEnvWithAddressBook(t) + const latestOffRampPackageID = "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + input := destLegInput() + + var report cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput] + err := lanes.WithConnectChainsEnvironment(env, func() error { + return lanes.WithSuiLatestPackageIDs(map[uint64]lanes.LatestPackageIDsConfig{ + suiTestnetSelector: {OffRamp: latestOffRampPackageID}, + }, func() error { + var execErr error + report, execErr = cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + testSuiChains(), + input, + ) + return execErr + }) + }) + require.NoError(t, err) + require.Len(t, report.Output.BatchOps, 1) + tx := report.Output.BatchOps[0].Transactions[0] + require.Equal(t, testOffRampPackageID, tx.To) + latestPackageIDFromTx, err := utils.TransactionLatestPackageID(tx) + require.NoError(t, err) + require.Equal(t, latestOffRampPackageID, latestPackageIDFromTx) +} + +func TestConfigureLaneLegAsDest_ValidationErrors(t *testing.T) { + env := testEnvWithAddressBook(t) + chains := testSuiChains() + + t.Run("requires ConnectChains scope", func(t *testing.T) { + _, err := cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + chains, + destLegInput(), + ) + require.Error(t, err) + require.Contains(t, err.Error(), "WithConnectChainsEnvironment") + }) + + t.Run("requires source OnRamp", func(t *testing.T) { + input := destLegInput() + input.Source.OnRamp = nil + err := lanes.WithConnectChainsEnvironment(env, func() error { + _, execErr := cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsDest, + chains, + input, + ) + return execErr + }) + require.Error(t, err) + require.Contains(t, err.Error(), "source OnRamp address required") + }) +} + +func TestConfigureLaneLegAsSource_ValidationErrors(t *testing.T) { + chains := testSuiChains() + router := common.HexToAddress(evmRouterAddress).Bytes() + + t.Run("requires ConnectChains scope", func(t *testing.T) { + _, err := cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsSource, + chains, + sourceLegInput(router), + ) + require.Error(t, err) + require.Contains(t, err.Error(), "WithConnectChainsEnvironment") + }) + + t.Run("requires dest Router", func(t *testing.T) { + _, err := cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsSource, + chains, + sourceLegInput(nil), + ) + require.Error(t, err) + require.Contains(t, err.Error(), "dest Router address required") + }) +} + +func TestConfigureLaneLegAsSource_RouterAddressBytes(t *testing.T) { + env := testEnvWithAddressBook(t) + chains := testSuiChains() + evmAddr := common.HexToAddress(evmRouterAddress) + + t.Run("20 byte EVM address", func(t *testing.T) { + _, err := runSourceLeg(t, env, chains, sourceLegInput(evmAddr.Bytes())) + require.NoError(t, err) + }) + + t.Run("32 byte left padded EVM address", func(t *testing.T) { + padded := make([]byte, 32) + copy(padded[12:], evmAddr.Bytes()) + _, err := runSourceLeg(t, env, chains, sourceLegInput(padded)) + require.NoError(t, err) + }) + + t.Run("32 byte native address", func(t *testing.T) { + native := make([]byte, 32) + for i := range native { + native[i] = byte(i + 1) + } + _, err := runSourceLeg(t, env, chains, sourceLegInput(native)) + require.NoError(t, err) + }) + + t.Run("invalid router length", func(t *testing.T) { + _, err := runSourceLeg(t, env, chains, sourceLegInput(make([]byte, 33))) + require.Error(t, err) + require.Contains(t, err.Error(), "address longer than 32 bytes") + }) +} + +func TestConfigureLaneLegAsSource_MCMSBatchOp_WithLatestPackageIDs(t *testing.T) { + env := testEnvWithAddressBook(t) + chains := testSuiChains() + input := sourceLegInput(common.HexToAddress(evmRouterAddress).Bytes()) + + const ( + // Address-book (registry-identity) package IDs for the source Sui chain. + ccipPkg = "0xece742a763bddf1e36629fa06b605497e413241afd14f05e558e80eef4f64e95" + onRampPkg = "0xf87c6010be571a304f0d860857204bc66f037842156f0f6c9d80be265fd83752" + routerPkg = "0xed4613bd35004954c07150c3e9b10230f5e23e3058bc2ca0e3e676cb43eb4dc1" + + latestCCIP = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + latestOnRamp = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + latestRouter = "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ) + + var report cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput] + err := lanes.WithConnectChainsEnvironment(env, func() error { + return lanes.WithSuiLatestPackageIDs(map[uint64]lanes.LatestPackageIDsConfig{ + suiTestnetSelector: {CCIP: latestCCIP, OnRamp: latestOnRamp, Router: latestRouter}, + }, func() error { + var execErr error + report, execErr = cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsSource, + chains, + input, + ) + return execErr + }) + }) + require.NoError(t, err) + require.Len(t, report.Output.BatchOps, 5) + + // Each op keeps the registry-identity package ID as the MCMS target (tx.To) while + // routing PTB execution through the upgraded (latest) package ID. + expected := []struct { + registry string + latest string + }{ + {ccipPkg, latestCCIP}, // FeeQuoter apply_token_transfer_fee_config_updates + {ccipPkg, latestCCIP}, // FeeQuoter apply_dest_chain_config_updates + {ccipPkg, latestCCIP}, // FeeQuoter apply_premium_multiplier_wei_per_eth_updates + {onRampPkg, latestOnRamp}, // OnRamp apply_dest_chain_config_updates + {routerPkg, latestRouter}, // Router set_on_ramps + } + for i, exp := range expected { + tx := report.Output.BatchOps[i].Transactions[0] + require.Equal(t, exp.registry, tx.To, "batch op %d To should be registry package id", i) + latest, err := utils.TransactionLatestPackageID(tx) + require.NoError(t, err) + require.Equal(t, exp.latest, latest, "batch op %d should route through latest package id", i) + } +} + +func runSourceLeg( + t *testing.T, + env cldf.Environment, + chains chain.BlockChains, + input laneapi.UpdateLanesInput, +) (cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput], error) { + t.Helper() + + var report cldf_ops.SequenceReport[laneapi.UpdateLanesInput, sequences.OnChainOutput] + err := lanes.WithConnectChainsEnvironment(env, func() error { + var execErr error + report, execErr = cldf_ops.ExecuteSequence( + mcmstest.Bundle(t), + lanes.ConfigureLaneLegAsSource, + chains, + input, + ) + return execErr + }) + return report, err +} diff --git a/deployment/lanes/defaults.go b/deployment/lanes/defaults.go new file mode 100644 index 000000000..647b2862e --- /dev/null +++ b/deployment/lanes/defaults.go @@ -0,0 +1,14 @@ +package lanes + +import suideploy "github.com/smartcontractkit/chainlink-sui/deployment" + +// Default token transfer fee fields for FeeQuoterApplyTokenTransferFeeConfigUpdatesOp on the +// Sui source leg. Values match deployment.DefaultCCIPSeqConfig / connect_sui_to_evm YAML. +var ( + DefaultLinkTokenTransferMinFeeUsdCents = suideploy.DefaultCCIPSeqConfig.AddMinFeeUsdCents[0] + DefaultLinkTokenTransferMaxFeeUsdCents = suideploy.DefaultCCIPSeqConfig.AddMaxFeeUsdCents[0] + DefaultLinkTokenTransferDeciBps = suideploy.DefaultCCIPSeqConfig.AddDeciBps[0] + DefaultLinkTokenTransferDestGasOverhead = suideploy.DefaultCCIPSeqConfig.AddDestGasOverhead[0] + DefaultLinkTokenTransferDestBytesOverhead = suideploy.DefaultCCIPSeqConfig.AddDestBytesOverhead[0] + DefaultLinkPremiumMultiplierWeiPerEth = suideploy.DefaultCCIPSeqConfig.PremiumMultiplierWeiPerEth[0] +) diff --git a/deployment/lanes/disable_remote_chain.go b/deployment/lanes/disable_remote_chain.go new file mode 100644 index 000000000..9ce43211e --- /dev/null +++ b/deployment/lanes/disable_remote_chain.go @@ -0,0 +1,24 @@ +package lanes + +import ( + "github.com/Masterminds/semver/v3" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// DisableRemoteChain is a no-op on Sui until remote-chain disable is implemented on-chain. +var DisableRemoteChain = cldf_ops.NewSequence( + "DisableRemoteChain", + semver.MustParse("1.6.0"), + "No-op disable remote chain sequence for Sui", + func(_ cldf_ops.Bundle, _ cldf_chain.BlockChains, _ laneapi.DisableRemoteChainInput) (sequences.OnChainOutput, error) { + return sequences.OnChainOutput{}, nil + }, +) + +func (a *SuiAdapter) DisableRemoteChain() *cldf_ops.Sequence[laneapi.DisableRemoteChainInput, sequences.OnChainOutput, cldf_chain.BlockChains] { + return DisableRemoteChain +} diff --git a/deployment/lanes/env.go b/deployment/lanes/env.go new file mode 100644 index 000000000..2c2846b6b --- /dev/null +++ b/deployment/lanes/env.go @@ -0,0 +1,105 @@ +package lanes + +import ( + "fmt" + "sync" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" +) + +// connectChainsEnv carries the active CLDF environment while ConnectChains runs. +// LaneAdapter address getters only receive datastore.DataStore, not Environment, so +// Sui resolves package IDs from addresses.json via LoadOnchainStatesui using this scope. +// +// CLD must invoke ConnectChains inside WithConnectChainsEnvironment. Other chain +// families are unaffected. +var connectChainsEnv struct { + mu sync.RWMutex + env cldf.Environment + active bool +} + +// WithConnectChainsEnvironment runs fn while SuiAdapter address getters can read +// ExistingAddresses from the given environment. +func WithConnectChainsEnvironment(e cldf.Environment, fn func() error) error { + connectChainsEnv.mu.Lock() + connectChainsEnv.env = e + connectChainsEnv.active = true + connectChainsEnv.mu.Unlock() + + defer func() { + connectChainsEnv.mu.Lock() + connectChainsEnv.active = false + connectChainsEnv.env = cldf.Environment{} + connectChainsEnv.mu.Unlock() + }() + + return fn() +} + +var latestPackageIDsEnv struct { + mu sync.RWMutex + bySelector map[uint64]LatestPackageIDsConfig + active bool +} + +// WithSuiLatestPackageIDs runs fn while lane sequences read optional upgraded package IDs +// keyed by Sui chain selector. CLD resolver populates this map from durable pipeline YAML. +func WithSuiLatestPackageIDs(bySelector map[uint64]LatestPackageIDsConfig, fn func() error) error { + latestPackageIDsEnv.mu.Lock() + latestPackageIDsEnv.bySelector = copyLatestPackageIDsBySelector(bySelector) + latestPackageIDsEnv.active = true + latestPackageIDsEnv.mu.Unlock() + + defer func() { + latestPackageIDsEnv.mu.Lock() + latestPackageIDsEnv.active = false + latestPackageIDsEnv.bySelector = nil + latestPackageIDsEnv.mu.Unlock() + }() + + return fn() +} + +// RunConnectChainsWithSuiScopes wraps ConnectChains with address-book and resolver latest-package-ID scopes. +func RunConnectChainsWithSuiScopes( + e cldf.Environment, + latestBySelector map[uint64]LatestPackageIDsConfig, + fn func() error, +) error { + return WithConnectChainsEnvironment(e, func() error { + return WithSuiLatestPackageIDs(latestBySelector, fn) + }) +} + +func currentLatestPackageIDs(chainSelector uint64) LatestPackageIDsConfig { + latestPackageIDsEnv.mu.RLock() + defer latestPackageIDsEnv.mu.RUnlock() + if !latestPackageIDsEnv.active || latestPackageIDsEnv.bySelector == nil { + return LatestPackageIDsConfig{} + } + return latestPackageIDsEnv.bySelector[chainSelector] +} + +func copyLatestPackageIDsBySelector(in map[uint64]LatestPackageIDsConfig) map[uint64]LatestPackageIDsConfig { + if len(in) == 0 { + return nil + } + out := make(map[uint64]LatestPackageIDsConfig, len(in)) + for selector, ids := range in { + out[selector] = ids + } + return out +} + +func connectChainsEnvironment() (cldf.Environment, error) { + connectChainsEnv.mu.RLock() + defer connectChainsEnv.mu.RUnlock() + + if !connectChainsEnv.active { + return cldf.Environment{}, fmt.Errorf( + "Sui lane address getters require ConnectChains to run inside lanes.WithConnectChainsEnvironment", + ) + } + return connectChainsEnv.env, nil +} diff --git a/deployment/lanes/latest_package_ids.go b/deployment/lanes/latest_package_ids.go new file mode 100644 index 000000000..32b634d35 --- /dev/null +++ b/deployment/lanes/latest_package_ids.go @@ -0,0 +1,19 @@ +package lanes + +// LatestPackageIDsConfig holds optional upgraded Sui package IDs for MCMS PTB routing. +// Package IDs in the address book remain the MCMS registry identity; latest IDs route +// execution to upgraded bytecode. +// +// Populated by CLD via WithSuiLatestPackageIDs from durable pipeline YAML resolver output. +type LatestPackageIDsConfig struct { + OffRamp string + CCIP string + OnRamp string + Router string +} + +// resolveLatestPackageIDs returns upgraded package IDs for MCMS execution on chainSelector. +// Empty when no resolver scope is active or the selector has no overrides (pre-upgrade is OK). +func resolveLatestPackageIDs(chainSelector uint64) LatestPackageIDsConfig { + return currentLatestPackageIDs(chainSelector) +} diff --git a/deployment/lanes/register.go b/deployment/lanes/register.go new file mode 100644 index 000000000..edf0eb59d --- /dev/null +++ b/deployment/lanes/register.go @@ -0,0 +1,13 @@ +package lanes + +import ( + "github.com/Masterminds/semver/v3" + + chainsel "github.com/smartcontractkit/chain-selectors" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" +) + +func init() { + laneapi.GetLaneAdapterRegistry().RegisterLaneAdapter(chainsel.FamilySui, semver.MustParse("1.6.0"), &SuiAdapter{}) +} diff --git a/deployment/lanes/remote_address_test.go b/deployment/lanes/remote_address_test.go new file mode 100644 index 000000000..612d999b1 --- /dev/null +++ b/deployment/lanes/remote_address_test.go @@ -0,0 +1,51 @@ +package lanes + +import ( + "encoding/hex" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestRemoteAddressBytesToHex(t *testing.T) { + t.Parallel() + + evmAddr := common.HexToAddress("0xd3E190f381f06DC0d289590fd452C42Fa2DAC586") + + t.Run("20 byte EVM address is left padded", func(t *testing.T) { + got, err := remoteAddressBytesToHex(evmAddr.Bytes()) + require.NoError(t, err) + require.Equal(t, "0x000000000000000000000000d3e190f381f06dc0d289590fd452c42fa2dac586", got) + }) + + t.Run("32 byte address is preserved", func(t *testing.T) { + raw := make([]byte, 32) + for i := range raw { + raw[i] = byte(i + 1) + } + got, err := remoteAddressBytesToHex(raw) + require.NoError(t, err) + require.Equal(t, "0x"+hex.EncodeToString(raw), got) + }) + + t.Run("32 byte left padded EVM address is preserved", func(t *testing.T) { + padded := make([]byte, 32) + copy(padded[12:], evmAddr.Bytes()) + got, err := remoteAddressBytesToHex(padded) + require.NoError(t, err) + require.Equal(t, "0x"+hex.EncodeToString(padded), got) + }) + + t.Run("empty bytes rejected", func(t *testing.T) { + _, err := remoteAddressBytesToHex(nil) + require.Error(t, err) + require.Contains(t, err.Error(), "empty address bytes") + }) + + t.Run("longer than 32 bytes rejected", func(t *testing.T) { + _, err := remoteAddressBytesToHex(make([]byte, 33)) + require.Error(t, err) + require.Contains(t, err.Error(), "address longer than 32 bytes") + }) +} diff --git a/deployment/lanes/state.go b/deployment/lanes/state.go new file mode 100644 index 000000000..872b313b2 --- /dev/null +++ b/deployment/lanes/state.go @@ -0,0 +1,95 @@ +package lanes + +import ( + "fmt" + + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + suideploy "github.com/smartcontractkit/chainlink-sui/deployment" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" +) + +func opTxDepsForChain(chains cldf_chain.BlockChains, chainSelector uint64) (sui_ops.OpTxDeps, error) { + chain, ok := chains.SuiChains()[chainSelector] + if !ok { + return sui_ops.OpTxDeps{}, fmt.Errorf("sui chain with selector %d not found in environment", chainSelector) + } + + return sui_ops.OpTxDeps{ + Client: chain.Client, + // Signer is intentionally nil. The cross-family lane connect flow always emits + // MCMS batch operations (CCIP contracts on Sui are timelock-owned) rather than + // executing transactions directly: a nil signer makes every op skip execution + // and return its encoded Call for proposal assembly (see appendMCMSBatchOpFromCall). + // Passing chain.Signer here would instead execute each update against the + // timelock-owned contracts (which fails) and produce no proposal. Direct, + // signer-based execution of these updates remains available via the dedicated + // ConnectSuiToEVM changeset. + Signer: nil, + GetCallOpts: func() *bind.CallOpts { + gasBudget := uint64(400_000_000) + return &bind.CallOpts{WaitForExecution: true, GasBudget: &gasBudget} + }, + SuiRPC: chain.URL, + }, nil +} + +func loadOffRampState(env cldf.Environment, chainSelector uint64) (suideploy.CCIPChainState, error) { + state, err := loadChainState(env, chainSelector) + if err != nil { + return suideploy.CCIPChainState{}, err + } + if state.CCIPObjectRef == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing CCIPObjectRef for sui chain %d in address book", chainSelector) + } + if state.OffRampAddress == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiOffRamp package for sui chain %d in address book", chainSelector) + } + if state.OffRampStateObjectId == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiOffRampStateObjectID for sui chain %d in address book", chainSelector) + } + if state.OffRampOwnerCapId == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiOffRampOwnerCapObjectID for sui chain %d in address book", chainSelector) + } + return state, nil +} + +func loadSourceChainState(env cldf.Environment, chainSelector uint64) (suideploy.CCIPChainState, error) { + state, err := loadChainState(env, chainSelector) + if err != nil { + return suideploy.CCIPChainState{}, err + } + if state.CCIPObjectRef == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing CCIPObjectRef for sui chain %d in address book", chainSelector) + } + if state.CCIPAddress == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiCCIP package for sui chain %d in address book", chainSelector) + } + if state.CCIPOwnerCapObjectId == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiCCIPOwnerCapObjectID for sui chain %d in address book", chainSelector) + } + if state.LinkTokenCoinMetadataId == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiLinkTokenObjectMetadataID for sui chain %d in address book", chainSelector) + } + if state.OnRampAddress == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiOnRamp package for sui chain %d in address book", chainSelector) + } + if state.OnRampStateObjectId == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiOnRampStateObjectID for sui chain %d in address book", chainSelector) + } + if state.OnRampOwnerCapObjectId == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiOnRampOwnerCapObjectID for sui chain %d in address book", chainSelector) + } + if state.CCIPRouterAddress == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiRouter package for sui chain %d in address book", chainSelector) + } + if state.CCIPRouterStateObjectID == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiRouterStateObjectID for sui chain %d in address book", chainSelector) + } + if state.CCIPRouterOwnerCapObjectId == "" { + return suideploy.CCIPChainState{}, fmt.Errorf("missing SuiRouterOwnerCapObjectID for sui chain %d in address book", chainSelector) + } + return state, nil +} diff --git a/deployment/lanes/translate.go b/deployment/lanes/translate.go new file mode 100644 index 000000000..a32c851c9 --- /dev/null +++ b/deployment/lanes/translate.go @@ -0,0 +1,44 @@ +package lanes + +import ( + "encoding/binary" + + laneapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" + + ccip_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip" +) + +// TranslateDestChainConfig maps a product-level FeeQuoterDestChainConfig to the Sui +// FeeQuoter op input for configuring a remote destination on the Sui chain. +// CCIP package/state object IDs are filled by the caller from address book state. +func TranslateDestChainConfig( + cfg laneapi.FeeQuoterDestChainConfig, + destChainSelector uint64, +) ccip_ops.FeeQuoterApplyDestChainConfigUpdatesInput { + var v1 laneapi.FeeQuoterV1Params + if cfg.V1Params != nil { + v1 = *cfg.V1Params + } + return ccip_ops.FeeQuoterApplyDestChainConfigUpdatesInput{ + DestChainSelector: destChainSelector, + IsEnabled: cfg.IsEnabled, + MaxNumberOfTokensPerMsg: v1.MaxNumberOfTokensPerMsg, + MaxDataBytes: cfg.MaxDataBytes, + MaxPerMsgGasLimit: cfg.MaxPerMsgGasLimit, + DestGasOverhead: cfg.DestGasOverhead, + DestGasPerPayloadByteBase: cfg.DestGasPerPayloadByteBase, + DestGasPerPayloadByteHigh: v1.DestGasPerPayloadByteHigh, + DestGasPerPayloadByteThreshold: v1.DestGasPerPayloadByteThreshold, + DestDataAvailabilityOverheadGas: v1.DestDataAvailabilityOverheadGas, + DestGasPerDataAvailabilityByte: v1.DestGasPerDataAvailabilityByte, + DestDataAvailabilityMultiplierBps: v1.DestDataAvailabilityMultiplierBps, + ChainFamilySelector: binary.BigEndian.AppendUint32(nil, cfg.ChainFamilySelector), + EnforceOutOfOrder: v1.EnforceOutOfOrder, + DefaultTokenFeeUsdCents: cfg.DefaultTokenFeeUSDCents, + DefaultTokenDestGasOverhead: cfg.DefaultTokenDestGasOverhead, + DefaultTxGasLimit: cfg.DefaultTxGasLimit, + GasMultiplierWeiPerEth: v1.GasMultiplierWeiPerEth, + GasPriceStalenessThreshold: v1.GasPriceStalenessThreshold, + NetworkFeeUsdCents: uint32(cfg.NetworkFeeUSDCents), + } +} diff --git a/deployment/ops/ccip/op_fee_quoter.go b/deployment/ops/ccip/op_fee_quoter.go index 2207946f4..f7bd86530 100644 --- a/deployment/ops/ccip/op_fee_quoter.go +++ b/deployment/ops/ccip/op_fee_quoter.go @@ -157,6 +157,7 @@ var FeeQuoterApplyFeeTokenUpdatesOp = cld_ops.NewOperation( // FEE QUOTER -- apply_token_transfer_fee_config_updates type FeeQuoterApplyTokenTransferFeeConfigUpdatesInput struct { CCIPPackageId string + LatestPackageId string // optional: upgraded package ID for PTB execution when CCIPPackageId is the MCMS registry identity StateObjectId string OwnerCapObjectId string DestChainSelector uint64 @@ -171,7 +172,11 @@ type FeeQuoterApplyTokenTransferFeeConfigUpdatesInput struct { } var applyTokenTransferFeeHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input FeeQuoterApplyTokenTransferFeeConfigUpdatesInput) (output sui_ops.OpTxResult[NoObjects], err error) { - contract, err := module_fee_quoter.NewFeeQuoter(input.CCIPPackageId, deps.Client) + binaryPkgId := input.CCIPPackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + contract, err := module_fee_quoter.NewFeeQuoter(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to create fee quoter contract: %w", err) } @@ -196,6 +201,10 @@ var applyTokenTransferFeeHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, if err != nil { return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID + call.PackageID = input.CCIPPackageId + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of ApplyTokenTransferFeeConfigUpdates on FeeQuoter as per no Signer provided") return sui_ops.OpTxResult[NoObjects]{ @@ -247,6 +256,7 @@ var FeeQuoterApplyTokenTransferFeeConfigUpdatesOp = cld_ops.NewOperation( // FEE QUOTER -- apply_dest_chain_config_updates type FeeQuoterApplyDestChainConfigUpdatesInput struct { CCIPPackageId string + LatestPackageId string // optional: upgraded package ID for PTB execution when CCIPPackageId is the MCMS registry identity StateObjectId string OwnerCapObjectId string DestChainSelector uint64 @@ -272,7 +282,11 @@ type FeeQuoterApplyDestChainConfigUpdatesInput struct { } var applyDestChainConfigHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input FeeQuoterApplyDestChainConfigUpdatesInput) (output sui_ops.OpTxResult[NoObjects], err error) { - contract, err := module_fee_quoter.NewFeeQuoter(input.CCIPPackageId, deps.Client) + binaryPkgId := input.CCIPPackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + contract, err := module_fee_quoter.NewFeeQuoter(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to create fee quoter contract: %w", err) } @@ -308,6 +322,10 @@ var applyDestChainConfigHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, if err != nil { return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID + call.PackageID = input.CCIPPackageId + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of ApplyDestChainConfigUpdates on FeeQuoter as per no Signer provided") return sui_ops.OpTxResult[NoObjects]{ @@ -370,6 +388,7 @@ var FeeQuoterApplyDestChainConfigUpdatesOp = cld_ops.NewOperation( // FEE QUOTER -- apply_premium_multiplier_wei_per_eth_updates type FeeQuoterApplyPremiumMultiplierWeiPerEthUpdatesInput struct { CCIPPackageId string + LatestPackageId string // optional: upgraded package ID for PTB execution when CCIPPackageId is the MCMS registry identity StateObjectId string OwnerCapObjectId string Tokens []string @@ -377,7 +396,11 @@ type FeeQuoterApplyPremiumMultiplierWeiPerEthUpdatesInput struct { } var applyPremiumMultiplierHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input FeeQuoterApplyPremiumMultiplierWeiPerEthUpdatesInput) (output sui_ops.OpTxResult[NoObjects], err error) { - contract, err := module_fee_quoter.NewFeeQuoter(input.CCIPPackageId, deps.Client) + binaryPkgId := input.CCIPPackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + contract, err := module_fee_quoter.NewFeeQuoter(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to create fee quoter contract: %w", err) } @@ -395,6 +418,10 @@ var applyPremiumMultiplierHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps if err != nil { return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID + call.PackageID = input.CCIPPackageId + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of ApplyPremiumMultiplierWeiPerEthUpdates on FeeQuoter as per no Signer provided") return sui_ops.OpTxResult[NoObjects]{ diff --git a/deployment/ops/ccip_offramp/op_deploy.go b/deployment/ops/ccip_offramp/op_deploy.go index 2f9678e2c..c2b334790 100644 --- a/deployment/ops/ccip_offramp/op_deploy.go +++ b/deployment/ops/ccip_offramp/op_deploy.go @@ -181,6 +181,7 @@ var setOCR3ConfigHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input S type ApplySourceChainConfigUpdateInput struct { CCIPObjectRef string OffRampPackageId string + LatestPackageId string // optional: upgraded package ID for PTB execution when OffRampPackageId is the MCMS registry identity OffRampStateId string OwnerCapObjectId string SourceChainsSelectors []uint64 @@ -190,7 +191,11 @@ type ApplySourceChainConfigUpdateInput struct { } var applySourceChainConfigUpdateHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input ApplySourceChainConfigUpdateInput) (output sui_ops.OpTxResult[DeployCCIPOffRampObjects], err error) { - offRampPackage, err := module_offramp.NewOfframp(input.OffRampPackageId, deps.Client) + binaryPkgId := input.OffRampPackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + offRampPackage, err := module_offramp.NewOfframp(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[DeployCCIPOffRampObjects]{}, err } @@ -211,6 +216,10 @@ var applySourceChainConfigUpdateHandler = func(b cld_ops.Bundle, deps sui_ops.Op if err != nil { return sui_ops.OpTxResult[DeployCCIPOffRampObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID + call.PackageID = input.OffRampPackageId + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of ApplySourceChainConfigUpdates on OffRamp as per no Signer provided") return sui_ops.OpTxResult[DeployCCIPOffRampObjects]{ diff --git a/deployment/ops/ccip_offramp/op_offramp_mcms_test.go b/deployment/ops/ccip_offramp/op_offramp_mcms_test.go index a5e29b803..5ce41bd31 100644 --- a/deployment/ops/ccip_offramp/op_offramp_mcms_test.go +++ b/deployment/ops/ccip_offramp/op_offramp_mcms_test.go @@ -62,6 +62,32 @@ func TestOffRampDualModeOps_ProposalDataMatchesBindingEncoder(t *testing.T) { mcmstest.AssertProposalDataMatches(t, report.Output.Call.Data, encoded, input.OffRampStateId, nil) }) + t.Run("apply_source_chain_config_updates_with_latest_package_id", func(t *testing.T) { + t.Parallel() + const latestPackageID = "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + input := ApplySourceChainConfigUpdateInput{ + CCIPObjectRef: mcmstest.StateObjectID, + OffRampPackageId: mcmstest.PackageID, + LatestPackageId: latestPackageID, + OffRampStateId: mcmstest.CoinMetadata, + OwnerCapObjectId: mcmstest.OwnerCapID, + SourceChainsSelectors: []uint64{mcmstest.DestChainSel}, + SourceChainsIsEnabled: []bool{true}, + SourceChainsIsRMNVerificationDisabled: []bool{false}, + SourceChainsOnRamp: [][]byte{{0x01}}, + } + report, err := cld_ops.ExecuteOperation(mcmstest.Bundle(t), ApplySourceChainConfigUpdatesOp, sui_ops.OpTxDeps{}, input) + require.NoError(t, err) + require.Equal(t, mcmstest.PackageID, report.Output.Call.PackageID) + require.Equal(t, latestPackageID, report.Output.Call.LatestPackageID) + + latestOfframp, err := module_offramp.NewOfframp(latestPackageID, nil) + require.NoError(t, err) + encoded, err := latestOfframp.Encoder().ApplySourceChainConfigUpdates(bind.Object{Id: input.CCIPObjectRef}, bind.Object{Id: input.OffRampStateId}, bind.Object{Id: input.OwnerCapObjectId}, input.SourceChainsSelectors, input.SourceChainsIsEnabled, input.SourceChainsIsRMNVerificationDisabled, input.SourceChainsOnRamp) + require.NoError(t, err) + mcmstest.AssertProposalDataMatches(t, report.Output.Call.Data, encoded, input.OffRampStateId, nil) + }) + t.Run("add_package_id", func(t *testing.T) { t.Parallel() input := AddPackageIdOffRampInput{ diff --git a/deployment/ops/ccip_onramp/op_deploy.go b/deployment/ops/ccip_onramp/op_deploy.go index 1f3ecb25e..b6527eda6 100644 --- a/deployment/ops/ccip_onramp/op_deploy.go +++ b/deployment/ops/ccip_onramp/op_deploy.go @@ -115,6 +115,7 @@ var InitializeHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input OnRa type ApplyDestChainConfigureOnRampInput struct { OnRampPackageId string + LatestPackageId string // optional: upgraded package ID for PTB execution when OnRampPackageId is the MCMS registry identity CCIPObjectRefId string OwnerCapObjectId string StateObjectId string @@ -124,7 +125,11 @@ type ApplyDestChainConfigureOnRampInput struct { } var ApplyDestChainUpdateHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input ApplyDestChainConfigureOnRampInput) (output sui_ops.OpTxResult[DeployCCIPOnRampObjects], err error) { - onRampPackage, err := module_onramp.NewOnramp(input.OnRampPackageId, deps.Client) + binaryPkgId := input.OnRampPackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + onRampPackage, err := module_onramp.NewOnramp(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[DeployCCIPOnRampObjects]{}, err } @@ -144,6 +149,10 @@ var ApplyDestChainUpdateHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, if err != nil { return sui_ops.OpTxResult[DeployCCIPOnRampObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID + call.PackageID = input.OnRampPackageId + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of ApplyDestChainConfigUpdates on OnRamp as per no Signer provided") return sui_ops.OpTxResult[DeployCCIPOnRampObjects]{ diff --git a/deployment/ops/ccip_router/op_deploy.go b/deployment/ops/ccip_router/op_deploy.go index 858b793e5..cd667cdbf 100644 --- a/deployment/ops/ccip_router/op_deploy.go +++ b/deployment/ops/ccip_router/op_deploy.go @@ -173,6 +173,7 @@ var deployHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input DeployCC type SetOnRampsInput struct { RouterPackageId string + LatestPackageId string // optional: upgraded package ID for PTB execution when RouterPackageId is the MCMS registry identity RouterStateObjectId string OwnerCapObjectId string DestChainSelectors []uint64 @@ -191,7 +192,11 @@ var SetOnRampsOp = cld_ops.NewOperation( ) var setOnRampsHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input SetOnRampsInput) (output sui_ops.OpTxResult[SetOnRampsObjects], err error) { - routerPackage, err := module_router.NewRouter(input.RouterPackageId, deps.Client) + binaryPkgId := input.RouterPackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + routerPackage, err := module_router.NewRouter(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[SetOnRampsObjects]{}, err } @@ -209,6 +214,10 @@ var setOnRampsHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input SetO if err != nil { return sui_ops.OpTxResult[SetOnRampsObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID + call.PackageID = input.RouterPackageId + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of SetOnRamps on Router as per no Signer provided", "destChainSelectors", input.DestChainSelectors, diff --git a/deployment/testdata/addresses.json b/deployment/testdata/addresses.json index 0acbc609e..59d562f4d 100644 --- a/deployment/testdata/addresses.json +++ b/deployment/testdata/addresses.json @@ -221,6 +221,10 @@ "Type": "SuiRouterStateObjectID", "Version": "1.0.0" }, + "0xb000000000000000000000000000000000000000000000000000000000000001": { + "Type": "SuiCCIPRouterOwnerCapObjectID", + "Version": "1.0.0" + }, "0xbeace36c3c1e1f37c5806c4954140d15cbc7b0002ef7ccb490de26e82f5ec4ca": { "Type": "SuiCCIPObjectRef", "Version": "1.0.0" diff --git a/deployment/utils/mcms.go b/deployment/utils/mcms.go index 8aeccf272..593d04c3f 100644 --- a/deployment/utils/mcms.go +++ b/deployment/utils/mcms.go @@ -86,6 +86,39 @@ func GenerateProposal(ctx context.Context, input GenerateProposalInput) (*mcms.T return builder.Build() } +// TransactionCallToMCMSTransaction converts an encoded Sui op call into an MCMS transaction. +// When call.LatestPackageID is set, it is propagated for upgraded-package PTB routing. +func TransactionCallToMCMSTransaction(call sui_ops.TransactionCall) (types.Transaction, error) { + tx, err := suisdk.NewTransactionWithStateObj( + call.Module, + call.Function, + call.PackageID, + call.Data, + call.Module, + []string{}, + call.StateObjID, + call.TypeArgs, + ) + if err != nil { + return types.Transaction{}, fmt.Errorf("create MCMS transaction: %w", err) + } + if call.LatestPackageID != "" { + if setErr := suisdk.SetLatestPackageID(&tx, call.LatestPackageID); setErr != nil { + return types.Transaction{}, fmt.Errorf("set latest package ID: %w", setErr) + } + } + return tx, nil +} + +// TransactionLatestPackageID returns the optional upgraded package ID stored on an MCMS transaction. +func TransactionLatestPackageID(tx types.Transaction) (string, error) { + var fields suisdk.AdditionalFields + if err := json.Unmarshal(tx.AdditionalFields, &fields); err != nil { + return "", fmt.Errorf("unmarshal transaction additional fields: %w", err) + } + return fields.LatestPackageID, nil +} + func ExtractTransactionCall(output interface{}, operationID string) (sui_ops.TransactionCall, error) { jsonBytes, err := json.Marshal(output) if err != nil { diff --git a/deployment/utils/mcms_tx_test.go b/deployment/utils/mcms_tx_test.go new file mode 100644 index 000000000..88b03d32f --- /dev/null +++ b/deployment/utils/mcms_tx_test.go @@ -0,0 +1,35 @@ +package utils_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + "github.com/smartcontractkit/chainlink-sui/deployment/utils" +) + +func TestTransactionCallToMCMSTransaction_LatestPackageID(t *testing.T) { + t.Parallel() + + const ( + originalPackageID = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + latestPackageID = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ) + + call := sui_ops.TransactionCall{ + PackageID: originalPackageID, + LatestPackageID: latestPackageID, + Module: "offramp", + Function: "apply_source_chain_config_updates", + Data: []byte{0x01}, + StateObjID: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + } + + tx, err := utils.TransactionCallToMCMSTransaction(call) + require.NoError(t, err) + require.Equal(t, originalPackageID, tx.To) + latestPackageIDFromTx, err := utils.TransactionLatestPackageID(tx) + require.NoError(t, err) + require.Equal(t, latestPackageID, latestPackageIDFromTx) +} diff --git a/go.mod b/go.mod index 79e32207e..3c38b9bf3 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc - github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee + github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 github.com/stretchr/testify v1.11.1 github.com/test-go/testify v1.1.4 diff --git a/go.sum b/go.sum index 2f71ffbf4..2225de6a4 100644 --- a/go.sum +++ b/go.sum @@ -296,8 +296,8 @@ github.com/smartcontractkit/chain-selectors v1.0.102 h1:qYP4+72HfvogCHR5ymwRFee3 github.com/smartcontractkit/chain-selectors v1.0.102/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc h1:Um9FBcf0JNSFuGbxgccDG1vM3cNrMGy0SdJ7r6VbX0o= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= -github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee h1:YAE9gMuCsjp3toJXBQge7pvSZhsFCv9GakTEjjoiE50= -github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee/go.mod h1:2xVZJ3o9udYFeJhwyHXAMlNhptJ99uoiGjzfOicYNX8= +github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb h1:uSXBvj/idCBg7hMLmek8YYNa0JKggqlTkHYdtIooeNM= +github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb/go.mod h1:2xVZJ3o9udYFeJhwyHXAMlNhptJ99uoiGjzfOicYNX8= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 h1:5z3LQ27MJmhiaeqp9S2TzbF5Wm4GGvUKAYOtE9AauR8= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89/go.mod h1:G2AII0QmWzXx8Ag9IKnGN3h/gwwNnhHUOCviJievdvo= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 4aa81b86f..27c59c671 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -13,7 +13,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 - github.com/smartcontractkit/chainlink-sui v0.0.0-20260610194843-349ea43d69ce + github.com/smartcontractkit/chainlink-sui v0.0.0 github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/mcms v0.48.0 github.com/stretchr/testify v1.11.1 @@ -134,8 +134,8 @@ require ( github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc // indirect github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320 // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 // indirect github.com/smartcontractkit/chainlink-protos/job-distributor v0.19.0 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 9321bb7ed..a74375a07 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -603,10 +603,10 @@ github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320 github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320/go.mod h1:WKmNUX4oy8IvB66ukudrE99uaXjlZ7WghCDwHOTyB1c= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee h1:YAE9gMuCsjp3toJXBQge7pvSZhsFCv9GakTEjjoiE50= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee/go.mod h1:2xVZJ3o9udYFeJhwyHXAMlNhptJ99uoiGjzfOicYNX8= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 h1:jkChf04hhdiMBApbb+lLDxHMY62Md6UeM7v++GSw3K8= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139/go.mod h1:wuhagkM/lU0GbV2YcrROOH0GlsfXJYwm6qmpa4CK70w= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 h1:tw3K4UkH5XfW5SoyYkvAlbzrccoGSLdz/XkxD6nyGC8= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139/go.mod h1:1WcontO9PeuKdUf5HXfs3nuICtzUvFNnyCmrHkTCF9Y= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb h1:TZUVwF0ZexyJKqBjutCGjR/kG3LOa1krTQt/EXFgMYg= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb/go.mod h1:xu0Jum/nGRkjBwT/Vq7WCElWOTBBkFRwG0ZIaw9tF2I= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb h1:2p+8KYL0bhHblGcOJKRH84i9QduKGcY72NYcLJzNdNc= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee h1:ScVlPpWyoswUlu3izqOH7N0oW5UCrp7lPifkOZ2Mgew= github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee/go.mod h1:xDXlDsou69NYOolOAj+KITRn9luER6Bg52NXelrLl+A= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 h1:5z3LQ27MJmhiaeqp9S2TzbF5Wm4GGvUKAYOtE9AauR8= diff --git a/scripts/compare_ccip_upgrade_compat.py b/scripts/compare_ccip_upgrade_compat.py new file mode 100644 index 000000000..ca9c1791c --- /dev/null +++ b/scripts/compare_ccip_upgrade_compat.py @@ -0,0 +1,845 @@ +#!/usr/bin/env python3 +"""Compare deployed CCIP package JSON (normalized RPC) against local Move source.""" + +from __future__ import annotations + +import argparse +import glob +import json +import re +import sys +import urllib.error +import urllib.request +from collections import Counter +from copy import deepcopy +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[1] +SRC_GLOB = [ + "contracts/ccip/ccip/sources/*.move", + "contracts/ccip/ccip/sources/util/*.move", +] + +ADDR_MAP_LOCAL = { + "std": "0x1", + "sui": "0x2", + "ccip": "0xSELF", + "mcms": "0xMCMS", + "fast_mcms": "0xMCMS", +} + +IMPLICIT_NAME = { + "TxContext": ("0x2", "tx_context", "TxContext"), + "UID": ("0x2", "object", "UID"), + "ID": ("0x2", "object", "ID"), + "Option": ("0x1", "option", "Option"), + "String": ("0x1", "string", "String"), +} + +RPC_URLS = { + "mainnet": [ + "https://fullnode.mainnet.sui.io:443", + "https://mainnet.sui.rpcpool.com", + ], + "testnet": [ + "https://fullnode.testnet.sui.io:443", + ], +} + + +@dataclass +class CompareReport: + label: str + issues: list[str] = field(default_factory=list) + unparsed: list[str] = field(default_factory=list) + additions_funs: list[str] = field(default_factory=list) + additions_structs: list[str] = field(default_factory=list) + stats: dict[str, Any] = field(default_factory=dict) + + +def fetch_normalized_modules(package_id: str, network: str) -> dict[str, Any]: + payload = json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "sui_getNormalizedMoveModulesByPackage", + "params": [package_id], + } + ).encode() + headers = {"Content-Type": "application/json"} + errors: list[str] = [] + for url in RPC_URLS.get(network, RPC_URLS["mainnet"]): + req = urllib.request.Request(url, data=payload, headers=headers, method="POST") + opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + try: + with opener.open(req, timeout=120) as resp: + data = json.loads(resp.read()) + if "error" in data: + errors.append(f"{url}: {data['error']}") + continue + result = data.get("result") + if not result: + errors.append(f"{url}: empty result") + continue + return result + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc: + errors.append(f"{url}: {exc}") + raise RuntimeError("RPC fetch failed:\n " + "\n ".join(errors)) + + +def fetch_upgrade_cap(package_id: str, upgrade_cap_id: str, network: str) -> dict[str, Any]: + payload = json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "sui_getObject", + "params": [upgrade_cap_id, {"showContent": True}], + } + ).encode() + headers = {"Content-Type": "application/json"} + for url in RPC_URLS.get(network, RPC_URLS["mainnet"]): + req = urllib.request.Request(url, data=payload, headers=headers, method="POST") + opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + try: + with opener.open(req, timeout=60) as resp: + data = json.loads(resp.read()) + fields = ( + data.get("result", {}) + .get("data", {}) + .get("content", {}) + .get("fields", {}) + ) + return { + "upgrade_cap_id": upgrade_cap_id, + "package_id": package_id, + "version": fields.get("version"), + "policy": fields.get("policy"), + } + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError): + continue + return {"upgrade_cap_id": upgrade_cap_id, "package_id": package_id, "version": None, "policy": None} + + +def discover_addr_map(deployed: dict[str, Any]) -> dict[str, str]: + counts: Counter[str] = Counter() + + def walk(x: Any) -> None: + if isinstance(x, dict): + if "Struct" in x: + counts[x["Struct"]["address"].lower()] += 1 + for a in x["Struct"].get("typeArguments", []): + walk(a) + elif "Reference" in x: + walk(x["Reference"]) + elif "MutableReference" in x: + walk(x["MutableReference"]) + elif "Vector" in x: + walk(x["Vector"]) + elif isinstance(x, list): + for it in x: + walk(it) + + for mod in deployed.values(): + for s in mod.get("structs", {}).values(): + for f in s.get("fields", []): + walk(f.get("type")) + for fn in mod.get("exposedFunctions", {}).values(): + for p in fn.get("parameters", []): + walk(p) + for r in fn.get("return", []): + walk(r) + + self_addr = counts.most_common(1)[0][0] if counts else "" + external = [ + a + for a in counts + if a not in (self_addr, "0x1", "0x2") + ] + mcms = external[0] if external else "" + return { + self_addr: "0xSELF", + mcms: "0xMCMS", + "0x2": "0x2", + "0x1": "0x1", + } + + +def canon_deployed(t: Any, addr_map: dict[str, str], type_param_names: list[str]) -> str: + if isinstance(t, str): + return { + "U8": "u8", + "U16": "u16", + "U32": "u32", + "U64": "u64", + "U128": "u128", + "U256": "u256", + "Bool": "bool", + "Address": "address", + "Signer": "signer", + }.get(t, t) + if isinstance(t, dict): + if "TypeParameter" in t: + idx = t["TypeParameter"] + return type_param_names[idx] if idx < len(type_param_names) else f"T{idx}" + if "Reference" in t: + return "&" + canon_deployed(t["Reference"], addr_map, type_param_names) + if "MutableReference" in t: + return "&mut " + canon_deployed(t["MutableReference"], addr_map, type_param_names) + if "Vector" in t: + return "vector<" + canon_deployed(t["Vector"], addr_map, type_param_names) + ">" + if "Struct" in t: + s = t["Struct"] + addr = addr_map.get(s["address"].lower(), s["address"].lower()) + base = f"{addr}::{s['module']}::{s['name']}" + args = s.get("typeArguments", []) + if args: + base += "<" + ", ".join( + canon_deployed(a, addr_map, type_param_names) for a in args + ) + ">" + return base + return repr(t) + + +def strip_comments(s: str) -> str: + s = re.sub(r"/\*.*?\*/", "", s, flags=re.DOTALL) + return re.sub(r"//[^\n]*", "", s) + + +def find_matching(src: str, start_idx: int, open_ch: str, close_ch: str) -> int: + depth = 1 + i = start_idx + while i < len(src) and depth > 0: + if src[i] == open_ch: + depth += 1 + elif src[i] == close_ch: + depth -= 1 + i += 1 + return i + + +def split_balanced(text: str, sep: str) -> list[str]: + out, cur, depth = [], "", 0 + for ch in text: + if ch in "<({[": + depth += 1 + cur += ch + elif ch in ">)}]": + depth -= 1 + cur += ch + elif ch == sep and depth == 0: + if cur.strip(): + out.append(cur.strip()) + cur = "" + else: + cur += ch + if cur.strip(): + out.append(cur.strip()) + return out + + +def find_balanced_end(text: str, start: int, open_ch: str = "<", close_ch: str = ">") -> int: + if start >= len(text) or text[start] != open_ch: + return -1 + depth = 1 + i = start + 1 + while i < len(text) and depth > 0: + if text[i] == open_ch: + depth += 1 + elif text[i] == close_ch: + depth -= 1 + i += 1 + return i - 1 if depth == 0 else -1 + + +def parse_type_params(text: str) -> list[tuple[str, list[str], bool]]: + out = [] + for it in split_balanced(text, ","): + s = it.strip() + phantom = s.startswith("phantom ") + if phantom: + s = s[len("phantom ") :] + if ":" in s: + name, abils = s.split(":", 1) + ab = sorted(a.strip().lower() for a in abils.split("+") if a.strip()) + out.append((name.strip(), ab, phantom)) + else: + out.append((s.strip(), [], phantom)) + return out + + +def parse_use(content: str) -> dict[str, tuple[str, str, str]]: + aliases: dict[str, tuple[str, str, str]] = {} + for um in re.finditer( + r"use\s+([\w:]+)(?:\s*::\s*\{([^}]+)\})?(?:\s+as\s+(\w+))?\s*;", content + ): + path, brace, as_alias = um.group(1), um.group(2), um.group(3) + path_parts = path.split("::") + addr = ADDR_MAP_LOCAL.get(path_parts[0], "0x" + path_parts[0].upper()) + if brace: + module_part = "::".join(path_parts[1:]) if len(path_parts) > 1 else "" + for item in [i.strip() for i in brace.split(",")]: + am = re.match(r"(\w+)(?:\s+as\s+(\w+))?", item) + if not am: + continue + orig = am.group(1) + alias = am.group(2) or orig + if orig == "Self": + last = path_parts[-1] + aliases[am.group(2) or last] = (addr, last, last) + else: + aliases[alias] = (addr, module_part or path_parts[-1], orig) + else: + if len(path_parts) == 2: + mod = path_parts[1] + aliases[as_alias or mod] = (addr, mod, mod) + elif len(path_parts) >= 3: + mod = path_parts[-2] + name = path_parts[-1] + aliases[as_alias or name] = (addr, mod, name) + return aliases + + +def canon_local_type( + t: str, current_module: str, aliases: dict[str, tuple[str, str, str]], fn_tp_names: list[str] +) -> str: + t = t.strip() + if not t: + return "" + if t.startswith("&mut "): + return "&mut " + canon_local_type(t[5:], current_module, aliases, fn_tp_names) + if t.startswith("&"): + return "&" + canon_local_type(t[1:].lstrip(), current_module, aliases, fn_tp_names) + if re.match(r"^vector\s*<", t): + lt = t.index("<") + gt = find_balanced_end(t, lt, "<", ">") + if gt < 0: + return f"<>" + return ( + "vector<" + + canon_local_type(t[lt + 1 : gt].strip(), current_module, aliases, fn_tp_names) + + ">" + ) + if t in ("u8", "u16", "u32", "u64", "u128", "u256", "bool", "address", "signer"): + return t + if "<" in t: + lt = t.index("<") + gt = find_balanced_end(t, lt, "<", ">") + if gt < 0: + return f"<>" + base = t[:lt].strip() + args = split_balanced(t[lt + 1 : gt], ",") + args_c = [canon_local_type(a, current_module, aliases, fn_tp_names) for a in args] + else: + base = t + args_c = [] + parts = base.split("::") + if len(parts) == 1: + name = parts[0] + if name in fn_tp_names: + return name + ("<" + ", ".join(args_c) + ">" if args_c else "") + if name in aliases: + addr, mod, item = aliases[name] + base_str = f"{addr}::{mod}::{item}" + elif name in IMPLICIT_NAME: + addr, mod, item = IMPLICIT_NAME[name] + base_str = f"{addr}::{mod}::{item}" + else: + base_str = f"0xSELF::{current_module}::{name}" + else: + mod_part = parts[0] + name = parts[-1] + if mod_part in aliases: + addr, mod, _ = aliases[mod_part] + base_str = f"{addr}::{mod}::{name}" + else: + base_str = f"0xSELF::{mod_part}::{name}" + if args_c: + return base_str + "<" + ", ".join(args_c) + ">" + return base_str + + +def parse_module_fn_sigs(path: Path) -> tuple[str | None, dict[str, Any]]: + content = path.read_text() + mm = re.search(r"^\s*module\s+ccip::(\w+)", content, re.M) + if not mm: + return None, {} + module_name = mm.group(1) + src = strip_comments(content) + aliases = parse_use(content) + fns: dict[str, Any] = {} + for fm in re.finditer(r"public(?:\s*\(\s*(\w+)\s*\))?(?:\s+entry)?\s+fun\s+(\w+)", src): + if fm.group(1) is not None: + continue + fname = fm.group(2) + if fname.endswith("_for_test") or fname == "test_init": + continue + start = fm.start() + if "#[test_only]" in content[max(0, start - 200) : start]: + continue + i = fm.end() + while i < len(src) and src[i] in " \t\n\r": + i += 1 + tp_list = [] + if i < len(src) and src[i] == "<": + tp_close = find_matching(src, i + 1, "<", ">") + tp_list = parse_type_params(src[i + 1 : tp_close - 1]) + i = tp_close + while i < len(src) and src[i] in " \t\n\r": + i += 1 + if i >= len(src) or src[i] != "(": + continue + p_close = find_matching(src, i + 1, "(", ")") + params_text = src[i + 1 : p_close - 1] + tp_names = [n for (n, _, _) in tp_list] + params = [] + for p in split_balanced(params_text, ","): + mp = re.match(r"^(?:mut\s+)?(\w+|_)\s*:\s*(.+)$", p, re.DOTALL) + if mp: + params.append( + ( + mp.group(1), + canon_local_type(mp.group(2).strip(), module_name, aliases, tp_names), + ) + ) + i = p_close + while i < len(src) and src[i] in " \t\n\r": + i += 1 + ret_canon = "" + if i < len(src) and src[i] == ":": + i += 1 + depth = 0 + j = i + while j < len(src): + ch = src[j] + if depth == 0 and ch in "{;": + break + if ch in "<([": + depth += 1 + elif ch in ">)]": + depth -= 1 + j += 1 + ret_text = src[i:j].strip() + if ret_text: + if ret_text.startswith("(") and ret_text.endswith(")"): + inner = ret_text[1:-1] + parts = split_balanced(inner, ",") + parts_c = [ + canon_local_type(p, module_name, aliases, tp_names) for p in parts + ] + ret_canon = parts_c[0] if len(parts_c) == 1 else "(" + ", ".join(parts_c) + ")" + else: + ret_canon = canon_local_type(ret_text, module_name, aliases, tp_names) + fns[fname] = { + "tp_names": tp_names, + "tp_constraints": [ab for (_, ab, _) in tp_list], + "tp_phantom": [ph for (_, _, ph) in tp_list], + "params": params, + "return": ret_canon, + } + return module_name, fns + + +def parse_module_structs(path: Path) -> tuple[str | None, dict[str, Any]]: + content = path.read_text() + mm = re.search(r"^\s*module\s+ccip::(\w+)", content, re.M) + if not mm: + return None, {} + module_name = mm.group(1) + src = strip_comments(content) + aliases = parse_use(content) + structs: dict[str, Any] = {} + for sm in re.finditer(r"public\s+struct\s+(\w+)", src): + sname = sm.group(1) + i = sm.end() + while i < len(src) and src[i] in " \t\n\r": + i += 1 + tp_list = [] + if i < len(src) and src[i] == "<": + tp_close = find_matching(src, i + 1, "<", ">") + tp_list = parse_type_params(src[i + 1 : tp_close - 1]) + i = tp_close + while i < len(src) and src[i] in " \t\n\r": + i += 1 + if src[i : i + 3] == "has": + j = i + 3 + while j < len(src) and src[j] != "{": + j += 1 + abilities = sorted(a.strip() for a in src[i + 3 : j].split(",") if a.strip()) + i = j + else: + abilities = [] + if i >= len(src) or src[i] != "{": + continue + body_end = find_matching(src, i + 1, "{", "}") + body = src[i + 1 : body_end - 1] + tp_names = [n for (n, _, _) in tp_list] + fields = [] + for line in split_balanced(body, ","): + mf = re.match(r"^(\w+)\s*:\s*(.+)$", line, re.DOTALL) + if mf: + fields.append( + ( + mf.group(1), + canon_local_type(mf.group(2).strip(), module_name, aliases, tp_names), + ) + ) + structs[sname] = { + "abilities": abilities, + "tp_names": tp_names, + "tp_constraints": [ab for (_, ab, _) in tp_list], + "tp_phantom": [ph for (_, _, ph) in tp_list], + "fields": fields, + } + return module_name, structs + + +def parse_local() -> tuple[dict[str, Any], dict[str, Any]]: + funs: dict[str, Any] = {} + structs: dict[str, Any] = {} + for pattern in SRC_GLOB: + for p in sorted(glob.glob(str(REPO_ROOT / pattern))): + mname, mod_funs = parse_module_fn_sigs(Path(p)) + if mname: + funs[mname] = mod_funs + mname2, mod_structs = parse_module_structs(Path(p)) + if mname2: + structs[mname2] = mod_structs + return funs, structs + + +def struct_abilities(s: dict[str, Any]) -> list[str]: + ab = s.get("abilities", []) + if isinstance(ab, dict): + ab = ab.get("abilities", []) + return sorted(a.lower() for a in ab) + + +def struct_tparam_info(s: dict[str, Any]) -> list[tuple[bool, list[str]]]: + out = [] + for tp in s.get("typeParameters", []): + c = tp.get("constraints", {}) + if isinstance(c, dict) and "constraints" in c: + ab = c["constraints"].get("abilities", []) + elif isinstance(c, dict): + ab = c.get("abilities", []) + else: + ab = [] + out.append((tp.get("isPhantom", False), sorted(a.lower() for a in ab))) + return out + + +def fn_tparam_info(fn: dict[str, Any]) -> list[tuple[bool, list[str]]]: + out = [] + for tp in fn.get("typeParameters", []): + c = tp.get("constraints", {}) + ab = c.get("abilities", []) if isinstance(c, dict) else [] + out.append((tp.get("isPhantom", False), sorted(a.lower() for a in ab))) + return out + + +def is_marker_struct(fields: list[dict[str, Any]]) -> bool: + return not fields or (len(fields) == 1 and fields[0]["name"] == "dummy_field") + + +def compare_deployed_to_deployed( + left: dict[str, Any], right: dict[str, Any], label: str +) -> CompareReport: + report = CompareReport(label=label) + left_addr = discover_addr_map(left) + right_addr = discover_addr_map(right) + if set(left.keys()) != set(right.keys()): + report.issues.append( + f"module set differs: left={sorted(left.keys())} right={sorted(right.keys())}" + ) + for mod in sorted(set(left.keys()) & set(right.keys())): + lf, rf = left[mod], right[mod] + for fname, fd in lf.get("exposedFunctions", {}).items(): + if fd.get("visibility") != "Public": + continue + rd = rf.get("exposedFunctions", {}).get(fname) + if rd is None: + report.issues.append(f"{mod}::{fname}: missing in right") + continue + if rd.get("visibility") != "Public": + report.issues.append(f"{mod}::{fname}: visibility changed") + tp = [f"T{i}" for i in range(len(fd.get("typeParameters", [])))] + d_params = [canon_deployed(p, left_addr, tp) for p in fd.get("parameters", [])] + r_params = [canon_deployed(p, right_addr, tp) for p in rd.get("parameters", [])] + d_rets = [canon_deployed(r, left_addr, tp) for r in fd.get("return", [])] + r_rets = [canon_deployed(r, right_addr, tp) for r in rd.get("return", [])] + if d_params != r_params or d_rets != r_rets: + report.issues.append(f"{mod}::{fname}: signature differs") + for sname, sd in lf.get("structs", {}).items(): + rd = rf.get("structs", {}).get(sname) + if rd is None: + report.issues.append(f"{mod}::{sname}: struct missing in right") + continue + if struct_abilities(sd) != struct_abilities(rd): + report.issues.append(f"{mod}::{sname}: abilities differ") + if is_marker_struct(sd.get("fields", [])) and is_marker_struct(rd.get("fields", [])): + continue + tp = [f"T{i}" for i in range(len(sd.get("typeParameters", [])))] + df = [(f["name"], canon_deployed(f["type"], left_addr, tp)) for f in sd.get("fields", [])] + rf_ = [(f["name"], canon_deployed(f["type"], right_addr, tp)) for f in rd.get("fields", [])] + if df != rf_: + report.issues.append(f"{mod}::{sname}: fields differ") + report.stats = { + "modules": len(left), + "issues": len(report.issues), + } + return report + + +def compare_deployed_to_local( + deployed: dict[str, Any], local_funs: dict[str, Any], local_structs: dict[str, Any], label: str +) -> CompareReport: + report = CompareReport(label=label) + addr_map = discover_addr_map(deployed) + report.stats["external_addrs"] = { + k: v for k, v in addr_map.items() if v in ("0xSELF", "0xMCMS") + } + + pub_fun_count = 0 + marker_struct_count = 0 + checked_struct_count = 0 + + for mod in sorted(deployed.keys()): + if mod not in local_funs or mod not in local_structs: + report.issues.append(f"MISSING MODULE in local: {mod}") + continue + dm = deployed[mod] + lfns = local_funs[mod] + lsts = local_structs[mod] + + for sname, sd in dm.get("structs", {}).items(): + ls = lsts.get(sname) + if ls is None: + report.issues.append(f"{mod}::{sname}: struct removed/renamed") + continue + if struct_abilities(sd) != ls["abilities"]: + report.issues.append( + f"{mod}::{sname}: abilities {struct_abilities(sd)} vs {ls['abilities']}" + ) + dtps = struct_tparam_info(sd) + if len(dtps) != len(ls["tp_names"]): + report.issues.append(f"{mod}::{sname}: tparam count {len(dtps)} vs {len(ls['tp_names'])}") + else: + for i, ((d_ph, d_ab), l_ab, l_ph) in enumerate( + zip(dtps, ls["tp_constraints"], ls["tp_phantom"]) + ): + if set(d_ab) - set(l_ab): + report.issues.append( + f"{mod}::{sname}: tparam[{i}] constraint tightened deployed={d_ab} local={l_ab}" + ) + if d_ph != l_ph: + report.issues.append(f"{mod}::{sname}: tparam[{i}] phantom flag differs") + + dfields = sd.get("fields", []) + if is_marker_struct(dfields): + marker_struct_count += 1 + if ls["fields"]: + if not (len(ls["fields"]) == 0): + report.issues.append(f"{mod}::{sname}: marker struct mismatch") + continue + + lfields = ls["fields"] + if len(dfields) != len(lfields): + report.issues.append( + f"{mod}::{sname}: field count {len(dfields)} vs {len(lfields)}" + ) + continue + checked_struct_count += 1 + tp_names = ls["tp_names"] + for (df, (ln, lt)) in zip(dfields, lfields): + d_canon = canon_deployed(df["type"], addr_map, tp_names) + if "<<" in lt: + report.unparsed.append(f"{mod}::{sname}.{ln}: {lt}") + continue + if df["name"] != ln: + report.issues.append(f"{mod}::{sname}: field name {df['name']} vs {ln}") + if d_canon != lt: + report.issues.append(f"{mod}::{sname}.{df['name']}: type {d_canon} vs {lt}") + + for sname in lsts: + if sname not in dm.get("structs", {}): + report.additions_structs.append(f"{mod}::{sname}") + + for fname, fd in dm.get("exposedFunctions", {}).items(): + if fd.get("visibility") != "Public": + continue + pub_fun_count += 1 + lf = lfns.get(fname) + if lf is None: + report.issues.append(f"{mod}::{fname}: function removed/renamed") + continue + tp_names = lf["tp_names"] + dtps = fn_tparam_info(fd) + if len(dtps) != len(lf["tp_names"]): + report.issues.append(f"{mod}::{fname}: tparam count") + else: + for i, ((d_ph, d_ab), l_ab, l_ph) in enumerate( + zip(dtps, lf["tp_constraints"], lf["tp_phantom"]) + ): + if set(d_ab) - set(l_ab): + report.issues.append( + f"{mod}::{fname}: tparam[{i}] constraint tightened deployed={d_ab} local={l_ab}" + ) + if d_ph != l_ph: + report.issues.append(f"{mod}::{fname}: tparam[{i}] phantom differs") + + d_params = [canon_deployed(p, addr_map, tp_names) for p in fd.get("parameters", [])] + d_ret_list = [canon_deployed(r, addr_map, tp_names) for r in fd.get("return", [])] + if not d_ret_list: + d_return = "" + elif len(d_ret_list) == 1: + d_return = d_ret_list[0] + else: + d_return = "(" + ", ".join(d_ret_list) + ")" + + if len(d_params) != len(lf["params"]): + report.issues.append( + f"{mod}::{fname}: param count {len(d_params)} vs {len(lf['params'])}" + ) + continue + for idx, (d_t, (pname, l_t)) in enumerate(zip(d_params, lf["params"])): + if "<<" in l_t: + report.unparsed.append(f"{mod}::{fname} arg[{idx}] ({pname}): {l_t}") + continue + if d_t != l_t: + report.issues.append(f"{mod}::{fname} arg[{idx}] ({pname}): {d_t} vs {l_t}") + if "<<" in lf["return"]: + report.unparsed.append(f"{mod}::{fname} return: {lf['return']}") + elif d_return != lf["return"]: + report.issues.append(f"{mod}::{fname} return: {d_return!r} vs {lf['return']!r}") + + deployed_public = { + fname + for fname, fd in dm.get("exposedFunctions", {}).items() + if fd.get("visibility") == "Public" + } + for fname in lfns: + if fname not in deployed_public: + report.additions_funs.append(f"{mod}::{fname}") + + report.stats.update( + { + "public_functions": pub_fun_count, + "non_marker_structs_checked": checked_struct_count, + "marker_structs": marker_struct_count, + "issues": len(report.issues), + "unparsed": len(report.unparsed), + "additive_funs": len(report.additions_funs), + "additive_structs": len(report.additions_structs), + } + ) + return report + + +def load_json(path: Path) -> dict[str, Any]: + data = json.loads(path.read_text()) + if "result" in data: + return data["result"] + return data + + +def save_json(path: Path, obj: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(obj, indent=2)) + + +def remap_package_addresses(modules: dict[str, Any], replacements: dict[str, str]) -> dict[str, Any]: + text = json.dumps(modules) + for old, new in replacements.items(): + text = text.replace(old, new) + text = text.replace(old.upper(), new) + return json.loads(text) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Compare CCIP upgrade compatibility") + parser.add_argument("--network", choices=["mainnet", "testnet"], default="mainnet") + parser.add_argument("--v1-package", required=True) + parser.add_argument("--latest-package", default="") + parser.add_argument("--upgrade-cap", default="") + parser.add_argument("--v1-json", type=Path, default=None) + parser.add_argument("--latest-json", type=Path, default=None) + parser.add_argument("--out-dir", type=Path, default=REPO_ROOT / ".upgrade-analysis") + parser.add_argument("--fetch", action="store_true") + parser.add_argument("--json-report", type=Path, default=None) + args = parser.parse_args() + + out_dir = args.out_dir + v1_path = args.v1_json or out_dir / f"ccip_{args.network}_v1_modules.json" + latest_path = args.latest_json or out_dir / f"ccip_{args.network}_latest_modules.json" + + upgrade_meta = {} + if args.fetch: + try: + v1_modules = fetch_normalized_modules(args.v1_package, args.network) + save_json(v1_path, {"jsonrpc": "2.0", "id": 1, "result": v1_modules}) + print(f"Fetched v1 modules -> {v1_path} ({len(v1_modules)} modules)") + except RuntimeError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + if args.upgrade_cap: + upgrade_meta = fetch_upgrade_cap(args.v1_package, args.upgrade_cap, args.network) + save_json(out_dir / f"ccip_{args.network}_upgrade_cap.json", upgrade_meta) + print(f"UpgradeCap version={upgrade_meta.get('version')} policy={upgrade_meta.get('policy')}") + + if not v1_path.exists(): + print(f"Missing v1 JSON: {v1_path}", file=sys.stderr) + return 1 + + v1 = load_json(v1_path) + latest = v1 + latest_note = "latest same as v1 (single published version)" + if args.latest_package and args.latest_package != args.v1_package: + if args.fetch: + try: + latest = fetch_normalized_modules(args.latest_package, args.network) + save_json(latest_path, {"jsonrpc": "2.0", "id": 1, "result": latest}) + except RuntimeError as exc: + print(f"WARN: could not fetch latest package: {exc}", file=sys.stderr) + elif latest_path.exists(): + latest = load_json(latest_path) + latest_note = f"loaded {latest_path}" + else: + print("WARN: latest package specified but no JSON available", file=sys.stderr) + + local_funs, local_structs = parse_local() + v1_vs_latest = compare_deployed_to_deployed(v1, latest, "v1_vs_latest") + v1_vs_local = compare_deployed_to_local(v1, local_funs, local_structs, "v1_vs_local") + + report = { + "network": args.network, + "v1_package": args.v1_package, + "latest_package": args.latest_package or args.v1_package, + "upgrade_cap": upgrade_meta or None, + "latest_note": latest_note, + "v1_vs_latest": { + "issues": v1_vs_latest.issues, + "stats": v1_vs_latest.stats, + }, + "v1_vs_local": { + "issues": v1_vs_local.issues, + "unparsed": v1_vs_local.unparsed, + "additions_funs": v1_vs_local.additions_funs, + "additions_structs": v1_vs_local.additions_structs, + "stats": v1_vs_local.stats, + }, + } + + if args.json_report: + save_json(args.json_report, report) + else: + save_json(out_dir / f"ccip_{args.network}_compat_report.json", report) + + print(json.dumps(report, indent=2)) + ok = not v1_vs_latest.issues and not v1_vs_local.issues and not v1_vs_local.unparsed + return 0 if ok else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/go.mod b/scripts/go.mod index ac85f9e12..0101c1d69 100644 --- a/scripts/go.mod +++ b/scripts/go.mod @@ -125,9 +125,9 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.102 // indirect github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc // indirect github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320 // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 // indirect - github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb // indirect + github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260624154507-ea7ff77a0ddb // indirect github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect github.com/smartcontractkit/chainlink-deployments-framework v0.111.1-0.20260612191326-e31c0ae4cd54 // indirect @@ -135,7 +135,7 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.19.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect - github.com/smartcontractkit/chainlink-sui v0.0.0-20260610194843-349ea43d69ce // indirect + github.com/smartcontractkit/chainlink-sui v0.0.0 // indirect github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect diff --git a/scripts/go.sum b/scripts/go.sum index 3dbf739b9..a51b4bd72 100644 --- a/scripts/go.sum +++ b/scripts/go.sum @@ -601,14 +601,14 @@ github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc h github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320 h1:ix4tCtSTB7S2XGll+uqnhrqAQ+2iW/Zk/vnPjBMYRB0= github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320/go.mod h1:WKmNUX4oy8IvB66ukudrE99uaXjlZ7WghCDwHOTyB1c= -github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee h1:YAE9gMuCsjp3toJXBQge7pvSZhsFCv9GakTEjjoiE50= -github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260625091148-e5618f5682ee/go.mod h1:2xVZJ3o9udYFeJhwyHXAMlNhptJ99uoiGjzfOicYNX8= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 h1:jkChf04hhdiMBApbb+lLDxHMY62Md6UeM7v++GSw3K8= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139/go.mod h1:wuhagkM/lU0GbV2YcrROOH0GlsfXJYwm6qmpa4CK70w= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 h1:tw3K4UkH5XfW5SoyYkvAlbzrccoGSLdz/XkxD6nyGC8= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139/go.mod h1:1WcontO9PeuKdUf5HXfs3nuICtzUvFNnyCmrHkTCF9Y= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee h1:ScVlPpWyoswUlu3izqOH7N0oW5UCrp7lPifkOZ2Mgew= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260625091148-e5618f5682ee/go.mod h1:xDXlDsou69NYOolOAj+KITRn9luER6Bg52NXelrLl+A= +github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb h1:uSXBvj/idCBg7hMLmek8YYNa0JKggqlTkHYdtIooeNM= +github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb/go.mod h1:2xVZJ3o9udYFeJhwyHXAMlNhptJ99uoiGjzfOicYNX8= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb h1:TZUVwF0ZexyJKqBjutCGjR/kG3LOa1krTQt/EXFgMYg= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260624154507-ea7ff77a0ddb/go.mod h1:xu0Jum/nGRkjBwT/Vq7WCElWOTBBkFRwG0ZIaw9tF2I= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb h1:2p+8KYL0bhHblGcOJKRH84i9QduKGcY72NYcLJzNdNc= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260624154507-ea7ff77a0ddb/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= +github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260624154507-ea7ff77a0ddb h1:lG7cBn+mRgkdPpp1MSGK8pLq72g8LHa3aVPHZv/UReQ= +github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260624154507-ea7ff77a0ddb/go.mod h1:xDXlDsou69NYOolOAj+KITRn9luER6Bg52NXelrLl+A= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 h1:5z3LQ27MJmhiaeqp9S2TzbF5Wm4GGvUKAYOtE9AauR8= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89/go.mod h1:G2AII0QmWzXx8Ag9IKnGN3h/gwwNnhHUOCviJievdvo= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg=