From 134adea2ed4d733091250b67ed277cebb5e2ebe8 Mon Sep 17 00:00:00 2001 From: Kashif Siddiqui Date: Tue, 24 Mar 2026 16:52:54 +0900 Subject: [PATCH 1/2] Connect EVM with EVM: Add support for skipping FQ v2 config if already configured during migration --- .../v1_6/cs_update_bidirectional_lanes.go | 37 +++++++++ .../cs_update_bidirectional_lanes_test.go | 83 +++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes.go b/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes.go index aec4fcf3f46..38407efaab5 100644 --- a/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes.go +++ b/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes.go @@ -7,6 +7,7 @@ import ( "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" fqv2ops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/fee_quoter" @@ -308,6 +309,10 @@ func UpdateLanesLogic(e cldf.Environment, mcmsConfig *proposalutils.TimelockConf if err != nil { return cldf.ChangesetOutput{}, fmt.Errorf("failed to convert v1.6 fee quoter destination updates for chain %d: %w", chainSel, err) } + destCfgs, err = FilterOutExistingDestChainConfigs(e, dests.Address, chainSel, destCfgs) + if err != nil { + return cldf.ChangesetOutput{}, err + } fqUpdate.DestChainConfigs = destCfgs } if prices, ok := feeQuoterPricesInput.UpdatesByChain[chainSel]; ok { @@ -349,6 +354,38 @@ func UpdateLanesLogic(e cldf.Environment, mcmsConfig *proposalutils.TimelockConf return output, nil } +// FilterOutExistingDestChainConfigs queries the on-chain v2 FeeQuoter and removes +// destination chain configs that are already enabled. This prevents overwriting existing +// configurations during lane updates when a destination was previously configured. +func FilterOutExistingDestChainConfigs( + e cldf.Environment, + fqAddr common.Address, + chainSel uint64, + destCfgs []fqv2ops.DestChainConfigArgs, +) ([]fqv2ops.DestChainConfigArgs, error) { + fqContract, err := fqv2ops.NewFeeQuoterContract(fqAddr, e.BlockChains.EVMChains()[chainSel].Client) + if err != nil { + return nil, fmt.Errorf("failed to bind v2 FeeQuoter on chain %d: %w", chainSel, err) + } + filtered := make([]fqv2ops.DestChainConfigArgs, 0, len(destCfgs)) + for _, destCfg := range destCfgs { + existing, err := fqContract.GetDestChainConfig(&bind.CallOpts{Context: e.GetContext()}, destCfg.DestChainSelector) + if err != nil { + return nil, fmt.Errorf("failed to query existing dest chain config on chain %d for dest %d: %w", + chainSel, destCfg.DestChainSelector, err) + } + if existing.IsEnabled { + e.Logger.Infow("skipping dest chain config already present on v2 FeeQuoter", + "sourceChain", chainSel, + "destChain", destCfg.DestChainSelector, + ) + continue + } + filtered = append(filtered, destCfg) + } + return filtered, nil +} + func ConvertV16FeeQuoterDestUpdatesToV2(in []fee_quoter.FeeQuoterDestChainConfigArgs) ([]fqv2ops.DestChainConfigArgs, error) { out := make([]fqv2ops.DestChainConfigArgs, 0, len(in)) for _, cfg := range in { diff --git a/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes_test.go b/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes_test.go index b1d4548cf46..66ee453aabf 100644 --- a/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes_test.go +++ b/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes_test.go @@ -543,6 +543,89 @@ func TestUpdateBidirectionalLanesChangesetWithV2FeeQuoter(t *testing.T) { } } +func TestFilterOutExistingDestChainConfigs(t *testing.T) { + t.Parallel() + + deployedEnvironment, _ := testhelpers.NewMemoryEnvironment(t, func(testCfg *testhelpers.TestConfigs) { + testCfg.Chains = 2 + }) + e := deployedEnvironment.Env + + state, err := stateview.LoadOnchainState(e) + require.NoError(t, err, "must load onchain state") + + selectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chain_selectors.FamilyEVM)) + require.Len(t, selectors, 2, "must have 2 chains") + + chainSel := selectors[0] + otherChainSel := selectors[1] + evmChain := e.BlockChains.EVMChains()[chainSel] + + parsedABI, err := abi.JSON(strings.NewReader(fqv2ops.FeeQuoterABI)) + require.NoError(t, err, "must parse v2 FeeQuoter ABI") + + // Deploy a v2 FeeQuoter, then configure one destination (otherChainSel) after deployment. + // Constructor-time dest config with sparse fields reverts, so we apply it post-deploy. + fqV2Addr, tx, _, err := bind.DeployContract( + evmChain.DeployerKey, + parsedABI, + common.FromHex(fqv2ops.FeeQuoterBin), + evmChain.Client, + fqv2ops.StaticConfig{ + MaxFeeJuelsPerMsg: big.NewInt(1e18), + LinkToken: state.Chains[chainSel].LinkToken.Address(), + }, + []common.Address{evmChain.DeployerKey.From}, + []fqv2ops.TokenTransferFeeConfigArgs{}, + []fqv2ops.DestChainConfigArgs{}, + ) + require.NoError(t, err, "must deploy v2 FeeQuoter") + + _, err = evmChain.Confirm(tx) + require.NoError(t, err, "must confirm v2 FeeQuoter deployment") + + fqV2, err := fqv2ops.NewFeeQuoterContract(fqV2Addr, evmChain.Client) + require.NoError(t, err, "must bind v2 FeeQuoter") + + // Convert a default v1.6 config to v2 format and apply it so otherChainSel is already enabled + v2Cfgs, err := v1_6.ConvertV16FeeQuoterDestUpdatesToV2([]fee_quoter.FeeQuoterDestChainConfigArgs{ + { + DestChainSelector: otherChainSel, + DestChainConfig: v1_6.DefaultFeeQuoterDestChainConfig(true), + }, + }) + require.NoError(t, err, "must convert v1.6 dest config to v2") + + applyTx, err := fqV2.ApplyDestChainConfigUpdates(evmChain.DeployerKey, v2Cfgs) + require.NoError(t, err, "must apply dest chain config") + _, err = evmChain.Confirm(applyTx) + require.NoError(t, err, "must confirm dest chain config tx") + + unconfiguredChainSel := uint64(999) + + // Call FilterOutExistingDestChainConfigs with both destinations + input := []fqv2ops.DestChainConfigArgs{ + { + DestChainSelector: otherChainSel, + DestChainConfig: fqv2ops.DestChainConfig{IsEnabled: true, MaxDataBytes: 50_000}, + }, + { + DestChainSelector: unconfiguredChainSel, + DestChainConfig: fqv2ops.DestChainConfig{IsEnabled: true, MaxDataBytes: 60_000}, + }, + } + + filtered, err := v1_6.FilterOutExistingDestChainConfigs(e, fqV2Addr, chainSel, input) + require.NoError(t, err, "FilterOutExistingDestChainConfigs must not error") + + // Only the unconfigured chain should remain + require.Len(t, filtered, 1, "must filter out the already-enabled destination") + assert.Equal(t, unconfiguredChainSel, filtered[0].DestChainSelector, + "remaining entry must be the unconfigured chain") + assert.Equal(t, uint32(60_000), filtered[0].DestChainConfig.MaxDataBytes, + "remaining entry must preserve original config") +} + func TestUpdateBidirectionalLanesChangesetWithV2FeeQuoterWithMCMS(t *testing.T) { t.Parallel() From a210dbbc069d06fc5c711978846587c46fc0c9dd Mon Sep 17 00:00:00 2001 From: simsonraj Date: Tue, 31 Mar 2026 14:14:31 +0530 Subject: [PATCH 2/2] Exclude updating 1.6 FQ if the destCfg is already present --- .../v1_6/cs_update_bidirectional_lanes.go | 76 +++++++++++++++---- .../cs_update_bidirectional_lanes_test.go | 31 ++++++++ 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes.go b/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes.go index 38407efaab5..ed95470cabf 100644 --- a/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes.go +++ b/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes.go @@ -252,7 +252,14 @@ func UpdateLanesLogic(e cldf.Environment, mcmsConfig *proposalutils.TimelockConf v2FeeQuoterChains[chainSel] = struct{}{} continue } - v1FeeQuoterDestsUpdates[chainSel] = update + filtered, err := FilterOutExistingDestChainConfigs(e, update.Address, chainSel, update.CallInput) + if err != nil { + return cldf.ChangesetOutput{}, err + } + if len(filtered) > 0 { + update.CallInput = filtered + v1FeeQuoterDestsUpdates[chainSel] = update + } } for chainSel, update := range feeQuoterPricesInput.UpdatesByChain { version, ok := feeQuoterVersionsByChain[chainSel] @@ -354,35 +361,72 @@ func UpdateLanesLogic(e cldf.Environment, mcmsConfig *proposalutils.TimelockConf return output, nil } -// FilterOutExistingDestChainConfigs queries the on-chain v2 FeeQuoter and removes -// destination chain configs that are already enabled. This prevents overwriting existing -// configurations during lane updates when a destination was previously configured. -func FilterOutExistingDestChainConfigs( +// destChainConfigType constrains the types accepted by FilterOutExistingDestChainConfigs. +type destChainConfigType interface { + fee_quoter.FeeQuoterDestChainConfigArgs | fqv2ops.DestChainConfigArgs +} + +// FilterOutExistingDestChainConfigs removes destination chain configs where the destination +// is already enabled on-chain. It automatically selects the correct FeeQuoter binding +// based on the concrete config type. +func FilterOutExistingDestChainConfigs[T destChainConfigType]( e cldf.Environment, fqAddr common.Address, chainSel uint64, - destCfgs []fqv2ops.DestChainConfigArgs, -) ([]fqv2ops.DestChainConfigArgs, error) { - fqContract, err := fqv2ops.NewFeeQuoterContract(fqAddr, e.BlockChains.EVMChains()[chainSel].Client) - if err != nil { - return nil, fmt.Errorf("failed to bind v2 FeeQuoter on chain %d: %w", chainSel, err) + destCfgs []T, +) ([]T, error) { + if len(destCfgs) == 0 { + return destCfgs, nil } - filtered := make([]fqv2ops.DestChainConfigArgs, 0, len(destCfgs)) + + var isDestEnabled func(T) (uint64, bool, error) + + switch any(destCfgs[0]).(type) { + case fee_quoter.FeeQuoterDestChainConfigArgs: + fq, err := fee_quoter.NewFeeQuoter(fqAddr, e.BlockChains.EVMChains()[chainSel].Client) + if err != nil { + return nil, fmt.Errorf("failed to bind FeeQuoter on chain %d: %w", chainSel, err) + } + isDestEnabled = func(cfg T) (uint64, bool, error) { + destSel := any(cfg).(fee_quoter.FeeQuoterDestChainConfigArgs).DestChainSelector + onChain, err := fq.GetDestChainConfig(&bind.CallOpts{Context: e.GetContext()}, destSel) + if err != nil { + return destSel, false, err + } + return destSel, onChain.IsEnabled, nil + } + case fqv2ops.DestChainConfigArgs: + fq, err := fqv2ops.NewFeeQuoterContract(fqAddr, e.BlockChains.EVMChains()[chainSel].Client) + if err != nil { + return nil, fmt.Errorf("failed to bind v2 FeeQuoter on chain %d: %w", chainSel, err) + } + isDestEnabled = func(cfg T) (uint64, bool, error) { + destSel := any(cfg).(fqv2ops.DestChainConfigArgs).DestChainSelector + onChain, err := fq.GetDestChainConfig(&bind.CallOpts{Context: e.GetContext()}, destSel) + if err != nil { + return destSel, false, err + } + return destSel, onChain.IsEnabled, nil + } + } + + filtered := make([]T, 0, len(destCfgs)) for _, destCfg := range destCfgs { - existing, err := fqContract.GetDestChainConfig(&bind.CallOpts{Context: e.GetContext()}, destCfg.DestChainSelector) + destSel, enabled, err := isDestEnabled(destCfg) if err != nil { return nil, fmt.Errorf("failed to query existing dest chain config on chain %d for dest %d: %w", - chainSel, destCfg.DestChainSelector, err) + chainSel, destSel, err) } - if existing.IsEnabled { - e.Logger.Infow("skipping dest chain config already present on v2 FeeQuoter", + if enabled { + e.Logger.Infow("skipping dest chain config already present on FeeQuoter", "sourceChain", chainSel, - "destChain", destCfg.DestChainSelector, + "destChain", destSel, ) continue } filtered = append(filtered, destCfg) } + return filtered, nil } diff --git a/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes_test.go b/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes_test.go index 66ee453aabf..f7c4e4c33bb 100644 --- a/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes_test.go +++ b/deployment/ccip/changeset/v1_6/cs_update_bidirectional_lanes_test.go @@ -624,6 +624,37 @@ func TestFilterOutExistingDestChainConfigs(t *testing.T) { "remaining entry must be the unconfigured chain") assert.Equal(t, uint32(60_000), filtered[0].DestChainConfig.MaxDataBytes, "remaining entry must preserve original config") + + // Configure otherChainSel on the v1.6 FeeQuoter so it's already enabled + fqV16 := state.Chains[chainSel].FeeQuoter + applyTxV16, err := fqV16.ApplyDestChainConfigUpdates(evmChain.DeployerKey, []fee_quoter.FeeQuoterDestChainConfigArgs{ + { + DestChainSelector: otherChainSel, + DestChainConfig: v1_6.DefaultFeeQuoterDestChainConfig(true), + }, + }) + require.NoError(t, err, "must apply v1.6 dest chain config") + _, err = evmChain.Confirm(applyTxV16) + require.NoError(t, err, "must confirm v1.6 dest chain config tx") + + // Call FilterOutExistingDestChainConfigs with v1.6 types + v16Input := []fee_quoter.FeeQuoterDestChainConfigArgs{ + { + DestChainSelector: otherChainSel, + DestChainConfig: v1_6.DefaultFeeQuoterDestChainConfig(true), + }, + { + DestChainSelector: unconfiguredChainSel, + DestChainConfig: v1_6.DefaultFeeQuoterDestChainConfig(true), + }, + } + + filteredV16, err := v1_6.FilterOutExistingDestChainConfigs(e, fqV16.Address(), chainSel, v16Input) + require.NoError(t, err, "FilterOutExistingDestChainConfigs must not error") + + require.Len(t, filteredV16, 1, "must filter out the already-enabled destination") + assert.Equal(t, unconfiguredChainSel, filteredV16[0].DestChainSelector, + "remaining entry must be the unconfigured chain") } func TestUpdateBidirectionalLanesChangesetWithV2FeeQuoterWithMCMS(t *testing.T) {