From cf5caf4481415f31280cc942d3ad879144b192e4 Mon Sep 17 00:00:00 2001 From: Adam Hamrick Date: Thu, 27 Jun 2024 13:32:01 -0400 Subject: [PATCH] [CCIP-2563] [CCIP-2422] Adds Different TransferConfig Scenarios to OnRamp and OffRamp Tests (#1030) ## Motivation Smoke test coverage is light on different transfer configuration combinations. ## Solution * Adds a transfer config smoke test to check different combinations of bps and agg limits to ensure they can transfer properly * Adds more explicit tests around `Capacity` and `AggregateRateLimits` for `OffRamp` and `OnRamp` by expanding the `TestSmokeCCIPOnRampLimits` and `TestSmokeCCIPOffRampLimits` tests. --- .github/workflows/integration-tests.yml | 14 +- .../ccip-tests/actions/ccip_helpers.go | 13 +- .../ccip-tests/chaos/ccip_test.go | 4 +- .../ccip-tests/contracts/contract_models.go | 1 + .../ccip-tests/load/ccip_loadgen.go | 2 +- integration-tests/ccip-tests/load/helper.go | 2 +- .../ccip-tests/smoke/ccip_test.go | 517 ++++++++++-------- .../ccip-tests/testconfig/ccip.go | 8 +- .../ccip-tests/testsetups/ccip.go | 35 +- 9 files changed, 362 insertions(+), 234 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8591c06457..a5ea517456 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -354,18 +354,24 @@ jobs: os: ubuntu-latest file: ccip run: -run ^TestSmokeCCIPManuallyExecuteAfterExecutionFailingDueToInsufficientGas$ - - name: ccip-smoke-self-serve-offramp-arl + - name: ccip-smoke-on-ramp-limits nodes: 1 dir: ccip-tests/smoke os: ubuntu-latest file: ccip - run: -run ^TestSmokeCCIPSelfServeRateLimitOffRamp$ - - name: ccip-smoke-self-serve-onramp-arl + run: -run ^TestSmokeCCIPOnRampLimits$ + - name: ccip-smoke-off-ramp-capacity nodes: 1 dir: ccip-tests/smoke os: ubuntu-latest file: ccip - run: -run ^TestSmokeCCIPSelfServeRateLimitOnRamp$ + run: -run ^TestSmokeCCIPOffRampCapacityLimit$ + - name: ccip-smoke-off-ramp-agg-rate-limit + nodes: 1 + dir: ccip-tests/smoke + os: ubuntu-latest + file: ccip + run: -run ^TestSmokeCCIPOffRampAggRateLimit$ - name: runlog id: runlog nodes: 2 diff --git a/integration-tests/ccip-tests/actions/ccip_helpers.go b/integration-tests/ccip-tests/actions/ccip_helpers.go index cd7ecd56c9..88cd28876c 100644 --- a/integration-tests/ccip-tests/actions/ccip_helpers.go +++ b/integration-tests/ccip-tests/actions/ccip_helpers.go @@ -84,6 +84,7 @@ const ( defaultUSDCDestBytesOverhead = 640 defaultUSDCDestGasOverhead = 120_000 + DefaultDestinationGasLimit = 600_000 // DefaultResubscriptionTimeout denotes the max backoff duration for resubscription for various watch events // if the subscription keeps failing even after this duration, the test will fail DefaultResubscriptionTimeout = 2 * time.Hour @@ -1502,6 +1503,9 @@ func (sourceCCIP *SourceCCIPModule) UpdateBalance( ) { if len(sourceCCIP.TransferAmount) > 0 { for i := range sourceCCIP.TransferAmount { + if sourceCCIP.TransferAmount[i] == nil { // nil transfer amount means no transfer for this token + continue + } // if length of sourceCCIP.TransferAmount is more than available bridge token use first bridge token token := sourceCCIP.Common.BridgeTokens[0] if i < len(sourceCCIP.Common.BridgeTokens) { @@ -1632,7 +1636,7 @@ func (sourceCCIP *SourceCCIPModule) AssertEventCCIPSendRequested( seqNum := sendRequestedEvent.SequenceNumber lggr = ptr.Ptr(lggr.With(). Uint64("SequenceNumber", seqNum). - Str("msgId ", fmt.Sprintf("0x%x", sendRequestedEvent.MessageId[:])). + Str("MsgID", fmt.Sprintf("0x%x", sendRequestedEvent.MessageId[:])). Logger()) // prevEventAt is the time when the message was successful, this should be same as the time when the event was emitted reqStat[i].UpdateState(lggr, seqNum, testreporters.CCIPSendRe, 0, testreporters.Success, @@ -1691,6 +1695,9 @@ func (sourceCCIP *SourceCCIPModule) CCIPMsg( tokenAndAmounts := []router.ClientEVMTokenAmount{} for i, amount := range sourceCCIP.TransferAmount { + if amount == nil { // make nil transfer amount 0 to avoid panics + sourceCCIP.TransferAmount[i] = big.NewInt(0) + } token := sourceCCIP.Common.BridgeTokens[0] // if length of sourceCCIP.TransferAmount is more than available bridge token use first bridge token if i < len(sourceCCIP.Common.BridgeTokens) { @@ -2755,7 +2762,7 @@ func (lane *CCIPLane) AddToSentReqs(txHash common.Hash, reqStats []*testreporter func (lane *CCIPLane) Multicall(noOfRequests int, multiSendAddr common.Address) error { var ccipMultipleMsg []contracts.CCIPMsgData feeToken := common.HexToAddress(lane.Source.Common.FeeToken.Address()) - genericMsg, err := lane.Source.CCIPMsg(lane.Dest.ReceiverDapp.EthAddress, big.NewInt(600_000)) + genericMsg, err := lane.Source.CCIPMsg(lane.Dest.ReceiverDapp.EthAddress, big.NewInt(DefaultDestinationGasLimit)) if err != nil { return fmt.Errorf("failed to form the ccip message: %w", err) } @@ -2984,7 +2991,7 @@ func (lane *CCIPLane) ExecuteManually(options ...ManualExecutionOption) error { OnRamp: lane.Source.OnRamp.Address(), OffRamp: lane.Dest.OffRamp.Address(), SendReqLogIndex: logIndex, - GasLimit: big.NewInt(600_000), + GasLimit: big.NewInt(DefaultDestinationGasLimit), } timeNow := time.Now().UTC() tx, err := args.ExecuteManually() diff --git a/integration-tests/ccip-tests/chaos/ccip_test.go b/integration-tests/ccip-tests/chaos/ccip_test.go index 30681eed92..4b1dda7a91 100644 --- a/integration-tests/ccip-tests/chaos/ccip_test.go +++ b/integration-tests/ccip-tests/chaos/ccip_test.go @@ -124,7 +124,7 @@ func TestChaosCCIP(t *testing.T) { lane.RecordStateBeforeTransfer() // Send the ccip-request and verify ocr2 is running - err := lane.SendRequests(1, big.NewInt(600_000)) + err := lane.SendRequests(1, big.NewInt(actions.DefaultDestinationGasLimit)) require.NoError(t, err) lane.ValidateRequests(nil) @@ -139,7 +139,7 @@ func TestChaosCCIP(t *testing.T) { }) lane.RecordStateBeforeTransfer() // Now send the ccip-request while the chaos is at play - err = lane.SendRequests(numOfRequests, big.NewInt(600_000)) + err = lane.SendRequests(numOfRequests, big.NewInt(actions.DefaultDestinationGasLimit)) require.NoError(t, err) if in.waitForChaosRecovery { // wait for chaos to be recovered before further validation diff --git a/integration-tests/ccip-tests/contracts/contract_models.go b/integration-tests/ccip-tests/contracts/contract_models.go index a75ddced67..08a88be569 100644 --- a/integration-tests/ccip-tests/contracts/contract_models.go +++ b/integration-tests/ccip-tests/contracts/contract_models.go @@ -1715,6 +1715,7 @@ func (onRamp *OnRamp) SetNops() error { return onRamp.client.ProcessTransaction(tx) } +// SetTokenTransferFeeConfig sets the token transfer fee configuration for the OnRamp func (onRamp *OnRamp) SetTokenTransferFeeConfig(tokenTransferFeeConfig []evm_2_evm_onramp.EVM2EVMOnRampTokenTransferFeeConfigArgs) error { opts, err := onRamp.client.TransactionOpts(onRamp.client.GetDefaultWallet()) if err != nil { diff --git a/integration-tests/ccip-tests/load/ccip_loadgen.go b/integration-tests/ccip-tests/load/ccip_loadgen.go index 48d41e43aa..4e142c9afe 100644 --- a/integration-tests/ccip-tests/load/ccip_loadgen.go +++ b/integration-tests/ccip-tests/load/ccip_loadgen.go @@ -379,7 +379,7 @@ func (c *CCIPE2ELoad) Validate(lggr zerolog.Logger, sendTx *types.Transaction, t for _, msgLog := range msgLogs { seqNum := msgLog.SequenceNumber var reqStat *testreporters.RequestStat - lggr = lggr.With().Str("msgId ", fmt.Sprintf("0x%x", msgLog.MessageId[:])).Logger() + lggr = lggr.With().Str("MsgID", fmt.Sprintf("0x%x", msgLog.MessageId[:])).Logger() for _, stat := range stats { if stat.SeqNum == seqNum { reqStat = stat diff --git a/integration-tests/ccip-tests/load/helper.go b/integration-tests/ccip-tests/load/helper.go index f8acd09953..64c43bc161 100644 --- a/integration-tests/ccip-tests/load/helper.go +++ b/integration-tests/ccip-tests/load/helper.go @@ -174,7 +174,7 @@ func (l *LoadArgs) ValidateCurseFollowedByUncurse() { lane.Source.TransferAmount = []*big.Int{} failedTx, _, _, err := lane.Source.SendRequest( lane.Dest.ReceiverDapp.EthAddress, - big.NewInt(600_000), // gas limit + big.NewInt(actions.DefaultDestinationGasLimit), // gas limit ) if lane.Source.Common.ChainClient.GetNetworkConfig().MinimumConfirmations > 0 { require.Error(l.t, err) diff --git a/integration-tests/ccip-tests/smoke/ccip_test.go b/integration-tests/ccip-tests/smoke/ccip_test.go index 73b4947075..9ba2b9caaa 100644 --- a/integration-tests/ccip-tests/smoke/ccip_test.go +++ b/integration-tests/ccip-tests/smoke/ccip_test.go @@ -128,17 +128,8 @@ func TestSmokeCCIPRateLimit(t *testing.T) { src := tc.lane.Source // add liquidity to pools on both networks if !pointer.GetBool(TestCfg.TestGroupInput.ExistingDeployment) { - addFund := func(ccipCommon *actions.CCIPCommon) { - for i, btp := range ccipCommon.BridgeTokenPools { - token := ccipCommon.BridgeTokens[i] - err := btp.AddLiquidity( - token, token.OwnerWallet, new(big.Int).Mul(AggregatedRateLimitCapacity, big.NewInt(20)), - ) - require.NoError(t, err) - } - } - addFund(src.Common) - addFund(tc.lane.Dest.Common) + addLiquidity(t, src.Common, new(big.Int).Mul(AggregatedRateLimitCapacity, big.NewInt(20))) + addLiquidity(t, tc.lane.Dest.Common, new(big.Int).Mul(AggregatedRateLimitCapacity, big.NewInt(20))) } log.Info(). Str("Source", tc.lane.SourceNetworkName). @@ -252,7 +243,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { require.NoError(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) failedTx, _, _, err := tc.lane.Source.SendRequest( tc.lane.Dest.ReceiverDapp.EthAddress, - big.NewInt(600_000), // gas limit + big.NewInt(actions.DefaultDestinationGasLimit), // gas limit ) require.NoError(t, err) require.Error(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) @@ -272,7 +263,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { tc.lane.Logger.Info().Str("tokensToSend", tokensToSend.String()).Msg("99% of Aggregated Capacity") tc.lane.RecordStateBeforeTransfer() src.TransferAmount[0] = tokensToSend - err = tc.lane.SendRequests(1, big.NewInt(600_000)) + err = tc.lane.SendRequests(1, big.NewInt(actions.DefaultDestinationGasLimit)) require.NoError(t, err) // try to send again with amount more than the amount refilled by rate and @@ -280,7 +271,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { src.TransferAmount[0] = new(big.Int).Mul(AggregatedRateLimitRate, big.NewInt(10)) failedTx, _, _, err = tc.lane.Source.SendRequest( tc.lane.Dest.ReceiverDapp.EthAddress, - big.NewInt(600_000), // gas limit + big.NewInt(actions.DefaultDestinationGasLimit), // gas limit ) tc.lane.Logger.Info().Str("tokensToSend", src.TransferAmount[0].String()).Msg("More than Aggregated Rate") require.NoError(t, err) @@ -347,7 +338,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { failedTx, _, _, err = tc.lane.Source.SendRequest( tc.lane.Dest.ReceiverDapp.EthAddress, - big.NewInt(600_000), // gas limit + big.NewInt(actions.DefaultDestinationGasLimit), // gas limit ) require.NoError(t, err) require.Error(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) @@ -367,7 +358,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { src.TransferAmount[0] = tokensToSend tc.lane.Logger.Info().Str("tokensToSend", tokensToSend.String()).Msg("99% of Token Pool Capacity") tc.lane.RecordStateBeforeTransfer() - err = tc.lane.SendRequests(1, big.NewInt(600_000)) + err = tc.lane.SendRequests(1, big.NewInt(actions.DefaultDestinationGasLimit)) require.NoError(t, err) // try to send again with amount more than the amount refilled by token pool rate and @@ -382,7 +373,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { require.NoError(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) failedTx, _, _, err = tc.lane.Source.SendRequest( tc.lane.Dest.ReceiverDapp.EthAddress, - big.NewInt(600_000), + big.NewInt(actions.DefaultDestinationGasLimit), ) require.NoError(t, err) require.Error(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) @@ -403,11 +394,14 @@ func TestSmokeCCIPRateLimit(t *testing.T) { } } -func TestSmokeCCIPSelfServeRateLimitOnRamp(t *testing.T) { +func TestSmokeCCIPOnRampLimits(t *testing.T) { t.Parallel() log := logging.GetTestLogger(t) - TestCfg := testsetups.NewCCIPTestConfig(t, log, testconfig.Smoke) + TestCfg := testsetups.NewCCIPTestConfig(t, log, testconfig.Smoke, testsetups.WithNoTokensPerMessage(4), testsetups.WithTokensPerChain(4)) + require.False(t, pointer.GetBool(TestCfg.TestGroupInput.ExistingDeployment), + "This test modifies contract state. Before running it, ensure you are willing and able to do so.", + ) err := contracts.MatchContractVersionsOrAbove(map[contracts.Name]contracts.Version{ contracts.OffRampContract: contracts.V1_5_0_dev, contracts.OnRampContract: contracts.V1_5_0_dev, @@ -431,10 +425,22 @@ func TestSmokeCCIPSelfServeRateLimitOnRamp(t *testing.T) { }) } - aggregateRateLimit := big.NewInt(1e16) + var ( + capacityLimit = big.NewInt(1e16) + overCapacityAmount = new(big.Int).Add(capacityLimit, big.NewInt(1)) + + // token without any transfer config + freeTokenIndex = 0 + // token with bps non-zero, no agg rate limit + bpsTokenIndex = 1 + // token with bps zero, with agg rate limit on + aggRateTokenIndex = 2 + // token with both bps and agg rate limit + bpsAndAggTokenIndex = 3 + ) for _, tc := range tests { - t.Run(fmt.Sprintf("%s - Self Serve Rate Limit OnRamp", tc.testName), func(t *testing.T) { + t.Run(fmt.Sprintf("%s - OnRamp Limits", tc.testName), func(t *testing.T) { tc.lane.Test = t src := tc.lane.Source dest := tc.lane.Dest @@ -442,242 +448,170 @@ func TestSmokeCCIPSelfServeRateLimitOnRamp(t *testing.T) { require.GreaterOrEqual(t, len(src.Common.BridgeTokenPools), 2, "At least two bridge token pools needed for test") require.GreaterOrEqual(t, len(dest.Common.BridgeTokens), 2, "At least two bridge tokens needed for test") require.GreaterOrEqual(t, len(dest.Common.BridgeTokenPools), 2, "At least two bridge token pools needed for test") - require.NotEqualValues(t, src.Common.ChainClient.GetDefaultWallet().Address(), src.Common.BridgeTokens[0].OwnerAddress.Hex(), "Token owner and CCIP wallet should be different") - // add liquidity to pools on both networks - if !pointer.GetBool(TestCfg.TestGroupInput.ExistingDeployment) { - addFund := func(ccipCommon *actions.CCIPCommon) { - for i, btp := range ccipCommon.BridgeTokenPools { - token := ccipCommon.BridgeTokens[i] - err := btp.AddLiquidity( - token, token.OwnerWallet, new(big.Int).Mul(aggregateRateLimit, big.NewInt(20)), - ) - require.NoError(t, err) - } - } - addFund(src.Common) - addFund(dest.Common) - } + addLiquidity(t, src.Common, new(big.Int).Mul(capacityLimit, big.NewInt(20))) + addLiquidity(t, dest.Common, new(big.Int).Mul(capacityLimit, big.NewInt(20))) var ( - freeTokenIndex = 0 - limitedTokenIndex = 1 - - freeSrcToken = src.Common.BridgeTokens[freeTokenIndex] - freeDestToken = dest.Common.BridgeTokens[freeTokenIndex] - limitedSrcToken = src.Common.BridgeTokens[limitedTokenIndex] - limitedDestToken = dest.Common.BridgeTokens[limitedTokenIndex] - overLimitAmount = new(big.Int).Add(aggregateRateLimit, big.NewInt(1)) + freeToken = src.Common.BridgeTokens[freeTokenIndex] + bpsToken = src.Common.BridgeTokens[bpsTokenIndex] + aggRateToken = src.Common.BridgeTokens[aggRateTokenIndex] + bpsAndAggToken = src.Common.BridgeTokens[bpsAndAggTokenIndex] ) tc.lane.Logger.Info(). - Str("Free Source Token", freeSrcToken.Address()). - Str("Free Dest Token", freeDestToken.Address()). - Str("Limited Source Token", limitedSrcToken.Address()). - Str("Limited Dest Token", limitedDestToken.Address()). + Str("Free Token", freeToken.ContractAddress.Hex()). + Str("BPS Token", bpsToken.ContractAddress.Hex()). + Str("Agg Rate Token", aggRateToken.ContractAddress.Hex()). + Str("BPS and Agg Rate Token", bpsAndAggToken.ContractAddress.Hex()). Msg("Tokens for rate limit testing") - err := tc.lane.DisableAllRateLimiting() require.NoError(t, err, "Error disabling rate limits") - // Send both tokens with no rate limits and ensure they succeed - src.TransferAmount[freeTokenIndex] = overLimitAmount - src.TransferAmount[limitedTokenIndex] = overLimitAmount - tc.lane.RecordStateBeforeTransfer() - err = tc.lane.SendRequests(1, big.NewInt(600_000)) - require.NoError(t, err) - tc.lane.ValidateRequests() - - // Enable aggregate rate limiting on the source chains for the limited token + // Set reasonable rate limits for the tokens err = src.OnRamp.SetTokenTransferFeeConfig([]evm_2_evm_onramp.EVM2EVMOnRampTokenTransferFeeConfigArgs{ { - Token: limitedSrcToken.ContractAddress, + Token: bpsToken.ContractAddress, + AggregateRateLimitEnabled: false, + DeciBps: 10, + }, + { + Token: aggRateToken.ContractAddress, + AggregateRateLimitEnabled: true, + }, + { + Token: bpsAndAggToken.ContractAddress, AggregateRateLimitEnabled: true, + DeciBps: 10, }, }) - require.NoError(t, err, "Error setting OnRamp rate limits") + require.NoError(t, err, "Error setting OnRamp transfer fee config") err = src.OnRamp.SetRateLimit(evm_2_evm_onramp.RateLimiterConfig{ IsEnabled: true, - Capacity: aggregateRateLimit, - Rate: aggregateRateLimit, + Capacity: capacityLimit, + Rate: new(big.Int).Mul(capacityLimit, big.NewInt(500)), // Set a high rate to avoid it getting in the way }) require.NoError(t, err, "Error setting OnRamp rate limits") err = src.Common.ChainClient.WaitForEvents() require.NoError(t, err, "Error waiting for events") - tc.lane.Logger.Debug().Str("Token", limitedSrcToken.ContractAddress.Hex()).Msg("Enabled aggregate rate limit on source chain") - // Send free token that should not have a rate limit and should succeed - src.TransferAmount[freeTokenIndex] = overLimitAmount - src.TransferAmount[limitedTokenIndex] = big.NewInt(0) + + // Send all tokens under their limits and ensure they succeed + src.TransferAmount[freeTokenIndex] = overCapacityAmount + src.TransferAmount[bpsTokenIndex] = overCapacityAmount + src.TransferAmount[aggRateTokenIndex] = big.NewInt(1) + src.TransferAmount[bpsAndAggTokenIndex] = big.NewInt(1) tc.lane.RecordStateBeforeTransfer() - err = tc.lane.SendRequests(1, big.NewInt(600_000)) - require.NoError(t, err, "Free token transfer failed") + err = tc.lane.SendRequests(1, big.NewInt(actions.DefaultDestinationGasLimit)) + require.NoError(t, err) tc.lane.ValidateRequests() - tc.lane.Logger.Info().Str("Token", freeSrcToken.ContractAddress.Hex()).Msg("Free token transfer succeeded") - // Send limited token with rate limit that should fail and revert on the source chain + // Check that capacity limits are enforced src.TransferAmount[freeTokenIndex] = big.NewInt(0) - src.TransferAmount[limitedTokenIndex] = overLimitAmount - tc.lane.Logger.Info().Str("Token", limitedSrcToken.ContractAddress.Hex()).Msg("Enabled aggregate rate limit on OnRamp") - failedTx, _, _, err := tc.lane.Source.SendRequest(tc.lane.Dest.ReceiverDapp.EthAddress, big.NewInt(600_000)) + src.TransferAmount[bpsTokenIndex] = big.NewInt(0) + src.TransferAmount[aggRateTokenIndex] = overCapacityAmount + src.TransferAmount[bpsAndAggTokenIndex] = big.NewInt(0) + failedTx, _, _, err := tc.lane.Source.SendRequest(tc.lane.Dest.ReceiverDapp.EthAddress, big.NewInt(actions.DefaultDestinationGasLimit)) require.Error(t, err, "Limited token transfer should immediately revert") errReason, _, err := src.Common.ChainClient.RevertReasonFromTx(failedTx, evm_2_evm_onramp.EVM2EVMOnRampABI) require.NoError(t, err) - require.Equal(t, "AggregateValueMaxCapacityExceeded", errReason, "Expected rate limit reached error") + require.Equal(t, "AggregateValueMaxCapacityExceeded", errReason, "Expected capacity limit reached error") tc.lane.Logger. Info(). - Str("Token", limitedSrcToken.ContractAddress.Hex()). + Str("Token", aggRateToken.ContractAddress.Hex()). Msg("Limited token transfer failed on source chain (a good thing in this context)") - }) - } -} - -func TestSmokeCCIPSelfServeRateLimitOffRamp(t *testing.T) { - t.Parallel() - - log := logging.GetTestLogger(t) - TestCfg := testsetups.NewCCIPTestConfig(t, log, testconfig.Smoke) - err := contracts.MatchContractVersionsOrAbove(map[contracts.Name]contracts.Version{ - contracts.OffRampContract: contracts.V1_5_0_dev, - }) - require.NoError(t, err, "Required contract versions not met") - require.True(t, TestCfg.SelectedNetworks[0].Simulated, "This test relies on timing assumptions and should only be run on simulated networks") - - // Set the default permissionless exec threshold lower so that we can manually execute the transactions faster - // Tuning this too low stops any transactions from being realistically executed - actions.DefaultPermissionlessExecThreshold = 1 * time.Minute - setUpOutput := testsetups.CCIPDefaultTestSetUp(t, &log, "smoke-ccip", nil, TestCfg) - if len(setUpOutput.Lanes) == 0 { - return - } - t.Cleanup(func() { - require.NoError(t, setUpOutput.TearDown()) - }) - - var tests []testDefinition - for _, lane := range setUpOutput.Lanes { - tests = append(tests, testDefinition{ - testName: fmt.Sprintf("Network %s to network %s", - lane.ForwardLane.SourceNetworkName, lane.ForwardLane.DestNetworkName), - lane: lane.ForwardLane, - }) - } - - aggregateRateLimit := big.NewInt(1e16) - - for _, tc := range tests { - t.Run(fmt.Sprintf("%s - Self Serve Rate Limit OffRamp", tc.testName), func(t *testing.T) { - tc.lane.Test = t - src := tc.lane.Source - dest := tc.lane.Dest - require.GreaterOrEqual(t, len(src.Common.BridgeTokens), 2, "At least two bridge tokens needed for test") - require.GreaterOrEqual(t, len(src.Common.BridgeTokenPools), 2, "At least two bridge token pools needed for test") - require.GreaterOrEqual(t, len(dest.Common.BridgeTokens), 2, "At least two bridge tokens needed for test") - require.GreaterOrEqual(t, len(dest.Common.BridgeTokenPools), 2, "At least two bridge token pools needed for test") - require.NotEqualValues(t, src.Common.ChainClient.GetDefaultWallet().Address(), src.Common.BridgeTokens[0].OwnerAddress.Hex(), "Token owner and CCIP wallet should be different") - // add liquidity to pools on both networks - if !pointer.GetBool(TestCfg.TestGroupInput.ExistingDeployment) { - addFund := func(ccipCommon *actions.CCIPCommon) { - for i, btp := range ccipCommon.BridgeTokenPools { - token := ccipCommon.BridgeTokens[i] - err := btp.AddLiquidity( - token, token.OwnerWallet, new(big.Int).Mul(aggregateRateLimit, big.NewInt(20)), - ) - require.NoError(t, err) - } - } - addFund(src.Common) - addFund(dest.Common) - } - - var ( - freeTokenIndex = 0 - limitedTokenIndex = 1 - - freeSrcToken = src.Common.BridgeTokens[freeTokenIndex] - freeDestToken = dest.Common.BridgeTokens[freeTokenIndex] - limitedSrcToken = src.Common.BridgeTokens[limitedTokenIndex] - limitedDestToken = dest.Common.BridgeTokens[limitedTokenIndex] - ) - tc.lane.Logger.Info(). - Str("Free Source Token", freeSrcToken.Address()). - Str("Free Dest Token", freeDestToken.Address()). - Str("Limited Source Token", limitedSrcToken.Address()). - Str("Limited Dest Token", limitedDestToken.Address()). - Msg("Tokens for rate limit testing") - err := tc.lane.DisableAllRateLimiting() - require.NoError(t, err, "Error disabling rate limits") - - // Send both tokens with no rate limits and ensure they succeed - overLimitAmount := new(big.Int).Add(aggregateRateLimit, big.NewInt(1)) - src.TransferAmount[freeTokenIndex] = overLimitAmount - src.TransferAmount[limitedTokenIndex] = overLimitAmount - tc.lane.RecordStateBeforeTransfer() - err = tc.lane.SendRequests(1, big.NewInt(600_000)) + src.TransferAmount[aggRateTokenIndex] = big.NewInt(0) + src.TransferAmount[bpsAndAggTokenIndex] = overCapacityAmount + failedTx, _, _, err = tc.lane.Source.SendRequest(tc.lane.Dest.ReceiverDapp.EthAddress, big.NewInt(actions.DefaultDestinationGasLimit)) + require.Error(t, err, "Limited token transfer should immediately revert") + errReason, _, err = src.Common.ChainClient.RevertReasonFromTx(failedTx, evm_2_evm_onramp.EVM2EVMOnRampABI) require.NoError(t, err) - tc.lane.ValidateRequests() + require.Equal(t, "AggregateValueMaxCapacityExceeded", errReason, "Expected capacity limit reached error") + tc.lane.Logger. + Info(). + Str("Token", aggRateToken.ContractAddress.Hex()). + Msg("Limited token transfer failed on source chain (a good thing in this context)") - // Enable aggregate rate limiting on the destination chain for the limited token - err = dest.AddRateLimitTokens([]*contracts.ERC20Token{limitedSrcToken}, []*contracts.ERC20Token{limitedDestToken}) - require.NoError(t, err, "Error setting destination rate limits") - err = dest.OffRamp.SetRateLimit(contracts.RateLimiterConfig{ + // Set a high price for the tokens to more easily trigger aggregate rate limits + err = src.Common.PriceRegistry.UpdatePrices([]contracts.InternalTokenPriceUpdate{ + { + SourceToken: aggRateToken.ContractAddress, + UsdPerToken: big.NewInt(100), + }, + { + SourceToken: bpsAndAggToken.ContractAddress, + UsdPerToken: big.NewInt(100), + }, + }, []contracts.InternalGasPriceUpdate{}) + require.NoError(t, err, "Error updating prices") + // Enable aggregate rate limiting for the limited tokens + err = src.OnRamp.SetRateLimit(evm_2_evm_onramp.RateLimiterConfig{ IsEnabled: true, - Capacity: aggregateRateLimit, - Rate: aggregateRateLimit, + Capacity: new(big.Int).Mul(capacityLimit, big.NewInt(5000)), // Set a high capacity to avoid it getting in the way + Rate: big.NewInt(1), }) - require.NoError(t, err, "Error setting destination rate limits") - err = dest.Common.ChainClient.WaitForEvents() + require.NoError(t, err, "Error setting OnRamp rate limits") + err = src.Common.ChainClient.WaitForEvents() require.NoError(t, err, "Error waiting for events") - tc.lane.Logger.Debug().Str("Token", limitedSrcToken.ContractAddress.Hex()).Msg("Enabled aggregate rate limit on destination chain") - // Send free token that should not have a rate limit and should succeed - src.TransferAmount[freeTokenIndex] = overLimitAmount - src.TransferAmount[limitedTokenIndex] = big.NewInt(0) + // Send aggregate unlimited tokens and ensure they succeed + src.TransferAmount[freeTokenIndex] = overCapacityAmount + src.TransferAmount[bpsTokenIndex] = overCapacityAmount + src.TransferAmount[aggRateTokenIndex] = big.NewInt(0) + src.TransferAmount[bpsAndAggTokenIndex] = big.NewInt(0) tc.lane.RecordStateBeforeTransfer() - err = tc.lane.SendRequests(1, big.NewInt(600_000)) - require.NoError(t, err, "Free token transfer failed") + err = tc.lane.SendRequests(1, big.NewInt(actions.DefaultDestinationGasLimit)) + require.NoError(t, err) tc.lane.ValidateRequests() - tc.lane.Logger.Info().Str("Token", freeSrcToken.ContractAddress.Hex()).Msg("Free token transfer succeeded") - // Send limited token with rate limit that should fail on the destination chain + // Check that aggregate rate limits are enforced on limited tokens src.TransferAmount[freeTokenIndex] = big.NewInt(0) - src.TransferAmount[limitedTokenIndex] = overLimitAmount - tc.lane.RecordStateBeforeTransfer() - err = tc.lane.SendRequests(1, big.NewInt(600_000)) - require.NoError(t, err, "Failed to send rate limited token transfer") - // Expect the ExecutionStateChanged event to never show up - // Since we're looking to confirm that an event has NOT occurred, this can lead to some imperfect assumptions and results - // We set the timeout to stop waiting for the event after a minute - // 99% of transactions occur in under a minute in ideal simulated conditions, so this is an okay assumption there - // but on real chains this risks false negatives - // If we don't set this timeout, this test can take a long time and hold up CI - tc.lane.ValidateRequests(actions.ExpectPhaseToFail(testreporters.ExecStateChanged, actions.WithTimeout(time.Minute))) - tc.lane.Logger.Info(). - Str("Token", limitedSrcToken.ContractAddress.Hex()). - Msg("Limited token transfer failed on destination chain (a good thing in this context)") + src.TransferAmount[bpsTokenIndex] = big.NewInt(0) + src.TransferAmount[aggRateTokenIndex] = capacityLimit + src.TransferAmount[bpsAndAggTokenIndex] = big.NewInt(0) + failedTx, _, _, err = tc.lane.Source.SendRequest(tc.lane.Dest.ReceiverDapp.EthAddress, big.NewInt(actions.DefaultDestinationGasLimit)) + require.Error(t, err, "Aggregate rate limited token transfer should immediately revert") + errReason, _, err = src.Common.ChainClient.RevertReasonFromTx(failedTx, evm_2_evm_onramp.EVM2EVMOnRampABI) + require.NoError(t, err) + require.Equal(t, "AggregateValueRateLimitReached", errReason, "Expected aggregate rate limit reached error") + tc.lane.Logger. + Info(). + Str("Token", aggRateToken.ContractAddress.Hex()). + Msg("Limited token transfer failed on source chain (a good thing in this context)") - // Manually execute the rate limited token transfer and expect a similar error - tc.lane.Logger.Info().Str("Wait Time", actions.DefaultPermissionlessExecThreshold.String()).Msg("Waiting for Exec Threshold to Expire") - time.Sleep(actions.DefaultPermissionlessExecThreshold) // Give time to exit the window - // See above comment on timeout - err = tc.lane.ExecuteManually(actions.WithConfirmationTimeout(time.Minute)) - require.Error(t, err, "There should be errors executing manually at this point") - tc.lane.Logger.Debug().Str("Error", err.Error()).Msg("Manually executed rate limited token transfer failed as expected") + src.TransferAmount[aggRateTokenIndex] = big.NewInt(0) + src.TransferAmount[bpsAndAggTokenIndex] = capacityLimit + failedTx, _, _, err = tc.lane.Source.SendRequest(tc.lane.Dest.ReceiverDapp.EthAddress, big.NewInt(actions.DefaultDestinationGasLimit)) + require.Error(t, err, "Aggregate rate limited token transfer should immediately revert") + errReason, _, err = src.Common.ChainClient.RevertReasonFromTx(failedTx, evm_2_evm_onramp.EVM2EVMOnRampABI) + require.NoError(t, err) + require.Equal(t, "AggregateValueRateLimitReached", errReason, "Expected aggregate rate limit reached error") + tc.lane.Logger. + Info(). + Str("Token", aggRateToken.ContractAddress.Hex()). + Msg("Limited token transfer failed on source chain (a good thing in this context)") + }) + } +} - // Change rate limit to make it viable - err = dest.OffRamp.SetRateLimit(contracts.RateLimiterConfig{ - IsEnabled: true, - Capacity: big.NewInt(0).Mul(aggregateRateLimit, big.NewInt(100)), - Rate: big.NewInt(0).Mul(aggregateRateLimit, big.NewInt(100)), - }) - require.NoError(t, err, "Error setting destination rate limits") - err = dest.Common.ChainClient.WaitForEvents() - require.NoError(t, err, "Error waiting for events") - tc.lane.Logger.Debug().Str("Token", limitedSrcToken.ContractAddress.Hex()).Msg("Enabled aggregate rate limit on destination chain") +func TestSmokeCCIPOffRampCapacityLimit(t *testing.T) { + t.Parallel() - // Execute again manually and expect a pass - err = tc.lane.ExecuteManually() - require.NoError(t, err, "Error manually executing transaction after rate limit is lifted") - }) + capacityLimited := contracts.RateLimiterConfig{ + IsEnabled: true, + Capacity: big.NewInt(1e16), + Rate: new(big.Int).Mul(big.NewInt(1e16), big.NewInt(10)), // Set a high rate limit to avoid it getting in the way + } + testOffRampRateLimits(t, capacityLimited) +} + +func TestSmokeCCIPOffRampAggRateLimit(t *testing.T) { + t.Parallel() + + aggRateLimited := contracts.RateLimiterConfig{ + IsEnabled: true, + Capacity: new(big.Int).Mul(big.NewInt(1e16), big.NewInt(10)), // Set a high capacity limit to avoid it getting in the way + Rate: big.NewInt(1), } + testOffRampRateLimits(t, aggRateLimited) } func TestSmokeCCIPMulticall(t *testing.T) { @@ -789,3 +723,160 @@ func TestSmokeCCIPManuallyExecuteAfterExecutionFailingDueToInsufficientGas(t *te }) } } + +// add liquidity to pools on both networks +func addLiquidity(t *testing.T, ccipCommon *actions.CCIPCommon, amount *big.Int) { + t.Helper() + + for i, btp := range ccipCommon.BridgeTokenPools { + token := ccipCommon.BridgeTokens[i] + err := btp.AddLiquidity( + token, token.OwnerWallet, amount, + ) + require.NoError(t, err) + } +} + +// testOffRampRateLimits tests the rate limiting functionality of the OffRamp contract +// it's broken into a helper to help parallelize and keep the tests DRY +func testOffRampRateLimits(t *testing.T, rateLimiterConfig contracts.RateLimiterConfig) { + t.Helper() + + log := logging.GetTestLogger(t) + TestCfg := testsetups.NewCCIPTestConfig(t, log, testconfig.Smoke) + require.False(t, pointer.GetBool(TestCfg.TestGroupInput.ExistingDeployment), + "This test modifies contract state. Before running it, ensure you are willing and able to do so.", + ) + err := contracts.MatchContractVersionsOrAbove(map[contracts.Name]contracts.Version{ + contracts.OffRampContract: contracts.V1_5_0_dev, + }) + require.NoError(t, err, "Required contract versions not met") + require.True(t, TestCfg.SelectedNetworks[0].Simulated, "This test relies on timing assumptions and should only be run on simulated networks") + require.False(t, pointer.GetBool(TestCfg.TestGroupInput.ExistingDeployment), "This test modifies contract state and cannot be run on existing deployments") + + // Set the default permissionless exec threshold lower so that we can manually execute the transactions faster + // Tuning this too low stops any transactions from being realistically executed + actions.DefaultPermissionlessExecThreshold = 1 * time.Minute + + setUpOutput := testsetups.CCIPDefaultTestSetUp(t, &log, "smoke-ccip", nil, TestCfg) + if len(setUpOutput.Lanes) == 0 { + return + } + t.Cleanup(func() { + require.NoError(t, setUpOutput.TearDown()) + }) + + var tests []testDefinition + for _, lane := range setUpOutput.Lanes { + tests = append(tests, testDefinition{ + testName: fmt.Sprintf("Network %s to network %s", + lane.ForwardLane.SourceNetworkName, lane.ForwardLane.DestNetworkName), + lane: lane.ForwardLane, + }) + } + + var ( + freeTokenIndex = 0 + limitedTokenIndex = 1 + ) + + for _, tc := range tests { + t.Run(fmt.Sprintf("%s - OffRamp Limits", tc.testName), func(t *testing.T) { + tc.lane.Test = t + src := tc.lane.Source + dest := tc.lane.Dest + var ( + capacityLimit = rateLimiterConfig.Capacity + overLimitAmount = new(big.Int).Add(capacityLimit, big.NewInt(1)) + ) + require.GreaterOrEqual(t, len(src.Common.BridgeTokens), 2, "At least two bridge tokens needed for test") + require.GreaterOrEqual(t, len(src.Common.BridgeTokenPools), 2, "At least two bridge token pools needed for test") + require.GreaterOrEqual(t, len(dest.Common.BridgeTokens), 2, "At least two bridge tokens needed for test") + require.GreaterOrEqual(t, len(dest.Common.BridgeTokenPools), 2, "At least two bridge token pools needed for test") + addLiquidity(t, src.Common, new(big.Int).Mul(capacityLimit, big.NewInt(20))) + addLiquidity(t, dest.Common, new(big.Int).Mul(capacityLimit, big.NewInt(20))) + + var ( + freeSrcToken = src.Common.BridgeTokens[freeTokenIndex] + freeDestToken = dest.Common.BridgeTokens[freeTokenIndex] + limitedSrcToken = src.Common.BridgeTokens[limitedTokenIndex] + limitedDestToken = dest.Common.BridgeTokens[limitedTokenIndex] + ) + tc.lane.Logger.Info(). + Str("Free Source Token", freeSrcToken.Address()). + Str("Free Dest Token", freeDestToken.Address()). + Str("Limited Source Token", limitedSrcToken.Address()). + Str("Limited Dest Token", limitedDestToken.Address()). + Msg("Tokens for rate limit testing") + + err := tc.lane.DisableAllRateLimiting() + require.NoError(t, err, "Error disabling rate limits") + + // Send both tokens with no rate limits and ensure they succeed + src.TransferAmount[freeTokenIndex] = overLimitAmount + src.TransferAmount[limitedTokenIndex] = overLimitAmount + tc.lane.RecordStateBeforeTransfer() + err = tc.lane.SendRequests(1, big.NewInt(actions.DefaultDestinationGasLimit)) + require.NoError(t, err) + tc.lane.ValidateRequests() + + // Enable capacity limiting on the destination chain for the limited token + err = dest.AddRateLimitTokens([]*contracts.ERC20Token{limitedSrcToken}, []*contracts.ERC20Token{limitedDestToken}) + require.NoError(t, err, "Error setting destination rate limits") + err = dest.OffRamp.SetRateLimit(rateLimiterConfig) + require.NoError(t, err, "Error setting destination rate limits") + err = dest.Common.ChainClient.WaitForEvents() + require.NoError(t, err, "Error waiting for events") + tc.lane.Logger.Debug().Str("Token", limitedSrcToken.ContractAddress.Hex()).Msg("Enabled capacity limit on destination chain") + + // Send free token that should not have a rate limit and should succeed + src.TransferAmount[freeTokenIndex] = overLimitAmount + src.TransferAmount[limitedTokenIndex] = big.NewInt(0) + tc.lane.RecordStateBeforeTransfer() + err = tc.lane.SendRequests(1, big.NewInt(actions.DefaultDestinationGasLimit)) + require.NoError(t, err, "Free token transfer failed") + tc.lane.ValidateRequests() + tc.lane.Logger.Info().Str("Token", freeSrcToken.ContractAddress.Hex()).Msg("Free token transfer succeeded") + + // Send limited token with rate limit that should fail on the destination chain + src.TransferAmount[freeTokenIndex] = big.NewInt(0) + src.TransferAmount[limitedTokenIndex] = overLimitAmount + tc.lane.RecordStateBeforeTransfer() + err = tc.lane.SendRequests(1, big.NewInt(actions.DefaultDestinationGasLimit)) + require.NoError(t, err, "Failed to send rate limited token transfer") + // Expect the ExecutionStateChanged event to never show up + // Since we're looking to confirm that an event has NOT occurred, this can lead to some imperfect assumptions and results + // We set the timeout to stop waiting for the event after a minute + // 99% of transactions occur in under a minute in ideal simulated conditions, so this is an okay assumption there + // but on real chains this risks false negatives + // If we don't set this timeout, this test can take a long time and hold up CI + tc.lane.ValidateRequests(actions.ExpectPhaseToFail(testreporters.ExecStateChanged, actions.WithTimeout(time.Minute))) + tc.lane.Logger.Info(). + Str("Token", limitedSrcToken.ContractAddress.Hex()). + Msg("Limited token transfer failed on destination chain (a good thing in this context)") + + // Manually execute the rate limited token transfer and expect a similar error + tc.lane.Logger.Info().Str("Wait Time", actions.DefaultPermissionlessExecThreshold.String()).Msg("Waiting for Exec Threshold to Expire") + time.Sleep(actions.DefaultPermissionlessExecThreshold) // Give time to exit the window + // See above comment on timeout + err = tc.lane.ExecuteManually(actions.WithConfirmationTimeout(time.Minute)) + require.Error(t, err, "There should be errors executing manually at this point") + tc.lane.Logger.Debug().Str("Error", err.Error()).Msg("Manually executed rate limited token transfer failed as expected") + + // Change limits to make it viable + err = dest.OffRamp.SetRateLimit(contracts.RateLimiterConfig{ + IsEnabled: true, + Capacity: new(big.Int).Mul(capacityLimit, big.NewInt(100)), + Rate: new(big.Int).Mul(capacityLimit, big.NewInt(100)), + }) + require.NoError(t, err, "Error setting destination rate limits") + err = dest.Common.ChainClient.WaitForEvents() + require.NoError(t, err, "Error waiting for events") + + // Execute again manually and expect a pass + err = tc.lane.ExecuteManually() + require.NoError(t, err, "Error manually executing transaction after rate limit is lifted") + }) + } + +} diff --git a/integration-tests/ccip-tests/testconfig/ccip.go b/integration-tests/ccip-tests/testconfig/ccip.go index dc73b06a28..1af5c5baee 100644 --- a/integration-tests/ccip-tests/testconfig/ccip.go +++ b/integration-tests/ccip-tests/testconfig/ccip.go @@ -19,12 +19,10 @@ import ( ) const ( - CONTRACTS_OVERRIDE_CONFIG = "BASE64_CCIP_CONFIG_OVERRIDE_CONTRACTS" + CONTRACTS_OVERRIDE_CONFIG string = "BASE64_CCIP_CONFIG_OVERRIDE_CONTRACTS" TokenOnlyTransfer string = "Token" - - DataOnlyTransfer string = "Data" - - DataAndTokenTransfer string = "DataWithToken" + DataOnlyTransfer string = "Data" + DataAndTokenTransfer string = "DataWithToken" ) type OffRampConfig struct { diff --git a/integration-tests/ccip-tests/testsetups/ccip.go b/integration-tests/ccip-tests/testsetups/ccip.go index 3a8fdf27c9..9a892a667a 100644 --- a/integration-tests/ccip-tests/testsetups/ccip.go +++ b/integration-tests/ccip-tests/testsetups/ccip.go @@ -347,14 +347,39 @@ func (c *CCIPTestConfig) SetOCRParams() error { // TestConfigOverrideOption is a function that modifies the test config and overrides any values passed in by test files // This is useful for setting up test specific configurations. +// The return should be a short, explanatory string that describes the change made by the override. +// This is logged at the beginning of the test run. type TestConfigOverrideOption func(*CCIPTestConfig) string -// WithCCIPOwnerTokens dictates that tokens be deployed and owned by the same account that owns all CCIP contracts. -// With Self-Serve tokens, this is unrealistic. -func WithCCIPOwnerTokens() TestConfigOverrideOption { +// UseCCIPOwnerTokens defines whether all tokens are deployed by the same address as the CCIP owner +func UseCCIPOwnerTokens(yes bool) TestConfigOverrideOption { return func(c *CCIPTestConfig) string { - c.TestGroupInput.TokenConfig.CCIPOwnerTokens = pointer.ToBool(true) - return "CCIPOwnerTokens set to true" + c.TestGroupInput.TokenConfig.CCIPOwnerTokens = pointer.ToBool(yes) + return fmt.Sprintf("CCIPOwnerTokens set to %t", yes) + } +} + +// WithTokensPerChain sets the number of tokens to deploy on each chain +func WithTokensPerChain(count int) TestConfigOverrideOption { + return func(c *CCIPTestConfig) string { + c.TestGroupInput.TokenConfig.NoOfTokensPerChain = pointer.ToInt(count) + return fmt.Sprintf("NoOfTokensPerChain set to %d", count) + } +} + +// WithMsgDetails sets the message details for the test +func WithMsgDetails(details *testconfig.MsgDetails) TestConfigOverrideOption { + return func(c *CCIPTestConfig) string { + c.TestGroupInput.MsgDetails = details + return "Message set" + } +} + +// WithNoTokensPerMessage sets how many tokens can be sent in a single message +func WithNoTokensPerMessage(noOfTokens int) TestConfigOverrideOption { + return func(c *CCIPTestConfig) string { + c.TestGroupInput.MsgDetails.NoOfTokens = pointer.ToInt(noOfTokens) + return fmt.Sprintf("MsgDetails.NoOfTokens set to %d", noOfTokens) } }