From 7f442f609f6a7186ce091fde7577fc45de8eef81 Mon Sep 17 00:00:00 2001 From: Jasmin Bakalovic Date: Fri, 15 May 2026 13:08:15 -0700 Subject: [PATCH] feat(ccip/v1_5_1): make Inbound/Outbound optional in RateLimiterPerChain Inbound and Outbound on RateLimiterConfig are now pointer-typed and optional for EVM ChainUpdates. When only one side is provided for an already-supported remote chain, the missing side is read from on-chain and left unchanged. At least one side must be set. Adding support for a not-yet-supported remote chain still requires both. Sol/Aptos/Sui chain updates continue to require both sides. --- .../solana_v0_1_1/cs_token_pool_test.go | 4 +- .../testhelpers/test_token_helpers.go | 4 +- .../v1_5_1/cs_configure_token_pools.go | 187 ++++++++++--- .../v1_5_1/cs_configure_token_pools_test.go | 248 +++++++++++++++++- .../cs_hybrid_token_pool_groups_test.go | 8 +- deployment/environment/crib/ccip_deployer.go | 4 +- 6 files changed, 411 insertions(+), 44 deletions(-) diff --git a/deployment/ccip/changeset/solana_v0_1_1/cs_token_pool_test.go b/deployment/ccip/changeset/solana_v0_1_1/cs_token_pool_test.go index 9f0fa465a1e..1e694a72516 100644 --- a/deployment/ccip/changeset/solana_v0_1_1/cs_token_pool_test.go +++ b/deployment/ccip/changeset/solana_v0_1_1/cs_token_pool_test.go @@ -724,12 +724,12 @@ func TestAddTokenPoolE2EWithMcms(t *testing.T) { SolChainUpdates: map[uint64]v1_5_1.SolChainUpdate{ solChain: { RateLimiterConfig: v1_5_1.RateLimiterConfig{ - Inbound: token_pool.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{ IsEnabled: false, Capacity: big.NewInt(0), Rate: big.NewInt(0), }, - Outbound: token_pool.RateLimiterConfig{ + Outbound: &token_pool.RateLimiterConfig{ IsEnabled: false, Capacity: big.NewInt(0), Rate: big.NewInt(0), diff --git a/deployment/ccip/changeset/testhelpers/test_token_helpers.go b/deployment/ccip/changeset/testhelpers/test_token_helpers.go index 0cc6a490b0f..7f465557193 100644 --- a/deployment/ccip/changeset/testhelpers/test_token_helpers.go +++ b/deployment/ccip/changeset/testhelpers/test_token_helpers.go @@ -44,12 +44,12 @@ const ( // CreateSymmetricRateLimits is a utility to quickly create a rate limiter config with equal inbound and outbound values. func CreateSymmetricRateLimits(rate int64, capacity int64) v1_5_1.RateLimiterConfig { return v1_5_1.RateLimiterConfig{ - Inbound: token_pool.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{ IsEnabled: rate != 0 || capacity != 0, Rate: big.NewInt(rate), Capacity: big.NewInt(capacity), }, - Outbound: token_pool.RateLimiterConfig{ + Outbound: &token_pool.RateLimiterConfig{ IsEnabled: rate != 0 || capacity != 0, Rate: big.NewInt(rate), Capacity: big.NewInt(capacity), diff --git a/deployment/ccip/changeset/v1_5_1/cs_configure_token_pools.go b/deployment/ccip/changeset/v1_5_1/cs_configure_token_pools.go index 126334a34a1..8283d637e7e 100644 --- a/deployment/ccip/changeset/v1_5_1/cs_configure_token_pools.go +++ b/deployment/ccip/changeset/v1_5_1/cs_configure_token_pools.go @@ -53,12 +53,24 @@ var ( ) // RateLimiterConfig defines the inbound and outbound rate limits for a remote chain. +// +// Inbound and Outbound are pointer-typed and optional at the input layer: +// - For EVM ChainUpdates (RateLimiterPerChain): at least one of Inbound or Outbound must be +// provided. When the remote chain is already supported by the pool, the missing side is +// read from on-chain at apply time and left unchanged. When the remote chain is not yet +// supported, both sides MUST be provided (we have no on-chain value to fall back to). +// - For Solana, Aptos, and Sui chain updates the partial-update semantics do NOT apply; +// both Inbound and Outbound are required. type RateLimiterConfig struct { // Inbound is the rate limiter config for inbound transfers from a remote chain. - Inbound token_pool.RateLimiterConfig `json:"inbound"` + // When nil for an EVM ChainUpdates entry against an already-supported remote chain, the + // existing on-chain inbound config is preserved. + Inbound *token_pool.RateLimiterConfig `json:"inbound,omitempty"` // Outbound is the rate limiter config for outbound transfers to a remote chain. - Outbound token_pool.RateLimiterConfig `json:"outbound"` + // When nil for an EVM ChainUpdates entry against an already-supported remote chain, the + // existing on-chain outbound config is preserved. + Outbound *token_pool.RateLimiterConfig `json:"outbound,omitempty"` } // validateRateLimterConfig validates rate and capacity in accordance with on-chain code. @@ -77,18 +89,101 @@ func validateRateLimiterConfig(rateLimiterConfig token_pool.RateLimiterConfig) e return nil } +// validateBidirectional ensures both Inbound and Outbound are set and individually valid. +// Used by chain families (Solana, Aptos, Sui) that require both sides to be supplied. +func (c RateLimiterConfig) validateBidirectional() error { + if c.Inbound == nil { + return errors.New("inbound rate limiter config must be set") + } + + if c.Outbound == nil { + return errors.New("outbound rate limiter config must be set") + } + + if err := validateRateLimiterConfig(*c.Inbound); err != nil { + return fmt.Errorf("validation of inbound rate limiter config failed: %w", err) + } + + if err := validateRateLimiterConfig(*c.Outbound); err != nil { + return fmt.Errorf("validation of outbound rate limiter config failed: %w", err) + } + + return nil +} + +// resolvePartialRateLimiterConfig returns the inbound and outbound rate limiter configs to +// pass to SetChainRateLimiterConfigs for an already-supported remote chain. When either side +// of the supplied chainUpdate is nil, the corresponding side is fetched from on-chain so that +// it remains unchanged after the call. At least one of Inbound/Outbound MUST be non-nil; this +// is enforced by RateLimiterPerChain.Validate prior to reaching this point, but we re-check +// defensively to surface a clear error if invoked elsewhere. +func resolvePartialRateLimiterConfig( + ctx context.Context, + tokenPool *token_pool.TokenPool, + remoteChainSelector uint64, + chainUpdate RateLimiterConfig, +) (token_pool.RateLimiterConfig, token_pool.RateLimiterConfig, error) { + if chainUpdate.Inbound == nil && chainUpdate.Outbound == nil { + return token_pool.RateLimiterConfig{}, token_pool.RateLimiterConfig{}, errors.New("at least one of inbound or outbound rate limiter config must be set") + } + + var inboundCfg token_pool.RateLimiterConfig + if chainUpdate.Inbound != nil { + inboundCfg = *chainUpdate.Inbound + } else { + state, err := tokenPool.GetCurrentInboundRateLimiterState(&bind.CallOpts{Context: ctx}, remoteChainSelector) + if err != nil { + return token_pool.RateLimiterConfig{}, token_pool.RateLimiterConfig{}, fmt.Errorf("failed to fetch on-chain inbound rate limiter state: %w", err) + } + + inboundCfg = token_pool.RateLimiterConfig{ + IsEnabled: state.IsEnabled, + Capacity: state.Capacity, + Rate: state.Rate, + } + } + + var outboundCfg token_pool.RateLimiterConfig + if chainUpdate.Outbound != nil { + outboundCfg = *chainUpdate.Outbound + } else { + state, err := tokenPool.GetCurrentOutboundRateLimiterState(&bind.CallOpts{Context: ctx}, remoteChainSelector) + if err != nil { + return token_pool.RateLimiterConfig{}, token_pool.RateLimiterConfig{}, fmt.Errorf("failed to fetch on-chain outbound rate limiter state: %w", err) + } + + outboundCfg = token_pool.RateLimiterConfig{ + IsEnabled: state.IsEnabled, + Capacity: state.Capacity, + Rate: state.Rate, + } + } + + return inboundCfg, outboundCfg, nil +} + // RateLimiterPerChain defines rate limits for remote chains. type RateLimiterPerChain map[uint64]RateLimiterConfig func (c RateLimiterPerChain) Validate() error { for chainSelector, chainConfig := range c { - if err := validateRateLimiterConfig(chainConfig.Inbound); err != nil { - return fmt.Errorf("validation of inbound rate limiter config for remote chain with selector %d failed: %w", chainSelector, err) + if chainConfig.Inbound == nil && chainConfig.Outbound == nil { + return fmt.Errorf("rate limiter config for remote chain with selector %d must define at least one of inbound or outbound", chainSelector) } - if err := validateRateLimiterConfig(chainConfig.Outbound); err != nil { - return fmt.Errorf("validation of outbound rate limiter config for remote chain with selector %d failed: %w", chainSelector, err) + + if chainConfig.Inbound != nil { + if err := validateRateLimiterConfig(*chainConfig.Inbound); err != nil { + return fmt.Errorf("validation of inbound rate limiter config for remote chain with selector %d failed: %w", chainSelector, err) + } + } + + if chainConfig.Outbound != nil { + if err := validateRateLimiterConfig(*chainConfig.Outbound); err != nil { + return fmt.Errorf("validation of outbound rate limiter config for remote chain with selector %d failed: %w", chainSelector, err) + } } } + return nil } @@ -147,12 +242,10 @@ func (c SolChainUpdate) GetSolanaTokenAndTokenPool(state solanastateview.CCIPCha } func (c SolChainUpdate) Validate(state solanastateview.CCIPChainState) error { - if err := validateRateLimiterConfig(c.RateLimiterConfig.Inbound); err != nil { - return fmt.Errorf("validation of inbound rate limiter config for solana chain failed: %w", err) - } - if err := validateRateLimiterConfig(c.RateLimiterConfig.Outbound); err != nil { - return fmt.Errorf("validation of outbound rate limiter config for solana chain failed: %w", err) + if err := c.RateLimiterConfig.validateBidirectional(); err != nil { + return fmt.Errorf("rate limiter config for solana chain is invalid: %w", err) } + _, _, err := c.GetSolanaTokenAndTokenPool(state) if err != nil { return fmt.Errorf("failed to get solana token and token pool: %w", err) @@ -494,6 +587,15 @@ func configureTokenPool( remotePoolAddressAdditions := make(map[uint64][]byte) for remoteChainSelector, chainUpdate := range poolUpdate.SolChainUpdates { + // Solana does not support partial rate limiter updates; both Inbound and Outbound + // are required. + if chainUpdate.RateLimiterConfig.Inbound == nil || chainUpdate.RateLimiterConfig.Outbound == nil { + return fmt.Errorf("both inbound and outbound rate limiter configs must be set for solana remote chain with selector %d", remoteChainSelector) + } + + inboundCfg := *chainUpdate.RateLimiterConfig.Inbound + outboundCfg := *chainUpdate.RateLimiterConfig.Outbound + remoteTokenAddress, remotePoolAddress, err := chainUpdate.GetSolanaTokenAndTokenPool(state.SolChains[remoteChainSelector]) if err != nil { return fmt.Errorf("failed to get solana token and token pool for chain with selector %d: %w", remoteChainSelector, err) @@ -534,8 +636,8 @@ func configureTokenPool( remotePoolAddressAdditions[remoteChainSelector] = common.LeftPadBytes(remotePoolAddress.Bytes(), 32) // Also update rate limits remoteChainSelectorsToUpdate = append(remoteChainSelectorsToUpdate, remoteChainSelector) - updatedOutboundConfigs = append(updatedOutboundConfigs, chainUpdate.RateLimiterConfig.Outbound) - updatedInboundConfigs = append(updatedInboundConfigs, chainUpdate.RateLimiterConfig.Inbound) + updatedOutboundConfigs = append(updatedOutboundConfigs, outboundCfg) + updatedInboundConfigs = append(updatedInboundConfigs, inboundCfg) } else { // Default behavior: Remove & later re-add the chain (spot replacement) chainRemovals = append(chainRemovals, remoteChainSelector) @@ -544,16 +646,16 @@ func configureTokenPool( default: // Remote pool is already supported, just update the rate limits remoteChainSelectorsToUpdate = append(remoteChainSelectorsToUpdate, remoteChainSelector) - updatedOutboundConfigs = append(updatedOutboundConfigs, chainUpdate.RateLimiterConfig.Outbound) - updatedInboundConfigs = append(updatedInboundConfigs, chainUpdate.RateLimiterConfig.Inbound) + updatedOutboundConfigs = append(updatedOutboundConfigs, outboundCfg) + updatedInboundConfigs = append(updatedInboundConfigs, inboundCfg) } } if addChain { chainAdditions = append(chainAdditions, token_pool.TokenPoolChainUpdate{ RemoteChainSelector: remoteChainSelector, - InboundRateLimiterConfig: chainUpdate.RateLimiterConfig.Inbound, - OutboundRateLimiterConfig: chainUpdate.RateLimiterConfig.Outbound, + InboundRateLimiterConfig: inboundCfg, + OutboundRateLimiterConfig: outboundCfg, RemoteTokenAddress: common.LeftPadBytes(remoteTokenAddress.Bytes(), 32), RemotePoolAddresses: [][]byte{common.LeftPadBytes(remotePoolAddress.Bytes(), 32)}, }) @@ -561,6 +663,13 @@ func configureTokenPool( } for remoteChainSelector, chainUpdate := range poolUpdate.AptosChainUpdates { + // Aptos does not support partial rate limiter updates; both Inbound and Outbound are required. + if chainUpdate.RateLimiterConfig.Inbound == nil || chainUpdate.RateLimiterConfig.Outbound == nil { + return fmt.Errorf("both inbound and outbound rate limiter configs must be set for aptos remote chain with selector %d", remoteChainSelector) + } + inboundCfg := *chainUpdate.RateLimiterConfig.Inbound + outboundCfg := *chainUpdate.RateLimiterConfig.Outbound + remoteTokenAddress, remotePoolAddress, err := chainUpdate.GetAptosTokenAndTokenPool(state.AptosChains[remoteChainSelector]) if err != nil { return fmt.Errorf("failed to get Aptos token and token pool for chain with selector %d: %w", remoteChainSelector, err) @@ -572,8 +681,8 @@ func configureTokenPool( if isSupportedChain { // Just update the rate limits if the chain is already supported remoteChainSelectorsToUpdate = append(remoteChainSelectorsToUpdate, remoteChainSelector) - updatedOutboundConfigs = append(updatedOutboundConfigs, chainUpdate.RateLimiterConfig.Outbound) - updatedInboundConfigs = append(updatedInboundConfigs, chainUpdate.RateLimiterConfig.Inbound) + updatedOutboundConfigs = append(updatedOutboundConfigs, outboundCfg) + updatedInboundConfigs = append(updatedInboundConfigs, inboundCfg) // Also, add a new remote pool if the token pool on the remote chain is being updated configuredRemotePools, err := tokenPool.GetRemotePools(&bind.CallOpts{Context: ctx}, remoteChainSelector) @@ -594,8 +703,8 @@ func configureTokenPool( } else { chainAdditions = append(chainAdditions, token_pool.TokenPoolChainUpdate{ RemoteChainSelector: remoteChainSelector, - InboundRateLimiterConfig: chainUpdate.RateLimiterConfig.Inbound, - OutboundRateLimiterConfig: chainUpdate.RateLimiterConfig.Outbound, + InboundRateLimiterConfig: inboundCfg, + OutboundRateLimiterConfig: outboundCfg, RemoteTokenAddress: common.LeftPadBytes(remoteTokenAddress[:], 32), RemotePoolAddresses: [][]byte{common.LeftPadBytes(remotePoolAddress[:], 32)}, }) @@ -603,6 +712,13 @@ func configureTokenPool( } for remoteChainSelector, chainUpdate := range poolUpdate.SuiChainUpdates { + // Sui does not support partial rate limiter updates; both Inbound and Outbound are required. + if chainUpdate.RateLimiterConfig.Inbound == nil || chainUpdate.RateLimiterConfig.Outbound == nil { + return fmt.Errorf("both inbound and outbound rate limiter configs must be set for sui remote chain with selector %d", remoteChainSelector) + } + inboundCfg := *chainUpdate.RateLimiterConfig.Inbound + outboundCfg := *chainUpdate.RateLimiterConfig.Outbound + remoteTokenAddressStr, remotePoolAddressStr, err := chainUpdate.GetSuiTokenAndTokenPool(state.SuiChains[remoteChainSelector]) if err != nil { return fmt.Errorf("failed to get Sui token and token pool for chain with selector %d: %w", remoteChainSelector, err) @@ -625,8 +741,8 @@ func configureTokenPool( if isSupportedChain { // Just update the rate limits if the chain is already supported remoteChainSelectorsToUpdate = append(remoteChainSelectorsToUpdate, remoteChainSelector) - updatedOutboundConfigs = append(updatedOutboundConfigs, chainUpdate.RateLimiterConfig.Outbound) - updatedInboundConfigs = append(updatedInboundConfigs, chainUpdate.RateLimiterConfig.Inbound) + updatedOutboundConfigs = append(updatedOutboundConfigs, outboundCfg) + updatedInboundConfigs = append(updatedInboundConfigs, inboundCfg) // Also, add a new remote pool if the token pool on the remote chain is being updated configuredRemotePools, err := tokenPool.GetRemotePools(&bind.CallOpts{Context: ctx}, remoteChainSelector) @@ -649,8 +765,8 @@ func configureTokenPool( } else { chainAdditions = append(chainAdditions, token_pool.TokenPoolChainUpdate{ RemoteChainSelector: remoteChainSelector, - InboundRateLimiterConfig: chainUpdate.RateLimiterConfig.Inbound, - OutboundRateLimiterConfig: chainUpdate.RateLimiterConfig.Outbound, + InboundRateLimiterConfig: inboundCfg, + OutboundRateLimiterConfig: outboundCfg, RemoteTokenAddress: common.LeftPadBytes(remoteTokenAddress, 32), RemotePoolAddresses: [][]byte{common.LeftPadBytes(remotePoolAddress, 32)}, }) @@ -704,15 +820,26 @@ func configureTokenPool( } if isSupportedChain { - // Just update the rate limits if the chain is already supported + // Just update the rate limits if the chain is already supported. + // If only one of Inbound/Outbound is provided, fetch the other from on-chain so + // that the unspecified side is left unchanged. + inboundCfg, outboundCfg, err := resolvePartialRateLimiterConfig(ctx, tokenPool, remoteChainSelector, chainUpdate) + if err != nil { + return fmt.Errorf("failed to resolve rate limiter config for chain with selector %d on pool with address %s on %s: %w", remoteChainSelector, tokenPool.Address(), chain.String(), err) + } + remoteChainSelectorsToUpdate = append(remoteChainSelectorsToUpdate, remoteChainSelector) - updatedOutboundConfigs = append(updatedOutboundConfigs, chainUpdate.Outbound) - updatedInboundConfigs = append(updatedInboundConfigs, chainUpdate.Inbound) + updatedOutboundConfigs = append(updatedOutboundConfigs, outboundCfg) + updatedInboundConfigs = append(updatedInboundConfigs, inboundCfg) // Also, add a new remote pool if the token pool on the remote chain is being updated if remoteTokenConfig.TokenPool != utils.ZeroAddress && remoteTokenConfig.TokenPool != remoteTokenPool.Address() { remotePoolAddressAdditions[remoteChainSelector] = common.LeftPadBytes(remoteTokenPool.Address().Bytes(), 32) } } else { + // Adding chain support requires both sides; we have no on-chain value to fall back to. + if chainUpdate.Inbound == nil || chainUpdate.Outbound == nil { + return fmt.Errorf("both inbound and outbound rate limiter configs must be set when adding new chain support for remote chain with selector %d on pool with address %s on %s", remoteChainSelector, tokenPool.Address(), chain.String()) + } // Add chain support if it doesn't yet exist // First, we need to assemble a list of valid remote pools // The desired token pool on the remote chain is added by default @@ -737,8 +864,8 @@ func configureTokenPool( } chainAdditions = append(chainAdditions, token_pool.TokenPoolChainUpdate{ RemoteChainSelector: remoteChainSelector, - InboundRateLimiterConfig: chainUpdate.Inbound, - OutboundRateLimiterConfig: chainUpdate.Outbound, + InboundRateLimiterConfig: *chainUpdate.Inbound, + OutboundRateLimiterConfig: *chainUpdate.Outbound, RemoteTokenAddress: common.LeftPadBytes(remoteTokenAddress.Bytes(), 32), RemotePoolAddresses: remotePoolAddresses, }) diff --git a/deployment/ccip/changeset/v1_5_1/cs_configure_token_pools_test.go b/deployment/ccip/changeset/v1_5_1/cs_configure_token_pools_test.go index ae571b71c4a..a0fa9142703 100644 --- a/deployment/ccip/changeset/v1_5_1/cs_configure_token_pools_test.go +++ b/deployment/ccip/changeset/v1_5_1/cs_configure_token_pools_test.go @@ -37,12 +37,12 @@ import ( // createSymmetricRateLimits is a utility to quickly create a rate limiter config with equal inbound and outbound values. func createSymmetricRateLimits(rate int64, capacity int64) v1_5_1.RateLimiterConfig { return v1_5_1.RateLimiterConfig{ - Inbound: token_pool.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{ IsEnabled: rate != 0 || capacity != 0, Rate: big.NewInt(rate), Capacity: big.NewInt(capacity), }, - Outbound: token_pool.RateLimiterConfig{ + Outbound: &token_pool.RateLimiterConfig{ IsEnabled: rate != 0 || capacity != 0, Rate: big.NewInt(rate), Capacity: big.NewInt(capacity), @@ -172,12 +172,12 @@ func TestValidateRemoteChains(t *testing.T) { t.Run(test.ErrStr, func(t *testing.T) { remoteChains := v1_5_1.RateLimiterPerChain{ 1: { - Inbound: token_pool.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{ IsEnabled: test.IsEnabled, Rate: test.Rate, Capacity: test.Capacity, }, - Outbound: token_pool.RateLimiterConfig{ + Outbound: &token_pool.RateLimiterConfig{ IsEnabled: test.IsEnabled, Rate: test.Rate, Capacity: test.Capacity, @@ -1039,3 +1039,243 @@ func TestValidateConfigureTokenPoolContractsForSolana(t *testing.T) { } } } + +func TestRateLimiterPerChainPartialValidate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config v1_5_1.RateLimiterConfig + errStr string + }{ + { + name: "rejects when neither inbound nor outbound is set", + config: v1_5_1.RateLimiterConfig{}, + errStr: "must define at least one of inbound or outbound", + }, + { + name: "accepts inbound-only", + config: v1_5_1.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{IsEnabled: false, Rate: big.NewInt(0), Capacity: big.NewInt(0)}, + }, + }, + { + name: "accepts outbound-only", + config: v1_5_1.RateLimiterConfig{ + Outbound: &token_pool.RateLimiterConfig{IsEnabled: true, Rate: big.NewInt(1), Capacity: big.NewInt(10)}, + }, + }, + { + name: "validates the side that is set even when the other is nil", + config: v1_5_1.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{IsEnabled: true, Rate: big.NewInt(10), Capacity: big.NewInt(10)}, + }, + errStr: "rate must be greater than 0 and less than capacity", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := v1_5_1.RateLimiterPerChain{1: tc.config}.Validate() + if tc.errStr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.Contains(t, err.Error(), tc.errStr) + }) + } +} + +func TestPartialRateLimiterUpdate(t *testing.T) { + t.Parallel() + + mcmsConfig := &proposalutils.TimelockConfig{MinDelay: 0 * time.Second} + + e, selectorA, selectorB, tokens := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.Test(t), true) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]v1_5_1.DeployTokenPoolInput{ + selectorA: { + Type: shared.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + selectorB: { + Type: shared.BurnMintTokenPool, + TokenAddress: tokens[selectorB].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, true) + + acceptLiquidity := false + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]v1_5_1.DeployTokenPoolInput{ + selectorA: { + Type: shared.LockReleaseTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + AcceptLiquidity: &acceptLiquidity, + }, + selectorB: { + Type: shared.LockReleaseTokenPool, + TokenAddress: tokens[selectorB].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + AcceptLiquidity: &acceptLiquidity, + }, + }, true) + + state, err := stateview.LoadOnchainState(e) + require.NoError(t, err) + + poolA, err := token_pool.NewTokenPool(state.Chains[selectorA].LockReleaseTokenPools[testhelpers.TestTokenSymbol][deployment.Version1_5_1].Address(), e.BlockChains.EVMChains()[selectorA].Client) + require.NoError(t, err) + poolB, err := token_pool.NewTokenPool(state.Chains[selectorB].LockReleaseTokenPools[testhelpers.TestTokenSymbol][deployment.Version1_5_1].Address(), e.BlockChains.EVMChains()[selectorB].Client) + require.NoError(t, err) + + fullCfg := func(rate, capacity int64) v1_5_1.RateLimiterConfig { + return v1_5_1.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{IsEnabled: true, Rate: big.NewInt(rate), Capacity: big.NewInt(capacity)}, + Outbound: &token_pool.RateLimiterConfig{IsEnabled: true, Rate: big.NewInt(rate), Capacity: big.NewInt(capacity)}, + } + } + + e, err = commonchangeset.Apply(t, e, + commonchangeset.Configure( + cldf.CreateLegacyChangeSet(v1_5_1.ConfigureTokenPoolContractsChangeset), + v1_5_1.ConfigureTokenPoolContractsConfig{ + TokenSymbol: testhelpers.TestTokenSymbol, + MCMS: mcmsConfig, + PoolUpdates: map[uint64]v1_5_1.TokenPoolConfig{ + selectorA: { + Type: shared.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + ChainUpdates: v1_5_1.RateLimiterPerChain{selectorB: fullCfg(100, 1000)}, + }, + selectorB: { + Type: shared.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + ChainUpdates: v1_5_1.RateLimiterPerChain{selectorA: fullCfg(100, 1000)}, + }, + }, + }, + ), + ) + require.NoError(t, err) + + _, err = commonchangeset.Apply(t, e, + commonchangeset.Configure( + cldf.CreateLegacyChangeSet(v1_5_1.ConfigureTokenPoolContractsChangeset), + v1_5_1.ConfigureTokenPoolContractsConfig{ + TokenSymbol: testhelpers.TestTokenSymbol, + MCMS: mcmsConfig, + PoolUpdates: map[uint64]v1_5_1.TokenPoolConfig{ + selectorA: { + Type: shared.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + ChainUpdates: v1_5_1.RateLimiterPerChain{ + selectorB: { + Inbound: &token_pool.RateLimiterConfig{IsEnabled: true, Rate: big.NewInt(200), Capacity: big.NewInt(2000)}, + }, + }, + }, + selectorB: { + Type: shared.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + ChainUpdates: v1_5_1.RateLimiterPerChain{ + selectorA: { + Outbound: &token_pool.RateLimiterConfig{IsEnabled: true, Rate: big.NewInt(300), Capacity: big.NewInt(3000)}, + }, + }, + }, + }, + }, + ), + ) + require.NoError(t, err) + + inboundA, err := poolA.GetCurrentInboundRateLimiterState(nil, selectorB) + require.NoError(t, err) + require.True(t, inboundA.IsEnabled) + require.Equal(t, int64(200), inboundA.Rate.Int64()) + require.Equal(t, int64(2000), inboundA.Capacity.Int64()) + + outboundA, err := poolA.GetCurrentOutboundRateLimiterState(nil, selectorB) + require.NoError(t, err) + require.True(t, outboundA.IsEnabled) + require.Equal(t, int64(100), outboundA.Rate.Int64(), "outbound rate on pool A should be unchanged from pass 1") + require.Equal(t, int64(1000), outboundA.Capacity.Int64(), "outbound capacity on pool A should be unchanged from pass 1") + + outboundB, err := poolB.GetCurrentOutboundRateLimiterState(nil, selectorA) + require.NoError(t, err) + require.True(t, outboundB.IsEnabled) + require.Equal(t, int64(300), outboundB.Rate.Int64()) + require.Equal(t, int64(3000), outboundB.Capacity.Int64()) + + inboundB, err := poolB.GetCurrentInboundRateLimiterState(nil, selectorA) + require.NoError(t, err) + require.True(t, inboundB.IsEnabled) + require.Equal(t, int64(100), inboundB.Rate.Int64(), "inbound rate on pool B should be unchanged from pass 1") + require.Equal(t, int64(1000), inboundB.Capacity.Int64(), "inbound capacity on pool B should be unchanged from pass 1") +} + +func TestPartialRateLimiterUpdateNewChainErrors(t *testing.T) { + t.Parallel() + + e, selectorA, selectorB, tokens := testhelpers.SetupTwoChainEnvironmentWithTokens(t, logger.Test(t), false) + + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]v1_5_1.DeployTokenPoolInput{ + selectorA: { + Type: shared.BurnMintTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + selectorB: { + Type: shared.BurnMintTokenPool, + TokenAddress: tokens[selectorB].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + }, + }, false) + + acceptLiquidity := false + e = testhelpers.DeployTestTokenPools(t, e, map[uint64]v1_5_1.DeployTokenPoolInput{ + selectorA: { + Type: shared.LockReleaseTokenPool, + TokenAddress: tokens[selectorA].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + AcceptLiquidity: &acceptLiquidity, + }, + selectorB: { + Type: shared.LockReleaseTokenPool, + TokenAddress: tokens[selectorB].Address, + LocalTokenDecimals: testhelpers.LocalTokenDecimals, + AcceptLiquidity: &acceptLiquidity, + }, + }, false) + + inboundOnly := v1_5_1.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{IsEnabled: false, Rate: big.NewInt(0), Capacity: big.NewInt(0)}, + } + + _, err := commonchangeset.Apply(t, e, + commonchangeset.Configure( + cldf.CreateLegacyChangeSet(v1_5_1.ConfigureTokenPoolContractsChangeset), + v1_5_1.ConfigureTokenPoolContractsConfig{ + TokenSymbol: testhelpers.TestTokenSymbol, + PoolUpdates: map[uint64]v1_5_1.TokenPoolConfig{ + selectorA: { + Type: shared.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + ChainUpdates: v1_5_1.RateLimiterPerChain{selectorB: inboundOnly}, + }, + selectorB: { + Type: shared.LockReleaseTokenPool, + Version: deployment.Version1_5_1, + ChainUpdates: v1_5_1.RateLimiterPerChain{selectorA: inboundOnly}, + }, + }, + }, + ), + ) + require.Error(t, err) + require.Contains(t, err.Error(), "both inbound and outbound rate limiter configs must be set when adding new chain support") +} diff --git a/deployment/ccip/changeset/v1_5_1/cs_hybrid_token_pool_groups_test.go b/deployment/ccip/changeset/v1_5_1/cs_hybrid_token_pool_groups_test.go index dfb4f579fad..88f8a9ea49d 100644 --- a/deployment/ccip/changeset/v1_5_1/cs_hybrid_token_pool_groups_test.go +++ b/deployment/ccip/changeset/v1_5_1/cs_hybrid_token_pool_groups_test.go @@ -33,8 +33,8 @@ func configureHybridTokenPoolChains(t *testing.T, e cldf.Environment, selectorA, Version: shared.HybridWithExternalMinterFastTransferTokenPoolVersion, ChainUpdates: v1_5_1.RateLimiterPerChain{ selectorB: v1_5_1.RateLimiterConfig{ - Inbound: ratelimiterConfig, - Outbound: ratelimiterConfig, + Inbound: &ratelimiterConfig, + Outbound: &ratelimiterConfig, }, }, }, @@ -43,8 +43,8 @@ func configureHybridTokenPoolChains(t *testing.T, e cldf.Environment, selectorA, Version: shared.HybridWithExternalMinterFastTransferTokenPoolVersion, ChainUpdates: v1_5_1.RateLimiterPerChain{ selectorA: v1_5_1.RateLimiterConfig{ - Inbound: ratelimiterConfig, - Outbound: ratelimiterConfig, + Inbound: &ratelimiterConfig, + Outbound: &ratelimiterConfig, }, }, }, diff --git a/deployment/environment/crib/ccip_deployer.go b/deployment/environment/crib/ccip_deployer.go index 8c268b898e8..8312bbe16d8 100644 --- a/deployment/environment/crib/ccip_deployer.go +++ b/deployment/environment/crib/ccip_deployer.go @@ -896,12 +896,12 @@ func setupEVM2EVMLanes(e *cldf.Environment, state stateview.CCIPOnChainState, la globalUpdateRouterChanges[src].OnRampUpdates[dst] = true rateLimitPerChain[dst] = v1_5_1.RateLimiterConfig{ - Inbound: token_pool.RateLimiterConfig{ + Inbound: &token_pool.RateLimiterConfig{ IsEnabled: false, Capacity: big.NewInt(0), Rate: big.NewInt(0), }, - Outbound: token_pool.RateLimiterConfig{ + Outbound: &token_pool.RateLimiterConfig{ IsEnabled: false, Capacity: big.NewInt(0), Rate: big.NewInt(0),