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..ed95470cabf 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" @@ -251,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] @@ -308,6 +316,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 +361,75 @@ func UpdateLanesLogic(e cldf.Environment, mcmsConfig *proposalutils.TimelockConf return output, nil } +// 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 []T, +) ([]T, error) { + if len(destCfgs) == 0 { + return destCfgs, nil + } + + 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 { + 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, destSel, err) + } + if enabled { + e.Logger.Infow("skipping dest chain config already present on FeeQuoter", + "sourceChain", chainSel, + "destChain", destSel, + ) + 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..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 @@ -543,6 +543,120 @@ 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") + + // 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) { t.Parallel()