From 9e74eee9d415b386db33bdf2dd44facc82cd3551 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Wed, 31 Jul 2024 10:23:21 +0200 Subject: [PATCH] Port ccip onchain to chainlink (#13941) * init port * install dependencies and create snapshots * add to foundry ci * fix readme * fix ci & add changeset * fix fuzz exclusion * update license & solhint * add ccip solidity CI * fix coverage pruning * Update LICENSE * Update LICENSE * fix job name --- .../detect-solidity-file-changes/action.yml | 19 +- .../action.yml | 31 + .github/workflows/solidity-foundry.yml | 85 +- .github/workflows/solidity-hardhat.yml | 2 +- .github/workflows/solidity.yml | 22 +- LICENSE | 15 +- .../.changeset/three-stingrays-compete.md | 5 + contracts/.prettierignore | 3 +- contracts/.prettierrc.js | 1 + contracts/.solhintignore | 4 +- contracts/GNUmakefile | 23 +- contracts/README.md | 6 +- contracts/STYLE_GUIDE.md | 16 +- contracts/foundry.toml | 20 + contracts/gas-snapshots/ccip.gas-snapshot | 943 +++++ .../liquiditymanager.gas-snapshot | 48 + contracts/hardhat.config.ts | 3 +- contracts/package.json | 4 +- contracts/pnpm-lock.yaml | 791 +++- contracts/remappings.txt | 1 + contracts/scripts/ccip_lcov_prune | 29 + contracts/scripts/native_solc_compile_all | 2 +- .../scripts/native_solc_compile_all_ccip | 97 + .../native_solc_compile_all_liquiditymanager | 67 + contracts/src/v0.8/ccip/ARMProxy.sol | 74 + .../src/v0.8/ccip/AggregateRateLimiter.sol | 92 + contracts/src/v0.8/ccip/CommitStore.sol | 314 ++ contracts/src/v0.8/ccip/LICENSE-MIT.md | 21 + contracts/src/v0.8/ccip/LICENSE.md | 56 + .../v0.8/ccip/MultiAggregateRateLimiter.sol | 272 ++ contracts/src/v0.8/ccip/NonceManager.sol | 147 + contracts/src/v0.8/ccip/PriceRegistry.sol | 888 +++++ contracts/src/v0.8/ccip/RMN.sol | 964 +++++ contracts/src/v0.8/ccip/Router.sol | 290 ++ .../ccip/applications/CCIPClientExample.sol | 173 + .../v0.8/ccip/applications/CCIPReceiver.sol | 59 + .../ccip/applications/DefensiveExample.sol | 117 + .../ccip/applications/EtherSenderReceiver.sol | 180 + .../v0.8/ccip/applications/PingPongDemo.sol | 102 + .../ccip/applications/SelfFundedPingPong.sol | 67 + .../src/v0.8/ccip/applications/TokenProxy.sol | 87 + .../src/v0.8/ccip/capability/CCIPConfig.sol | 476 +++ .../interfaces/ICapabilitiesRegistry.sol | 31 + .../interfaces/IOCR3ConfigEncoder.sol | 11 + .../capability/libraries/CCIPConfigTypes.sol | 57 + .../ccip/docs/multi-chain-overview-ocr3.png | Bin 0 -> 818615 bytes .../ccip/docs/multi-chain-overview.drawio | 2060 ++++++++++ .../interfaces/IAny2EVMMessageReceiver.sol | 15 + .../v0.8/ccip/interfaces/IAny2EVMOffRamp.sol | 9 + .../src/v0.8/ccip/interfaces/ICommitStore.sol | 17 + .../v0.8/ccip/interfaces/IEVM2AnyOnRamp.sol | 15 + .../ccip/interfaces/IEVM2AnyOnRampClient.sol | 42 + .../v0.8/ccip/interfaces/IGetCCIPAdmin.sol | 8 + .../ccip/interfaces/IMessageInterceptor.sol | 22 + .../v0.8/ccip/interfaces/INonceManager.sol | 23 + contracts/src/v0.8/ccip/interfaces/IOwner.sol | 8 + contracts/src/v0.8/ccip/interfaces/IPool.sol | 35 + .../v0.8/ccip/interfaces/IPoolPriorTo1_5.sol | 46 + .../v0.8/ccip/interfaces/IPriceRegistry.sol | 109 + contracts/src/v0.8/ccip/interfaces/IRMN.sol | 21 + .../src/v0.8/ccip/interfaces/IRouter.sol | 35 + .../v0.8/ccip/interfaces/IRouterClient.sol | 37 + .../ccip/interfaces/ITokenAdminRegistry.sol | 12 + .../v0.8/ccip/interfaces/IWrappedNative.sol | 10 + .../interfaces/automation/ILinkAvailable.sol | 8 + contracts/src/v0.8/ccip/libraries/Client.sol | 55 + .../src/v0.8/ccip/libraries/Internal.sol | 319 ++ .../v0.8/ccip/libraries/MerkleMultiProof.sol | 113 + contracts/src/v0.8/ccip/libraries/Pool.sol | 58 + .../src/v0.8/ccip/libraries/RateLimiter.sol | 157 + .../ccip/libraries/USDPriceWith18Decimals.sol | 45 + contracts/src/v0.8/ccip/ocr/MultiOCR3Base.sol | 323 ++ contracts/src/v0.8/ccip/ocr/OCR2Abstract.sol | 122 + contracts/src/v0.8/ccip/ocr/OCR2Base.sol | 291 ++ .../src/v0.8/ccip/ocr/OCR2BaseNoChecks.sol | 242 ++ .../v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol | 914 +++++ .../src/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol | 721 ++++ .../v0.8/ccip/onRamp/EVM2EVMMultiOnRamp.sol | 339 ++ .../src/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol | 916 +++++ .../v0.8/ccip/pools/BurnFromMintTokenPool.sol | 38 + .../src/v0.8/ccip/pools/BurnMintTokenPool.sol | 30 + .../ccip/pools/BurnMintTokenPoolAbstract.sol | 49 + .../ccip/pools/BurnMintTokenPoolAndProxy.sol | 62 + .../ccip/pools/BurnWithFromMintTokenPool.sol | 38 + .../src/v0.8/ccip/pools/LegacyPoolWrapper.sol | 81 + .../v0.8/ccip/pools/LockReleaseTokenPool.sol | 151 + .../pools/LockReleaseTokenPoolAndProxy.sol | 159 + contracts/src/v0.8/ccip/pools/TokenPool.sol | 424 ++ .../ccip/pools/USDC/IMessageTransmitter.sol | 46 + .../v0.8/ccip/pools/USDC/ITokenMessenger.sol | 65 + .../v0.8/ccip/pools/USDC/USDCTokenPool.sol | 241 ++ contracts/src/v0.8/ccip/test/BaseTest.t.sol | 125 + .../src/v0.8/ccip/test/NonceManager.t.sol | 649 ++++ contracts/src/v0.8/ccip/test/README.md | 89 + contracts/src/v0.8/ccip/test/TokenSetup.t.sol | 179 + contracts/src/v0.8/ccip/test/WETH9.sol | 82 + .../test/applications/DefensiveExample.t.sol | 97 + .../applications/EtherSenderReceiver.t.sol | 718 ++++ .../test/applications/ImmutableExample.t.sol | 61 + .../ccip/test/applications/PingPongDemo.t.sol | 121 + .../applications/SelfFundedPingPong.t.sol | 99 + .../ccip/test/applications/TokenProxy.t.sol | 211 + .../src/v0.8/ccip/test/arm/ARMProxy.t.sol | 43 + .../ccip/test/arm/ARMProxy_standalone.t.sol | 78 + contracts/src/v0.8/ccip/test/arm/RMN.t.sol | 1068 +++++ .../src/v0.8/ccip/test/arm/RMNSetup.t.sol | 144 + .../v0.8/ccip/test/arm/RMN_benchmark.t.sol | 217 ++ .../ccip/test/attacks/onRamp/FacadeClient.sol | 54 + .../MultiOnRampTokenPoolReentrancy.t.sol | 118 + .../onRamp/OnRampTokenPoolReentrancy.t.sol | 116 + .../onRamp/ReentrantMaliciousTokenPool.sol | 50 + .../ccip/test/capability/CCIPConfig.t.sol | 1681 ++++++++ .../ccip/test/commitStore/CommitStore.t.sol | 618 +++ .../src/v0.8/ccip/test/e2e/End2End.t.sol | 116 + .../v0.8/ccip/test/e2e/MultiRampsEnd2End.sol | 260 ++ .../helpers/AggregateRateLimiterHelper.sol | 19 + .../test/helpers/BurnMintERC677Helper.sol | 18 + .../test/helpers/BurnMintMultiTokenPool.sol | 56 + .../ccip/test/helpers/CCIPConfigHelper.sol | 66 + .../ccip/test/helpers/CommitStoreHelper.sol | 13 + .../helpers/EVM2EVMMultiOffRampHelper.sol | 103 + .../test/helpers/EVM2EVMMultiOnRampHelper.sol | 12 + .../test/helpers/EVM2EVMOffRampHelper.sol | 59 + .../ccip/test/helpers/EVM2EVMOnRampHelper.sol | 47 + .../helpers/EtherSenderReceiverHelper.sol | 21 + .../ccip/test/helpers/IgnoreContractSize.sol | 10 + .../MaybeRevertingBurnMintTokenPool.sol | 70 + .../v0.8/ccip/test/helpers/MerkleHelper.sol | 52 + .../v0.8/ccip/test/helpers/MessageHasher.sol | 71 + .../test/helpers/MessageInterceptorHelper.sol | 30 + .../MultiAggregateRateLimiterHelper.sol | 17 + .../ccip/test/helpers/MultiOCR3Helper.sol | 45 + .../v0.8/ccip/test/helpers/MultiTokenPool.sol | 420 ++ .../src/v0.8/ccip/test/helpers/OCR2Helper.sol | 38 + .../ccip/test/helpers/OCR2NoChecksHelper.sol | 38 + .../ccip/test/helpers/PriceRegistryHelper.sol | 72 + .../ccip/test/helpers/RateLimiterHelper.sol | 36 + .../v0.8/ccip/test/helpers/ReportCodec.sol | 18 + .../ccip/test/helpers/TokenPoolHelper.sol | 42 + .../ccip/test/helpers/USDCTokenPoolHelper.sol | 21 + .../helpers/receivers/ConformingReceiver.sol | 15 + .../receivers/MaybeRevertMessageReceiver.sol | 54 + .../MaybeRevertMessageReceiverNo165.sol | 27 + .../helpers/receivers/ReentrancyAbuser.sol | 40 + .../receivers/ReentrancyAbuserMultiRamp.sol | 44 + .../ccip/test/legacy/BurnMintTokenPool1_2.sol | 353 ++ .../ccip/test/legacy/BurnMintTokenPool1_4.sol | 402 ++ .../ccip/test/legacy/TokenPoolAndProxy.t.sol | 771 ++++ .../test/libraries/MerkleMultiProof.t.sol | 196 + .../ccip/test/libraries/RateLimiter.t.sol | 297 ++ .../v0.8/ccip/test/mocks/MockCommitStore.sol | 42 + .../test/mocks/MockE2EUSDCTokenMessenger.sol | 103 + .../test/mocks/MockE2EUSDCTransmitter.sol | 168 + .../src/v0.8/ccip/test/mocks/MockRMN.sol | 55 + .../src/v0.8/ccip/test/mocks/MockRMN1_0.sol | 91 + .../src/v0.8/ccip/test/mocks/MockRouter.sol | 148 + .../test/mocks/MockUSDCTokenMessenger.sol | 52 + .../IMessageTransmitterWithRelay.sol | 55 + .../ccip/test/mocks/test/MockRouterTest.t.sol | 68 + .../v0.8/ccip/test/ocr/MultiOCR3Base.t.sol | 921 +++++ .../ccip/test/ocr/MultiOCR3BaseSetup.t.sol | 113 + .../src/v0.8/ccip/test/ocr/OCR2Base.t.sol | 305 ++ .../v0.8/ccip/test/ocr/OCR2BaseNoChecks.t.sol | 208 + .../src/v0.8/ccip/test/ocr/OCR2Setup.t.sol | 31 + .../test/offRamp/EVM2EVMMultiOffRamp.t.sol | 3429 +++++++++++++++++ .../offRamp/EVM2EVMMultiOffRampSetup.t.sol | 491 +++ .../ccip/test/offRamp/EVM2EVMOffRamp.t.sol | 1986 ++++++++++ .../test/offRamp/EVM2EVMOffRampSetup.t.sol | 264 ++ .../ccip/test/onRamp/EVM2EVMMultiOnRamp.t.sol | 720 ++++ .../test/onRamp/EVM2EVMMultiOnRampSetup.t.sol | 180 + .../v0.8/ccip/test/onRamp/EVM2EVMOnRamp.t.sol | 1986 ++++++++++ .../ccip/test/onRamp/EVM2EVMOnRampSetup.t.sol | 261 ++ .../test/pools/BurnFromMintTokenPool.t.sol | 104 + .../v0.8/ccip/test/pools/BurnMintSetup.t.sol | 43 + .../ccip/test/pools/BurnMintTokenPool.t.sol | 171 + .../pools/BurnWithFromMintTokenPool.t.sol | 105 + .../test/pools/LockReleaseTokenPool.t.sol | 512 +++ .../src/v0.8/ccip/test/pools/TokenPool.t.sol | 767 ++++ .../v0.8/ccip/test/pools/USDCTokenPool.t.sol | 690 ++++ .../test/priceRegistry/PriceRegistry.t.sol | 2542 ++++++++++++ .../rateLimiter/AggregateRateLimiter.t.sol | 234 ++ .../MultiAggregateRateLimiter.t.sol | 1201 ++++++ .../src/v0.8/ccip/test/router/Router.t.sol | 889 +++++ .../v0.8/ccip/test/router/RouterSetup.t.sol | 47 + .../RegistryModuleOwnerCustom.t.sol | 104 + .../TokenAdminRegistry.t.sol | 393 ++ .../RegistryModuleOwnerCustom.sol | 54 + .../tokenAdminRegistry/TokenAdminRegistry.sol | 223 ++ .../src/v0.8/ccip/v1.4-CCIP-License-grants.md | 5 + .../liquiditymanager/LiquidityManager.sol | 575 +++ .../ArbitrumL1BridgeAdapter.sol | 175 + .../ArbitrumL2BridgeAdapter.sol | 78 + .../OptimismL1BridgeAdapter.sol | 196 + .../OptimismL2BridgeAdapter.sol | 119 + .../OptimismL1BridgeAdapterEncoder.sol | 21 + .../liquiditymanager/interfaces/IBridge.sol | 49 + .../interfaces/ILiquidityContainer.sol | 16 + .../interfaces/ILiquidityManager.sol | 62 + .../IAbstractArbitrumTokenGateway.sol | 7 + .../interfaces/arbitrum/IArbRollupCore.sol | 7 + .../interfaces/arbitrum/IArbSys.sol | 7 + .../arbitrum/IArbitrumGatewayRouter.sol | 7 + .../interfaces/arbitrum/IArbitrumInbox.sol | 7 + .../arbitrum/IArbitrumL1GatewayRouter.sol | 7 + .../arbitrum/IArbitrumTokenGateway.sol | 7 + .../arbitrum/IL2ArbitrumGateway.sol | 7 + .../arbitrum/IL2ArbitrumMessenger.sol | 7 + .../interfaces/arbitrum/INodeInterface.sol | 7 + .../interfaces/optimism/DisputeTypes.sol | 23 + .../IOptimismCrossDomainMessenger.sol | 31 + .../optimism/IOptimismDisputeGameFactory.sol | 31 + .../optimism/IOptimismL1StandardBridge.sol | 18 + .../optimism/IOptimismL2OutputOracle.sol | 19 + .../optimism/IOptimismL2ToL1MessagePasser.sol | 23 + .../interfaces/optimism/IOptimismPortal.sol | 26 + .../interfaces/optimism/IOptimismPortal2.sol | 12 + .../optimism/IOptimismStandardBridge.sol | 40 + .../interfaces/optimism/Types.sol | 72 + .../liquiditymanager/ocr/OCR3Abstract.sol | 108 + .../v0.8/liquiditymanager/ocr/OCR3Base.sol | 284 ++ .../test/LiquidityManager.t.sol | 945 +++++ .../test/LiquidityManagerBaseTest.t.sol | 43 + .../ArbitrumL1BridgeAdapter.t.sol | 98 + .../ArbitrumL2BridgeAdapter.t.sol | 51 + .../OptimismL1BridgeAdapter.t.sol | 129 + .../test/helpers/LiquidityManagerHelper.sol | 22 + .../test/helpers/OCR3Helper.sol | 41 + .../test/helpers/ReportEncoder.sol | 10 + .../test/mocks/MockBridgeAdapter.sol | 191 + .../liquiditymanager/test/mocks/NoOpOCR3.sol | 18 + .../liquiditymanager/test/ocr/OCR3Base.t.sol | 337 ++ .../liquiditymanager/test/ocr/OCR3Setup.t.sol | 34 + .../utils/introspection/ERC165Checker.sol | 127 + 233 files changed, 49601 insertions(+), 87 deletions(-) create mode 100644 .github/actions/detect-solidity-readonly-file-changes/action.yml create mode 100644 contracts/.changeset/three-stingrays-compete.md create mode 100644 contracts/gas-snapshots/ccip.gas-snapshot create mode 100644 contracts/gas-snapshots/liquiditymanager.gas-snapshot create mode 100755 contracts/scripts/ccip_lcov_prune create mode 100755 contracts/scripts/native_solc_compile_all_ccip create mode 100755 contracts/scripts/native_solc_compile_all_liquiditymanager create mode 100644 contracts/src/v0.8/ccip/ARMProxy.sol create mode 100644 contracts/src/v0.8/ccip/AggregateRateLimiter.sol create mode 100644 contracts/src/v0.8/ccip/CommitStore.sol create mode 100644 contracts/src/v0.8/ccip/LICENSE-MIT.md create mode 100644 contracts/src/v0.8/ccip/LICENSE.md create mode 100644 contracts/src/v0.8/ccip/MultiAggregateRateLimiter.sol create mode 100644 contracts/src/v0.8/ccip/NonceManager.sol create mode 100644 contracts/src/v0.8/ccip/PriceRegistry.sol create mode 100644 contracts/src/v0.8/ccip/RMN.sol create mode 100644 contracts/src/v0.8/ccip/Router.sol create mode 100644 contracts/src/v0.8/ccip/applications/CCIPClientExample.sol create mode 100644 contracts/src/v0.8/ccip/applications/CCIPReceiver.sol create mode 100644 contracts/src/v0.8/ccip/applications/DefensiveExample.sol create mode 100644 contracts/src/v0.8/ccip/applications/EtherSenderReceiver.sol create mode 100644 contracts/src/v0.8/ccip/applications/PingPongDemo.sol create mode 100644 contracts/src/v0.8/ccip/applications/SelfFundedPingPong.sol create mode 100644 contracts/src/v0.8/ccip/applications/TokenProxy.sol create mode 100644 contracts/src/v0.8/ccip/capability/CCIPConfig.sol create mode 100644 contracts/src/v0.8/ccip/capability/interfaces/ICapabilitiesRegistry.sol create mode 100644 contracts/src/v0.8/ccip/capability/interfaces/IOCR3ConfigEncoder.sol create mode 100644 contracts/src/v0.8/ccip/capability/libraries/CCIPConfigTypes.sol create mode 100644 contracts/src/v0.8/ccip/docs/multi-chain-overview-ocr3.png create mode 100644 contracts/src/v0.8/ccip/docs/multi-chain-overview.drawio create mode 100644 contracts/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IAny2EVMOffRamp.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/ICommitStore.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IEVM2AnyOnRamp.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IEVM2AnyOnRampClient.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IGetCCIPAdmin.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IMessageInterceptor.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/INonceManager.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IOwner.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IPool.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IPoolPriorTo1_5.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IPriceRegistry.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IRMN.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IRouter.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IRouterClient.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/ITokenAdminRegistry.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/IWrappedNative.sol create mode 100644 contracts/src/v0.8/ccip/interfaces/automation/ILinkAvailable.sol create mode 100644 contracts/src/v0.8/ccip/libraries/Client.sol create mode 100644 contracts/src/v0.8/ccip/libraries/Internal.sol create mode 100644 contracts/src/v0.8/ccip/libraries/MerkleMultiProof.sol create mode 100644 contracts/src/v0.8/ccip/libraries/Pool.sol create mode 100644 contracts/src/v0.8/ccip/libraries/RateLimiter.sol create mode 100644 contracts/src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol create mode 100644 contracts/src/v0.8/ccip/ocr/MultiOCR3Base.sol create mode 100644 contracts/src/v0.8/ccip/ocr/OCR2Abstract.sol create mode 100644 contracts/src/v0.8/ccip/ocr/OCR2Base.sol create mode 100644 contracts/src/v0.8/ccip/ocr/OCR2BaseNoChecks.sol create mode 100644 contracts/src/v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol create mode 100644 contracts/src/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol create mode 100644 contracts/src/v0.8/ccip/onRamp/EVM2EVMMultiOnRamp.sol create mode 100644 contracts/src/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol create mode 100644 contracts/src/v0.8/ccip/pools/BurnFromMintTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/pools/BurnMintTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol create mode 100644 contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAndProxy.sol create mode 100644 contracts/src/v0.8/ccip/pools/BurnWithFromMintTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/pools/LegacyPoolWrapper.sol create mode 100644 contracts/src/v0.8/ccip/pools/LockReleaseTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/pools/LockReleaseTokenPoolAndProxy.sol create mode 100644 contracts/src/v0.8/ccip/pools/TokenPool.sol create mode 100644 contracts/src/v0.8/ccip/pools/USDC/IMessageTransmitter.sol create mode 100644 contracts/src/v0.8/ccip/pools/USDC/ITokenMessenger.sol create mode 100644 contracts/src/v0.8/ccip/pools/USDC/USDCTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/test/BaseTest.t.sol create mode 100644 contracts/src/v0.8/ccip/test/NonceManager.t.sol create mode 100644 contracts/src/v0.8/ccip/test/README.md create mode 100644 contracts/src/v0.8/ccip/test/TokenSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/WETH9.sol create mode 100644 contracts/src/v0.8/ccip/test/applications/DefensiveExample.t.sol create mode 100644 contracts/src/v0.8/ccip/test/applications/EtherSenderReceiver.t.sol create mode 100644 contracts/src/v0.8/ccip/test/applications/ImmutableExample.t.sol create mode 100644 contracts/src/v0.8/ccip/test/applications/PingPongDemo.t.sol create mode 100644 contracts/src/v0.8/ccip/test/applications/SelfFundedPingPong.t.sol create mode 100644 contracts/src/v0.8/ccip/test/applications/TokenProxy.t.sol create mode 100644 contracts/src/v0.8/ccip/test/arm/ARMProxy.t.sol create mode 100644 contracts/src/v0.8/ccip/test/arm/ARMProxy_standalone.t.sol create mode 100644 contracts/src/v0.8/ccip/test/arm/RMN.t.sol create mode 100644 contracts/src/v0.8/ccip/test/arm/RMNSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/arm/RMN_benchmark.t.sol create mode 100644 contracts/src/v0.8/ccip/test/attacks/onRamp/FacadeClient.sol create mode 100644 contracts/src/v0.8/ccip/test/attacks/onRamp/MultiOnRampTokenPoolReentrancy.t.sol create mode 100644 contracts/src/v0.8/ccip/test/attacks/onRamp/OnRampTokenPoolReentrancy.t.sol create mode 100644 contracts/src/v0.8/ccip/test/attacks/onRamp/ReentrantMaliciousTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/test/capability/CCIPConfig.t.sol create mode 100644 contracts/src/v0.8/ccip/test/commitStore/CommitStore.t.sol create mode 100644 contracts/src/v0.8/ccip/test/e2e/End2End.t.sol create mode 100644 contracts/src/v0.8/ccip/test/e2e/MultiRampsEnd2End.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/AggregateRateLimiterHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/BurnMintERC677Helper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/BurnMintMultiTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/CCIPConfigHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/CommitStoreHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOffRampHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOnRampHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/EVM2EVMOffRampHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/EVM2EVMOnRampHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/EtherSenderReceiverHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/IgnoreContractSize.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/MaybeRevertingBurnMintTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/MerkleHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/MessageHasher.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/MessageInterceptorHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/MultiAggregateRateLimiterHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/MultiOCR3Helper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/MultiTokenPool.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/OCR2Helper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/OCR2NoChecksHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/PriceRegistryHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/RateLimiterHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/ReportCodec.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/TokenPoolHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/USDCTokenPoolHelper.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/receivers/ConformingReceiver.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/receivers/MaybeRevertMessageReceiver.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/receivers/MaybeRevertMessageReceiverNo165.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/receivers/ReentrancyAbuser.sol create mode 100644 contracts/src/v0.8/ccip/test/helpers/receivers/ReentrancyAbuserMultiRamp.sol create mode 100644 contracts/src/v0.8/ccip/test/legacy/BurnMintTokenPool1_2.sol create mode 100644 contracts/src/v0.8/ccip/test/legacy/BurnMintTokenPool1_4.sol create mode 100644 contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol create mode 100644 contracts/src/v0.8/ccip/test/libraries/MerkleMultiProof.t.sol create mode 100644 contracts/src/v0.8/ccip/test/libraries/RateLimiter.t.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/MockCommitStore.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/MockE2EUSDCTokenMessenger.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/MockE2EUSDCTransmitter.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/MockRMN.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/MockRMN1_0.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/MockRouter.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/MockUSDCTokenMessenger.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/interfaces/IMessageTransmitterWithRelay.sol create mode 100644 contracts/src/v0.8/ccip/test/mocks/test/MockRouterTest.t.sol create mode 100644 contracts/src/v0.8/ccip/test/ocr/MultiOCR3Base.t.sol create mode 100644 contracts/src/v0.8/ccip/test/ocr/MultiOCR3BaseSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/ocr/OCR2Base.t.sol create mode 100644 contracts/src/v0.8/ccip/test/ocr/OCR2BaseNoChecks.t.sol create mode 100644 contracts/src/v0.8/ccip/test/ocr/OCR2Setup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/offRamp/EVM2EVMMultiOffRamp.t.sol create mode 100644 contracts/src/v0.8/ccip/test/offRamp/EVM2EVMMultiOffRampSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/offRamp/EVM2EVMOffRamp.t.sol create mode 100644 contracts/src/v0.8/ccip/test/offRamp/EVM2EVMOffRampSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/onRamp/EVM2EVMMultiOnRamp.t.sol create mode 100644 contracts/src/v0.8/ccip/test/onRamp/EVM2EVMMultiOnRampSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/onRamp/EVM2EVMOnRamp.t.sol create mode 100644 contracts/src/v0.8/ccip/test/onRamp/EVM2EVMOnRampSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/pools/BurnFromMintTokenPool.t.sol create mode 100644 contracts/src/v0.8/ccip/test/pools/BurnMintSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/pools/BurnMintTokenPool.t.sol create mode 100644 contracts/src/v0.8/ccip/test/pools/BurnWithFromMintTokenPool.t.sol create mode 100644 contracts/src/v0.8/ccip/test/pools/LockReleaseTokenPool.t.sol create mode 100644 contracts/src/v0.8/ccip/test/pools/TokenPool.t.sol create mode 100644 contracts/src/v0.8/ccip/test/pools/USDCTokenPool.t.sol create mode 100644 contracts/src/v0.8/ccip/test/priceRegistry/PriceRegistry.t.sol create mode 100644 contracts/src/v0.8/ccip/test/rateLimiter/AggregateRateLimiter.t.sol create mode 100644 contracts/src/v0.8/ccip/test/rateLimiter/MultiAggregateRateLimiter.t.sol create mode 100644 contracts/src/v0.8/ccip/test/router/Router.t.sol create mode 100644 contracts/src/v0.8/ccip/test/router/RouterSetup.t.sol create mode 100644 contracts/src/v0.8/ccip/test/tokenAdminRegistry/RegistryModuleOwnerCustom.t.sol create mode 100644 contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenAdminRegistry.t.sol create mode 100644 contracts/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol create mode 100644 contracts/src/v0.8/ccip/tokenAdminRegistry/TokenAdminRegistry.sol create mode 100644 contracts/src/v0.8/ccip/v1.4-CCIP-License-grants.md create mode 100644 contracts/src/v0.8/liquiditymanager/LiquidityManager.sol create mode 100644 contracts/src/v0.8/liquiditymanager/bridge-adapters/ArbitrumL1BridgeAdapter.sol create mode 100644 contracts/src/v0.8/liquiditymanager/bridge-adapters/ArbitrumL2BridgeAdapter.sol create mode 100644 contracts/src/v0.8/liquiditymanager/bridge-adapters/OptimismL1BridgeAdapter.sol create mode 100644 contracts/src/v0.8/liquiditymanager/bridge-adapters/OptimismL2BridgeAdapter.sol create mode 100644 contracts/src/v0.8/liquiditymanager/encoders/OptimismL1BridgeAdapterEncoder.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/IBridge.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/ILiquidityContainer.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/ILiquidityManager.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IAbstractArbitrumTokenGateway.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbRollupCore.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbSys.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumGatewayRouter.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumInbox.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumL1GatewayRouter.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumTokenGateway.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IL2ArbitrumGateway.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IL2ArbitrumMessenger.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/INodeInterface.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/DisputeTypes.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismDisputeGameFactory.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL2OutputOracle.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL2ToL1MessagePasser.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismPortal.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismPortal2.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/Types.sol create mode 100644 contracts/src/v0.8/liquiditymanager/ocr/OCR3Abstract.sol create mode 100644 contracts/src/v0.8/liquiditymanager/ocr/OCR3Base.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/LiquidityManager.t.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/LiquidityManagerBaseTest.t.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/bridge-adapters/ArbitrumL1BridgeAdapter.t.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/bridge-adapters/ArbitrumL2BridgeAdapter.t.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/bridge-adapters/OptimismL1BridgeAdapter.t.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/helpers/LiquidityManagerHelper.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/helpers/OCR3Helper.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/helpers/ReportEncoder.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/mocks/MockBridgeAdapter.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/mocks/NoOpOCR3.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/ocr/OCR3Base.t.sol create mode 100644 contracts/src/v0.8/liquiditymanager/test/ocr/OCR3Setup.t.sol create mode 100644 contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol diff --git a/.github/actions/detect-solidity-file-changes/action.yml b/.github/actions/detect-solidity-file-changes/action.yml index 37cb871d68d..b86c91dbb4d 100644 --- a/.github/actions/detect-solidity-file-changes/action.yml +++ b/.github/actions/detect-solidity-file-changes/action.yml @@ -1,5 +1,5 @@ -name: 'Detect Changes Composite Action' -description: 'Detects changes in solidity files and fails if read-only files are modified.' +name: 'Detect Solidity File Changes Composite Action' +description: 'Detects changes in solidity files and outputs the result.' outputs: changes: description: 'Whether or not changes were detected' @@ -19,18 +19,3 @@ runs: - '.github/workflows/solidity.yml' - '.github/workflows/solidity-foundry.yml' - '.github/workflows/solidity-wrappers.yml' - read_only_sol: - - 'contracts/src/v0.8/interfaces/**/*' - - 'contracts/src/v0.8/automation/v1_2/**/*' - - 'contracts/src/v0.8/automation/v1_3/**/*' - - 'contracts/src/v0.8/automation/v2_0/**/*' - - - name: Fail if read-only files have changed - if: ${{ steps.changed_files.outputs.read_only_sol == 'true' }} - shell: bash - run: | - echo "One or more read-only Solidity file(s) has changed." - for file in ${{ steps.changed_files.outputs.read_only_sol_files }}; do - echo "$file was changed" - done - exit 1 diff --git a/.github/actions/detect-solidity-readonly-file-changes/action.yml b/.github/actions/detect-solidity-readonly-file-changes/action.yml new file mode 100644 index 00000000000..faca16d53f0 --- /dev/null +++ b/.github/actions/detect-solidity-readonly-file-changes/action.yml @@ -0,0 +1,31 @@ +name: 'Detect Solidity Readonly Files Changes Composite Action' +description: 'Detects changes in readonly solidity files and fails if they are modified.' +outputs: + changes: + description: 'Whether or not changes were detected' + value: ${{ steps.changed_files.outputs.src }} +runs: + using: 'composite' + steps: + + - name: Filter paths + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changed_files + with: + list-files: 'csv' + filters: | + read_only_sol: + - 'contracts/src/v0.8/interfaces/**/*' + - 'contracts/src/v0.8/automation/v1_2/**/*' + - 'contracts/src/v0.8/automation/v1_3/**/*' + - 'contracts/src/v0.8/automation/v2_0/**/*' + + - name: Fail if read-only files have changed + if: ${{ steps.changed_files.outputs.read_only_sol == 'true' }} + shell: bash + run: | + echo "One or more read-only Solidity file(s) has changed." + for file in ${{ steps.changed_files.outputs.read_only_sol_files }}; do + echo "$file was changed" + done + exit 1 diff --git a/.github/workflows/solidity-foundry.yml b/.github/workflows/solidity-foundry.yml index a7ced0f5653..4ec9e424471 100644 --- a/.github/workflows/solidity-foundry.yml +++ b/.github/workflows/solidity-foundry.yml @@ -3,6 +3,8 @@ on: [pull_request] env: FOUNDRY_PROFILE: ci + # Has to match the `make foundry` version in `contracts/GNUmakefile` + FOUNDRY_VERSION: nightly-de33b6af53005037b463318d2628b5cfcaf39916 jobs: changes: @@ -27,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - product: [automation, functions, keystone, l2ep, llo-feeds, operatorforwarder, shared, vrf] + product: [automation, ccip, functions, keystone, l2ep, liquiditymanager, llo-feeds, operatorforwarder, shared, vrf] needs: [changes] name: Foundry Tests ${{ matrix.product }} # See https://github.com/foundry-rs/foundry/issues/3827 @@ -52,8 +54,7 @@ jobs: if: needs.changes.outputs.changes == 'true' uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 with: - # Has to match the `make foundry` version. - version: nightly-de33b6af53005037b463318d2628b5cfcaf39916 + version: ${{ env.FOUNDRY_VERSION }} - name: Run Forge build if: needs.changes.outputs.changes == 'true' @@ -77,12 +78,36 @@ jobs: - name: Run Forge snapshot if: ${{ !contains(fromJson('["vrf"]'), matrix.product) && !contains(fromJson('["automation"]'), matrix.product) && !contains(fromJson('["keystone"]'), matrix.product) && needs.changes.outputs.changes == 'true' }} run: | - forge snapshot --nmt "testFuzz_\w{1,}?" --check gas-snapshots/${{ matrix.product }}.gas-snapshot + forge snapshot --nmt "test_?Fuzz_\w{1,}?" --check gas-snapshots/${{ matrix.product }}.gas-snapshot id: snapshot working-directory: contracts env: FOUNDRY_PROFILE: ${{ matrix.product }} + - name: Run coverage + if: ${{ contains(fromJson('["ccip"]'), matrix.product) && needs.changes.outputs.changes == 'true' }} + working-directory: contracts + run: forge coverage --report lcov + env: + FOUNDRY_PROFILE: ${{ matrix.product }} + + - name: Prune report + if: ${{ contains(fromJson('["ccip"]'), matrix.product) && needs.changes.outputs.changes == 'true' }} + run: | + sudo apt-get install lcov + ./contracts/scripts/ccip_lcov_prune ./contracts/lcov.info ./lcov.info.pruned + + - name: Report code coverage + if: ${{ contains(fromJson('["ccip"]'), matrix.product) && needs.changes.outputs.changes == 'true' }} + uses: zgosalvez/github-actions-report-lcov@a546f89a65a0cdcd82a92ae8d65e74d450ff3fbc # v4.1.4 + with: + update-comment: true + coverage-files: lcov.info.pruned + minimum-coverage: 98.5 + artifact-name: code-coverage-report + working-directory: ./contracts + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Collect Metrics if: needs.changes.outputs.changes == 'true' id: collect-gha-metrics @@ -94,3 +119,55 @@ jobs: hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} this-job-name: Foundry Tests ${{ matrix.product }} continue-on-error: true + + solidity-forge-fmt: + strategy: + fail-fast: false + matrix: + product: [ ccip ] + needs: [ changes ] + name: Forge fmt ${{ matrix.product }} + # See https://github.com/foundry-rs/foundry/issues/3827 + runs-on: ubuntu-22.04 + + # The if statements for steps after checkout repo is workaround for + # passing required check for PRs that don't have filtered changes. + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + submodules: recursive + + # Only needed because we use the NPM versions of packages + # and not native Foundry. This is to make sure the dependencies + # stay in sync. + - name: Setup NodeJS + if: needs.changes.outputs.changes == 'true' + uses: ./.github/actions/setup-nodejs + + - name: Install Foundry + if: needs.changes.outputs.changes == 'true' + uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 + with: + version: ${{ env.FOUNDRY_VERSION }} + + - name: Run Forge fmt + if: needs.changes.outputs.changes == 'true' + run: | + forge fmt --check + id: fmt + working-directory: contracts + env: + FOUNDRY_PROFILE: ${{ matrix.product }} + + - name: Collect Metrics + if: needs.changes.outputs.changes == 'true' + id: collect-gha-metrics + uses: smartcontractkit/push-gha-metrics-action@dea9b546553cb4ca936607c2267a09c004e4ab3f # v3.0.0 + with: + id: solidity-forge-fmt + org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} + basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} + hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} + this-job-name: Foundry Tests ${{ matrix.product }} + continue-on-error: true diff --git a/.github/workflows/solidity-hardhat.yml b/.github/workflows/solidity-hardhat.yml index fb6ba6fef43..f28cf499072 100644 --- a/.github/workflows/solidity-hardhat.yml +++ b/.github/workflows/solidity-hardhat.yml @@ -25,7 +25,7 @@ jobs: with: filters: | src: - - 'contracts/src/!(v0.8/(ccip|functions|keystone|l2ep|llo-feeds|transmission|vrf)/**)/**/*' + - 'contracts/src/!(v0.8/(ccip|functions|keystone|l2ep|liquiditymanager|llo-feeds|transmission|vrf)/**)/**/*' - 'contracts/test/**/*' - 'contracts/package.json' - 'contracts/pnpm-lock.yaml' diff --git a/.github/workflows/solidity.yml b/.github/workflows/solidity.yml index dff35b3cc93..10193bfc2ec 100644 --- a/.github/workflows/solidity.yml +++ b/.github/workflows/solidity.yml @@ -9,6 +9,18 @@ defaults: shell: bash jobs: + readonly_changes: + name: Detect readonly solidity file changes + runs-on: ubuntu-latest + outputs: + changes: ${{ steps.ch.outputs.changes }} + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Detect readonly solidity file changes + id: ch + uses: ./.github/actions/detect-solidity-readonly-file-changes + changes: name: Detect changes runs-on: ubuntu-latest @@ -31,15 +43,15 @@ jobs: release-version: ${{ steps.release-tag-check.outputs.release-version }} pre-release-version: ${{ steps.release-tag-check.outputs.pre-release-version }} steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - name: Check release tag id: release-tag-check - uses: smartcontractkit/chainlink-github-actions/release/release-tag-check@2031e56eb4edb8115ce8ba07cbbfb457149d865d # v2.3.8 + uses: smartcontractkit/chainlink-github-actions/release/release-tag-check@5dd916d08c03cb5f9a97304f4f174820421bb946 # v2.3.11 env: # Match semver git tags with a "contracts-" prefix. RELEASE_REGEX: '^contracts-v[0-9]+\.[0-9]+\.[0-9]+$' PRE_RELEASE_REGEX: '^contracts-v[0-9]+\.[0-9]+\.[0-9]+-(.+)$' - # Get the version by stripping the "contracts-v" prefix. + # Get the version by stripping the "contracts-v" prefix. VERSION_PREFIX: 'contracts-v' prepublish-test: @@ -171,7 +183,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - name: Setup NodeJS uses: ./.github/actions/setup-nodejs @@ -211,7 +223,7 @@ jobs: contents: write steps: - name: Checkout the repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - name: Setup NodeJS uses: ./.github/actions/setup-nodejs diff --git a/LICENSE b/LICENSE index 1fa3822f510..9723bc8be9a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,8 @@ -The MIT License (MIT) +Copyright (c) 2018 SmartContract ChainLink Limited SEZC + +Portions of this software are licensed as follows: -Copyright (c) 2018 SmartContract ChainLink, Ltd. +The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +21,12 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +*All content residing under (1) “/contracts/src/v0.8/ccip”; (2) +“/core/services/ocr2/plugins/ccip” are licensed under “Business Source +License 1.1” with a Change Date of May 23, 2027 and Change License to + “MIT License” + +* Content outside of the above mentioned directories or restrictions +above is available under the "MIT" license as defined above. \ No newline at end of file diff --git a/contracts/.changeset/three-stingrays-compete.md b/contracts/.changeset/three-stingrays-compete.md new file mode 100644 index 00000000000..613b2784657 --- /dev/null +++ b/contracts/.changeset/three-stingrays-compete.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': minor +--- + +add ccip contracts to the repo diff --git a/contracts/.prettierignore b/contracts/.prettierignore index 7c3131db424..440cf95afa2 100644 --- a/contracts/.prettierignore +++ b/contracts/.prettierignore @@ -21,6 +21,7 @@ solc LinkToken.json typechain **/vendor +src/v0.8/ccip/** # Ignore TS definition and map files **/**.d.ts @@ -35,4 +36,4 @@ venv/ .solhint.json src/v0.8/mocks/FunctionsOracleEventsMock.sol -src/v0.8/mocks/FunctionsBillingRegistryEventsMock.sol \ No newline at end of file +src/v0.8/mocks/FunctionsBillingRegistryEventsMock.sol diff --git a/contracts/.prettierrc.js b/contracts/.prettierrc.js index 774a5e964e8..17662841223 100644 --- a/contracts/.prettierrc.js +++ b/contracts/.prettierrc.js @@ -5,6 +5,7 @@ module.exports = { endOfLine: 'auto', tabWidth: 2, trailingComma: 'all', + plugins: ['prettier-plugin-solidity'], overrides: [ { files: '*.sol', diff --git a/contracts/.solhintignore b/contracts/.solhintignore index bab41a57940..bad1935442b 100644 --- a/contracts/.solhintignore +++ b/contracts/.solhintignore @@ -1,6 +1,3 @@ -# 344 warnings -#./src/v0.8/automation - # Ignore frozen Automation code ./src/v0.8/automation/v1_2 ./src/v0.8/automation/interfaces/v1_2 @@ -39,6 +36,7 @@ ./src/v0.8/llo-feeds/test ./src/v0.8/vrf/testhelpers ./src/v0.8/functions/tests +./src/v0.8/ccip/test # Always ignore vendor ./src/v0.8/vendor diff --git a/contracts/GNUmakefile b/contracts/GNUmakefile index c3e69464698..0ebad8446e5 100644 --- a/contracts/GNUmakefile +++ b/contracts/GNUmakefile @@ -1,6 +1,6 @@ # ALL_FOUNDRY_PRODUCTS contains a list of all products that have a foundry -# profile defined and use the Foundry snapshots. -ALL_FOUNDRY_PRODUCTS = functions keystone l2ep llo-feeds operatorforwarder shared transmission +# profile defined and use the Foundry snapshots. +ALL_FOUNDRY_PRODUCTS = ccip functions keystone l2ep liquiditymanager llo-feeds operatorforwarder shared transmission # To make a snapshot for a specific product, either set the `FOUNDRY_PROFILE` env var # or call the target with `FOUNDRY_PROFILE=product` @@ -16,11 +16,11 @@ ALL_FOUNDRY_PRODUCTS = functions keystone l2ep llo-feeds operatorforwarder share # a static fuzz seed by default, flaky gas results per platform are still observed. .PHONY: snapshot snapshot: ## Make a snapshot for a specific product. - export FOUNDRY_PROFILE=$(FOUNDRY_PROFILE) && forge snapshot --nmt "testFuzz_\w{1,}?" --snap gas-snapshots/$(FOUNDRY_PROFILE).gas-snapshot + export FOUNDRY_PROFILE=$(FOUNDRY_PROFILE) && forge snapshot --nmt "test_?Fuzz_\w{1,}?" --snap gas-snapshots/$(FOUNDRY_PROFILE).gas-snapshot .PHONY: snapshot-diff snapshot-diff: ## Make a snapshot for a specific product. - export FOUNDRY_PROFILE=$(FOUNDRY_PROFILE) && forge snapshot --nmt "testFuzz_\w{1,}?" --diff gas-snapshots/$(FOUNDRY_PROFILE).gas-snapshot + export FOUNDRY_PROFILE=$(FOUNDRY_PROFILE) && forge snapshot --nmt "test_?Fuzz_\w{1,}?" --diff gas-snapshots/$(FOUNDRY_PROFILE).gas-snapshot .PHONY: snapshot-all @@ -50,6 +50,21 @@ foundry-refresh: foundry git submodule deinit -f . git submodule update --init --recursive +ccip-precommit: export FOUNDRY_PROFILE=ccip +.PHONY: ccip-precommit +ccip-precommit: + forge test + make snapshot + forge fmt + pnpm solhint + +ccip-lcov: export FOUNDRY_PROFILE=ccip +.PHONY: ccip-lcov +ccip-lcov: + forge coverage --report lcov + ../tools/ci/ccip_lcov_prune ./lcov.info ./lcov.info.pruned + genhtml -o report lcov.info.pruned --branch-coverage + # To generate gethwrappers for a specific product, either set the `FOUNDRY_PROFILE` # env var or call the target with `FOUNDRY_PROFILE=product` # This uses FOUNDRY_PROFILE, even though it does support non-foundry products. This diff --git a/contracts/README.md b/contracts/README.md index 26b0a823298..182891ceef7 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -67,5 +67,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## License +Most of the contracts are licensed under the [MIT](https://choosealicense.com/licenses/mit/) license. +An exception to this is the ccip folder, which defaults to be licensed under the [BUSL-1.1](./src/v0.8/ccip/LICENSE.md) license, however, there are a few exceptions -[MIT](https://choosealicense.com/licenses/mit/) +- `src/v0.8/ccip/applications/*` is licensed under the [MIT](./src/v0.8/ccip/LICENSE-MIT.md) license +- `src/v0.8/ccip/interfaces/*` is licensed under the [MIT](./src/v0.8/ccip/LICENSE-MIT.md) license +- `src/v0.8/ccip/libraries/{Client.sol, Internal.sol}` is licensed under the [MIT](./src/v0.8/ccip/LICENSE-MIT.md) license \ No newline at end of file diff --git a/contracts/STYLE_GUIDE.md b/contracts/STYLE_GUIDE.md index b9294de5765..c5dc20abeab 100644 --- a/contracts/STYLE_GUIDE.md +++ b/contracts/STYLE_GUIDE.md @@ -1,7 +1,7 @@ # Structure -This guide is split into two sections: [Guidelines](#guidelines) and [Rules](#rules). -Guidelines are recommendations that should be followed but are hard to enforce in an automated way. +This guide is split into two sections: [Guidelines](#guidelines) and [Rules](#rules). +Guidelines are recommendations that should be followed but are hard to enforce in an automated way. Rules are all enforced through CI, this can be through Solhint rules or other tools. ## Background @@ -76,11 +76,11 @@ uint256 networkFeeUSDCents; // good struct FeeTokenConfigArgs { address token; // ────────────╮ Token address uint32 networkFeeUSD; // │ Flat network fee to charge for messages, multiples of 0.01 USD - // │ multiline comments should work like this. More fee info + // │ multiline comments should work like this. More fee info uint64 gasMultiplier; // ─────╯ Price multiplier for gas costs, 1e18 based so 11e17 = 10% extra cost uint64 premiumMultiplier; // ─╮ Multiplier for fee-token-specific premiums bool enabled; // ─────────────╯ Whether this fee token is enabled - uint256 fee; // The flat fee the user pays in juels + uint256 fee; // The flat fee the user pays in juels } ``` ## Functions @@ -132,7 +132,7 @@ assembly { // call and return whether we succeeded. ignore return data // call(gas,addr,value,argsOffset,argsLength,retOffset,retLength) success := call(gasLimit, target, 0, add(payload, 0x20), mload(payload), 0, 0) - + // limit our copy to maxReturnBytes bytes let toCopy := returndatasize() if gt(toCopy, maxReturnBytes) { @@ -242,7 +242,7 @@ contract AccessControlledFoo is Foo { contract OffchainAggregator is ITypeAndVersion { string public constant override typeAndVersion = "OffchainAggregator 1.0.0"; - + function getData() public returns(uint256) { return 4; } @@ -310,8 +310,8 @@ import {IPool} from "../interfaces/pools/IPool.sol"; import {AggregateRateLimiter} from "../AggregateRateLimiter.sol"; import {Client} from "../libraries/Client.sol"; -import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.0/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.0/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; ``` ## Variables diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 08940b4e9ff..c755ba6437b 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -16,6 +16,19 @@ gas_price = 1 block_timestamp = 1234567890 block_number = 12345 +[fmt] +tab_width = 2 +multiline_func_header = "params_first" +sort_imports = true +single_line_statement_blocks = "preserve" + +[profile.ccip] +solc_version = '0.8.24' +src = 'src/v0.8/ccip' +test = 'src/v0.8/ccip/test' +optimizer_runs = 3_600 +evm_version = 'paris' + [profile.functions] solc_version = '0.8.19' src = 'src/v0.8/functions/dev/v1_X' @@ -52,6 +65,13 @@ src = 'src/v0.8/llo-feeds' test = 'src/v0.8/llo-feeds/test' solc_version = '0.8.19' +[profile.liquiditymanager] +optimizer_runs = 1000000 +src = 'src/v0.8/liquiditymanager' +test = 'src/v0.8/liquiditymanager/test' +solc_version = '0.8.24' +evm_version = 'paris' + [profile.keystone] optimizer_runs = 1_000_000 solc_version = '0.8.24' diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot new file mode 100644 index 00000000000..5fc99a9a409 --- /dev/null +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -0,0 +1,943 @@ +ARMProxyStandaloneTest:test_ARMCallEmptyContractRevert() (gas: 19600) +ARMProxyStandaloneTest:test_Constructor() (gas: 374544) +ARMProxyStandaloneTest:test_SetARM() (gas: 16494) +ARMProxyStandaloneTest:test_SetARMzero() (gas: 11216) +ARMProxyTest:test_ARMCallRevertReasonForwarded() (gas: 47793) +ARMProxyTest:test_ARMIsBlessed_Success() (gas: 36269) +ARMProxyTest:test_ARMIsCursed_Success() (gas: 49740) +AggregateTokenLimiter_constructor:test_Constructor_Success() (gas: 26920) +AggregateTokenLimiter_getTokenBucket:test_GetTokenBucket_Success() (gas: 19691) +AggregateTokenLimiter_getTokenBucket:test_Refill_Success() (gas: 40911) +AggregateTokenLimiter_getTokenBucket:test_TimeUnderflow_Revert() (gas: 15368) +AggregateTokenLimiter_getTokenLimitAdmin:test_GetTokenLimitAdmin_Success() (gas: 10531) +AggregateTokenLimiter_getTokenValue:test_GetTokenValue_Success() (gas: 19696) +AggregateTokenLimiter_getTokenValue:test_NoTokenPrice_Reverts() (gas: 21281) +AggregateTokenLimiter_rateLimitValue:test_AggregateValueMaxCapacityExceeded_Revert() (gas: 16418) +AggregateTokenLimiter_rateLimitValue:test_RateLimitValueSuccess_gas() (gas: 18306) +AggregateTokenLimiter_setAdmin:test_OnlyOwnerOrAdmin_Revert() (gas: 13047) +AggregateTokenLimiter_setAdmin:test_Owner_Success() (gas: 18989) +AggregateTokenLimiter_setRateLimiterConfig:test_OnlyOnlyCallableByAdminOrOwner_Revert() (gas: 17479) +AggregateTokenLimiter_setRateLimiterConfig:test_Owner_Success() (gas: 30062) +AggregateTokenLimiter_setRateLimiterConfig:test_TokenLimitAdmin_Success() (gas: 32071) +BurnFromMintTokenPool_lockOrBurn:test_ChainNotAllowed_Revert() (gas: 28675) +BurnFromMintTokenPool_lockOrBurn:test_PoolBurnRevertNotHealthy_Revert() (gas: 55158) +BurnFromMintTokenPool_lockOrBurn:test_PoolBurn_Success() (gas: 243525) +BurnFromMintTokenPool_lockOrBurn:test_Setup_Success() (gas: 23907) +BurnMintTokenPool_lockOrBurn:test_ChainNotAllowed_Revert() (gas: 27565) +BurnMintTokenPool_lockOrBurn:test_PoolBurnRevertNotHealthy_Revert() (gas: 55158) +BurnMintTokenPool_lockOrBurn:test_PoolBurn_Success() (gas: 241416) +BurnMintTokenPool_lockOrBurn:test_Setup_Success() (gas: 17633) +BurnMintTokenPool_releaseOrMint:test_ChainNotAllowed_Revert() (gas: 28537) +BurnMintTokenPool_releaseOrMint:test_PoolMintNotHealthy_Revert() (gas: 55991) +BurnMintTokenPool_releaseOrMint:test_PoolMint_Success() (gas: 110657) +BurnWithFromMintTokenPool_lockOrBurn:test_ChainNotAllowed_Revert() (gas: 28675) +BurnWithFromMintTokenPool_lockOrBurn:test_PoolBurnRevertNotHealthy_Revert() (gas: 55158) +BurnWithFromMintTokenPool_lockOrBurn:test_PoolBurn_Success() (gas: 243552) +BurnWithFromMintTokenPool_lockOrBurn:test_Setup_Success() (gas: 24260) +CCIPClientExample_sanity:test_ImmutableExamples_Success() (gas: 2131281) +CCIPConfigSetup:test_getCapabilityConfiguration_Success() (gas: 9495) +CCIPConfig_ConfigStateMachine:test__computeConfigDigest_Success() (gas: 70755) +CCIPConfig_ConfigStateMachine:test__computeNewConfigWithMeta_InitToRunning_Success() (gas: 363647) +CCIPConfig_ConfigStateMachine:test__computeNewConfigWithMeta_RunningToStaging_Success() (gas: 488774) +CCIPConfig_ConfigStateMachine:test__computeNewConfigWithMeta_StagingToRunning_Success() (gas: 453384) +CCIPConfig_ConfigStateMachine:test__groupByPluginType_TooManyOCR3Configs_Reverts() (gas: 37027) +CCIPConfig_ConfigStateMachine:test__groupByPluginType_threeCommitConfigs_Reverts() (gas: 61043) +CCIPConfig_ConfigStateMachine:test__groupByPluginType_threeExecutionConfigs_Reverts() (gas: 60963) +CCIPConfig_ConfigStateMachine:test__stateFromConfigLength_Success() (gas: 11764) +CCIPConfig_ConfigStateMachine:test__validateConfigStateTransition_Success() (gas: 8765) +CCIPConfig_ConfigStateMachine:test__validateConfigTransition_InitToRunning_Success() (gas: 311991) +CCIPConfig_ConfigStateMachine:test__validateConfigTransition_InitToRunning_WrongConfigCount_Reverts() (gas: 49663) +CCIPConfig_ConfigStateMachine:test__validateConfigTransition_NonExistentConfigTransition_Reverts() (gas: 32275) +CCIPConfig_ConfigStateMachine:test__validateConfigTransition_RunningToStaging_Success() (gas: 376576) +CCIPConfig_ConfigStateMachine:test__validateConfigTransition_RunningToStaging_WrongConfigCount_Reverts() (gas: 120943) +CCIPConfig_ConfigStateMachine:test__validateConfigTransition_RunningToStaging_WrongConfigDigestBlueGreen_Reverts() (gas: 157105) +CCIPConfig_ConfigStateMachine:test__validateConfigTransition_StagingToRunning_Success() (gas: 376352) +CCIPConfig_ConfigStateMachine:test__validateConfigTransition_StagingToRunning_WrongConfigDigest_Reverts() (gas: 157172) +CCIPConfig_ConfigStateMachine:test_getCapabilityConfiguration_Success() (gas: 9583) +CCIPConfig__updatePluginConfig:test__updatePluginConfig_InitToRunning_Success() (gas: 1057393) +CCIPConfig__updatePluginConfig:test__updatePluginConfig_InvalidConfigLength_Reverts() (gas: 27539) +CCIPConfig__updatePluginConfig:test__updatePluginConfig_InvalidConfigStateTransition_Reverts() (gas: 23105) +CCIPConfig__updatePluginConfig:test__updatePluginConfig_RunningToStaging_Success() (gas: 2009309) +CCIPConfig__updatePluginConfig:test__updatePluginConfig_StagingToRunning_Success() (gas: 2616177) +CCIPConfig__updatePluginConfig:test_getCapabilityConfiguration_Success() (gas: 9583) +CCIPConfig_beforeCapabilityConfigSet:test_beforeCapabilityConfigSet_CommitAndExecConfig_Success() (gas: 1851188) +CCIPConfig_beforeCapabilityConfigSet:test_beforeCapabilityConfigSet_CommitConfigOnly_Success() (gas: 1068362) +CCIPConfig_beforeCapabilityConfigSet:test_beforeCapabilityConfigSet_ExecConfigOnly_Success() (gas: 1068393) +CCIPConfig_beforeCapabilityConfigSet:test_beforeCapabilityConfigSet_OnlyCapabilitiesRegistryCanCall_Reverts() (gas: 9599) +CCIPConfig_beforeCapabilityConfigSet:test_beforeCapabilityConfigSet_ZeroLengthConfig_Success() (gas: 16070) +CCIPConfig_beforeCapabilityConfigSet:test_getCapabilityConfiguration_Success() (gas: 9583) +CCIPConfig_chainConfig:test__applyChainConfigUpdates_FChainNotPositive_Reverts() (gas: 184703) +CCIPConfig_chainConfig:test_applyChainConfigUpdates_addChainConfigs_Success() (gas: 344332) +CCIPConfig_chainConfig:test_applyChainConfigUpdates_nodeNotInRegistry_Reverts() (gas: 20258) +CCIPConfig_chainConfig:test_applyChainConfigUpdates_removeChainConfigs_Success() (gas: 267558) +CCIPConfig_chainConfig:test_applyChainConfigUpdates_selectorNotFound_Reverts() (gas: 14829) +CCIPConfig_chainConfig:test_getCapabilityConfiguration_Success() (gas: 9626) +CCIPConfig_validateConfig:test__validateConfig_BootstrapP2PIdsHasDuplicates_Reverts() (gas: 294893) +CCIPConfig_validateConfig:test__validateConfig_BootstrapP2PIdsNotASubsetOfP2PIds_Reverts() (gas: 298325) +CCIPConfig_validateConfig:test__validateConfig_BootstrapP2PIdsNotSorted_Reverts() (gas: 295038) +CCIPConfig_validateConfig:test__validateConfig_ChainSelectorNotFound_Reverts() (gas: 294357) +CCIPConfig_validateConfig:test__validateConfig_ChainSelectorNotSet_Reverts() (gas: 291431) +CCIPConfig_validateConfig:test__validateConfig_FMustBePositive_Reverts() (gas: 292396) +CCIPConfig_validateConfig:test__validateConfig_FTooHigh_Reverts() (gas: 292540) +CCIPConfig_validateConfig:test__validateConfig_NodeNotInRegistry_Reverts() (gas: 299420) +CCIPConfig_validateConfig:test__validateConfig_NotEnoughTransmitters_Reverts() (gas: 1160094) +CCIPConfig_validateConfig:test__validateConfig_OfframpAddressCannotBeZero_Reverts() (gas: 291260) +CCIPConfig_validateConfig:test__validateConfig_P2PIdsHasDuplicates_Reverts() (gas: 295907) +CCIPConfig_validateConfig:test__validateConfig_P2PIdsLengthNotMatching_Reverts() (gas: 293229) +CCIPConfig_validateConfig:test__validateConfig_P2PIdsNotSorted_Reverts() (gas: 295623) +CCIPConfig_validateConfig:test__validateConfig_Success() (gas: 302186) +CCIPConfig_validateConfig:test__validateConfig_TooManyBootstrapP2PIds_Reverts() (gas: 294539) +CCIPConfig_validateConfig:test__validateConfig_TooManySigners_Reverts() (gas: 1215861) +CCIPConfig_validateConfig:test__validateConfig_TooManyTransmitters_Reverts() (gas: 1214264) +CCIPConfig_validateConfig:test_getCapabilityConfiguration_Success() (gas: 9562) +CommitStore_constructor:test_Constructor_Success() (gas: 3091326) +CommitStore_isUnpausedAndRMNHealthy:test_RMN_Success() (gas: 73420) +CommitStore_report:test_InvalidIntervalMinLargerThanMax_Revert() (gas: 28670) +CommitStore_report:test_InvalidInterval_Revert() (gas: 28610) +CommitStore_report:test_InvalidRootRevert() (gas: 27843) +CommitStore_report:test_OnlyGasPriceUpdates_Success() (gas: 53253) +CommitStore_report:test_OnlyPriceUpdateStaleReport_Revert() (gas: 59049) +CommitStore_report:test_OnlyTokenPriceUpdates_Success() (gas: 53251) +CommitStore_report:test_Paused_Revert() (gas: 21259) +CommitStore_report:test_ReportAndPriceUpdate_Success() (gas: 84242) +CommitStore_report:test_ReportOnlyRootSuccess_gas() (gas: 56313) +CommitStore_report:test_RootAlreadyCommitted_Revert() (gas: 63969) +CommitStore_report:test_StaleReportWithRoot_Success() (gas: 119420) +CommitStore_report:test_Unhealthy_Revert() (gas: 44751) +CommitStore_report:test_ValidPriceUpdateThenStaleReportWithRoot_Success() (gas: 100758) +CommitStore_report:test_ZeroEpochAndRound_Revert() (gas: 27626) +CommitStore_resetUnblessedRoots:test_OnlyOwner_Revert() (gas: 11325) +CommitStore_resetUnblessedRoots:test_ResetUnblessedRoots_Success() (gas: 143718) +CommitStore_setDynamicConfig:test_InvalidCommitStoreConfig_Revert() (gas: 37263) +CommitStore_setDynamicConfig:test_OnlyOwner_Revert() (gas: 37399) +CommitStore_setDynamicConfig:test_PriceEpochCleared_Success() (gas: 129098) +CommitStore_setLatestPriceEpochAndRound:test_OnlyOwner_Revert() (gas: 11047) +CommitStore_setLatestPriceEpochAndRound:test_SetLatestPriceEpochAndRound_Success() (gas: 20642) +CommitStore_setMinSeqNr:test_OnlyOwner_Revert() (gas: 11046) +CommitStore_verify:test_Blessed_Success() (gas: 96389) +CommitStore_verify:test_NotBlessed_Success() (gas: 61374) +CommitStore_verify:test_Paused_Revert() (gas: 18496) +CommitStore_verify:test_TooManyLeaves_Revert() (gas: 36785) +DefensiveExampleTest:test_HappyPath_Success() (gas: 200018) +DefensiveExampleTest:test_Recovery() (gas: 424253) +E2E:test_E2E_3MessagesSuccess_gas() (gas: 1103438) +EVM2EVMMultiOffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_NotACompatiblePool_Revert() (gas: 38157) +EVM2EVMMultiOffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_Success() (gas: 108343) +EVM2EVMMultiOffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_TokenHandlingError_revert_Revert() (gas: 116811) +EVM2EVMMultiOffRamp_applySourceChainConfigUpdates:test_AddMultipleChains_Success() (gas: 460560) +EVM2EVMMultiOffRamp_applySourceChainConfigUpdates:test_AddNewChain_Success() (gas: 95542) +EVM2EVMMultiOffRamp_applySourceChainConfigUpdates:test_ApplyZeroUpdates_Success() (gas: 12463) +EVM2EVMMultiOffRamp_applySourceChainConfigUpdates:test_ReplaceExistingChainOnRamp_Revert() (gas: 90385) +EVM2EVMMultiOffRamp_applySourceChainConfigUpdates:test_ReplaceExistingChain_Success() (gas: 105586) +EVM2EVMMultiOffRamp_applySourceChainConfigUpdates:test_ZeroOnRampAddress_Revert() (gas: 15719) +EVM2EVMMultiOffRamp_applySourceChainConfigUpdates:test_ZeroSourceChainSelector_Revert() (gas: 13057) +EVM2EVMMultiOffRamp_batchExecute:test_MultipleReportsDifferentChains_Success() (gas: 298564) +EVM2EVMMultiOffRamp_batchExecute:test_MultipleReportsSameChain_Success() (gas: 239899) +EVM2EVMMultiOffRamp_batchExecute:test_MultipleReportsSkipDuplicate_Success() (gas: 158863) +EVM2EVMMultiOffRamp_batchExecute:test_OutOfBoundsGasLimitsAccess_Revert() (gas: 189303) +EVM2EVMMultiOffRamp_batchExecute:test_SingleReport_Success() (gas: 147582) +EVM2EVMMultiOffRamp_batchExecute:test_Unhealthy_Revert() (gas: 521508) +EVM2EVMMultiOffRamp_batchExecute:test_ZeroReports_Revert() (gas: 10459) +EVM2EVMMultiOffRamp_ccipReceive:test_Reverts() (gas: 15662) +EVM2EVMMultiOffRamp_commit:test_InvalidIntervalMinLargerThanMax_Revert() (gas: 67195) +EVM2EVMMultiOffRamp_commit:test_InvalidInterval_Revert() (gas: 59698) +EVM2EVMMultiOffRamp_commit:test_InvalidRootRevert() (gas: 58778) +EVM2EVMMultiOffRamp_commit:test_NoConfigWithOtherConfigPresent_Revert() (gas: 6394741) +EVM2EVMMultiOffRamp_commit:test_NoConfig_Revert() (gas: 5977968) +EVM2EVMMultiOffRamp_commit:test_OnlyGasPriceUpdates_Success() (gas: 106229) +EVM2EVMMultiOffRamp_commit:test_OnlyPriceUpdateStaleReport_Revert() (gas: 116228) +EVM2EVMMultiOffRamp_commit:test_OnlyTokenPriceUpdates_Success() (gas: 106272) +EVM2EVMMultiOffRamp_commit:test_PriceSequenceNumberCleared_Success() (gas: 351414) +EVM2EVMMultiOffRamp_commit:test_ReportAndPriceUpdate_Success() (gas: 159132) +EVM2EVMMultiOffRamp_commit:test_ReportOnlyRootSuccess_gas() (gas: 136253) +EVM2EVMMultiOffRamp_commit:test_RootAlreadyCommitted_Revert() (gas: 136831) +EVM2EVMMultiOffRamp_commit:test_SourceChainNotEnabled_Revert() (gas: 59046) +EVM2EVMMultiOffRamp_commit:test_StaleReportWithRoot_Success() (gas: 227807) +EVM2EVMMultiOffRamp_commit:test_UnauthorizedTransmitter_Revert() (gas: 117527) +EVM2EVMMultiOffRamp_commit:test_Unhealthy_Revert() (gas: 77605) +EVM2EVMMultiOffRamp_commit:test_ValidPriceUpdateThenStaleReportWithRoot_Success() (gas: 207057) +EVM2EVMMultiOffRamp_commit:test_WrongConfigWithoutSigners_Revert() (gas: 6389130) +EVM2EVMMultiOffRamp_commit:test_ZeroEpochAndRound_Revert() (gas: 47785) +EVM2EVMMultiOffRamp_constructor:test_Constructor_Success() (gas: 5981174) +EVM2EVMMultiOffRamp_constructor:test_SourceChainSelector_Revert() (gas: 157326) +EVM2EVMMultiOffRamp_constructor:test_ZeroChainSelector_Revert() (gas: 103815) +EVM2EVMMultiOffRamp_constructor:test_ZeroNonceManager_Revert() (gas: 101686) +EVM2EVMMultiOffRamp_constructor:test_ZeroOnRampAddress_Revert() (gas: 159832) +EVM2EVMMultiOffRamp_constructor:test_ZeroRMNProxy_Revert() (gas: 101585) +EVM2EVMMultiOffRamp_constructor:test_ZeroTokenAdminRegistry_Revert() (gas: 101652) +EVM2EVMMultiOffRamp_execute:test_IncorrectArrayType_Revert() (gas: 17280) +EVM2EVMMultiOffRamp_execute:test_LargeBatch_Success() (gas: 1559406) +EVM2EVMMultiOffRamp_execute:test_MultipleReportsWithPartialValidationFailures_Success() (gas: 342924) +EVM2EVMMultiOffRamp_execute:test_MultipleReports_Success() (gas: 260178) +EVM2EVMMultiOffRamp_execute:test_NoConfigWithOtherConfigPresent_Revert() (gas: 6445247) +EVM2EVMMultiOffRamp_execute:test_NoConfig_Revert() (gas: 6028193) +EVM2EVMMultiOffRamp_execute:test_NonArray_Revert() (gas: 27681) +EVM2EVMMultiOffRamp_execute:test_SingleReport_Success() (gas: 165181) +EVM2EVMMultiOffRamp_execute:test_UnauthorizedTransmitter_Revert() (gas: 149137) +EVM2EVMMultiOffRamp_execute:test_WrongConfigWithSigners_Revert() (gas: 6807322) +EVM2EVMMultiOffRamp_execute:test_ZeroReports_Revert() (gas: 17154) +EVM2EVMMultiOffRamp_executeSingleMessage:test_MessageSender_Revert() (gas: 18413) +EVM2EVMMultiOffRamp_executeSingleMessage:test_NonContractWithTokens_Success() (gas: 249368) +EVM2EVMMultiOffRamp_executeSingleMessage:test_NonContract_Success() (gas: 20672) +EVM2EVMMultiOffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 201673) +EVM2EVMMultiOffRamp_executeSingleMessage:test_ZeroGasDONExecution_Revert() (gas: 48860) +EVM2EVMMultiOffRamp_executeSingleMessage:test_executeSingleMessage_NoTokens_Success() (gas: 48381) +EVM2EVMMultiOffRamp_executeSingleMessage:test_executeSingleMessage_WithFailingValidationNoRouterCall_Revert() (gas: 232798) +EVM2EVMMultiOffRamp_executeSingleMessage:test_executeSingleMessage_WithFailingValidation_Revert() (gas: 89392) +EVM2EVMMultiOffRamp_executeSingleMessage:test_executeSingleMessage_WithTokens_Success() (gas: 278146) +EVM2EVMMultiOffRamp_executeSingleMessage:test_executeSingleMessage_WithValidation_Success() (gas: 93615) +EVM2EVMMultiOffRamp_executeSingleReport:test_DisabledSourceChain_Revert() (gas: 35083) +EVM2EVMMultiOffRamp_executeSingleReport:test_EmptyReport_Revert() (gas: 23907) +EVM2EVMMultiOffRamp_executeSingleReport:test_InvalidSourcePoolAddress_Success() (gas: 451358) +EVM2EVMMultiOffRamp_executeSingleReport:test_ManualExecutionNotYetEnabled_Revert() (gas: 54475) +EVM2EVMMultiOffRamp_executeSingleReport:test_MismatchingDestChainSelector_Revert() (gas: 35917) +EVM2EVMMultiOffRamp_executeSingleReport:test_MismatchingOnRampRoot_Revert() (gas: 154369) +EVM2EVMMultiOffRamp_executeSingleReport:test_NonExistingSourceChain_Revert() (gas: 35317) +EVM2EVMMultiOffRamp_executeSingleReport:test_ReceiverError_Success() (gas: 181353) +EVM2EVMMultiOffRamp_executeSingleReport:test_RetryFailedMessageWithoutManualExecution_Revert() (gas: 190627) +EVM2EVMMultiOffRamp_executeSingleReport:test_RootNotCommitted_Revert() (gas: 48053) +EVM2EVMMultiOffRamp_executeSingleReport:test_RouterYULCall_Revert() (gas: 443030) +EVM2EVMMultiOffRamp_executeSingleReport:test_SingleMessageNoTokensOtherChain_Success() (gas: 251770) +EVM2EVMMultiOffRamp_executeSingleReport:test_SingleMessageNoTokensUnordered_Success() (gas: 173962) +EVM2EVMMultiOffRamp_executeSingleReport:test_SingleMessageNoTokens_Success() (gas: 193657) +EVM2EVMMultiOffRamp_executeSingleReport:test_SingleMessageToNonCCIPReceiver_Success() (gas: 259648) +EVM2EVMMultiOffRamp_executeSingleReport:test_SingleMessagesNoTokensSuccess_gas() (gas: 129585) +EVM2EVMMultiOffRamp_executeSingleReport:test_SkippedIncorrectNonceStillExecutes_Success() (gas: 391710) +EVM2EVMMultiOffRamp_executeSingleReport:test_SkippedIncorrectNonce_Success() (gas: 65899) +EVM2EVMMultiOffRamp_executeSingleReport:test_TokenDataMismatch_Revert() (gas: 80955) +EVM2EVMMultiOffRamp_executeSingleReport:test_TwoMessagesWithTokensAndGE_Success() (gas: 535429) +EVM2EVMMultiOffRamp_executeSingleReport:test_TwoMessagesWithTokensSuccess_gas() (gas: 480345) +EVM2EVMMultiOffRamp_executeSingleReport:test_UnexpectedTokenData_Revert() (gas: 35763) +EVM2EVMMultiOffRamp_executeSingleReport:test_UnhealthySingleChainCurse_Revert() (gas: 520344) +EVM2EVMMultiOffRamp_executeSingleReport:test_Unhealthy_Revert() (gas: 517712) +EVM2EVMMultiOffRamp_executeSingleReport:test_WithCurseOnAnotherSourceChain_Success() (gas: 487848) +EVM2EVMMultiOffRamp_executeSingleReport:test__execute_SkippedAlreadyExecutedMessageUnordered_Success() (gas: 127921) +EVM2EVMMultiOffRamp_executeSingleReport:test__execute_SkippedAlreadyExecutedMessage_Success() (gas: 157144) +EVM2EVMMultiOffRamp_getExecutionState:test_FillExecutionState_Success() (gas: 3655340) +EVM2EVMMultiOffRamp_getExecutionState:test_GetDifferentChainExecutionState_Success() (gas: 118224) +EVM2EVMMultiOffRamp_getExecutionState:test_GetExecutionState_Success() (gas: 87461) +EVM2EVMMultiOffRamp_manuallyExecute:test_ManualExecGasLimitMismatchSingleReport_Revert() (gas: 75600) +EVM2EVMMultiOffRamp_manuallyExecute:test_ManualExecInvalidGasLimit_Revert() (gas: 26461) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_DoesNotRevertIfUntouched_Success() (gas: 163081) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_FailedTx_Revert() (gas: 207379) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_ForkedChain_Revert() (gas: 26004) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_GasLimitMismatchMultipleReports_Revert() (gas: 152867) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_LowGasLimit_Success() (gas: 507480) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_ReentrancyFails() (gas: 2307925) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_Success() (gas: 209633) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_WithGasOverride_Success() (gas: 210210) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_WithMultiReportGasOverride_Success() (gas: 668610) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_WithPartialMessages_Success() (gas: 299477) +EVM2EVMMultiOffRamp_releaseOrMintTokens:test_TokenHandlingError_Reverts() (gas: 160598) +EVM2EVMMultiOffRamp_releaseOrMintTokens:test__releaseOrMintTokens_PoolIsNotAPool_Reverts() (gas: 24131) +EVM2EVMMultiOffRamp_releaseOrMintTokens:test_releaseOrMintTokens_InvalidDataLengthReturnData_Revert() (gas: 59105) +EVM2EVMMultiOffRamp_releaseOrMintTokens:test_releaseOrMintTokens_InvalidEVMAddress_Revert() (gas: 40405) +EVM2EVMMultiOffRamp_releaseOrMintTokens:test_releaseOrMintTokens_PoolDoesNotSupportDest_Reverts() (gas: 76130) +EVM2EVMMultiOffRamp_releaseOrMintTokens:test_releaseOrMintTokens_Success() (gas: 178951) +EVM2EVMMultiOffRamp_releaseOrMintTokens:test_releaseOrMintTokens_destDenominatedDecimals_Success() (gas: 278805) +EVM2EVMMultiOffRamp_resetUnblessedRoots:test_OnlyOwner_Revert() (gas: 11379) +EVM2EVMMultiOffRamp_resetUnblessedRoots:test_ResetUnblessedRoots_Success() (gas: 215406) +EVM2EVMMultiOffRamp_setDynamicConfig:test_NonOwner_Revert() (gas: 14374) +EVM2EVMMultiOffRamp_setDynamicConfig:test_PriceRegistryZeroAddress_Revert() (gas: 11898) +EVM2EVMMultiOffRamp_setDynamicConfig:test_RouterZeroAddress_Revert() (gas: 14054) +EVM2EVMMultiOffRamp_setDynamicConfig:test_SetDynamicConfigWithValidator_Success() (gas: 55771) +EVM2EVMMultiOffRamp_setDynamicConfig:test_SetDynamicConfig_Success() (gas: 33781) +EVM2EVMMultiOffRamp_trialExecute:test_RateLimitError_Success() (gas: 238004) +EVM2EVMMultiOffRamp_trialExecute:test_TokenHandlingErrorIsCaught_Success() (gas: 246667) +EVM2EVMMultiOffRamp_trialExecute:test_TokenPoolIsNotAContract_Success() (gas: 299499) +EVM2EVMMultiOffRamp_trialExecute:test_trialExecute_Success() (gas: 280579) +EVM2EVMMultiOffRamp_verify:test_Blessed_Success() (gas: 176604) +EVM2EVMMultiOffRamp_verify:test_NotBlessedWrongChainSelector_Success() (gas: 178672) +EVM2EVMMultiOffRamp_verify:test_NotBlessed_Success() (gas: 141533) +EVM2EVMMultiOffRamp_verify:test_TooManyLeaves_Revert() (gas: 51508) +EVM2EVMMultiOnRamp_constructor:test_Constructor_InvalidConfigChainSelectorEqZero_Revert() (gas: 94528) +EVM2EVMMultiOnRamp_constructor:test_Constructor_InvalidConfigNonceManagerEqAddressZero_Revert() (gas: 92480) +EVM2EVMMultiOnRamp_constructor:test_Constructor_InvalidConfigRMNProxyEqAddressZero_Revert() (gas: 97483) +EVM2EVMMultiOnRamp_constructor:test_Constructor_InvalidConfigTokenAdminRegistryEqAddressZero_Revert() (gas: 92538) +EVM2EVMMultiOnRamp_constructor:test_Constructor_Success() (gas: 2260144) +EVM2EVMMultiOnRamp_forwardFromRouter:test_CannotSendZeroTokens_Revert() (gas: 90987) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ForwardFromRouterExtraArgsV2AllowOutOfOrderTrue_Success() (gas: 130983) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ForwardFromRouterExtraArgsV2_Success() (gas: 161753) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ForwardFromRouterSuccessCustomExtraArgs() (gas: 161306) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ForwardFromRouterSuccessEmptyExtraArgs() (gas: 159506) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ForwardFromRouterSuccessLegacyExtraArgs() (gas: 161536) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ForwardFromRouter_Success() (gas: 160928) +EVM2EVMMultiOnRamp_forwardFromRouter:test_InvalidExtraArgsTag_Revert() (gas: 26206) +EVM2EVMMultiOnRamp_forwardFromRouter:test_MessageValidationError_Revert() (gas: 134082) +EVM2EVMMultiOnRamp_forwardFromRouter:test_MesssageFeeTooHigh_Revert() (gas: 24272) +EVM2EVMMultiOnRamp_forwardFromRouter:test_OriginalSender_Revert() (gas: 12819) +EVM2EVMMultiOnRamp_forwardFromRouter:test_Paused_Revert() (gas: 30695) +EVM2EVMMultiOnRamp_forwardFromRouter:test_Permissions_Revert() (gas: 15675) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ShouldIncrementNonceOnlyOnOrdered_Success() (gas: 198276) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ShouldIncrementSeqNumAndNonce_Success() (gas: 224545) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ShouldStoreLinkFees() (gas: 140840) +EVM2EVMMultiOnRamp_forwardFromRouter:test_ShouldStoreNonLinkFees() (gas: 162262) +EVM2EVMMultiOnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3803257) +EVM2EVMMultiOnRamp_forwardFromRouter:test_UnsupportedToken_Revert() (gas: 127615) +EVM2EVMMultiOnRamp_forwardFromRouter:test_forwardFromRouter_UnsupportedToken_Revert() (gas: 93044) +EVM2EVMMultiOnRamp_forwardFromRouter:test_forwardFromRouter_WithValidation_Success() (gas: 282576) +EVM2EVMMultiOnRamp_getFee:test_EmptyMessage_Success() (gas: 104423) +EVM2EVMMultiOnRamp_getFee:test_EnforceOutOfOrder_Revert() (gas: 74041) +EVM2EVMMultiOnRamp_getFee:test_SingleTokenMessage_Success() (gas: 119755) +EVM2EVMMultiOnRamp_getFee:test_Unhealthy_Revert() (gas: 43657) +EVM2EVMMultiOnRamp_getSupportedTokens:test_GetSupportedTokens_Revert() (gas: 10438) +EVM2EVMMultiOnRamp_getTokenPool:test_GetTokenPool_Success() (gas: 35204) +EVM2EVMMultiOnRamp_setDynamicConfig:test_SetConfigInvalidConfigFeeAggregatorEqAddressZero_Revert() (gas: 11356) +EVM2EVMMultiOnRamp_setDynamicConfig:test_SetConfigInvalidConfigPriceRegistryEqAddressZero_Revert() (gas: 12956) +EVM2EVMMultiOnRamp_setDynamicConfig:test_SetConfigInvalidConfig_Revert() (gas: 11313) +EVM2EVMMultiOnRamp_setDynamicConfig:test_SetConfigOnlyOwner_Revert() (gas: 16287) +EVM2EVMMultiOnRamp_setDynamicConfig:test_SetDynamicConfig_Success() (gas: 58439) +EVM2EVMMultiOnRamp_withdrawFeeTokens:test_WithdrawFeeTokens_Success() (gas: 97185) +EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_NotACompatiblePool_Revert() (gas: 38028) +EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_Success() (gas: 108191) +EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_TokenHandlingError_revert_Revert() (gas: 116732) +EVM2EVMOffRamp__releaseOrMintTokens:test_OverValueWithARLOff_Success() (gas: 391880) +EVM2EVMOffRamp__releaseOrMintTokens:test_PriceNotFoundForToken_Reverts() (gas: 145379) +EVM2EVMOffRamp__releaseOrMintTokens:test_RateLimitErrors_Reverts() (gas: 788000) +EVM2EVMOffRamp__releaseOrMintTokens:test_TokenHandlingError_Reverts() (gas: 176208) +EVM2EVMOffRamp__releaseOrMintTokens:test__releaseOrMintTokens_NotACompatiblePool_Reverts() (gas: 29700) +EVM2EVMOffRamp__releaseOrMintTokens:test_releaseOrMintTokens_InvalidDataLengthReturnData_Revert() (gas: 63325) +EVM2EVMOffRamp__releaseOrMintTokens:test_releaseOrMintTokens_InvalidEVMAddress_Revert() (gas: 44501) +EVM2EVMOffRamp__releaseOrMintTokens:test_releaseOrMintTokens_Success() (gas: 214151) +EVM2EVMOffRamp__releaseOrMintTokens:test_releaseOrMintTokens_destDenominatedDecimals_Success() (gas: 306912) +EVM2EVMOffRamp__report:test_Report_Success() (gas: 127459) +EVM2EVMOffRamp__trialExecute:test_RateLimitError_Success() (gas: 255047) +EVM2EVMOffRamp__trialExecute:test_TokenHandlingErrorIsCaught_Success() (gas: 263638) +EVM2EVMOffRamp__trialExecute:test_TokenPoolIsNotAContract_Success() (gas: 335707) +EVM2EVMOffRamp__trialExecute:test_trialExecute_Success() (gas: 314443) +EVM2EVMOffRamp_ccipReceive:test_Reverts() (gas: 17009) +EVM2EVMOffRamp_constructor:test_CommitStoreAlreadyInUse_Revert() (gas: 153427) +EVM2EVMOffRamp_constructor:test_Constructor_Success() (gas: 5464875) +EVM2EVMOffRamp_constructor:test_ZeroOnRampAddress_Revert() (gas: 144183) +EVM2EVMOffRamp_execute:test_EmptyReport_Revert() (gas: 21345) +EVM2EVMOffRamp_execute:test_InvalidMessageId_Revert() (gas: 36442) +EVM2EVMOffRamp_execute:test_InvalidSourceChain_Revert() (gas: 51701) +EVM2EVMOffRamp_execute:test_InvalidSourcePoolAddress_Success() (gas: 473575) +EVM2EVMOffRamp_execute:test_ManualExecutionNotYetEnabled_Revert() (gas: 46423) +EVM2EVMOffRamp_execute:test_MessageTooLarge_Revert() (gas: 152453) +EVM2EVMOffRamp_execute:test_Paused_Revert() (gas: 101458) +EVM2EVMOffRamp_execute:test_ReceiverError_Success() (gas: 165036) +EVM2EVMOffRamp_execute:test_RetryFailedMessageWithoutManualExecution_Revert() (gas: 177824) +EVM2EVMOffRamp_execute:test_RootNotCommitted_Revert() (gas: 41317) +EVM2EVMOffRamp_execute:test_RouterYULCall_Revert() (gas: 402506) +EVM2EVMOffRamp_execute:test_SingleMessageNoTokensUnordered_Success() (gas: 159387) +EVM2EVMOffRamp_execute:test_SingleMessageNoTokens_Success() (gas: 174622) +EVM2EVMOffRamp_execute:test_SingleMessageToNonCCIPReceiver_Success() (gas: 248634) +EVM2EVMOffRamp_execute:test_SingleMessagesNoTokensSuccess_gas() (gas: 115017) +EVM2EVMOffRamp_execute:test_SkippedIncorrectNonceStillExecutes_Success() (gas: 409338) +EVM2EVMOffRamp_execute:test_SkippedIncorrectNonce_Success() (gas: 54173) +EVM2EVMOffRamp_execute:test_StrictUntouchedToSuccess_Success() (gas: 132056) +EVM2EVMOffRamp_execute:test_TokenDataMismatch_Revert() (gas: 52200) +EVM2EVMOffRamp_execute:test_TwoMessagesWithTokensAndGE_Success() (gas: 560178) +EVM2EVMOffRamp_execute:test_TwoMessagesWithTokensSuccess_gas() (gas: 499424) +EVM2EVMOffRamp_execute:test_UnexpectedTokenData_Revert() (gas: 35442) +EVM2EVMOffRamp_execute:test_Unhealthy_Revert() (gas: 546987) +EVM2EVMOffRamp_execute:test_UnsupportedNumberOfTokens_Revert() (gas: 64045) +EVM2EVMOffRamp_execute:test__execute_SkippedAlreadyExecutedMessageUnordered_Success() (gas: 123223) +EVM2EVMOffRamp_execute:test__execute_SkippedAlreadyExecutedMessage_Success() (gas: 143388) +EVM2EVMOffRamp_executeSingleMessage:test_MessageSender_Revert() (gas: 20582) +EVM2EVMOffRamp_executeSingleMessage:test_NonContractWithTokens_Success() (gas: 281891) +EVM2EVMOffRamp_executeSingleMessage:test_NonContract_Success() (gas: 20231) +EVM2EVMOffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 219228) +EVM2EVMOffRamp_executeSingleMessage:test_ZeroGasDONExecution_Revert() (gas: 48632) +EVM2EVMOffRamp_executeSingleMessage:test_executeSingleMessage_NoTokens_Success() (gas: 48120) +EVM2EVMOffRamp_executeSingleMessage:test_executeSingleMessage_WithTokens_Success() (gas: 316477) +EVM2EVMOffRamp_executeSingleMessage:test_executeSingleMessage_ZeroGasZeroData_Success() (gas: 72423) +EVM2EVMOffRamp_execute_upgrade:test_V2NonceNewSenderStartsAtZero_Success() (gas: 231326) +EVM2EVMOffRamp_execute_upgrade:test_V2NonceStartsAtV1Nonce_Success() (gas: 279867) +EVM2EVMOffRamp_execute_upgrade:test_V2OffRampNonceSkipsIfMsgInFlight_Success() (gas: 261109) +EVM2EVMOffRamp_execute_upgrade:test_V2SenderNoncesReadsPreviousRamp_Success() (gas: 229397) +EVM2EVMOffRamp_execute_upgrade:test_V2_Success() (gas: 131682) +EVM2EVMOffRamp_getAllRateLimitTokens:test_GetAllRateLimitTokens_Success() (gas: 38408) +EVM2EVMOffRamp_getExecutionState:test_FillExecutionState_Success() (gas: 3213556) +EVM2EVMOffRamp_getExecutionState:test_GetExecutionState_Success() (gas: 83091) +EVM2EVMOffRamp_manuallyExecute:test_LowGasLimitManualExec_Success() (gas: 483328) +EVM2EVMOffRamp_manuallyExecute:test_ManualExecFailedTx_Revert() (gas: 186413) +EVM2EVMOffRamp_manuallyExecute:test_ManualExecForkedChain_Revert() (gas: 25824) +EVM2EVMOffRamp_manuallyExecute:test_ManualExecGasLimitMismatch_Revert() (gas: 43449) +EVM2EVMOffRamp_manuallyExecute:test_ManualExecInvalidGasLimit_Revert() (gas: 25927) +EVM2EVMOffRamp_manuallyExecute:test_ManualExecWithGasOverride_Success() (gas: 188518) +EVM2EVMOffRamp_manuallyExecute:test_ManualExec_Success() (gas: 187965) +EVM2EVMOffRamp_manuallyExecute:test_ReentrancyManualExecuteFails() (gas: 2027441) +EVM2EVMOffRamp_manuallyExecute:test_manuallyExecute_DoesNotRevertIfUntouched_Success() (gas: 143803) +EVM2EVMOffRamp_metadataHash:test_MetadataHash_Success() (gas: 8871) +EVM2EVMOffRamp_setDynamicConfig:test_NonOwner_Revert() (gas: 40429) +EVM2EVMOffRamp_setDynamicConfig:test_RouterZeroAddress_Revert() (gas: 38804) +EVM2EVMOffRamp_setDynamicConfig:test_SetDynamicConfig_Success() (gas: 146790) +EVM2EVMOffRamp_updateRateLimitTokens:test_updateRateLimitTokens_AddsAndRemoves_Success() (gas: 162464) +EVM2EVMOffRamp_updateRateLimitTokens:test_updateRateLimitTokens_NonOwner_Revert() (gas: 16667) +EVM2EVMOffRamp_updateRateLimitTokens:test_updateRateLimitTokens_Success() (gas: 197660) +EVM2EVMOnRamp_constructor:test_Constructor_Success() (gas: 5619710) +EVM2EVMOnRamp_forwardFromRouter:test_CannotSendZeroTokens_Revert() (gas: 35778) +EVM2EVMOnRamp_forwardFromRouter:test_EnforceOutOfOrder_Revert() (gas: 99470) +EVM2EVMOnRamp_forwardFromRouter:test_ForwardFromRouterExtraArgsV2AllowOutOfOrderTrue_Success() (gas: 114210) +EVM2EVMOnRamp_forwardFromRouter:test_ForwardFromRouterExtraArgsV2_Success() (gas: 114252) +EVM2EVMOnRamp_forwardFromRouter:test_ForwardFromRouterSuccessCustomExtraArgs() (gas: 130118) +EVM2EVMOnRamp_forwardFromRouter:test_ForwardFromRouterSuccessLegacyExtraArgs() (gas: 138650) +EVM2EVMOnRamp_forwardFromRouter:test_ForwardFromRouter_Success() (gas: 129804) +EVM2EVMOnRamp_forwardFromRouter:test_InvalidAddressEncodePacked_Revert() (gas: 38254) +EVM2EVMOnRamp_forwardFromRouter:test_InvalidAddress_Revert() (gas: 38370) +EVM2EVMOnRamp_forwardFromRouter:test_InvalidChainSelector_Revert() (gas: 25511) +EVM2EVMOnRamp_forwardFromRouter:test_InvalidExtraArgsTag_Revert() (gas: 25297) +EVM2EVMOnRamp_forwardFromRouter:test_MaxCapacityExceeded_Revert() (gas: 86041) +EVM2EVMOnRamp_forwardFromRouter:test_MaxFeeBalanceReached_Revert() (gas: 36457) +EVM2EVMOnRamp_forwardFromRouter:test_MessageGasLimitTooHigh_Revert() (gas: 29037) +EVM2EVMOnRamp_forwardFromRouter:test_MessageTooLarge_Revert() (gas: 107526) +EVM2EVMOnRamp_forwardFromRouter:test_OriginalSender_Revert() (gas: 22635) +EVM2EVMOnRamp_forwardFromRouter:test_OverValueWithARLOff_Success() (gas: 223665) +EVM2EVMOnRamp_forwardFromRouter:test_Paused_Revert() (gas: 53935) +EVM2EVMOnRamp_forwardFromRouter:test_Permissions_Revert() (gas: 25481) +EVM2EVMOnRamp_forwardFromRouter:test_PriceNotFoundForToken_Revert() (gas: 59303) +EVM2EVMOnRamp_forwardFromRouter:test_ShouldIncrementNonceOnlyOnOrdered_Success() (gas: 179141) +EVM2EVMOnRamp_forwardFromRouter:test_ShouldIncrementSeqNumAndNonce_Success() (gas: 177355) +EVM2EVMOnRamp_forwardFromRouter:test_ShouldStoreNonLinkFees() (gas: 137297) +EVM2EVMOnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3731767) +EVM2EVMOnRamp_forwardFromRouter:test_TooManyTokens_Revert() (gas: 30187) +EVM2EVMOnRamp_forwardFromRouter:test_Unhealthy_Revert() (gas: 43300) +EVM2EVMOnRamp_forwardFromRouter:test_UnsupportedToken_Revert() (gas: 109258) +EVM2EVMOnRamp_forwardFromRouter:test_ZeroAddressReceiver_Revert() (gas: 312351) +EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_ShouldStoreLinkFees_Success() (gas: 112319) +EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_UnsupportedToken_Revert() (gas: 72181) +EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2NonceNewSenderStartsAtZero_Success() (gas: 147614) +EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2NonceStartsAtV1Nonce_Success() (gas: 190454) +EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2SenderNoncesReadsPreviousRamp_Success() (gas: 121245) +EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2_Success() (gas: 95324) +EVM2EVMOnRamp_getDataAvailabilityCost:test_EmptyMessageCalculatesDataAvailabilityCost_Success() (gas: 20760) +EVM2EVMOnRamp_getDataAvailabilityCost:test_SimpleMessageCalculatesDataAvailabilityCost_Success() (gas: 21128) +EVM2EVMOnRamp_getFee:test_EmptyMessage_Success() (gas: 78242) +EVM2EVMOnRamp_getFee:test_HighGasMessage_Success() (gas: 234090) +EVM2EVMOnRamp_getFee:test_MessageGasLimitTooHigh_Revert() (gas: 16715) +EVM2EVMOnRamp_getFee:test_MessageTooLarge_Revert() (gas: 95271) +EVM2EVMOnRamp_getFee:test_MessageWithDataAndTokenTransfer_Success() (gas: 159220) +EVM2EVMOnRamp_getFee:test_NotAFeeToken_Revert() (gas: 24089) +EVM2EVMOnRamp_getFee:test_SingleTokenMessage_Success() (gas: 117858) +EVM2EVMOnRamp_getFee:test_TooManyTokens_Revert() (gas: 19902) +EVM2EVMOnRamp_getFee:test_ZeroDataAvailabilityMultiplier_Success() (gas: 65663) +EVM2EVMOnRamp_getSupportedTokens:test_GetSupportedTokens_Revert() (gas: 10460) +EVM2EVMOnRamp_getTokenPool:test_GetTokenPool_Success() (gas: 35195) +EVM2EVMOnRamp_getTokenTransferCost:test_CustomTokenBpsFee_Success() (gas: 45037) +EVM2EVMOnRamp_getTokenTransferCost:test_FeeTokenBpsFee_Success() (gas: 33041) +EVM2EVMOnRamp_getTokenTransferCost:test_LargeTokenTransferChargesMaxFeeAndGas_Success() (gas: 28296) +EVM2EVMOnRamp_getTokenTransferCost:test_MixedTokenTransferFee_Success() (gas: 130189) +EVM2EVMOnRamp_getTokenTransferCost:test_NoTokenTransferChargesZeroFee_Success() (gas: 15260) +EVM2EVMOnRamp_getTokenTransferCost:test_SmallTokenTransferChargesMinFeeAndGas_Success() (gas: 28104) +EVM2EVMOnRamp_getTokenTransferCost:test_UnsupportedToken_Revert() (gas: 21248) +EVM2EVMOnRamp_getTokenTransferCost:test_WETHTokenBpsFee_Success() (gas: 38922) +EVM2EVMOnRamp_getTokenTransferCost:test_ZeroAmountTokenTransferChargesMinFeeAndGas_Success() (gas: 28149) +EVM2EVMOnRamp_getTokenTransferCost:test_ZeroFeeConfigChargesMinFee_Success() (gas: 38615) +EVM2EVMOnRamp_getTokenTransferCost:test__getTokenTransferCost_selfServeUsesDefaults_Success() (gas: 29527) +EVM2EVMOnRamp_linkAvailableForPayment:test_InsufficientLinkBalance_Success() (gas: 32615) +EVM2EVMOnRamp_linkAvailableForPayment:test_LinkAvailableForPayment_Success() (gas: 134833) +EVM2EVMOnRamp_payNops:test_AdminPayNops_Success() (gas: 143054) +EVM2EVMOnRamp_payNops:test_InsufficientBalance_Revert() (gas: 26543) +EVM2EVMOnRamp_payNops:test_NoFeesToPay_Revert() (gas: 127367) +EVM2EVMOnRamp_payNops:test_NoNopsToPay_Revert() (gas: 133251) +EVM2EVMOnRamp_payNops:test_NopPayNops_Success() (gas: 146341) +EVM2EVMOnRamp_payNops:test_OwnerPayNops_Success() (gas: 140916) +EVM2EVMOnRamp_payNops:test_PayNopsSuccessAfterSetNops() (gas: 297485) +EVM2EVMOnRamp_payNops:test_WrongPermissions_Revert() (gas: 15294) +EVM2EVMOnRamp_setDynamicConfig:test_SetConfigInvalidConfig_Revert() (gas: 43376) +EVM2EVMOnRamp_setDynamicConfig:test_SetConfigOnlyOwner_Revert() (gas: 21646) +EVM2EVMOnRamp_setDynamicConfig:test_SetDynamicConfig_Success() (gas: 55086) +EVM2EVMOnRamp_setFeeTokenConfig:test_OnlyCallableByOwnerOrAdmin_Revert() (gas: 13464) +EVM2EVMOnRamp_setFeeTokenConfig:test_SetFeeTokenConfigByAdmin_Success() (gas: 16449) +EVM2EVMOnRamp_setFeeTokenConfig:test_SetFeeTokenConfig_Success() (gas: 13994) +EVM2EVMOnRamp_setNops:test_AdminCanSetNops_Success() (gas: 61759) +EVM2EVMOnRamp_setNops:test_IncludesPayment_Success() (gas: 469097) +EVM2EVMOnRamp_setNops:test_LinkTokenCannotBeNop_Revert() (gas: 57255) +EVM2EVMOnRamp_setNops:test_NonOwnerOrAdmin_Revert() (gas: 14665) +EVM2EVMOnRamp_setNops:test_NotEnoughFundsForPayout_Revert() (gas: 84455) +EVM2EVMOnRamp_setNops:test_SetNopsRemovesOldNopsCompletely_Success() (gas: 60637) +EVM2EVMOnRamp_setNops:test_SetNops_Success() (gas: 173677) +EVM2EVMOnRamp_setNops:test_TooManyNops_Revert() (gas: 190338) +EVM2EVMOnRamp_setNops:test_ZeroAddressCannotBeNop_Revert() (gas: 53596) +EVM2EVMOnRamp_setTokenTransferFeeConfig:test__setTokenTransferFeeConfig_InvalidDestBytesOverhead_Revert() (gas: 14493) +EVM2EVMOnRamp_setTokenTransferFeeConfig:test__setTokenTransferFeeConfig_OnlyCallableByOwnerOrAdmin_Revert() (gas: 14277) +EVM2EVMOnRamp_setTokenTransferFeeConfig:test__setTokenTransferFeeConfig_Success() (gas: 84017) +EVM2EVMOnRamp_setTokenTransferFeeConfig:test__setTokenTransferFeeConfig_byAdmin_Success() (gas: 17369) +EVM2EVMOnRamp_withdrawNonLinkFees:test_LinkBalanceNotSettled_Revert() (gas: 82980) +EVM2EVMOnRamp_withdrawNonLinkFees:test_NonOwnerOrAdmin_Revert() (gas: 15275) +EVM2EVMOnRamp_withdrawNonLinkFees:test_SettlingBalance_Success() (gas: 272015) +EVM2EVMOnRamp_withdrawNonLinkFees:test_WithdrawNonLinkFees_Success() (gas: 53446) +EVM2EVMOnRamp_withdrawNonLinkFees:test_WithdrawToZeroAddress_Revert() (gas: 12830) +EtherSenderReceiverTest_ccipReceive:test_ccipReceive_fallbackToWethTransfer() (gas: 96729) +EtherSenderReceiverTest_ccipReceive:test_ccipReceive_happyPath() (gas: 47688) +EtherSenderReceiverTest_ccipReceive:test_ccipReceive_wrongToken() (gas: 17384) +EtherSenderReceiverTest_ccipReceive:test_ccipReceive_wrongTokenAmount() (gas: 15677) +EtherSenderReceiverTest_ccipSend:test_ccipSend_reverts_insufficientFee_feeToken() (gas: 99741) +EtherSenderReceiverTest_ccipSend:test_ccipSend_reverts_insufficientFee_native() (gas: 76096) +EtherSenderReceiverTest_ccipSend:test_ccipSend_reverts_insufficientFee_weth() (gas: 99748) +EtherSenderReceiverTest_ccipSend:test_ccipSend_success_feeToken() (gas: 144569) +EtherSenderReceiverTest_ccipSend:test_ccipSend_success_native() (gas: 80259) +EtherSenderReceiverTest_ccipSend:test_ccipSend_success_nativeExcess() (gas: 80446) +EtherSenderReceiverTest_ccipSend:test_ccipSend_success_weth() (gas: 95713) +EtherSenderReceiverTest_constructor:test_constructor() (gas: 17511) +EtherSenderReceiverTest_getFee:test_getFee() (gas: 27289) +EtherSenderReceiverTest_validateFeeToken:test_validateFeeToken_reverts_feeToken_tokenAmountNotEqualToMsgValue() (gas: 20333) +EtherSenderReceiverTest_validateFeeToken:test_validateFeeToken_valid_feeToken() (gas: 16715) +EtherSenderReceiverTest_validateFeeToken:test_validateFeeToken_valid_native() (gas: 16654) +EtherSenderReceiverTest_validatedMessage:test_validatedMessage_dataOverwrittenToMsgSender() (gas: 25415) +EtherSenderReceiverTest_validatedMessage:test_validatedMessage_emptyDataOverwrittenToMsgSender() (gas: 25265) +EtherSenderReceiverTest_validatedMessage:test_validatedMessage_invalidTokenAmounts() (gas: 17895) +EtherSenderReceiverTest_validatedMessage:test_validatedMessage_tokenOverwrittenToWeth() (gas: 25287) +EtherSenderReceiverTest_validatedMessage:test_validatedMessage_validMessage_extraArgs() (gas: 26292) +LockReleaseTokenPoolAndProxy_setRateLimitAdmin:test_SetRateLimitAdmin_Revert() (gas: 11058) +LockReleaseTokenPoolAndProxy_setRateLimitAdmin:test_SetRateLimitAdmin_Success() (gas: 35097) +LockReleaseTokenPoolAndProxy_setRebalancer:test_SetRebalancer_Revert() (gas: 10970) +LockReleaseTokenPoolAndProxy_setRebalancer:test_SetRebalancer_Success() (gas: 18036) +LockReleaseTokenPoolPoolAndProxy_canAcceptLiquidity:test_CanAcceptLiquidity_Success() (gas: 3313980) +LockReleaseTokenPoolPoolAndProxy_provideLiquidity:test_LiquidityNotAccepted_Revert() (gas: 3310379) +LockReleaseTokenPoolPoolAndProxy_provideLiquidity:test_Unauthorized_Revert() (gas: 11380) +LockReleaseTokenPoolPoolAndProxy_setChainRateLimiterConfig:test_NonExistentChain_Revert() (gas: 17135) +LockReleaseTokenPoolPoolAndProxy_setChainRateLimiterConfig:test_OnlyOwnerOrRateLimitAdmin_Revert() (gas: 69142) +LockReleaseTokenPoolPoolAndProxy_setChainRateLimiterConfig:test_OnlyOwner_Revert() (gas: 17319) +LockReleaseTokenPoolPoolAndProxy_supportsInterface:test_SupportsInterface_Success() (gas: 9977) +LockReleaseTokenPoolPoolAndProxy_withdrawalLiquidity:test_InsufficientLiquidity_Revert() (gas: 60043) +LockReleaseTokenPoolPoolAndProxy_withdrawalLiquidity:test_Unauthorized_Revert() (gas: 11355) +LockReleaseTokenPool_canAcceptLiquidity:test_CanAcceptLiquidity_Success() (gas: 3067883) +LockReleaseTokenPool_lockOrBurn:test_LockOrBurnWithAllowList_Revert() (gas: 29942) +LockReleaseTokenPool_lockOrBurn:test_LockOrBurnWithAllowList_Success() (gas: 79844) +LockReleaseTokenPool_lockOrBurn:test_PoolBurnRevertNotHealthy_Revert() (gas: 59464) +LockReleaseTokenPool_provideLiquidity:test_LiquidityNotAccepted_Revert() (gas: 3064325) +LockReleaseTokenPool_provideLiquidity:test_Unauthorized_Revert() (gas: 11380) +LockReleaseTokenPool_releaseOrMint:test_ChainNotAllowed_Revert() (gas: 72662) +LockReleaseTokenPool_releaseOrMint:test_PoolMintNotHealthy_Revert() (gas: 56131) +LockReleaseTokenPool_releaseOrMint:test_ReleaseOrMint_Success() (gas: 238673) +LockReleaseTokenPool_setChainRateLimiterConfig:test_NonExistentChain_Revert() (gas: 17102) +LockReleaseTokenPool_setChainRateLimiterConfig:test_OnlyOwnerOrRateLimitAdmin_Revert() (gas: 69075) +LockReleaseTokenPool_setChainRateLimiterConfig:test_OnlyOwner_Revert() (gas: 17297) +LockReleaseTokenPool_setRateLimitAdmin:test_SetRateLimitAdmin_Revert() (gas: 11057) +LockReleaseTokenPool_setRateLimitAdmin:test_SetRateLimitAdmin_Success() (gas: 35140) +LockReleaseTokenPool_setRebalancer:test_SetRebalancer_Revert() (gas: 10992) +LockReleaseTokenPool_setRebalancer:test_SetRebalancer_Success() (gas: 17926) +LockReleaseTokenPool_supportsInterface:test_SupportsInterface_Success() (gas: 9977) +LockReleaseTokenPool_withdrawalLiquidity:test_InsufficientLiquidity_Revert() (gas: 60043) +LockReleaseTokenPool_withdrawalLiquidity:test_Unauthorized_Revert() (gas: 11355) +MerkleMultiProofTest:test_CVE_2023_34459() (gas: 5451) +MerkleMultiProofTest:test_EmptyLeaf_Revert() (gas: 3552) +MerkleMultiProofTest:test_MerkleRoot256() (gas: 394876) +MerkleMultiProofTest:test_MerkleRootSingleLeaf_Success() (gas: 3649) +MerkleMultiProofTest:test_SpecSync_gas() (gas: 34123) +MockRouterTest:test_ccipSendWithInsufficientNativeTokens_Revert() (gas: 33965) +MockRouterTest:test_ccipSendWithInvalidMsgValue_Revert() (gas: 60758) +MockRouterTest:test_ccipSendWithLinkFeeTokenAndValidMsgValue_Success() (gas: 126294) +MockRouterTest:test_ccipSendWithLinkFeeTokenbutInsufficientAllowance_Revert() (gas: 63302) +MockRouterTest:test_ccipSendWithSufficientNativeFeeTokens_Success() (gas: 43853) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_MultipleConfigsBothLanes_Success() (gas: 132031) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_MultipleConfigs_Success() (gas: 312057) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_OnlyCallableByOwner_Revert() (gas: 17717) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_SingleConfigOutbound_Success() (gas: 75784) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_SingleConfig_Success() (gas: 75700) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_UpdateExistingConfigWithNoDifference_Success() (gas: 38133) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_UpdateExistingConfig_Success() (gas: 53092) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_ZeroChainSelector_Revert() (gas: 17019) +MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_ZeroConfigs_Success() (gas: 12295) +MultiAggregateRateLimiter_constructor:test_ConstructorNoAuthorizedCallers_Success() (gas: 1971805) +MultiAggregateRateLimiter_constructor:test_Constructor_Success() (gas: 2085252) +MultiAggregateRateLimiter_getTokenBucket:test_GetTokenBucket_Success() (gas: 30248) +MultiAggregateRateLimiter_getTokenBucket:test_Refill_Success() (gas: 47358) +MultiAggregateRateLimiter_getTokenBucket:test_TimeUnderflow_Revert() (gas: 15821) +MultiAggregateRateLimiter_getTokenValue:test_GetTokenValue_Success() (gas: 19668) +MultiAggregateRateLimiter_getTokenValue:test_NoTokenPrice_Reverts() (gas: 21253) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageFromUnauthorizedCaller_Revert() (gas: 14527) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithDifferentTokensOnDifferentChains_Success() (gas: 189450) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithDisabledRateLimitToken_Success() (gas: 59927) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithNoTokens_Success() (gas: 17593) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithRateLimitDisabled_Success() (gas: 44895) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithRateLimitExceeded_Revert() (gas: 50598) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithRateLimitReset_Success() (gas: 78780) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithTokensOnDifferentChains_Success() (gas: 263510) +MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithTokens_Success() (gas: 54784) +MultiAggregateRateLimiter_onOutboundMessage:test_RateLimitValueDifferentLanes_Success() (gas: 9223372036854754743) +MultiAggregateRateLimiter_onOutboundMessage:test_ValidateMessageWithNoTokens_Success() (gas: 19104) +MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageFromUnauthorizedCaller_Revert() (gas: 15778) +MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithDifferentTokensOnDifferentChains_Success() (gas: 189438) +MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithDisabledRateLimitToken_Success() (gas: 61662) +MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithRateLimitDisabled_Success() (gas: 46683) +MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithRateLimitExceeded_Revert() (gas: 52371) +MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithRateLimitReset_Success() (gas: 79845) +MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithTokensOnDifferentChains_Success() (gas: 263724) +MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithTokens_Success() (gas: 56541) +MultiAggregateRateLimiter_setPriceRegistry:test_OnlyOwner_Revert() (gas: 11336) +MultiAggregateRateLimiter_setPriceRegistry:test_Owner_Success() (gas: 19124) +MultiAggregateRateLimiter_setPriceRegistry:test_ZeroAddress_Revert() (gas: 10608) +MultiAggregateRateLimiter_updateRateLimitTokens:test_NonOwner_Revert() (gas: 16085) +MultiAggregateRateLimiter_updateRateLimitTokens:test_UpdateRateLimitTokensMultipleChains_Success() (gas: 225643) +MultiAggregateRateLimiter_updateRateLimitTokens:test_UpdateRateLimitTokensSingleChain_Success() (gas: 200192) +MultiAggregateRateLimiter_updateRateLimitTokens:test_UpdateRateLimitTokens_AddsAndRemoves_Success() (gas: 162053) +MultiAggregateRateLimiter_updateRateLimitTokens:test_UpdateRateLimitTokens_RemoveNonExistentToken_Success() (gas: 28509) +MultiAggregateRateLimiter_updateRateLimitTokens:test_ZeroDestToken_Revert() (gas: 17430) +MultiAggregateRateLimiter_updateRateLimitTokens:test_ZeroSourceToken_Revert() (gas: 17485) +MultiOCR3Base_setOCR3Configs:test_FMustBePositive_Revert() (gas: 59331) +MultiOCR3Base_setOCR3Configs:test_FTooHigh_Revert() (gas: 44298) +MultiOCR3Base_setOCR3Configs:test_RepeatSignerAddress_Revert() (gas: 283711) +MultiOCR3Base_setOCR3Configs:test_RepeatTransmitterAddress_Revert() (gas: 422848) +MultiOCR3Base_setOCR3Configs:test_SetConfigIgnoreSigners_Success() (gas: 511694) +MultiOCR3Base_setOCR3Configs:test_SetConfigWithSigners_Success() (gas: 829593) +MultiOCR3Base_setOCR3Configs:test_SetConfigWithoutSigners_Success() (gas: 457446) +MultiOCR3Base_setOCR3Configs:test_SetConfigsZeroInput_Success() (gas: 12376) +MultiOCR3Base_setOCR3Configs:test_SetMultipleConfigs_Success() (gas: 2143220) +MultiOCR3Base_setOCR3Configs:test_SignerCannotBeZeroAddress_Revert() (gas: 141744) +MultiOCR3Base_setOCR3Configs:test_StaticConfigChange_Revert() (gas: 808478) +MultiOCR3Base_setOCR3Configs:test_TooManySigners_Revert() (gas: 171331) +MultiOCR3Base_setOCR3Configs:test_TooManyTransmitters_Revert() (gas: 30298) +MultiOCR3Base_setOCR3Configs:test_TransmitterCannotBeZeroAddress_Revert() (gas: 254454) +MultiOCR3Base_setOCR3Configs:test_UpdateConfigSigners_Success() (gas: 861521) +MultiOCR3Base_setOCR3Configs:test_UpdateConfigTransmittersWithoutSigners_Success() (gas: 475825) +MultiOCR3Base_transmit:test_ConfigDigestMismatch_Revert() (gas: 42837) +MultiOCR3Base_transmit:test_ForkedChain_Revert() (gas: 48442) +MultiOCR3Base_transmit:test_InsufficientSignatures_Revert() (gas: 76930) +MultiOCR3Base_transmit:test_NonUniqueSignature_Revert() (gas: 66127) +MultiOCR3Base_transmit:test_SignatureOutOfRegistration_Revert() (gas: 33419) +MultiOCR3Base_transmit:test_TooManySignatures_Revert() (gas: 79521) +MultiOCR3Base_transmit:test_TransmitSigners_gas_Success() (gas: 34131) +MultiOCR3Base_transmit:test_TransmitWithExtraCalldataArgs_Revert() (gas: 47114) +MultiOCR3Base_transmit:test_TransmitWithLessCalldataArgs_Revert() (gas: 25682) +MultiOCR3Base_transmit:test_TransmitWithoutSignatureVerification_gas_Success() (gas: 18726) +MultiOCR3Base_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 24191) +MultiOCR3Base_transmit:test_UnauthorizedSigner_Revert() (gas: 61409) +MultiOCR3Base_transmit:test_UnconfiguredPlugin_Revert() (gas: 39890) +MultiOCR3Base_transmit:test_ZeroSignatures_Revert() (gas: 32973) +MultiOnRampTokenPoolReentrancy:test_OnRampTokenPoolReentrancy_Success() (gas: 412349) +MultiRampsE2E:test_E2E_3MessagesSuccess_gas() (gas: 1426976) +NonceManager_NonceIncrementation:test_getIncrementedOutboundNonce_Success() (gas: 37907) +NonceManager_NonceIncrementation:test_incrementInboundNonce_Skip() (gas: 23694) +NonceManager_NonceIncrementation:test_incrementInboundNonce_Success() (gas: 38763) +NonceManager_NonceIncrementation:test_incrementNoncesInboundAndOutbound_Success() (gas: 71847) +NonceManager_OffRampUpgrade:test_NoPrevOffRampForChain_Success() (gas: 252566) +NonceManager_OffRampUpgrade:test_UpgradedNonceNewSenderStartsAtZero_Success() (gas: 254866) +NonceManager_OffRampUpgrade:test_UpgradedNonceStartsAtV1Nonce_Success() (gas: 307885) +NonceManager_OffRampUpgrade:test_UpgradedOffRampNonceSkipsIfMsgInFlight_Success() (gas: 290962) +NonceManager_OffRampUpgrade:test_UpgradedSenderNoncesReadsPreviousRampTransitive_Success() (gas: 247990) +NonceManager_OffRampUpgrade:test_UpgradedSenderNoncesReadsPreviousRamp_Success() (gas: 236024) +NonceManager_OffRampUpgrade:test_Upgraded_Success() (gas: 144774) +NonceManager_OnRampUpgrade:test_UpgradeNonceNewSenderStartsAtZero_Success() (gas: 186669) +NonceManager_OnRampUpgrade:test_UpgradeNonceStartsAtV1Nonce_Success() (gas: 237737) +NonceManager_OnRampUpgrade:test_UpgradeSenderNoncesReadsPreviousRamp_Success() (gas: 124995) +NonceManager_OnRampUpgrade:test_Upgrade_Success() (gas: 125923) +NonceManager_applyPreviousRampsUpdates:test_MultipleRampsUpdates() (gas: 122899) +NonceManager_applyPreviousRampsUpdates:test_PreviousRampAlreadySetOffRamp_Revert() (gas: 42959) +NonceManager_applyPreviousRampsUpdates:test_PreviousRampAlreadySetOnRampAndOffRamp_Revert() (gas: 64282) +NonceManager_applyPreviousRampsUpdates:test_PreviousRampAlreadySetOnRamp_Revert() (gas: 42823) +NonceManager_applyPreviousRampsUpdates:test_SingleRampUpdate() (gas: 66548) +NonceManager_applyPreviousRampsUpdates:test_ZeroInput() (gas: 12025) +OCR2BaseNoChecks_setOCR2Config:test_FMustBePositive_Revert() (gas: 12171) +OCR2BaseNoChecks_setOCR2Config:test_RepeatAddress_Revert() (gas: 42233) +OCR2BaseNoChecks_setOCR2Config:test_SetConfigSuccess_gas() (gas: 84124) +OCR2BaseNoChecks_setOCR2Config:test_TooManyTransmitter_Revert() (gas: 36938) +OCR2BaseNoChecks_setOCR2Config:test_TransmitterCannotBeZeroAddress_Revert() (gas: 24158) +OCR2BaseNoChecks_transmit:test_ConfigDigestMismatch_Revert() (gas: 17448) +OCR2BaseNoChecks_transmit:test_ForkedChain_Revert() (gas: 26726) +OCR2BaseNoChecks_transmit:test_TransmitSuccess_gas() (gas: 27478) +OCR2BaseNoChecks_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 21296) +OCR2Base_setOCR2Config:test_FMustBePositive_Revert() (gas: 12189) +OCR2Base_setOCR2Config:test_FTooHigh_Revert() (gas: 12345) +OCR2Base_setOCR2Config:test_OracleOutOfRegister_Revert() (gas: 14892) +OCR2Base_setOCR2Config:test_RepeatAddress_Revert() (gas: 45442) +OCR2Base_setOCR2Config:test_SetConfigSuccess_gas() (gas: 155192) +OCR2Base_setOCR2Config:test_SingerCannotBeZeroAddress_Revert() (gas: 24407) +OCR2Base_setOCR2Config:test_TooManySigners_Revert() (gas: 20508) +OCR2Base_setOCR2Config:test_TransmitterCannotBeZeroAddress_Revert() (gas: 47298) +OCR2Base_transmit:test_ConfigDigestMismatch_Revert() (gas: 19623) +OCR2Base_transmit:test_ForkedChain_Revert() (gas: 37683) +OCR2Base_transmit:test_NonUniqueSignature_Revert() (gas: 55309) +OCR2Base_transmit:test_SignatureOutOfRegistration_Revert() (gas: 20962) +OCR2Base_transmit:test_Transmit2SignersSuccess_gas() (gas: 51686) +OCR2Base_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 23484) +OCR2Base_transmit:test_UnauthorizedSigner_Revert() (gas: 39665) +OCR2Base_transmit:test_WrongNumberOfSignatures_Revert() (gas: 20557) +OnRampTokenPoolReentrancy:test_OnRampTokenPoolReentrancy_Success() (gas: 380360) +PingPong_ccipReceive:test_CcipReceive_Success() (gas: 148380) +PingPong_plumbing:test_Pausing_Success() (gas: 17803) +PingPong_startPingPong:test_StartPingPong_Success() (gas: 178340) +PriceRegistry_applyDestChainConfigUpdates:test_InvalidChainFamilySelector_Revert() (gas: 16719) +PriceRegistry_applyDestChainConfigUpdates:test_InvalidDestBytesOverhead_Revert() (gas: 16784) +PriceRegistry_applyDestChainConfigUpdates:test_InvalidDestChainConfigDestChainSelectorEqZero_Revert() (gas: 16611) +PriceRegistry_applyDestChainConfigUpdates:test_applyDestChainConfigUpdatesDefaultTxGasLimitEqZero_Revert() (gas: 16675) +PriceRegistry_applyDestChainConfigUpdates:test_applyDestChainConfigUpdatesDefaultTxGasLimitGtMaxPerMessageGasLimit_Revert() (gas: 40953) +PriceRegistry_applyDestChainConfigUpdates:test_applyDestChainConfigUpdatesZeroIntput_Success() (gas: 12341) +PriceRegistry_applyDestChainConfigUpdates:test_applyDestChainConfigUpdates_Success() (gas: 139564) +PriceRegistry_applyFeeTokensUpdates:test_ApplyFeeTokensUpdates_Success() (gas: 80002) +PriceRegistry_applyFeeTokensUpdates:test_OnlyCallableByOwner_Revert() (gas: 12603) +PriceRegistry_applyPremiumMultiplierWeiPerEthUpdates:test_OnlyCallableByOwnerOrAdmin_Revert() (gas: 11465) +PriceRegistry_applyPremiumMultiplierWeiPerEthUpdates:test_applyPremiumMultiplierWeiPerEthUpdatesMultipleTokens_Success() (gas: 54149) +PriceRegistry_applyPremiumMultiplierWeiPerEthUpdates:test_applyPremiumMultiplierWeiPerEthUpdatesSingleToken_Success() (gas: 44835) +PriceRegistry_applyPremiumMultiplierWeiPerEthUpdates:test_applyPremiumMultiplierWeiPerEthUpdatesZeroInput() (gas: 12301) +PriceRegistry_applyTokenTransferFeeConfigUpdates:test_ApplyTokenTransferFeeConfig_Success() (gas: 86826) +PriceRegistry_applyTokenTransferFeeConfigUpdates:test_ApplyTokenTransferFeeZeroInput() (gas: 13089) +PriceRegistry_applyTokenTransferFeeConfigUpdates:test_InvalidDestBytesOverhead_Revert() (gas: 17045) +PriceRegistry_applyTokenTransferFeeConfigUpdates:test_OnlyCallableByOwnerOrAdmin_Revert() (gas: 12240) +PriceRegistry_constructor:test_InvalidLinkTokenEqZeroAddress_Revert() (gas: 105966) +PriceRegistry_constructor:test_InvalidMaxFeeJuelsPerMsg_Revert() (gas: 110316) +PriceRegistry_constructor:test_InvalidStalenessThreshold_Revert() (gas: 110369) +PriceRegistry_constructor:test_Setup_Success() (gas: 4650895) +PriceRegistry_convertTokenAmount:test_ConvertTokenAmount_Success() (gas: 72751) +PriceRegistry_convertTokenAmount:test_LinkTokenNotSupported_Revert() (gas: 30981) +PriceRegistry_getDataAvailabilityCost:test_EmptyMessageCalculatesDataAvailabilityCost_Success() (gas: 95575) +PriceRegistry_getDataAvailabilityCost:test_SimpleMessageCalculatesDataAvailabilityCostUnsupportedDestChainSelector_Success() (gas: 14636) +PriceRegistry_getDataAvailabilityCost:test_SimpleMessageCalculatesDataAvailabilityCost_Success() (gas: 20614) +PriceRegistry_getTokenAndGasPrices:test_GetFeeTokenAndGasPrices_Success() (gas: 70449) +PriceRegistry_getTokenAndGasPrices:test_StaleGasPrice_Revert() (gas: 16838) +PriceRegistry_getTokenAndGasPrices:test_UnsupportedChain_Revert() (gas: 16140) +PriceRegistry_getTokenAndGasPrices:test_ZeroGasPrice_Success() (gas: 45734) +PriceRegistry_getTokenPrice:test_GetTokenPriceFromFeed_Success() (gas: 62311) +PriceRegistry_getTokenPrices:test_GetTokenPrices_Success() (gas: 84774) +PriceRegistry_getTokenTransferCost:test_CustomTokenBpsFee_Success() (gas: 41283) +PriceRegistry_getTokenTransferCost:test_FeeTokenBpsFee_Success() (gas: 34733) +PriceRegistry_getTokenTransferCost:test_LargeTokenTransferChargesMaxFeeAndGas_Success() (gas: 27807) +PriceRegistry_getTokenTransferCost:test_MixedTokenTransferFee_Success() (gas: 108018) +PriceRegistry_getTokenTransferCost:test_NoTokenTransferChargesZeroFee_Success() (gas: 20359) +PriceRegistry_getTokenTransferCost:test_SmallTokenTransferChargesMinFeeAndGas_Success() (gas: 27615) +PriceRegistry_getTokenTransferCost:test_WETHTokenBpsFee_Success() (gas: 40668) +PriceRegistry_getTokenTransferCost:test_ZeroAmountTokenTransferChargesMinFeeAndGas_Success() (gas: 27638) +PriceRegistry_getTokenTransferCost:test_ZeroFeeConfigChargesMinFee_Success() (gas: 40015) +PriceRegistry_getTokenTransferCost:test_getTokenTransferCost_selfServeUsesDefaults_Success() (gas: 29343) +PriceRegistry_getValidatedFee:test_DestinationChainNotEnabled_Revert() (gas: 18203) +PriceRegistry_getValidatedFee:test_EmptyMessage_Success() (gas: 81464) +PriceRegistry_getValidatedFee:test_EnforceOutOfOrder_Revert() (gas: 55184) +PriceRegistry_getValidatedFee:test_HighGasMessage_Success() (gas: 237926) +PriceRegistry_getValidatedFee:test_InvalidEVMAddress_Revert() (gas: 19971) +PriceRegistry_getValidatedFee:test_MessageGasLimitTooHigh_Revert() (gas: 31775) +PriceRegistry_getValidatedFee:test_MessageTooLarge_Revert() (gas: 97714) +PriceRegistry_getValidatedFee:test_MessageWithDataAndTokenTransfer_Success() (gas: 143193) +PriceRegistry_getValidatedFee:test_NotAFeeToken_Revert() (gas: 29435) +PriceRegistry_getValidatedFee:test_SingleTokenMessage_Success() (gas: 112283) +PriceRegistry_getValidatedFee:test_TooManyTokens_Revert() (gas: 20107) +PriceRegistry_getValidatedFee:test_ZeroDataAvailabilityMultiplier_Success() (gas: 62956) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPriceFromFeedErc20Above18Decimals_Success() (gas: 2094532) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPriceFromFeedErc20Below18Decimals_Success() (gas: 2094490) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPriceFromFeedFeedAt0Decimals_Success() (gas: 2074609) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPriceFromFeedFeedAt18Decimals_Success() (gas: 2094264) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPriceFromFeedFlippedDecimals_Success() (gas: 2094468) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPriceFromFeedMaxInt224Value_Success() (gas: 2094280) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPriceFromFeedOverStalenessPeriod_Success() (gas: 61997) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPriceFromFeed_Success() (gas: 61877) +PriceRegistry_getValidatedTokenPrice:test_GetValidatedTokenPrice_Success() (gas: 60998) +PriceRegistry_getValidatedTokenPrice:test_OverflowFeedPrice_Revert() (gas: 2093992) +PriceRegistry_getValidatedTokenPrice:test_StaleFeeToken_Success() (gas: 61525) +PriceRegistry_getValidatedTokenPrice:test_TokenNotSupportedFeed_Revert() (gas: 109113) +PriceRegistry_getValidatedTokenPrice:test_TokenNotSupported_Revert() (gas: 13819) +PriceRegistry_getValidatedTokenPrice:test_UnderflowFeedPrice_Revert() (gas: 2092670) +PriceRegistry_parseEVMExtraArgsFromBytes:test_EVMExtraArgsDefault_Success() (gas: 17360) +PriceRegistry_parseEVMExtraArgsFromBytes:test_EVMExtraArgsEnforceOutOfOrder_Revert() (gas: 21454) +PriceRegistry_parseEVMExtraArgsFromBytes:test_EVMExtraArgsGasLimitTooHigh_Revert() (gas: 18551) +PriceRegistry_parseEVMExtraArgsFromBytes:test_EVMExtraArgsInvalidExtraArgsTag_Revert() (gas: 18075) +PriceRegistry_parseEVMExtraArgsFromBytes:test_EVMExtraArgsV1_Success() (gas: 18452) +PriceRegistry_parseEVMExtraArgsFromBytes:test_EVMExtraArgsV2_Success() (gas: 18569) +PriceRegistry_processMessageArgs:test_InvalidExtraArgs_Revert() (gas: 18306) +PriceRegistry_processMessageArgs:test_MalformedEVMExtraArgs_Revert() (gas: 18852) +PriceRegistry_processMessageArgs:test_MessageFeeTooHigh_Revert() (gas: 16360) +PriceRegistry_processMessageArgs:test_WitEVMExtraArgsV2_Success() (gas: 26236) +PriceRegistry_processMessageArgs:test_WithConvertedTokenAmount_Success() (gas: 32410) +PriceRegistry_processMessageArgs:test_WithEVMExtraArgsV1_Success() (gas: 25848) +PriceRegistry_processMessageArgs:test_WithEmptyEVMExtraArgs_Success() (gas: 23663) +PriceRegistry_processMessageArgs:test_WithLinkTokenAmount_Success() (gas: 17320) +PriceRegistry_updatePrices:test_OnlyCallableByUpdater_Revert() (gas: 12080) +PriceRegistry_updatePrices:test_OnlyGasPrice_Success() (gas: 23599) +PriceRegistry_updatePrices:test_OnlyTokenPrice_Success() (gas: 30637) +PriceRegistry_updatePrices:test_UpdatableByAuthorizedCaller_Success() (gas: 76043) +PriceRegistry_updatePrices:test_UpdateMultiplePrices_Success() (gas: 151521) +PriceRegistry_updateTokenPriceFeeds:test_FeedNotUpdated() (gas: 50699) +PriceRegistry_updateTokenPriceFeeds:test_FeedUnset_Success() (gas: 63882) +PriceRegistry_updateTokenPriceFeeds:test_FeedUpdatedByNonOwner_Revert() (gas: 19998) +PriceRegistry_updateTokenPriceFeeds:test_MultipleFeedUpdate_Success() (gas: 89162) +PriceRegistry_updateTokenPriceFeeds:test_SingleFeedUpdate_Success() (gas: 50949) +PriceRegistry_updateTokenPriceFeeds:test_ZeroFeeds_Success() (gas: 12362) +PriceRegistry_validateDestFamilyAddress:test_InvalidEVMAddressEncodePacked_Revert() (gas: 10572) +PriceRegistry_validateDestFamilyAddress:test_InvalidEVMAddressPrecompiles_Revert() (gas: 3916546) +PriceRegistry_validateDestFamilyAddress:test_InvalidEVMAddress_Revert() (gas: 10756) +PriceRegistry_validateDestFamilyAddress:test_ValidEVMAddress_Success() (gas: 6660) +PriceRegistry_validateDestFamilyAddress:test_ValidNonEVMAddress_Success() (gas: 6440) +PriceRegistry_validatePoolReturnData:test_InvalidEVMAddressDestToken_Revert() (gas: 35457) +PriceRegistry_validatePoolReturnData:test_SourceTokenDataTooLarge_Revert() (gas: 90631) +PriceRegistry_validatePoolReturnData:test_TokenAmountArraysMismatching_Revert() (gas: 32749) +PriceRegistry_validatePoolReturnData:test_WithSingleToken_Success() (gas: 31293) +RMN_constructor:test_Constructor_Success() (gas: 48838) +RMN_getRecordedCurseRelatedOps:test_OpsPostDeployment() (gas: 19666) +RMN_lazyVoteToCurseUpdate_Benchmark:test_VoteToCurseLazilyRetain3VotersUponConfigChange_gas() (gas: 152152) +RMN_ownerUnbless:test_Unbless_Success() (gas: 74699) +RMN_ownerUnvoteToCurse:test_CanBlessAndCurseAfterGlobalCurseIsLifted() (gas: 470965) +RMN_ownerUnvoteToCurse:test_IsIdempotent() (gas: 397532) +RMN_ownerUnvoteToCurse:test_NonOwner_Revert() (gas: 18591) +RMN_ownerUnvoteToCurse:test_OwnerUnvoteToCurseSuccess_gas() (gas: 357403) +RMN_ownerUnvoteToCurse:test_UnknownVoter_Revert() (gas: 32980) +RMN_ownerUnvoteToCurse_Benchmark:test_OwnerUnvoteToCurse_1Voter_LiftsCurse_gas() (gas: 261985) +RMN_permaBlessing:test_PermaBlessing() (gas: 202686) +RMN_setConfig:test_BlessVoterIsZeroAddress_Revert() (gas: 15494) +RMN_setConfig:test_EitherThresholdIsZero_Revert() (gas: 21095) +RMN_setConfig:test_NonOwner_Revert() (gas: 14713) +RMN_setConfig:test_RepeatedAddress_Revert() (gas: 18213) +RMN_setConfig:test_SetConfigSuccess_gas() (gas: 104204) +RMN_setConfig:test_TotalWeightsSmallerThanEachThreshold_Revert() (gas: 30173) +RMN_setConfig:test_VoteToBlessByEjectedVoter_Revert() (gas: 130303) +RMN_setConfig:test_VotersLengthIsZero_Revert() (gas: 12128) +RMN_setConfig:test_WeightIsZeroAddress_Revert() (gas: 15734) +RMN_setConfig_Benchmark_1:test_SetConfig_7Voters_gas() (gas: 659123) +RMN_setConfig_Benchmark_2:test_ResetConfig_7Voters_gas() (gas: 212156) +RMN_unvoteToCurse:test_InvalidCursesHash() (gas: 26364) +RMN_unvoteToCurse:test_OwnerSkips() (gas: 33753) +RMN_unvoteToCurse:test_OwnerSucceeds() (gas: 63909) +RMN_unvoteToCurse:test_UnauthorizedVoter() (gas: 47478) +RMN_unvoteToCurse:test_ValidCursesHash() (gas: 61067) +RMN_unvoteToCurse:test_VotersCantLiftCurseButOwnerCan() (gas: 627750) +RMN_voteToBless:test_Curse_Revert() (gas: 472823) +RMN_voteToBless:test_IsAlreadyBlessed_Revert() (gas: 114829) +RMN_voteToBless:test_RootSuccess() (gas: 555559) +RMN_voteToBless:test_SenderAlreadyVoted_Revert() (gas: 96730) +RMN_voteToBless:test_UnauthorizedVoter_Revert() (gas: 17087) +RMN_voteToBless_Benchmark:test_1RootSuccess_gas() (gas: 44667) +RMN_voteToBless_Benchmark:test_3RootSuccess_gas() (gas: 98565) +RMN_voteToBless_Benchmark:test_5RootSuccess_gas() (gas: 152401) +RMN_voteToBless_Blessed_Benchmark:test_1RootSuccessBecameBlessed_gas() (gas: 29619) +RMN_voteToBless_Blessed_Benchmark:test_1RootSuccess_gas() (gas: 27565) +RMN_voteToBless_Blessed_Benchmark:test_3RootSuccess_gas() (gas: 81485) +RMN_voteToBless_Blessed_Benchmark:test_5RootSuccess_gas() (gas: 135299) +RMN_voteToCurse:test_CurseOnlyWhenThresholdReached_Success() (gas: 1648701) +RMN_voteToCurse:test_EmptySubjects_Revert() (gas: 14019) +RMN_voteToCurse:test_EvenIfAlreadyCursed_Success() (gas: 534332) +RMN_voteToCurse:test_OwnerCanCurseAndUncurse() (gas: 399001) +RMN_voteToCurse:test_RepeatedSubject_Revert() (gas: 144225) +RMN_voteToCurse:test_ReusedCurseId_Revert() (gas: 146738) +RMN_voteToCurse:test_UnauthorizedVoter_Revert() (gas: 12600) +RMN_voteToCurse:test_VoteToCurse_NoCurse_Success() (gas: 187244) +RMN_voteToCurse:test_VoteToCurse_YesCurse_Success() (gas: 472452) +RMN_voteToCurse_2:test_VotesAreDroppedIfSubjectIsNotCursedDuringConfigChange() (gas: 370468) +RMN_voteToCurse_2:test_VotesAreRetainedIfSubjectIsCursedDuringConfigChange() (gas: 1151909) +RMN_voteToCurse_Benchmark_1:test_VoteToCurse_NewSubject_NewVoter_NoCurse_gas() (gas: 140968) +RMN_voteToCurse_Benchmark_1:test_VoteToCurse_NewSubject_NewVoter_YesCurse_gas() (gas: 165087) +RMN_voteToCurse_Benchmark_2:test_VoteToCurse_OldSubject_NewVoter_NoCurse_gas() (gas: 121305) +RMN_voteToCurse_Benchmark_2:test_VoteToCurse_OldSubject_OldVoter_NoCurse_gas() (gas: 98247) +RMN_voteToCurse_Benchmark_3:test_VoteToCurse_OldSubject_NewVoter_YesCurse_gas() (gas: 145631) +RateLimiter_constructor:test_Constructor_Success() (gas: 19650) +RateLimiter_consume:test_AggregateValueMaxCapacityExceeded_Revert() (gas: 15916) +RateLimiter_consume:test_AggregateValueRateLimitReached_Revert() (gas: 22222) +RateLimiter_consume:test_ConsumeAggregateValue_Success() (gas: 31353) +RateLimiter_consume:test_ConsumeTokens_Success() (gas: 20336) +RateLimiter_consume:test_ConsumeUnlimited_Success() (gas: 40285) +RateLimiter_consume:test_ConsumingMoreThanUint128_Revert() (gas: 15720) +RateLimiter_consume:test_RateLimitReachedOverConsecutiveBlocks_Revert() (gas: 25594) +RateLimiter_consume:test_Refill_Success() (gas: 37222) +RateLimiter_consume:test_TokenMaxCapacityExceeded_Revert() (gas: 18250) +RateLimiter_consume:test_TokenRateLimitReached_Revert() (gas: 24706) +RateLimiter_currentTokenBucketState:test_CurrentTokenBucketState_Success() (gas: 38647) +RateLimiter_currentTokenBucketState:test_Refill_Success() (gas: 46384) +RateLimiter_setTokenBucketConfig:test_SetRateLimiterConfig_Success() (gas: 38017) +RegistryModuleOwnerCustom_constructor:test_constructor_Revert() (gas: 36031) +RegistryModuleOwnerCustom_registerAdminViaGetCCIPAdmin:test_registerAdminViaGetCCIPAdmin_Revert() (gas: 19637) +RegistryModuleOwnerCustom_registerAdminViaGetCCIPAdmin:test_registerAdminViaGetCCIPAdmin_Success() (gas: 129918) +RegistryModuleOwnerCustom_registerAdminViaOwner:test_registerAdminViaOwner_Revert() (gas: 19451) +RegistryModuleOwnerCustom_registerAdminViaOwner:test_registerAdminViaOwner_Success() (gas: 129731) +Router_applyRampUpdates:test_OffRampMismatch_Revert() (gas: 89288) +Router_applyRampUpdates:test_OffRampUpdatesWithRouting() (gas: 10642128) +Router_applyRampUpdates:test_OnRampDisable() (gas: 55913) +Router_applyRampUpdates:test_OnlyOwner_Revert() (gas: 12311) +Router_ccipSend:test_CCIPSendLinkFeeNoTokenSuccess_gas() (gas: 113861) +Router_ccipSend:test_CCIPSendLinkFeeOneTokenSuccess_gas() (gas: 200634) +Router_ccipSend:test_CCIPSendNativeFeeNoTokenSuccess_gas() (gas: 128508) +Router_ccipSend:test_CCIPSendNativeFeeOneTokenSuccess_gas() (gas: 215283) +Router_ccipSend:test_FeeTokenAmountTooLow_Revert() (gas: 66275) +Router_ccipSend:test_InvalidMsgValue() (gas: 31963) +Router_ccipSend:test_NativeFeeTokenInsufficientValue() (gas: 68711) +Router_ccipSend:test_NativeFeeTokenOverpay_Success() (gas: 173605) +Router_ccipSend:test_NativeFeeTokenZeroValue() (gas: 56037) +Router_ccipSend:test_NativeFeeToken_Success() (gas: 172199) +Router_ccipSend:test_NonLinkFeeToken_Success() (gas: 242707) +Router_ccipSend:test_UnsupportedDestinationChain_Revert() (gas: 24749) +Router_ccipSend:test_WhenNotHealthy_Revert() (gas: 44724) +Router_ccipSend:test_WrappedNativeFeeToken_Success() (gas: 174415) +Router_ccipSend:test_ZeroFeeAndGasPrice_Success() (gas: 245121) +Router_constructor:test_Constructor_Success() (gas: 13074) +Router_getArmProxy:test_getArmProxy() (gas: 10561) +Router_getFee:test_GetFeeSupportedChain_Success() (gas: 46464) +Router_getFee:test_UnsupportedDestinationChain_Revert() (gas: 17138) +Router_getSupportedTokens:test_GetSupportedTokens_Revert() (gas: 10460) +Router_recoverTokens:test_RecoverTokensInvalidRecipient_Revert() (gas: 11316) +Router_recoverTokens:test_RecoverTokensNoFunds_Revert() (gas: 17761) +Router_recoverTokens:test_RecoverTokensNonOwner_Revert() (gas: 11159) +Router_recoverTokens:test_RecoverTokensValueReceiver_Revert() (gas: 422138) +Router_recoverTokens:test_RecoverTokens_Success() (gas: 50437) +Router_routeMessage:test_AutoExec_Success() (gas: 42684) +Router_routeMessage:test_ExecutionEvent_Success() (gas: 158002) +Router_routeMessage:test_ManualExec_Success() (gas: 35381) +Router_routeMessage:test_OnlyOffRamp_Revert() (gas: 25116) +Router_routeMessage:test_WhenNotHealthy_Revert() (gas: 44724) +Router_setWrappedNative:test_OnlyOwner_Revert() (gas: 10985) +SelfFundedPingPong_ccipReceive:test_FundingIfNotANop_Revert() (gas: 53540) +SelfFundedPingPong_ccipReceive:test_Funding_Success() (gas: 416930) +SelfFundedPingPong_setCountIncrBeforeFunding:test_setCountIncrBeforeFunding() (gas: 20157) +TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole_OnlyPendingAdministrator_Revert() (gas: 51085) +TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole_Success() (gas: 43947) +TokenAdminRegistry_addRegistryModule:test_addRegistryModule_OnlyOwner_Revert() (gas: 12629) +TokenAdminRegistry_addRegistryModule:test_addRegistryModule_Success() (gas: 67011) +TokenAdminRegistry_getAllConfiguredTokens:test_getAllConfiguredTokens_outOfBounds_Success() (gas: 11350) +TokenAdminRegistry_getPool:test_getPool_Success() (gas: 17581) +TokenAdminRegistry_getPools:test_getPools_Success() (gas: 39902) +TokenAdminRegistry_isAdministrator:test_isAdministrator_Success() (gas: 105922) +TokenAdminRegistry_proposeAdministrator:test_proposeAdministrator_AlreadyRegistered_Revert() (gas: 104001) +TokenAdminRegistry_proposeAdministrator:test_proposeAdministrator_OnlyRegistryModule_Revert() (gas: 15481) +TokenAdminRegistry_proposeAdministrator:test_proposeAdministrator_ZeroAddress_Revert() (gas: 15026) +TokenAdminRegistry_proposeAdministrator:test_proposeAdministrator_module_Success() (gas: 112536) +TokenAdminRegistry_proposeAdministrator:test_proposeAdministrator_owner_Success() (gas: 107656) +TokenAdminRegistry_proposeAdministrator:test_proposeAdministrator_reRegisterWhileUnclaimed_Success() (gas: 115686) +TokenAdminRegistry_removeRegistryModule:test_removeRegistryModule_OnlyOwner_Revert() (gas: 12585) +TokenAdminRegistry_removeRegistryModule:test_removeRegistryModule_Success() (gas: 54473) +TokenAdminRegistry_setPool:test_setPool_InvalidTokenPoolToken_Revert() (gas: 19148) +TokenAdminRegistry_setPool:test_setPool_OnlyAdministrator_Revert() (gas: 18020) +TokenAdminRegistry_setPool:test_setPool_Success() (gas: 35943) +TokenAdminRegistry_setPool:test_setPool_ZeroAddressRemovesPool_Success() (gas: 30617) +TokenAdminRegistry_transferAdminRole:test_transferAdminRole_OnlyAdministrator_Revert() (gas: 18043) +TokenAdminRegistry_transferAdminRole:test_transferAdminRole_Success() (gas: 49390) +TokenPoolAndProxy:test_lockOrBurn_burnMint_Success() (gas: 6036775) +TokenPoolAndProxy:test_lockOrBurn_lockRelease_Success() (gas: 6282531) +TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_2() (gas: 6883397) +TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_4() (gas: 7067512) +TokenPoolWithAllowList_applyAllowListUpdates:test_AllowListNotEnabled_Revert() (gas: 2169749) +TokenPoolWithAllowList_applyAllowListUpdates:test_OnlyOwner_Revert() (gas: 12089) +TokenPoolWithAllowList_applyAllowListUpdates:test_SetAllowListSkipsZero_Success() (gas: 23280) +TokenPoolWithAllowList_applyAllowListUpdates:test_SetAllowList_Success() (gas: 177516) +TokenPoolWithAllowList_getAllowList:test_GetAllowList_Success() (gas: 23648) +TokenPoolWithAllowList_getAllowListEnabled:test_GetAllowListEnabled_Success() (gas: 8363) +TokenPoolWithAllowList_setRouter:test_SetRouter_Success() (gas: 24765) +TokenPool_applyChainUpdates:test_applyChainUpdates_DisabledNonZeroRateLimit_Revert() (gas: 271305) +TokenPool_applyChainUpdates:test_applyChainUpdates_InvalidRateLimitRate_Revert() (gas: 541162) +TokenPool_applyChainUpdates:test_applyChainUpdates_NonExistentChain_Revert() (gas: 18344) +TokenPool_applyChainUpdates:test_applyChainUpdates_OnlyCallableByOwner_Revert() (gas: 11385) +TokenPool_applyChainUpdates:test_applyChainUpdates_Success() (gas: 476472) +TokenPool_applyChainUpdates:test_applyChainUpdates_ZeroAddressNotAllowed_Revert() (gas: 157074) +TokenPool_constructor:test_ZeroAddressNotAllowed_Revert() (gas: 70676) +TokenPool_constructor:test_immutableFields_Success() (gas: 20522) +TokenPool_getRemotePool:test_getRemotePool_Success() (gas: 273962) +TokenPool_onlyOffRamp:test_CallerIsNotARampOnRouter_Revert() (gas: 276952) +TokenPool_onlyOffRamp:test_ChainNotAllowed_Revert() (gas: 289509) +TokenPool_onlyOffRamp:test_onlyOffRamp_Success() (gas: 349763) +TokenPool_onlyOnRamp:test_CallerIsNotARampOnRouter_Revert() (gas: 276643) +TokenPool_onlyOnRamp:test_ChainNotAllowed_Revert() (gas: 253466) +TokenPool_onlyOnRamp:test_onlyOnRamp_Success() (gas: 304761) +TokenPool_setChainRateLimiterConfig:test_NonExistentChain_Revert() (gas: 14906) +TokenPool_setChainRateLimiterConfig:test_OnlyOwner_Revert() (gas: 12565) +TokenPool_setRemotePool:test_setRemotePool_NonExistentChain_Reverts() (gas: 15598) +TokenPool_setRemotePool:test_setRemotePool_OnlyOwner_Reverts() (gas: 13173) +TokenPool_setRemotePool:test_setRemotePool_Success() (gas: 281890) +TokenProxy_ccipSend:test_CcipSendGasShouldBeZero_Revert() (gas: 17109) +TokenProxy_ccipSend:test_CcipSendInsufficientAllowance_Revert() (gas: 136351) +TokenProxy_ccipSend:test_CcipSendInvalidToken_Revert() (gas: 15919) +TokenProxy_ccipSend:test_CcipSendNative_Success() (gas: 244483) +TokenProxy_ccipSend:test_CcipSendNoDataAllowed_Revert() (gas: 16303) +TokenProxy_ccipSend:test_CcipSend_Success() (gas: 261100) +TokenProxy_constructor:test_Constructor() (gas: 13812) +TokenProxy_getFee:test_GetFeeGasShouldBeZero_Revert() (gas: 16827) +TokenProxy_getFee:test_GetFeeInvalidToken_Revert() (gas: 12658) +TokenProxy_getFee:test_GetFeeNoDataAllowed_Revert() (gas: 15849) +TokenProxy_getFee:test_GetFee_Success() (gas: 86948) +USDCTokenPool__validateMessage:test_ValidateInvalidMessage_Revert() (gas: 24960) +USDCTokenPool_lockOrBurn:test_CallerIsNotARampOnRouter_Revert() (gas: 35312) +USDCTokenPool_lockOrBurn:test_LockOrBurnWithAllowList_Revert() (gas: 30063) +USDCTokenPool_lockOrBurn:test_LockOrBurn_Success() (gas: 132864) +USDCTokenPool_lockOrBurn:test_UnknownDomain_Revert() (gas: 477209) +USDCTokenPool_lockOrBurn:test_lockOrBurn_InvalidReceiver_Revert() (gas: 52606) +USDCTokenPool_releaseOrMint:test_ReleaseOrMintRealTx_Success() (gas: 289268) +USDCTokenPool_releaseOrMint:test_TokenMaxCapacityExceeded_Revert() (gas: 50682) +USDCTokenPool_releaseOrMint:test_UnlockingUSDCFailed_Revert() (gas: 119185) +USDCTokenPool_setDomains:test_InvalidDomain_Revert() (gas: 66150) +USDCTokenPool_setDomains:test_OnlyOwner_Revert() (gas: 11339) +USDCTokenPool_supportsInterface:test_SupportsInterface_Success() (gas: 9876) \ No newline at end of file diff --git a/contracts/gas-snapshots/liquiditymanager.gas-snapshot b/contracts/gas-snapshots/liquiditymanager.gas-snapshot new file mode 100644 index 00000000000..53483ed6c7c --- /dev/null +++ b/contracts/gas-snapshots/liquiditymanager.gas-snapshot @@ -0,0 +1,48 @@ +LiquidityManager__report:test_EmptyReportReverts() (gas: 11181) +LiquidityManager_addLiquidity:test_addLiquiditySuccess() (gas: 279154) +LiquidityManager_rebalanceLiquidity:test_InsufficientLiquidityReverts() (gas: 206745) +LiquidityManager_rebalanceLiquidity:test_InvalidRemoteChainReverts() (gas: 192319) +LiquidityManager_rebalanceLiquidity:test_rebalanceBetweenPoolsSuccess() (gas: 9141768) +LiquidityManager_rebalanceLiquidity:test_rebalanceBetweenPoolsSuccess_AlreadyFinalized() (gas: 8898695) +LiquidityManager_rebalanceLiquidity:test_rebalanceBetweenPools_MultiStageFinalization() (gas: 8893901) +LiquidityManager_rebalanceLiquidity:test_rebalanceBetweenPools_NativeRewrap() (gas: 8821699) +LiquidityManager_rebalanceLiquidity:test_rebalanceLiquiditySuccess() (gas: 382897) +LiquidityManager_receive:test_receive_success() (gas: 21182) +LiquidityManager_removeLiquidity:test_InsufficientLiquidityReverts() (gas: 184869) +LiquidityManager_removeLiquidity:test_OnlyFinanceRoleReverts() (gas: 10872) +LiquidityManager_removeLiquidity:test_removeLiquiditySuccess() (gas: 236342) +LiquidityManager_setCrossChainRebalancer:test_OnlyOwnerReverts() (gas: 17005) +LiquidityManager_setCrossChainRebalancer:test_ZeroAddressReverts() (gas: 21624) +LiquidityManager_setCrossChainRebalancer:test_ZeroChainSelectorReverts() (gas: 13099) +LiquidityManager_setCrossChainRebalancer:test_setCrossChainRebalancerSuccess() (gas: 162186) +LiquidityManager_setFinanceRole:test_OnlyOwnerReverts() (gas: 10987) +LiquidityManager_setFinanceRole:test_setFinanceRoleSuccess() (gas: 21836) +LiquidityManager_setLocalLiquidityContainer:test_OnlyOwnerReverts() (gas: 11052) +LiquidityManager_setLocalLiquidityContainer:test_ReverstWhen_CalledWithTheZeroAddress() (gas: 10643) +LiquidityManager_setLocalLiquidityContainer:test_setLocalLiquidityContainerSuccess() (gas: 3436651) +LiquidityManager_setMinimumLiquidity:test_OnlyOwnerReverts() (gas: 10925) +LiquidityManager_setMinimumLiquidity:test_setMinimumLiquiditySuccess() (gas: 36389) +LiquidityManager_withdrawERC20:test_withdrawERC20Reverts() (gas: 180359) +LiquidityManager_withdrawERC20:test_withdrawERC20Success() (gas: 205858) +LiquidityManager_withdrawNative:test_OnlyFinanceRoleReverts() (gas: 13046) +LiquidityManager_withdrawNative:test_withdrawNative_success() (gas: 51398) +OCR3Base_setOCR3Config:testFMustBePositiveReverts() (gas: 12245) +OCR3Base_setOCR3Config:testFTooHighReverts() (gas: 12429) +OCR3Base_setOCR3Config:testOracleOutOfRegisterReverts() (gas: 14847) +OCR3Base_setOCR3Config:testRepeatAddressReverts() (gas: 44932) +OCR3Base_setOCR3Config:testSetConfigSuccess() (gas: 154642) +OCR3Base_setOCR3Config:testSignerCannotBeZeroAddressReverts() (gas: 23712) +OCR3Base_setOCR3Config:testTooManySignersReverts() (gas: 19832) +OCR3Base_setOCR3Config:testTransmitterCannotBeZeroAddressReverts() (gas: 46539) +OCR3Base_transmit:testConfigDigestMismatchReverts() (gas: 24827) +OCR3Base_transmit:testForkedChainReverts() (gas: 42846) +OCR3Base_transmit:testNonIncreasingSequenceNumberReverts() (gas: 30522) +OCR3Base_transmit:testNonUniqueSignatureReverts() (gas: 60370) +OCR3Base_transmit:testSignatureOutOfRegistrationReverts() (gas: 26128) +OCR3Base_transmit:testTransmit2SignersSuccess_gas() (gas: 56783) +OCR3Base_transmit:testUnAuthorizedTransmitterReverts() (gas: 28618) +OCR3Base_transmit:testUnauthorizedSignerReverts() (gas: 44759) +OCR3Base_transmit:testWrongNumberOfSignaturesReverts() (gas: 25678) +OptimismL1BridgeAdapter_finalizeWithdrawERC20:testFinalizeWithdrawERC20Reverts() (gas: 12932) +OptimismL1BridgeAdapter_finalizeWithdrawERC20:testfinalizeWithdrawERC20FinalizeSuccess() (gas: 16972) +OptimismL1BridgeAdapter_finalizeWithdrawERC20:testfinalizeWithdrawERC20proveWithdrawalSuccess() (gas: 20758) \ No newline at end of file diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 1b2ac1bdf19..73e70081e9a 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -21,7 +21,8 @@ subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS).setAction( async (_, __, runSuper) => { const paths = await runSuper() const noTests = paths.filter((p: string) => !p.endsWith('.t.sol')) - return noTests.filter( + const noCCIP = noTests.filter((p: string) => !p.includes('/v0.8/ccip')) + return noCCIP.filter( (p: string) => !p.includes('src/v0.8/vendor/forge-std'), ) }, diff --git a/contracts/package.json b/contracts/package.json index 5a13d561f0c..85bae226c43 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -18,7 +18,7 @@ "prepublishOnly": "pnpm compile && ./scripts/prepublish_generate_abi_folder", "publish-beta": "pnpm publish --tag beta", "publish-prod": "pnpm publish --tag latest", - "solhint": "solhint --max-warnings 2 \"./src/v0.8/**/*.sol\"" + "solhint": "solhint --max-warnings 0 \"./src/v0.8/**/*.sol\"" }, "files": [ "src/v0.8", @@ -78,6 +78,8 @@ "typescript": "^5.4.5" }, "dependencies": { + "@arbitrum/nitro-contracts": "1.1.1", + "@arbitrum/token-bridge-contracts": "1.1.2", "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "~2.27.3", "@eth-optimism/contracts": "0.6.0", diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index 4ad8deda991..825715f4160 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: .: dependencies: + '@arbitrum/nitro-contracts': + specifier: 1.1.1 + version: 1.1.1 + '@arbitrum/token-bridge-contracts': + specifier: 1.1.2 + version: 1.1.2 '@changesets/changelog-github': specifier: ^0.5.0 version: 0.5.0 @@ -50,22 +56,22 @@ importers: version: 5.7.2 '@nomicfoundation/hardhat-chai-matchers': specifier: ^1.0.6 - version: 1.0.6(@nomiclabs/hardhat-ethers@2.2.3)(chai@4.4.1)(ethers@5.7.2)(hardhat@2.20.1) + version: 1.0.6(@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)))(chai@4.4.1)(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)) '@nomicfoundation/hardhat-ethers': specifier: ^3.0.6 - version: 3.0.6(ethers@5.7.2)(hardhat@2.20.1) + version: 3.0.6(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)) '@nomicfoundation/hardhat-network-helpers': specifier: ^1.0.9 - version: 1.0.10(hardhat@2.20.1) + version: 1.0.10(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)) '@nomicfoundation/hardhat-verify': specifier: ^2.0.7 - version: 2.0.7(hardhat@2.20.1) + version: 2.0.7(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)) '@typechain/ethers-v5': specifier: ^7.2.0 - version: 7.2.0(@ethersproject/abi@5.7.0)(@ethersproject/bytes@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.2)(typescript@5.4.5) + version: 7.2.0(@ethersproject/abi@5.7.0)(@ethersproject/bytes@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.2(typescript@5.4.5))(typescript@5.4.5) '@typechain/hardhat': specifier: ^7.0.0 - version: 7.0.0(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(@typechain/ethers-v5@7.2.0)(ethers@5.7.2)(hardhat@2.20.1)(typechain@8.3.2) + version: 7.0.0(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(@typechain/ethers-v5@7.2.0(@ethersproject/abi@5.7.0)(@ethersproject/bytes@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.2(typescript@5.4.5))(typescript@5.4.5))(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5))(typechain@8.3.2(typescript@5.4.5)) '@types/cbor': specifier: ~5.0.1 version: 5.0.1 @@ -86,7 +92,7 @@ importers: version: 20.12.12 '@typescript-eslint/eslint-plugin': specifier: ^7.10.0 - version: 7.10.0(@typescript-eslint/parser@7.10.0)(eslint@8.57.0)(typescript@5.4.5) + version: 7.10.0(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/parser': specifier: ^7.10.0 version: 7.10.0(eslint@8.57.0)(typescript@5.4.5) @@ -113,16 +119,16 @@ importers: version: 9.1.0(eslint@8.57.0) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) + version: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5) ethers: specifier: ~5.7.2 version: 5.7.2 hardhat: specifier: ~2.20.1 - version: 2.20.1(ts-node@10.9.2)(typescript@5.4.5) + version: 2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) hardhat-abi-exporter: specifier: ^2.10.1 - version: 2.10.1(hardhat@2.20.1) + version: 2.10.1(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)) hardhat-ignore-warnings: specifier: ^0.2.6 version: 0.2.11 @@ -143,7 +149,7 @@ importers: version: '@chainlink/solhint-plugin-chainlink-solidity@https://codeload.github.com/smartcontractkit/chainlink-solhint-rules/tar.gz/1b4c0c2663fcd983589d4f33a2e73908624ed43c' solhint-plugin-prettier: specifier: ^0.1.0 - version: 0.1.0(prettier-plugin-solidity@1.3.1)(prettier@3.2.5) + version: 0.1.0(prettier-plugin-solidity@1.3.1(prettier@3.2.5))(prettier@3.2.5) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.12.12)(typescript@5.4.5) @@ -160,6 +166,12 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + '@arbitrum/nitro-contracts@1.1.1': + resolution: {integrity: sha512-4Tyk3XVHz+bm8UujUC78LYSw3xAxyYvBCxfEX4z3qE4/ww7Qck/rmce5gbHMzQjArEAzAP2YSfYIFuIFuRXtfg==} + + '@arbitrum/token-bridge-contracts@1.1.2': + resolution: {integrity: sha512-k7AZXiB2HFecJ1KfaDBqgOKe3Loo1ttGLC7hUOVB+0YrihIR6cYpJRuqKSKK4YCy+FF21AUDtaG3x57OFM667Q==} + '@babel/code-frame@7.18.6': resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} engines: {node: '>=6.9.0'} @@ -178,7 +190,6 @@ packages: '@chainlink/solhint-plugin-chainlink-solidity@https://codeload.github.com/smartcontractkit/chainlink-solhint-rules/tar.gz/1b4c0c2663fcd983589d4f33a2e73908624ed43c': resolution: {tarball: https://codeload.github.com/smartcontractkit/chainlink-solhint-rules/tar.gz/1b4c0c2663fcd983589d4f33a2e73908624ed43c} - name: '@chainlink/solhint-plugin-chainlink-solidity' version: 1.2.0 '@changesets/apply-release-plan@7.0.1': @@ -572,12 +583,37 @@ packages: ethers: ^5.0.0 hardhat: ^2.0.0 + '@offchainlabs/upgrade-executor@1.1.0-beta.0': + resolution: {integrity: sha512-mpn6PHjH/KDDjNX0pXHEKdyv8m6DVGQiI2nGzQn0JbM1nOSHJpWx6fvfjtH7YxHJ6zBZTcsKkqGkFKDtCfoSLw==} + + '@openzeppelin/contracts-upgradeable@4.5.2': + resolution: {integrity: sha512-xgWZYaPlrEOQo3cBj97Ufiuv79SPd8Brh4GcFYhPgb6WvAq4ppz8dWKL6h+jLAK01rUqMRp/TS9AdXgAeNvCLA==} + + '@openzeppelin/contracts-upgradeable@4.7.3': + resolution: {integrity: sha512-+wuegAMaLcZnLCJIvrVUDzA9z/Wp93f0Dla/4jJvIhijRrPabjQbZe6fWiECLaJyfn5ci9fqf9vTw3xpQOad2A==} + + '@openzeppelin/contracts-upgradeable@4.8.3': + resolution: {integrity: sha512-SXDRl7HKpl2WDoJpn7CK/M9U4Z8gNXDHHChAKh0Iz+Wew3wu6CmFYBeie3je8V0GSXZAIYYwUktSrnW/kwVPtg==} + '@openzeppelin/contracts-upgradeable@4.9.3': resolution: {integrity: sha512-jjaHAVRMrE4UuZNfDwjlLGDxTHWIOwTJS2ldnc278a0gevfXfPr8hxKEVBGFBE96kl2G3VHDZhUimw/+G3TG2A==} + '@openzeppelin/contracts@4.5.0': + resolution: {integrity: sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA==} + + '@openzeppelin/contracts@4.7.3': + resolution: {integrity: sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==} + + '@openzeppelin/contracts@4.8.3': + resolution: {integrity: sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg==} + '@openzeppelin/contracts@4.9.3': resolution: {integrity: sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg==} + '@openzeppelin/upgrades-core@1.34.4': + resolution: {integrity: sha512-iGN3StqYHYVqqSKs8hWY+Gz6VkiEqOkQccBhHl7lHLGBJF91QUZ8wNMZ59SA5Usg1Fstu/HurvZTCEshPJAZ8w==} + hasBin: true + '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -821,6 +857,9 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + abi-to-sol@0.6.6: resolution: {integrity: sha512-PRn81rSpv6NXFPYQSw7ujruqIP6UkwZ/XoFldtiqCX8+2kHVc73xVaUVvdbro06vvBVZiwnxhEIGdI4BRMwGHw==} hasBin: true @@ -924,10 +963,18 @@ packages: array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + array.prototype.flat@1.3.2: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} @@ -936,6 +983,10 @@ packages: resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} engines: {node: '>= 0.4'} + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + arrify@1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} @@ -958,6 +1009,10 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + balanced-match@1.0.0: resolution: {integrity: sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==} @@ -1056,6 +1111,10 @@ packages: call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1083,6 +1142,10 @@ packages: resolution: {integrity: sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==} engines: {node: '>=12.19'} + cbor@9.0.2: + resolution: {integrity: sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==} + engines: {node: '>=16'} + chai-as-promised@7.1.1: resolution: {integrity: sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==} peerDependencies: @@ -1183,6 +1246,9 @@ packages: commander@3.0.2: resolution: {integrity: sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1215,6 +1281,10 @@ packages: cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1232,6 +1302,18 @@ packages: resolution: {integrity: sha512-QTaY0XjjhTQOdguARF0lGKm5/mEq9PD9/VhZZegHDIBq2tQwgNpHc3dneD4mGo2iJs+fTKv5Bp0fZ+BRuY3Z0g==} engines: {node: '>= 0.1.90'} + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} @@ -1285,6 +1367,10 @@ packages: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -1349,10 +1435,30 @@ packages: resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} engines: {node: '>= 0.4'} + es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} @@ -1520,6 +1626,9 @@ packages: find-yarn-workspace-root2@1.2.16: resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} + find-yarn-workspace-root@2.0.0: + resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} + flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1592,6 +1701,10 @@ packages: get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + get-stream@5.1.0: resolution: {integrity: sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==} engines: {node: '>=8'} @@ -1604,6 +1717,10 @@ packages: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1692,10 +1809,17 @@ packages: has-property-descriptors@1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -1704,6 +1828,10 @@ packages: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -1719,6 +1847,10 @@ packages: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -1790,12 +1922,20 @@ packages: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + io-ts@1.10.4: resolution: {integrity: sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==} is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1814,13 +1954,26 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} + is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + is-core-module@2.10.0: resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + is-date-object@1.0.2: resolution: {integrity: sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==} engines: {node: '>= 0.4'} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1844,6 +1997,10 @@ packages: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -1871,6 +2028,10 @@ packages: is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -1887,6 +2048,10 @@ packages: resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -1901,6 +2066,10 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -1970,6 +2139,9 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + klaw-sync@6.0.0: + resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==} + klaw@1.3.1: resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} @@ -2170,6 +2342,9 @@ packages: neodoc@2.0.2: resolution: {integrity: sha512-NAppJ0YecKWdhSXFYCHbo6RutiX8vOt/Jo3l46mUg6pQlpJNaqc5cGxdrW2jITQm5JIYySbFVPDl3RrREXNyPw==} + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} @@ -2227,12 +2402,20 @@ packages: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + obliterator@2.0.4: resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -2313,6 +2496,11 @@ packages: pascal-case@2.0.1: resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==} + patch-package@6.5.1: + resolution: {integrity: sha512-I/4Zsalfhc6bphmJTlrLoOcAF87jcxko4q0qsv4bGcurbr8IskEOtdnt9iCmsQVGL1B+iUhSQqweyTLJfCF9rA==} + engines: {node: '>=10', npm: '>5'} + hasBin: true + path-case@2.1.1: resolution: {integrity: sha512-Ou0N05MioItesaLr9q8TtHVWmJ6fxWdqKB2RohFmNWVyJ+2zeKIeDNWAN6B/Pe7wpzWChhZX6nONYmOnMeJQ/Q==} @@ -2328,6 +2516,10 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2366,6 +2558,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + preferred-pm@3.1.3: resolution: {integrity: sha512-MkXsENfftWSRpzCzImcp4FRsCc3y1opwB73CfCNWyzMqArju2CrlMHlqB7VexKiPEOjGMbttv1r9fSCn5S610w==} engines: {node: '>=10'} @@ -2394,6 +2590,9 @@ packages: engines: {node: '>=14'} hasBin: true + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -2464,6 +2663,10 @@ packages: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} engines: {node: '>= 0.4'} + regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + registry-auth-token@5.0.2: resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} engines: {node: '>=14'} @@ -2504,6 +2707,10 @@ packages: responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2536,6 +2743,10 @@ packages: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -2545,6 +2756,10 @@ packages: safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2581,6 +2796,10 @@ packages: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} engines: {node: '>= 0.4'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} engines: {node: '>= 0.4'} @@ -2620,6 +2839,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2651,6 +2874,9 @@ packages: resolution: {integrity: sha512-QeQLS9HGCnIiibt+xiOa/+MuP7BWz9N7C5+Mj9pLHshdkNhuo3AzCpWmjfWVZBUuwIUO3YyCRVIcYLR3YOKGfg==} hasBin: true + solidity-ast@0.4.56: + resolution: {integrity: sha512-HgmsA/Gfklm/M8GFbCX/J1qkVH0spXHgALCNZ8fA8x5X+MFdn/8CP2gr5OVyXjXw6RZTPC/Sxl2RUDQOXyNMeA==} + solidity-comments-darwin-arm64@0.0.2: resolution: {integrity: sha512-HidWkVLSh7v+Vu0CA7oI21GWP/ZY7ro8g8OmIxE8oTqyMwgMbE8F1yc58Sj682Hj199HCZsjmtn1BE4PCbLiGA==} engines: {node: '>= 10'} @@ -2768,12 +2994,23 @@ packages: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + string.prototype.trimend@1.0.7: resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + string.prototype.trimstart@1.0.7: resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2952,17 +3189,33 @@ packages: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + typed-array-byte-length@1.0.0: resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} engines: {node: '>= 0.4'} + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + typed-array-byte-offset@1.0.0: resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} engines: {node: '>= 0.4'} + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -3050,6 +3303,10 @@ packages: resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} engines: {node: '>= 0.4'} + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -3115,6 +3372,10 @@ packages: yallist@2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -3155,6 +3416,24 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} + '@arbitrum/nitro-contracts@1.1.1': + dependencies: + '@offchainlabs/upgrade-executor': 1.1.0-beta.0 + '@openzeppelin/contracts': 4.5.0 + '@openzeppelin/contracts-upgradeable': 4.5.2 + patch-package: 6.5.1 + + '@arbitrum/token-bridge-contracts@1.1.2': + dependencies: + '@arbitrum/nitro-contracts': 1.1.1 + '@offchainlabs/upgrade-executor': 1.1.0-beta.0 + '@openzeppelin/contracts': 4.8.3 + '@openzeppelin/contracts-upgradeable': 4.8.3 + optionalDependencies: + '@openzeppelin/upgrades-core': 1.34.4 + transitivePeerDependencies: + - supports-color + '@babel/code-frame@7.18.6': dependencies: '@babel/highlight': 7.18.6 @@ -3787,11 +4066,12 @@ snapshots: '@nomicfoundation/ethereumjs-rlp': 5.0.4 '@nomicfoundation/ethereumjs-trie': 6.0.4 '@nomicfoundation/ethereumjs-util': 9.0.4 - '@nomicfoundation/ethereumjs-verkle': 0.0.2 debug: 4.3.4(supports-color@8.1.1) ethereum-cryptography: 0.1.3 js-sdsl: 4.4.2 lru-cache: 10.2.2 + optionalDependencies: + '@nomicfoundation/ethereumjs-verkle': 0.0.2 transitivePeerDependencies: - c-kzg - supports-color @@ -3846,40 +4126,40 @@ snapshots: - c-kzg - supports-color - '@nomicfoundation/hardhat-chai-matchers@1.0.6(@nomiclabs/hardhat-ethers@2.2.3)(chai@4.4.1)(ethers@5.7.2)(hardhat@2.20.1)': + '@nomicfoundation/hardhat-chai-matchers@1.0.6(@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)))(chai@4.4.1)(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5))': dependencies: '@ethersproject/abi': 5.7.0 - '@nomiclabs/hardhat-ethers': 2.2.3(ethers@5.7.2)(hardhat@2.20.1) + '@nomiclabs/hardhat-ethers': 2.2.3(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)) '@types/chai-as-promised': 7.1.8 chai: 4.4.1 chai-as-promised: 7.1.1(chai@4.4.1) deep-eql: 4.1.3 ethers: 5.7.2 - hardhat: 2.20.1(ts-node@10.9.2)(typescript@5.4.5) + hardhat: 2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) ordinal: 1.0.3 - '@nomicfoundation/hardhat-ethers@3.0.6(ethers@5.7.2)(hardhat@2.20.1)': + '@nomicfoundation/hardhat-ethers@3.0.6(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5))': dependencies: debug: 4.3.4(supports-color@8.1.1) ethers: 5.7.2 - hardhat: 2.20.1(ts-node@10.9.2)(typescript@5.4.5) + hardhat: 2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) lodash.isequal: 4.5.0 transitivePeerDependencies: - supports-color - '@nomicfoundation/hardhat-network-helpers@1.0.10(hardhat@2.20.1)': + '@nomicfoundation/hardhat-network-helpers@1.0.10(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5))': dependencies: ethereumjs-util: 7.1.5 - hardhat: 2.20.1(ts-node@10.9.2)(typescript@5.4.5) + hardhat: 2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) - '@nomicfoundation/hardhat-verify@2.0.7(hardhat@2.20.1)': + '@nomicfoundation/hardhat-verify@2.0.7(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5))': dependencies: '@ethersproject/abi': 5.7.0 '@ethersproject/address': 5.7.0 cbor: 8.1.0 chalk: 2.4.2 debug: 4.3.4(supports-color@8.1.1) - hardhat: 2.20.1(ts-node@10.9.2)(typescript@5.4.5) + hardhat: 2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) lodash.clonedeep: 4.5.0 semver: 6.3.0 table: 6.8.1 @@ -3930,15 +4210,46 @@ snapshots: '@nomicfoundation/solidity-analyzer-win32-ia32-msvc': 0.1.0 '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.1.0 - '@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.20.1)': + '@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5))': dependencies: ethers: 5.7.2 - hardhat: 2.20.1(ts-node@10.9.2)(typescript@5.4.5) + hardhat: 2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) + + '@offchainlabs/upgrade-executor@1.1.0-beta.0': + dependencies: + '@openzeppelin/contracts': 4.7.3 + '@openzeppelin/contracts-upgradeable': 4.7.3 + + '@openzeppelin/contracts-upgradeable@4.5.2': {} + + '@openzeppelin/contracts-upgradeable@4.7.3': {} + + '@openzeppelin/contracts-upgradeable@4.8.3': {} '@openzeppelin/contracts-upgradeable@4.9.3': {} + '@openzeppelin/contracts@4.5.0': {} + + '@openzeppelin/contracts@4.7.3': {} + + '@openzeppelin/contracts@4.8.3': {} + '@openzeppelin/contracts@4.9.3': {} + '@openzeppelin/upgrades-core@1.34.4': + dependencies: + cbor: 9.0.2 + chalk: 4.1.2 + compare-versions: 6.1.1 + debug: 4.3.4(supports-color@8.1.1) + ethereumjs-util: 7.1.5 + minimist: 1.2.8 + proper-lockfile: 4.1.2 + solidity-ast: 0.4.56 + transitivePeerDependencies: + - supports-color + optional: true + '@pkgr/core@0.1.1': {} '@pnpm/config.env-replace@1.1.0': {} @@ -4052,7 +4363,7 @@ snapshots: '@tsconfig/node16@1.0.3': {} - '@typechain/ethers-v5@7.2.0(@ethersproject/abi@5.7.0)(@ethersproject/bytes@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.2)(typescript@5.4.5)': + '@typechain/ethers-v5@7.2.0(@ethersproject/abi@5.7.0)(@ethersproject/bytes@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.2(typescript@5.4.5))(typescript@5.4.5)': dependencies: '@ethersproject/abi': 5.7.0 '@ethersproject/bytes': 5.7.0 @@ -4063,14 +4374,14 @@ snapshots: typechain: 8.3.2(typescript@5.4.5) typescript: 5.4.5 - '@typechain/hardhat@7.0.0(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(@typechain/ethers-v5@7.2.0)(ethers@5.7.2)(hardhat@2.20.1)(typechain@8.3.2)': + '@typechain/hardhat@7.0.0(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(@typechain/ethers-v5@7.2.0(@ethersproject/abi@5.7.0)(@ethersproject/bytes@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.2(typescript@5.4.5))(typescript@5.4.5))(ethers@5.7.2)(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5))(typechain@8.3.2(typescript@5.4.5))': dependencies: '@ethersproject/abi': 5.7.0 '@ethersproject/providers': 5.7.2 - '@typechain/ethers-v5': 7.2.0(@ethersproject/abi@5.7.0)(@ethersproject/bytes@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.2)(typescript@5.4.5) + '@typechain/ethers-v5': 7.2.0(@ethersproject/abi@5.7.0)(@ethersproject/bytes@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.2(typescript@5.4.5))(typescript@5.4.5) ethers: 5.7.2 fs-extra: 9.1.0 - hardhat: 2.20.1(ts-node@10.9.2)(typescript@5.4.5) + hardhat: 2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) typechain: 8.3.2(typescript@5.4.5) '@types/bn.js@4.11.6': @@ -4147,7 +4458,7 @@ snapshots: '@types/semver@7.5.0': {} - '@typescript-eslint/eslint-plugin@7.10.0(@typescript-eslint/parser@7.10.0)(eslint@8.57.0)(typescript@5.4.5)': + '@typescript-eslint/eslint-plugin@7.10.0(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 7.10.0(eslint@8.57.0)(typescript@5.4.5) @@ -4160,6 +4471,7 @@ snapshots: ignore: 5.3.1 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -4172,6 +4484,7 @@ snapshots: '@typescript-eslint/visitor-keys': 7.10.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 + optionalDependencies: typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -4188,6 +4501,7 @@ snapshots: debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -4204,6 +4518,7 @@ snapshots: minimatch: 9.0.4 semver: 7.6.2 ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -4226,6 +4541,8 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@yarnpkg/lockfile@1.1.0': {} + abi-to-sol@0.6.6: dependencies: '@truffle/abi-utils': 0.3.2 @@ -4328,8 +4645,24 @@ snapshots: call-bind: 1.0.5 is-array-buffer: 3.0.2 + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + optional: true + array-union@2.1.0: {} + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + optional: true + array.prototype.flat@1.3.2: dependencies: call-bind: 1.0.5 @@ -4347,6 +4680,18 @@ snapshots: is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + optional: true + arrify@1.0.1: {} assertion-error@1.1.0: {} @@ -4359,6 +4704,11 @@ snapshots: available-typed-arrays@1.0.5: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + optional: true + balanced-match@1.0.0: {} base-x@3.0.9: @@ -4473,6 +4823,15 @@ snapshots: get-intrinsic: 1.2.2 set-function-length: 1.1.1 + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + optional: true + callsites@3.1.0: {} camel-case@3.0.0: @@ -4499,6 +4858,11 @@ snapshots: dependencies: nofilter: 3.1.0 + cbor@9.0.2: + dependencies: + nofilter: 3.1.0 + optional: true + chai-as-promised@7.1.1(chai@4.4.1): dependencies: chai: 4.4.1 @@ -4635,6 +4999,9 @@ snapshots: commander@3.0.2: {} + compare-versions@6.1.1: + optional: true + concat-map@0.0.1: {} config-chain@1.1.13: @@ -4683,6 +5050,14 @@ snapshots: shebang-command: 1.2.0 which: 1.3.1 + cross-spawn@6.0.5: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.1 + shebang-command: 1.2.0 + which: 1.3.1 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -4702,11 +5077,33 @@ snapshots: csv-stringify: 5.6.5 stream-transform: 2.1.3 + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + optional: true + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + optional: true + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + optional: true + dataloader@1.4.0: {} debug@4.3.4(supports-color@8.1.1): dependencies: ms: 2.1.2 + optionalDependencies: supports-color: 8.1.1 decamelize-keys@1.1.1: @@ -4747,6 +5144,13 @@ snapshots: gopd: 1.0.1 has-property-descriptors: 1.0.0 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + optional: true + define-properties@1.2.1: dependencies: define-data-property: 1.1.1 @@ -4850,12 +5254,82 @@ snapshots: unbox-primitive: 1.0.2 which-typed-array: 1.1.13 + es-abstract@1.23.3: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + optional: true + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + optional: true + + es-errors@1.3.0: + optional: true + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + optional: true + es-set-tostringtag@2.0.2: dependencies: get-intrinsic: 1.2.2 has-tostringtag: 1.0.0 hasown: 2.0.0 + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + optional: true + es-shim-unscopables@1.0.2: dependencies: hasown: 2.0.0 @@ -4876,13 +5350,14 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5): + eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5): dependencies: eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) prettier: 3.2.5 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 + optionalDependencies: + eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-scope@7.2.2: dependencies: @@ -5120,6 +5595,10 @@ snapshots: micromatch: 4.0.5 pkg-dir: 4.2.0 + find-yarn-workspace-root@2.0.0: + dependencies: + micromatch: 4.0.5 + flat-cache@3.2.0: dependencies: flatted: 3.3.1 @@ -5131,7 +5610,7 @@ snapshots: flatted@3.3.1: {} follow-redirects@1.15.6(debug@4.3.4): - dependencies: + optionalDependencies: debug: 4.3.4(supports-color@8.1.1) for-each@0.3.3: @@ -5196,6 +5675,15 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.0 + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + optional: true + get-stream@5.1.0: dependencies: pump: 3.0.0 @@ -5207,6 +5695,13 @@ snapshots: call-bind: 1.0.5 get-intrinsic: 1.2.2 + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + optional: true + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -5295,11 +5790,11 @@ snapshots: hard-rejection@2.1.0: {} - hardhat-abi-exporter@2.10.1(hardhat@2.20.1): + hardhat-abi-exporter@2.10.1(hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)): dependencies: '@ethersproject/abi': 5.7.0 delete-empty: 3.0.0 - hardhat: 2.20.1(ts-node@10.9.2)(typescript@5.4.5) + hardhat: 2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) hardhat-ignore-warnings@0.2.11: dependencies: @@ -5307,7 +5802,7 @@ snapshots: node-interval-tree: 2.1.2 solidity-comments: 0.0.2 - hardhat@2.20.1(ts-node@10.9.2)(typescript@5.4.5): + hardhat@2.20.1(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5): dependencies: '@ethersproject/abi': 5.7.0 '@metamask/eth-sig-util': 4.0.1 @@ -5355,12 +5850,13 @@ snapshots: solc: 0.7.3(debug@4.3.4) source-map-support: 0.5.21 stacktrace-parser: 0.1.10 - ts-node: 10.9.2(@types/node@20.12.12)(typescript@5.4.5) tsort: 0.0.1 - typescript: 5.4.5 undici: 5.28.4 uuid: 8.3.2 ws: 7.5.9 + optionalDependencies: + ts-node: 10.9.2(@types/node@20.12.12)(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - bufferutil - c-kzg @@ -5377,14 +5873,27 @@ snapshots: dependencies: get-intrinsic: 1.2.2 + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + optional: true + has-proto@1.0.1: {} + has-proto@1.0.3: + optional: true + has-symbols@1.0.3: {} has-tostringtag@1.0.0: dependencies: has-symbols: 1.0.3 + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + optional: true + has@1.0.3: dependencies: function-bind: 1.1.2 @@ -5404,6 +5913,11 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + optional: true + he@1.2.0: {} header-case@1.0.1: @@ -5477,6 +5991,13 @@ snapshots: hasown: 2.0.0 side-channel: 1.0.4 + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.4 + optional: true + io-ts@1.10.4: dependencies: fp-ts: 1.19.3 @@ -5487,6 +6008,12 @@ snapshots: get-intrinsic: 1.2.2 is-typed-array: 1.1.12 + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + optional: true + is-arrayish@0.2.1: {} is-bigint@1.0.4: @@ -5504,12 +6031,23 @@ snapshots: is-callable@1.2.7: {} + is-ci@2.0.0: + dependencies: + ci-info: 2.0.0 + is-core-module@2.10.0: dependencies: has: 1.0.3 + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + optional: true + is-date-object@1.0.2: {} + is-docker@2.2.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -5526,6 +6064,9 @@ snapshots: is-negative-zero@2.0.2: {} + is-negative-zero@2.0.3: + optional: true + is-number-object@1.0.7: dependencies: has-tostringtag: 1.0.0 @@ -5547,6 +6088,11 @@ snapshots: dependencies: call-bind: 1.0.5 + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + optional: true + is-string@1.0.7: dependencies: has-tostringtag: 1.0.0 @@ -5563,6 +6109,11 @@ snapshots: dependencies: which-typed-array: 1.1.13 + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + optional: true + is-unicode-supported@0.1.0: {} is-upper-case@1.1.2: @@ -5575,6 +6126,10 @@ snapshots: is-windows@1.0.2: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isarray@2.0.5: {} isexe@2.0.0: {} @@ -5641,6 +6196,10 @@ snapshots: kind-of@6.0.3: {} + klaw-sync@6.0.0: + dependencies: + graceful-fs: 4.2.10 + klaw@1.3.1: optionalDependencies: graceful-fs: 4.2.10 @@ -5839,6 +6398,8 @@ snapshots: dependencies: ansi-regex: 2.1.1 + nice-try@1.0.5: {} + no-case@2.3.2: dependencies: lower-case: 1.1.4 @@ -5886,12 +6447,25 @@ snapshots: has-symbols: 1.0.3 object-keys: 1.1.1 + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + optional: true + obliterator@2.0.4: {} once@1.4.0: dependencies: wrappy: 1.0.2 + open@7.4.2: + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + optionator@0.9.3: dependencies: '@aashutoshrathi/word-wrap': 1.2.6 @@ -5974,6 +6548,23 @@ snapshots: camel-case: 3.0.0 upper-case-first: 1.1.2 + patch-package@6.5.1: + dependencies: + '@yarnpkg/lockfile': 1.1.0 + chalk: 4.1.2 + cross-spawn: 6.0.5 + find-yarn-workspace-root: 2.0.0 + fs-extra: 9.1.0 + is-ci: 2.0.0 + klaw-sync: 6.0.0 + minimist: 1.2.8 + open: 7.4.2 + rimraf: 2.7.1 + semver: 5.7.1 + slash: 2.0.0 + tmp: 0.0.33 + yaml: 1.10.2 + path-case@2.1.1: dependencies: no-case: 2.3.2 @@ -5984,6 +6575,8 @@ snapshots: path-is-absolute@1.0.1: {} + path-key@2.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} @@ -6012,6 +6605,9 @@ snapshots: pluralize@8.0.0: {} + possible-typed-array-names@1.0.0: + optional: true + preferred-pm@3.1.3: dependencies: find-up: 5.0.0 @@ -6044,6 +6640,13 @@ snapshots: prettier@3.2.5: {} + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.10 + retry: 0.12.0 + signal-exit: 3.0.7 + optional: true + proto-list@1.2.4: {} pseudomap@1.0.2: {} @@ -6124,6 +6727,14 @@ snapshots: define-properties: 1.2.1 set-function-name: 2.0.1 + regexp.prototype.flags@1.5.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.1 + optional: true + registry-auth-token@5.0.2: dependencies: '@pnpm/npm-conf': 2.2.2 @@ -6158,6 +6769,9 @@ snapshots: dependencies: lowercase-keys: 2.0.0 + retry@0.12.0: + optional: true + reusify@1.0.4: {} rimraf@2.7.1: @@ -6192,6 +6806,14 @@ snapshots: has-symbols: 1.0.3 isarray: 2.0.5 + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + optional: true + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -6202,6 +6824,13 @@ snapshots: get-intrinsic: 1.2.2 is-regex: 1.1.4 + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + optional: true + safer-buffer@2.1.2: {} scrypt-js@3.0.1: {} @@ -6236,6 +6865,16 @@ snapshots: gopd: 1.0.1 has-property-descriptors: 1.0.0 + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + optional: true + set-function-name@2.0.1: dependencies: define-data-property: 1.1.1 @@ -6273,6 +6912,8 @@ snapshots: signal-exit@3.0.7: {} + slash@2.0.0: {} + slash@3.0.0: {} slice-ansi@4.0.0: @@ -6308,7 +6949,7 @@ snapshots: transitivePeerDependencies: - debug - solhint-plugin-prettier@0.1.0(prettier-plugin-solidity@1.3.1)(prettier@3.2.5): + solhint-plugin-prettier@0.1.0(prettier-plugin-solidity@1.3.1(prettier@3.2.5))(prettier@3.2.5): dependencies: '@prettier/sync': 0.3.0(prettier@3.2.5) prettier: 3.2.5 @@ -6338,6 +6979,11 @@ snapshots: optionalDependencies: prettier: 2.8.8 + solidity-ast@0.4.56: + dependencies: + array.prototype.findlast: 1.2.5 + optional: true + solidity-comments-darwin-arm64@0.0.2: optional: true @@ -6439,18 +7085,40 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.22.3 + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + optional: true + string.prototype.trimend@1.0.7: dependencies: call-bind: 1.0.5 define-properties: 1.2.1 es-abstract: 1.22.3 + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + optional: true + string.prototype.trimstart@1.0.7: dependencies: call-bind: 1.0.5 define-properties: 1.2.1 es-abstract: 1.22.3 + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + optional: true + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -6628,6 +7296,13 @@ snapshots: get-intrinsic: 1.2.2 is-typed-array: 1.1.12 + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + optional: true + typed-array-byte-length@1.0.0: dependencies: call-bind: 1.0.5 @@ -6635,6 +7310,15 @@ snapshots: has-proto: 1.0.1 is-typed-array: 1.1.12 + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + optional: true + typed-array-byte-offset@1.0.0: dependencies: available-typed-arrays: 1.0.5 @@ -6643,12 +7327,32 @@ snapshots: has-proto: 1.0.1 is-typed-array: 1.1.12 + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + optional: true + typed-array-length@1.0.4: dependencies: call-bind: 1.0.5 for-each: 0.3.3 is-typed-array: 1.1.12 + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + optional: true + typescript@5.4.5: {} typical@4.0.0: {} @@ -6741,6 +7445,15 @@ snapshots: gopd: 1.0.1 has-tostringtag: 1.0.0 + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + optional: true + which@1.3.1: dependencies: isexe: 2.0.0 @@ -6784,6 +7497,8 @@ snapshots: yallist@2.1.2: {} + yaml@1.10.2: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 diff --git a/contracts/remappings.txt b/contracts/remappings.txt index 1428f50b316..ec64b1b2118 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1,6 +1,7 @@ forge-std/=src/v0.8/vendor/forge-std/src/ @openzeppelin/=node_modules/@openzeppelin/ +@arbitrum/=node_modules/@arbitrum/ hardhat/=node_modules/hardhat/ @eth-optimism/=node_modules/@eth-optimism/ @scroll-tech/=node_modules/@scroll-tech/ diff --git a/contracts/scripts/ccip_lcov_prune b/contracts/scripts/ccip_lcov_prune new file mode 100755 index 00000000000..002e5a3f133 --- /dev/null +++ b/contracts/scripts/ccip_lcov_prune @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -e + +# src/v0.8/ccip/libraries/Internal.sol +# src/v0.8/ccip/libraries/RateLimiter.sol +# src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol +# src/v0.8/ccip/libraries/MerkleMultiProof.sol +# src/v0.8/ccip/libraries/Pool.sol +# excluded because Foundry doesn't support coverage on library files + +# BurnWithFromMintTokenPool is excluded because Forge doesn't seem to +# register coverage, even though it is 100% covered. + +lcov --remove $1 -o $2 \ + '*/ccip/test/*' \ + '*/vendor/*' \ + '*/shared/*' \ + 'src/v0.8/ccip/ocr/OCR2Abstract.sol' \ + 'src/v0.8/ccip/libraries/Internal.sol' \ + 'src/v0.8/ccip/libraries/RateLimiter.sol' \ + 'src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol' \ + 'src/v0.8/ccip/libraries/MerkleMultiProof.sol' \ + 'src/v0.8/ccip/libraries/Pool.sol' \ + 'src/v0.8/ConfirmedOwnerWithProposal.sol' \ + 'src/v0.8/tests/MockV3Aggregator.sol' \ + 'src/v0.8/ccip/applications/CCIPClientExample.sol' \ + 'src/v0.8/ccip/pools/BurnWithFromMintTokenPool.sol' \ + --rc lcov_branch_coverage=1 diff --git a/contracts/scripts/native_solc_compile_all b/contracts/scripts/native_solc_compile_all index 542337a191a..6e9f17561dd 100755 --- a/contracts/scripts/native_solc_compile_all +++ b/contracts/scripts/native_solc_compile_all @@ -12,7 +12,7 @@ python3 -m pip install --require-hashes -r $SCRIPTPATH/requirements.txt # 6 and 7 are legacy contracts, for each other product we have a native_solc_compile_all_$product script # These scripts can be run individually, or all together with this script. # To add new CL products, simply write a native_solc_compile_all_$product script and add it to the list below. -for product in automation events_mock feeds functions keystone llo-feeds logpoller operatorforwarder shared transmission vrf +for product in automation events_mock feeds functions keystone llo-feeds logpoller operatorforwarder shared transmission vrf ccip liquiditymanager do $SCRIPTPATH/native_solc_compile_all_$product done diff --git a/contracts/scripts/native_solc_compile_all_ccip b/contracts/scripts/native_solc_compile_all_ccip new file mode 100755 index 00000000000..1dbb70502d6 --- /dev/null +++ b/contracts/scripts/native_solc_compile_all_ccip @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +set -e + +echo " ┌──────────────────────────────────────────────┐" +echo " │ Compiling CCIP contracts... │" +echo " └──────────────────────────────────────────────┘" + +SOLC_VERSION="0.8.24" +OPTIMIZE_RUNS=26000 +OPTIMIZE_RUNS_OFFRAMP=18000 +OPTIMIZE_RUNS_ONRAMP=4100 +OPTIMIZE_RUNS_MULTI_OFFRAMP=2500 + + +SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +python3 -m pip install --require-hashes -r "$SCRIPTPATH"/requirements.txt +solc-select install $SOLC_VERSION +solc-select use $SOLC_VERSION +export SOLC_VERSION=$SOLC_VERSION + +ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; cd ../../ && pwd -P )" + +compileContract () { + local contract + contract=$(basename "$1" ".sol") + + local optimize_runs=$OPTIMIZE_RUNS + + case $1 in + "ccip/offRamp/EVM2EVMOffRamp.sol") + echo "OffRamp uses $OPTIMIZE_RUNS_OFFRAMP optimizer runs." + optimize_runs=$OPTIMIZE_RUNS_OFFRAMP + ;; + "ccip/offRamp/EVM2EVMMultiOffRamp.sol") + echo "MultiOffRamp uses $OPTIMIZE_RUNS_MULTI_OFFRAMP optimizer runs." + optimize_runs=$OPTIMIZE_RUNS_MULTI_OFFRAMP + ;; + "ccip/onRamp/EVM2EVMOnRamp.sol") + echo "OnRamp uses $OPTIMIZE_RUNS_ONRAMP optimizer runs." + optimize_runs=$OPTIMIZE_RUNS_ONRAMP + ;; + esac + + solc --overwrite --optimize --optimize-runs $optimize_runs --metadata-hash none \ + -o "$ROOT"/contracts/solc/v$SOLC_VERSION/"$contract" \ + --abi --bin --allow-paths "$ROOT"/contracts/src/v0.8 \ + --evm-version paris \ + "$ROOT"/contracts/src/v0.8/"$1" +} + + +# Solc produces and overwrites intermediary contracts. +# Contracts should be ordered in reverse-import-complexity-order to minimize overwrite risks. +compileContract ccip/offRamp/EVM2EVMOffRamp.sol +compileContract ccip/offRamp/EVM2EVMMultiOffRamp.sol +compileContract ccip/applications/PingPongDemo.sol +compileContract ccip/applications/SelfFundedPingPong.sol +compileContract ccip/applications/EtherSenderReceiver.sol +compileContract ccip/onRamp/EVM2EVMMultiOnRamp.sol +compileContract ccip/onRamp/EVM2EVMOnRamp.sol +compileContract ccip/CommitStore.sol +compileContract ccip/MultiAggregateRateLimiter.sol +compileContract ccip/Router.sol +compileContract ccip/PriceRegistry.sol +compileContract ccip/pools/LockReleaseTokenPool.sol +compileContract ccip/pools/BurnMintTokenPool.sol +compileContract ccip/pools/BurnFromMintTokenPool.sol +compileContract ccip/pools/BurnWithFromMintTokenPool.sol +compileContract ccip/pools/LockReleaseTokenPoolAndProxy.sol +compileContract ccip/pools/BurnMintTokenPoolAndProxy.sol +compileContract ccip/pools/TokenPool.sol +compileContract shared/token/ERC677/BurnMintERC677.sol +compileContract ccip/RMN.sol +compileContract ccip/ARMProxy.sol +compileContract ccip/tokenAdminRegistry/TokenAdminRegistry.sol +compileContract ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol +compileContract ccip/capability/CCIPConfig.sol +compileContract ccip/capability/interfaces/IOCR3ConfigEncoder.sol +compileContract ccip/NonceManager.sol + +# Test helpers +compileContract ccip/test/helpers/BurnMintERC677Helper.sol +compileContract ccip/test/helpers/CommitStoreHelper.sol +compileContract ccip/test/helpers/MessageHasher.sol +compileContract ccip/test/helpers/ReportCodec.sol +compileContract ccip/test/helpers/receivers/MaybeRevertMessageReceiver.sol +compileContract ccip/test/helpers/MultiOCR3Helper.sol +compileContract ccip/test/mocks/MockRMN1_0.sol +compileContract ccip/test/mocks/MockE2EUSDCTokenMessenger.sol +compileContract ccip/test/mocks/MockE2EUSDCTransmitter.sol +compileContract ccip/test/WETH9.sol + +# Customer contracts +compileContract ccip/pools/USDC/USDCTokenPool.sol + +compileContract tests/MockV3Aggregator.sol diff --git a/contracts/scripts/native_solc_compile_all_liquiditymanager b/contracts/scripts/native_solc_compile_all_liquiditymanager new file mode 100755 index 00000000000..a29f041c77b --- /dev/null +++ b/contracts/scripts/native_solc_compile_all_liquiditymanager @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -e + +echo " ┌──────────────────────────────────────────────┐" +echo " │ Compiling LiquidityManager contracts... │" +echo " └──────────────────────────────────────────────┘" + +SOLC_VERSION="0.8.24" +OPTIMIZE_RUNS=1000000 + + +SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +python3 -m pip install --require-hashes -r "$SCRIPTPATH"/requirements.txt +solc-select install $SOLC_VERSION +solc-select use $SOLC_VERSION +export SOLC_VERSION=$SOLC_VERSION + +ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; cd ../../ && pwd -P )" + +compileContract () { + local contract + contract=$(basename "$1" ".sol") + + solc @arbitrum/="$ROOT"/contracts/node_modules/@arbitrum/ \ + @eth-optimism/="$ROOT"/contracts/node_modules/@eth-optimism/ \ + @openzeppelin/="$ROOT"/contracts/node_modules/@openzeppelin/ \ + --overwrite --optimize --optimize-runs $OPTIMIZE_RUNS --metadata-hash none \ + -o "$ROOT"/contracts/solc/v$SOLC_VERSION/"$contract" \ + --abi --bin --allow-paths "$ROOT"/contracts/src/v0.8,"$ROOT"/contracts/node_modules \ + --evm-version paris \ + "$ROOT"/contracts/src/v0.8/"$1" +} + + +# Liquidity Management +compileContract liquiditymanager/LiquidityManager.sol +compileContract liquiditymanager/bridge-adapters/ArbitrumL1BridgeAdapter.sol +compileContract liquiditymanager/bridge-adapters/ArbitrumL2BridgeAdapter.sol +compileContract liquiditymanager/bridge-adapters/OptimismL1BridgeAdapter.sol +compileContract liquiditymanager/bridge-adapters/OptimismL2BridgeAdapter.sol +compileContract liquiditymanager/test/mocks/NoOpOCR3.sol +compileContract liquiditymanager/test/mocks/MockBridgeAdapter.sol +compileContract liquiditymanager/test/helpers/ReportEncoder.sol + +# Arbitrum helpers +compileContract liquiditymanager/interfaces/arbitrum/IArbSys.sol +compileContract liquiditymanager/interfaces/arbitrum/INodeInterface.sol +compileContract liquiditymanager/interfaces/arbitrum/IL2ArbitrumGateway.sol +compileContract liquiditymanager/interfaces/arbitrum/IL2ArbitrumMessenger.sol +compileContract liquiditymanager/interfaces/arbitrum/IArbRollupCore.sol +compileContract liquiditymanager/interfaces/arbitrum/IArbitrumL1GatewayRouter.sol +compileContract liquiditymanager/interfaces/arbitrum/IArbitrumInbox.sol +compileContract liquiditymanager/interfaces/arbitrum/IArbitrumGatewayRouter.sol +compileContract liquiditymanager/interfaces/arbitrum/IArbitrumTokenGateway.sol +compileContract liquiditymanager/interfaces/arbitrum/IAbstractArbitrumTokenGateway.sol + +# Optimism helpers +compileContract liquiditymanager/interfaces/optimism/IOptimismPortal.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismL2OutputOracle.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismL2ToL1MessagePasser.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismPortal2.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismDisputeGameFactory.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol +compileContract liquiditymanager/encoders/OptimismL1BridgeAdapterEncoder.sol diff --git a/contracts/src/v0.8/ccip/ARMProxy.sol b/contracts/src/v0.8/ccip/ARMProxy.sol new file mode 100644 index 00000000000..e9ccde8680b --- /dev/null +++ b/contracts/src/v0.8/ccip/ARMProxy.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; + +import {OwnerIsCreator} from "./../shared/access/OwnerIsCreator.sol"; + +/// @notice The ARMProxy serves to allow CCIP contracts +/// to point to a static address for ARM queries, which saves gas +/// since each contract need not store an ARM address in storage. That way +/// we can add ARM queries along many code paths for increased defense in depth +/// with minimal additional cost. +contract ARMProxy is OwnerIsCreator, ITypeAndVersion { + error ZeroAddressNotAllowed(); + + event ARMSet(address arm); + + // STATIC CONFIG + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables + string public constant override typeAndVersion = "ARMProxy 1.0.0"; + + // DYNAMIC CONFIG + address private s_arm; + + constructor(address arm) { + setARM(arm); + } + + /// @notice SetARM sets the ARM implementation contract address. + /// @param arm The address of the arm implementation contract. + function setARM(address arm) public onlyOwner { + if (arm == address(0)) revert ZeroAddressNotAllowed(); + s_arm = arm; + emit ARMSet(arm); + } + + /// @notice getARM gets the ARM implementation contract address. + /// @return arm The address of the arm implementation contract. + function getARM() external view returns (address) { + return s_arm; + } + + // We use a fallback function instead of explicit implementations of the functions + // defined in IRMN.sol to preserve compatibility with future additions to the IRMN + // interface. Calling IRMN interface methods in ARMProxy should be transparent, i.e. + // their input/output behaviour should be identical to calling the proxied s_arm + // contract directly. (If s_arm doesn't point to a contract, we always revert.) + // solhint-disable-next-line payable-fallback, no-complex-fallback + fallback() external { + address arm = s_arm; + // solhint-disable-next-line no-inline-assembly + assembly { + // Revert if no contract present at destination address, otherwise call + // might succeed unintentionally. + if iszero(extcodesize(arm)) { revert(0, 0) } + // We use memory starting at zero, overwriting anything that might already + // be stored there. This messes with Solidity's expectations around memory + // layout, but it's fine because we always exit execution of this contract + // inside this assembly block, i.e. we don't cede control to code generated + // by the Solidity compiler that might have expectations around memory + // layout. + // Copy calldatasize() bytes from calldata offset 0 to memory offset 0. + calldatacopy(0, 0, calldatasize()) + // Call the underlying ARM implementation. out and outsize are 0 because + // we don't know the size yet. We hardcode value to zero. + let success := call(gas(), arm, 0, 0, calldatasize(), 0, 0) + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + // Pass through successful return or revert and associated data. + if success { return(0, returndatasize()) } + revert(0, returndatasize()) + } + } +} diff --git a/contracts/src/v0.8/ccip/AggregateRateLimiter.sol b/contracts/src/v0.8/ccip/AggregateRateLimiter.sol new file mode 100644 index 00000000000..7401df2ed49 --- /dev/null +++ b/contracts/src/v0.8/ccip/AggregateRateLimiter.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPriceRegistry} from "./interfaces/IPriceRegistry.sol"; + +import {OwnerIsCreator} from "./../shared/access/OwnerIsCreator.sol"; +import {Client} from "./libraries/Client.sol"; +import {RateLimiter} from "./libraries/RateLimiter.sol"; +import {USDPriceWith18Decimals} from "./libraries/USDPriceWith18Decimals.sol"; + +/// @notice The aggregate rate limiter is a wrapper of the token bucket rate limiter +/// which permits rate limiting based on the aggregate value of a group of +/// token transfers, using a price registry to convert to a numeraire asset (e.g. USD). +contract AggregateRateLimiter is OwnerIsCreator { + using RateLimiter for RateLimiter.TokenBucket; + using USDPriceWith18Decimals for uint224; + + error PriceNotFoundForToken(address token); + + event AdminSet(address newAdmin); + + // The address of the token limit admin that has the same permissions as the owner. + address internal s_admin; + + // The token bucket object that contains the bucket state. + RateLimiter.TokenBucket private s_rateLimiter; + + /// @param config The RateLimiter.Config + constructor(RateLimiter.Config memory config) { + s_rateLimiter = RateLimiter.TokenBucket({ + rate: config.rate, + capacity: config.capacity, + tokens: config.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: config.isEnabled + }); + } + + /// @notice Consumes value from the rate limiter bucket based on the token value given. + function _rateLimitValue(uint256 value) internal { + s_rateLimiter._consume(value, address(0)); + } + + function _getTokenValue( + Client.EVMTokenAmount memory tokenAmount, + IPriceRegistry priceRegistry + ) internal view returns (uint256) { + // not fetching validated price, as price staleness is not important for value-based rate limiting + // we only need to verify the price is not 0 + uint224 pricePerToken = priceRegistry.getTokenPrice(tokenAmount.token).value; + if (pricePerToken == 0) revert PriceNotFoundForToken(tokenAmount.token); + return pricePerToken._calcUSDValueFromTokenAmount(tokenAmount.amount); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function currentRateLimiterState() external view returns (RateLimiter.TokenBucket memory) { + return s_rateLimiter._currentTokenBucketState(); + } + + /// @notice Sets the rate limited config. + /// @param config The new rate limiter config. + /// @dev should only be callable by the owner or token limit admin. + function setRateLimiterConfig(RateLimiter.Config memory config) external onlyAdminOrOwner { + s_rateLimiter._setTokenBucketConfig(config); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Gets the token limit admin address. + /// @return the token limit admin address. + function getTokenLimitAdmin() external view returns (address) { + return s_admin; + } + + /// @notice Sets the token limit admin address. + /// @param newAdmin the address of the new admin. + /// @dev setting this to address(0) indicates there is no active admin. + function setAdmin(address newAdmin) external onlyAdminOrOwner { + s_admin = newAdmin; + emit AdminSet(newAdmin); + } + + /// @notice a modifier that allows the owner or the s_tokenLimitAdmin call the functions + /// it is applied to. + modifier onlyAdminOrOwner() { + if (msg.sender != owner() && msg.sender != s_admin) revert RateLimiter.OnlyCallableByAdminOrOwner(); + _; + } +} diff --git a/contracts/src/v0.8/ccip/CommitStore.sol b/contracts/src/v0.8/ccip/CommitStore.sol new file mode 100644 index 00000000000..27388b6dcc2 --- /dev/null +++ b/contracts/src/v0.8/ccip/CommitStore.sol @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; +import {ICommitStore} from "./interfaces/ICommitStore.sol"; +import {IPriceRegistry} from "./interfaces/IPriceRegistry.sol"; +import {IRMN} from "./interfaces/IRMN.sol"; + +import {Internal} from "./libraries/Internal.sol"; +import {MerkleMultiProof} from "./libraries/MerkleMultiProof.sol"; +import {OCR2Base} from "./ocr/OCR2Base.sol"; + +contract CommitStore is ICommitStore, ITypeAndVersion, OCR2Base { + error StaleReport(); + error PausedError(); + error InvalidInterval(Interval interval); + error InvalidRoot(); + error InvalidCommitStoreConfig(); + error CursedByRMN(); + error RootAlreadyCommitted(); + + event Paused(address account); + event Unpaused(address account); + /// @dev RMN depends on this event, if changing, please notify the RMN maintainers. + event ReportAccepted(CommitReport report); + event ConfigSet(StaticConfig staticConfig, DynamicConfig dynamicConfig); + event RootRemoved(bytes32 root); + event SequenceNumberSet(uint64 oldSeqNum, uint64 newSeqNum); + event LatestPriceEpochAndRoundSet(uint40 oldEpochAndRound, uint40 newEpochAndRound); + + /// @notice Static commit store config + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + //solhint-disable gas-struct-packing + struct StaticConfig { + uint64 chainSelector; // ───────╮ Destination chainSelector + uint64 sourceChainSelector; // ─╯ Source chainSelector + address onRamp; // OnRamp address on the source chain + address rmnProxy; // RMN proxy address + } + + /// @notice Dynamic commit store config + struct DynamicConfig { + address priceRegistry; // Price registry address on the destination chain + } + + /// @notice a sequenceNumber interval + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct Interval { + uint64 min; // ───╮ Minimum sequence number, inclusive + uint64 max; // ───╯ Maximum sequence number, inclusive + } + + /// @notice Report that is committed by the observing DON at the committing phase + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct CommitReport { + Internal.PriceUpdates priceUpdates; + Interval interval; + bytes32 merkleRoot; + } + + // STATIC CONFIG + string public constant override typeAndVersion = "CommitStore 1.5.0-dev"; + // Chain ID of this chain + uint64 internal immutable i_chainSelector; + // Chain ID of the source chain + uint64 internal immutable i_sourceChainSelector; + // The onRamp address on the source chain + address internal immutable i_onRamp; + // The address of the rmn proxy + address internal immutable i_rmnProxy; + + // DYNAMIC CONFIG + // The dynamic commitStore config + DynamicConfig internal s_dynamicConfig; + + // STATE + // The min sequence number expected for future messages + uint64 private s_minSeqNr = 1; + /// @dev The epoch and round of the last report + uint40 private s_latestPriceEpochAndRound; + /// @dev Whether this CommitStore is paused or not + bool private s_paused = false; + // merkleRoot => timestamp when received + mapping(bytes32 merkleRoot => uint256 timestamp) private s_roots; + + /// @param staticConfig Containing the static part of the commitStore config + /// @dev When instantiating OCR2Base we set UNIQUE_REPORTS to false, which means + /// that we do not require 2f+1 signatures on a report, only f+1 to save gas. 2f+1 is required + /// only if one must strictly ensure that for a given round there is only one valid report ever generated by + /// the DON. In our case additional valid reports (i.e. approved by >= f+1 oracles) are not a problem, as they will + /// will either be ignored (reverted as an invalid interval) or will be accepted as an additional valid price update. + constructor(StaticConfig memory staticConfig) OCR2Base(false) { + if ( + staticConfig.onRamp == address(0) || staticConfig.chainSelector == 0 || staticConfig.sourceChainSelector == 0 + || staticConfig.rmnProxy == address(0) + ) revert InvalidCommitStoreConfig(); + + i_chainSelector = staticConfig.chainSelector; + i_sourceChainSelector = staticConfig.sourceChainSelector; + i_onRamp = staticConfig.onRamp; + i_rmnProxy = staticConfig.rmnProxy; + } + + // ================================================================ + // │ Verification │ + // ================================================================ + + /// @notice Returns the next expected sequence number. + /// @return the next expected sequenceNumber. + function getExpectedNextSequenceNumber() external view returns (uint64) { + return s_minSeqNr; + } + + /// @notice Sets the minimum sequence number. + /// @param minSeqNr The new minimum sequence number. + function setMinSeqNr(uint64 minSeqNr) external onlyOwner { + uint64 oldSeqNum = s_minSeqNr; + + s_minSeqNr = minSeqNr; + + emit SequenceNumberSet(oldSeqNum, minSeqNr); + } + + /// @notice Returns the epoch and round of the last price update. + /// @return the latest price epoch and round. + function getLatestPriceEpochAndRound() external view returns (uint64) { + return s_latestPriceEpochAndRound; + } + + /// @notice Sets the latest epoch and round for price update. + /// @param latestPriceEpochAndRound The new epoch and round for prices. + function setLatestPriceEpochAndRound(uint40 latestPriceEpochAndRound) external onlyOwner { + uint40 oldEpochAndRound = s_latestPriceEpochAndRound; + + s_latestPriceEpochAndRound = latestPriceEpochAndRound; + + emit LatestPriceEpochAndRoundSet(oldEpochAndRound, latestPriceEpochAndRound); + } + + /// @notice Returns the timestamp of a potentially previously committed merkle root. + /// If the root was never committed 0 will be returned. + /// @param root The merkle root to check the commit status for. + /// @return the timestamp of the committed root or zero in the case that it was never + /// committed. + function getMerkleRoot(bytes32 root) external view returns (uint256) { + return s_roots[root]; + } + + /// @notice Returns if a root is blessed or not. + /// @param root The merkle root to check the blessing status for. + /// @return whether the root is blessed or not. + function isBlessed(bytes32 root) public view returns (bool) { + return IRMN(i_rmnProxy).isBlessed(IRMN.TaggedRoot({commitStore: address(this), root: root})); + } + + /// @notice Used by the owner in case an invalid sequence of roots has been + /// posted and needs to be removed. The interval in the report is trusted. + /// @param rootToReset The roots that will be reset. This function will only + /// reset roots that are not blessed. + function resetUnblessedRoots(bytes32[] calldata rootToReset) external onlyOwner { + for (uint256 i = 0; i < rootToReset.length; ++i) { + bytes32 root = rootToReset[i]; + if (!isBlessed(root)) { + delete s_roots[root]; + emit RootRemoved(root); + } + } + } + + /// @inheritdoc ICommitStore + function verify( + bytes32[] calldata hashedLeaves, + bytes32[] calldata proofs, + uint256 proofFlagBits + ) external view override whenNotPaused returns (uint256 timestamp) { + bytes32 root = MerkleMultiProof.merkleRoot(hashedLeaves, proofs, proofFlagBits); + // Only return non-zero if present and blessed. + if (!isBlessed(root)) { + return 0; + } + return s_roots[root]; + } + + /// @inheritdoc OCR2Base + /// @dev A commitReport can have two distinct parts (batched together to amortize the cost of checking sigs): + /// 1. Price updates + /// 2. A merkle root and sequence number interval + /// Both have their own, separate, staleness checks, with price updates using the epoch and round + /// number of the latest price update. The merkle root checks for staleness based on the seqNums. + /// They need to be separate because a price report for round t+2 might be included before a report + /// containing a merkle root for round t+1. This merkle root report for round t+1 is still valid + /// and should not be rejected. When a report with a stale root but valid price updates is submitted, + /// we are OK to revert to preserve the invariant that we always revert on invalid sequence number ranges. + /// If that happens, prices will be updates in later rounds. + function _report(bytes calldata encodedReport, uint40 epochAndRound) internal override whenNotPaused { + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(i_sourceChainSelector)))) revert CursedByRMN(); + + CommitReport memory report = abi.decode(encodedReport, (CommitReport)); + + // Check if the report contains price updates + if (report.priceUpdates.tokenPriceUpdates.length > 0 || report.priceUpdates.gasPriceUpdates.length > 0) { + // Check for price staleness based on the epoch and round + if (s_latestPriceEpochAndRound < epochAndRound) { + // If prices are not stale, update the latest epoch and round + s_latestPriceEpochAndRound = epochAndRound; + // And update the prices in the price registry + IPriceRegistry(s_dynamicConfig.priceRegistry).updatePrices(report.priceUpdates); + + // If there is no root, the report only contained fee updated and + // we return to not revert on the empty root check below. + if (report.merkleRoot == bytes32(0)) return; + } else { + // If prices are stale and the report doesn't contain a root, this report + // does not have any valid information and we revert. + // If it does contain a merkle root, continue to the root checking section. + if (report.merkleRoot == bytes32(0)) revert StaleReport(); + } + } + + // If we reached this section, the report should contain a valid root + if (s_minSeqNr != report.interval.min || report.interval.min > report.interval.max) { + revert InvalidInterval(report.interval); + } + + if (report.merkleRoot == bytes32(0)) revert InvalidRoot(); + // Disallow duplicate roots as that would reset the timestamp and + // delay potential manual execution. + if (s_roots[report.merkleRoot] != 0) revert RootAlreadyCommitted(); + + s_minSeqNr = report.interval.max + 1; + s_roots[report.merkleRoot] = block.timestamp; + emit ReportAccepted(report); + } + + // ================================================================ + // │ Config │ + // ================================================================ + + /// @notice Returns the static commit store config. + /// @dev RMN depends on this function, if changing, please notify the RMN maintainers. + /// @return the configuration. + function getStaticConfig() external view returns (StaticConfig memory) { + return StaticConfig({ + chainSelector: i_chainSelector, + sourceChainSelector: i_sourceChainSelector, + onRamp: i_onRamp, + rmnProxy: i_rmnProxy + }); + } + + /// @notice Returns the dynamic commit store config. + /// @return the configuration. + function getDynamicConfig() external view returns (DynamicConfig memory) { + return s_dynamicConfig; + } + + /// @notice Sets the dynamic config. This function is called during `setOCR2Config` flow + function _beforeSetConfig(bytes memory onchainConfig) internal override { + DynamicConfig memory dynamicConfig = abi.decode(onchainConfig, (DynamicConfig)); + + if (dynamicConfig.priceRegistry == address(0)) revert InvalidCommitStoreConfig(); + + s_dynamicConfig = dynamicConfig; + // When the OCR config changes, we reset the price epoch and round + // since epoch and rounds are scoped per config digest. + // Note that s_minSeqNr/roots do not need to be reset as the roots persist + // across reconfigurations and are de-duplicated separately. + s_latestPriceEpochAndRound = 0; + + emit ConfigSet( + StaticConfig({ + chainSelector: i_chainSelector, + sourceChainSelector: i_sourceChainSelector, + onRamp: i_onRamp, + rmnProxy: i_rmnProxy + }), + dynamicConfig + ); + } + + // ================================================================ + // │ Access and RMN │ + // ================================================================ + + /// @notice Single function to check the status of the commitStore. + function isUnpausedAndNotCursed() external view returns (bool) { + return !IRMN(i_rmnProxy).isCursed(bytes16(uint128(i_sourceChainSelector))) && !s_paused; + } + + /// @notice Modifier to make a function callable only when the contract is not paused. + modifier whenNotPaused() { + if (paused()) revert PausedError(); + _; + } + + /// @notice Returns true if the contract is paused, and false otherwise. + function paused() public view returns (bool) { + return s_paused; + } + + /// @notice Pause the contract + /// @dev only callable by the owner + function pause() external onlyOwner { + s_paused = true; + emit Paused(msg.sender); + } + + /// @notice Unpause the contract + /// @dev only callable by the owner + function unpause() external onlyOwner { + s_paused = false; + emit Unpaused(msg.sender); + } +} diff --git a/contracts/src/v0.8/ccip/LICENSE-MIT.md b/contracts/src/v0.8/ccip/LICENSE-MIT.md new file mode 100644 index 00000000000..812debd8e9b --- /dev/null +++ b/contracts/src/v0.8/ccip/LICENSE-MIT.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 SmartContract ChainLink, Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/contracts/src/v0.8/ccip/LICENSE.md b/contracts/src/v0.8/ccip/LICENSE.md new file mode 100644 index 00000000000..5f2783f7a34 --- /dev/null +++ b/contracts/src/v0.8/ccip/LICENSE.md @@ -0,0 +1,56 @@ +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Parameters + +Licensor: SmartContract Chainlink Limited SEZC + +Licensed Work: Cross-Chain Interoperability Protocol v1.4 +The Licensed Work is (c) 2023 SmartContract Chainlink Limited SEZC + +Additional Use Grant: Any uses listed and defined at [v1.4-CCIP-License-grants]( +./v1.4-CCIP-License-grants) + +Change Date: May 23, 2027 + +Change License: MIT + +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. + +If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. + +MariaDB hereby grants you permission to use this License’s text to license your works, and to refer to it using the trademark "Business Source License", as long as you comply with the Covenants of Licensor below. + +----------------------------------------------------------------------------- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business Source License" name and trademark, Licensor covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, or a license that is compatible with GPL Version 2.0 or a later version, where "compatible" means that software provided under the Change License can be included in a program with software provided under GPL Version 2.0 or a later version. Licensor may specify additional Change Licenses without limitation. + +2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the right granted in this License, as the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. \ No newline at end of file diff --git a/contracts/src/v0.8/ccip/MultiAggregateRateLimiter.sol b/contracts/src/v0.8/ccip/MultiAggregateRateLimiter.sol new file mode 100644 index 00000000000..2a9d087a26c --- /dev/null +++ b/contracts/src/v0.8/ccip/MultiAggregateRateLimiter.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IMessageInterceptor} from "./interfaces/IMessageInterceptor.sol"; +import {IPriceRegistry} from "./interfaces/IPriceRegistry.sol"; + +import {AuthorizedCallers} from "../shared/access/AuthorizedCallers.sol"; +import {EnumerableMapAddresses} from "./../shared/enumerable/EnumerableMapAddresses.sol"; +import {Client} from "./libraries/Client.sol"; +import {RateLimiter} from "./libraries/RateLimiter.sol"; +import {USDPriceWith18Decimals} from "./libraries/USDPriceWith18Decimals.sol"; + +import {EnumerableSet} from "./../vendor/openzeppelin-solidity/v4.7.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice The aggregate rate limiter is a wrapper of the token bucket rate limiter +/// which permits rate limiting based on the aggregate value of a group of +/// token transfers, using a price registry to convert to a numeraire asset (e.g. USD). +/// The contract is a standalone multi-lane message validator contract, which can be called by authorized +/// ramp contracts to apply rate limit changes to lanes, and revert when the rate limits get breached. +contract MultiAggregateRateLimiter is IMessageInterceptor, AuthorizedCallers { + using RateLimiter for RateLimiter.TokenBucket; + using USDPriceWith18Decimals for uint224; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableSet for EnumerableSet.AddressSet; + + error PriceNotFoundForToken(address token); + error ZeroChainSelectorNotAllowed(); + + event RateLimiterConfigUpdated(uint64 indexed remoteChainSelector, bool isOutboundLane, RateLimiter.Config config); + event PriceRegistrySet(address newPriceRegistry); + event TokenAggregateRateLimitAdded(uint64 remoteChainSelector, bytes32 remoteToken, address localToken); + event TokenAggregateRateLimitRemoved(uint64 remoteChainSelector, address localToken); + + /// @notice RemoteRateLimitToken struct containing the local token address with the chain selector + /// The struct is used for removals and updates, since the local -> remote token mappings are scoped per-chain + struct LocalRateLimitToken { + uint64 remoteChainSelector; // ────╮ Remote chain selector for which to update the rate limit token mapping + address localToken; // ────────────╯ Token on the chain on which the multi-ARL is deployed + } + + /// @notice RateLimitToken struct containing both the local and remote token addresses + struct RateLimitTokenArgs { + LocalRateLimitToken localTokenArgs; // Local token update args scoped to one remote chain + bytes32 remoteToken; // Token on the remote chain (for OnRamp - dest, of OffRamp - source) + } + + /// @notice Update args for a single rate limiter config update + struct RateLimiterConfigArgs { + uint64 remoteChainSelector; // ────╮ Chain selector to set config for + bool isOutboundLane; // ───────────╯ If set to true, represents the outbound message lane (OnRamp), and the inbound message lane otherwise (OffRamp) + RateLimiter.Config rateLimiterConfig; // Rate limiter config to set + } + + /// @notice Struct to store rate limit token buckets for both lane directions + struct RateLimiterBuckets { + RateLimiter.TokenBucket inboundLaneBucket; // Bucket for the inbound lane (remote -> local) + RateLimiter.TokenBucket outboundLaneBucket; // Bucket for the outbound lane (local -> remote) + } + + /// @dev Tokens that should be included in Aggregate Rate Limiting (from local chain (this chain) -> remote), + /// grouped per-remote chain. + mapping(uint64 remoteChainSelector => EnumerableMapAddresses.AddressToBytes32Map tokensLocalToRemote) internal + s_rateLimitedTokensLocalToRemote; + + /// @notice The address of the PriceRegistry used to query token values for ratelimiting + address internal s_priceRegistry; + + /// @notice Rate limiter token bucket states per chain, with separate buckets for inbound and outbound lanes. + mapping(uint64 remoteChainSelector => RateLimiterBuckets buckets) internal s_rateLimitersByChainSelector; + + /// @param priceRegistry the price registry to set + /// @param authorizedCallers the authorized callers to set + constructor(address priceRegistry, address[] memory authorizedCallers) AuthorizedCallers(authorizedCallers) { + _setPriceRegistry(priceRegistry); + } + + /// @inheritdoc IMessageInterceptor + function onInboundMessage(Client.Any2EVMMessage memory message) external onlyAuthorizedCallers { + _applyRateLimit(message.sourceChainSelector, message.destTokenAmounts, false); + } + + /// @inheritdoc IMessageInterceptor + function onOutboundMessage( + uint64 destChainSelector, + Client.EVM2AnyMessage calldata message + ) external onlyAuthorizedCallers { + _applyRateLimit(destChainSelector, message.tokenAmounts, true); + } + + /// @notice Applies the rate limit to the token bucket if enabled + /// @param remoteChainSelector The remote chain selector + /// @param tokenAmounts The tokens and amounts to rate limit + /// @param isOutgoingLane if set to true, fetches the bucket for the outgoing message lane (OnRamp). + function _applyRateLimit( + uint64 remoteChainSelector, + Client.EVMTokenAmount[] memory tokenAmounts, + bool isOutgoingLane + ) private { + RateLimiter.TokenBucket storage tokenBucket = _getTokenBucket(remoteChainSelector, isOutgoingLane); + + // Skip rate limiting if it is disabled + if (tokenBucket.isEnabled) { + uint256 value; + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + if (s_rateLimitedTokensLocalToRemote[remoteChainSelector].contains(tokenAmounts[i].token)) { + value += _getTokenValue(tokenAmounts[i]); + } + } + // Rate limit on aggregated token value + if (value > 0) tokenBucket._consume(value, address(0)); + } + } + + /// @param remoteChainSelector chain selector to retrieve token bucket for + /// @param isOutboundLane if set to true, fetches the bucket for the outbound message lane (OnRamp). + /// Otherwise fetches for the inbound message lane (OffRamp). + /// @return bucket Storage pointer to the token bucket representing a specific lane + function _getTokenBucket( + uint64 remoteChainSelector, + bool isOutboundLane + ) internal view returns (RateLimiter.TokenBucket storage) { + RateLimiterBuckets storage rateLimiterBuckets = s_rateLimitersByChainSelector[remoteChainSelector]; + if (isOutboundLane) { + return rateLimiterBuckets.outboundLaneBucket; + } else { + return rateLimiterBuckets.inboundLaneBucket; + } + } + + /// @notice Retrieves the token value for a token using the PriceRegistry + /// @return tokenValue USD value in 18 decimals + function _getTokenValue(Client.EVMTokenAmount memory tokenAmount) internal view returns (uint256) { + // not fetching validated price, as price staleness is not important for value-based rate limiting + // we only need to verify the price is not 0 + uint224 pricePerToken = IPriceRegistry(s_priceRegistry).getTokenPrice(tokenAmount.token).value; + if (pricePerToken == 0) revert PriceNotFoundForToken(tokenAmount.token); + return pricePerToken._calcUSDValueFromTokenAmount(tokenAmount.amount); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @param remoteChainSelector chain selector to retrieve state for + /// @param isOutboundLane if set to true, fetches the rate limit state for the outbound message lane (OnRamp). + /// Otherwise fetches for the inbound message lane (OffRamp). + /// The outbound and inbound message rate limit state is completely separated. + /// @return The token bucket. + function currentRateLimiterState( + uint64 remoteChainSelector, + bool isOutboundLane + ) external view returns (RateLimiter.TokenBucket memory) { + return _getTokenBucket(remoteChainSelector, isOutboundLane)._currentTokenBucketState(); + } + + /// @notice Applies the provided rate limiter config updates. + /// @param rateLimiterUpdates Rate limiter updates + /// @dev should only be callable by the owner + function applyRateLimiterConfigUpdates(RateLimiterConfigArgs[] memory rateLimiterUpdates) external onlyOwner { + for (uint256 i = 0; i < rateLimiterUpdates.length; ++i) { + RateLimiterConfigArgs memory updateArgs = rateLimiterUpdates[i]; + RateLimiter.Config memory configUpdate = updateArgs.rateLimiterConfig; + uint64 remoteChainSelector = updateArgs.remoteChainSelector; + + if (remoteChainSelector == 0) { + revert ZeroChainSelectorNotAllowed(); + } + + bool isOutboundLane = updateArgs.isOutboundLane; + + RateLimiter.TokenBucket storage tokenBucket = _getTokenBucket(remoteChainSelector, isOutboundLane); + + if (tokenBucket.lastUpdated == 0) { + // Token bucket needs to be newly added + RateLimiter.TokenBucket memory newTokenBucket = RateLimiter.TokenBucket({ + rate: configUpdate.rate, + capacity: configUpdate.capacity, + tokens: configUpdate.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: configUpdate.isEnabled + }); + + if (isOutboundLane) { + s_rateLimitersByChainSelector[remoteChainSelector].outboundLaneBucket = newTokenBucket; + } else { + s_rateLimitersByChainSelector[remoteChainSelector].inboundLaneBucket = newTokenBucket; + } + } else { + tokenBucket._setTokenBucketConfig(configUpdate); + } + emit RateLimiterConfigUpdated(remoteChainSelector, isOutboundLane, configUpdate); + } + } + + /// @notice Get all tokens which are included in Aggregate Rate Limiting. + /// @param remoteChainSelector chain selector to get rate limit tokens for + /// @return localTokens The local chain representation of the tokens that are rate limited. + /// @return remoteTokens The remote representation of the tokens that are rate limited. + /// @dev the order of IDs in the list is **not guaranteed**, therefore, if ordering matters when + /// making successive calls, one should keep the block height constant to ensure a consistent result. + function getAllRateLimitTokens(uint64 remoteChainSelector) + external + view + returns (address[] memory localTokens, bytes32[] memory remoteTokens) + { + uint256 tokenCount = s_rateLimitedTokensLocalToRemote[remoteChainSelector].length(); + + localTokens = new address[](tokenCount); + remoteTokens = new bytes32[](tokenCount); + + for (uint256 i = 0; i < tokenCount; ++i) { + (address localToken, bytes32 remoteToken) = s_rateLimitedTokensLocalToRemote[remoteChainSelector].at(i); + localTokens[i] = localToken; + remoteTokens[i] = remoteToken; + } + return (localTokens, remoteTokens); + } + + /// @notice Adds or removes tokens from being used in Aggregate Rate Limiting. + /// @param removes - A list of one or more tokens to be removed. + /// @param adds - A list of one or more tokens to be added. + function updateRateLimitTokens( + LocalRateLimitToken[] memory removes, + RateLimitTokenArgs[] memory adds + ) external onlyOwner { + for (uint256 i = 0; i < removes.length; ++i) { + address localToken = removes[i].localToken; + uint64 remoteChainSelector = removes[i].remoteChainSelector; + + if (s_rateLimitedTokensLocalToRemote[remoteChainSelector].remove(localToken)) { + emit TokenAggregateRateLimitRemoved(remoteChainSelector, localToken); + } + } + + for (uint256 i = 0; i < adds.length; ++i) { + LocalRateLimitToken memory localTokenArgs = adds[i].localTokenArgs; + bytes32 remoteToken = adds[i].remoteToken; + address localToken = localTokenArgs.localToken; + + if (localToken == address(0) || remoteToken == bytes32("")) { + revert ZeroAddressNotAllowed(); + } + + uint64 remoteChainSelector = localTokenArgs.remoteChainSelector; + + if (s_rateLimitedTokensLocalToRemote[remoteChainSelector].set(localToken, remoteToken)) { + emit TokenAggregateRateLimitAdded(remoteChainSelector, remoteToken, localToken); + } + } + } + + /// @return priceRegistry The configured PriceRegistry address + function getPriceRegistry() external view returns (address) { + return s_priceRegistry; + } + + /// @notice Sets the Price Registry address + /// @param newPriceRegistry the address of the new PriceRegistry + /// @dev precondition The address must be a non-zero address + function setPriceRegistry(address newPriceRegistry) external onlyOwner { + _setPriceRegistry(newPriceRegistry); + } + + /// @notice Sets the Price Registry address + /// @param newPriceRegistry the address of the new PriceRegistry + /// @dev precondition The address must be a non-zero address + function _setPriceRegistry(address newPriceRegistry) internal { + if (newPriceRegistry == address(0)) { + revert ZeroAddressNotAllowed(); + } + + s_priceRegistry = newPriceRegistry; + emit PriceRegistrySet(newPriceRegistry); + } +} diff --git a/contracts/src/v0.8/ccip/NonceManager.sol b/contracts/src/v0.8/ccip/NonceManager.sol new file mode 100644 index 00000000000..2cfcbbe9e2b --- /dev/null +++ b/contracts/src/v0.8/ccip/NonceManager.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IEVM2AnyOnRamp} from "./interfaces/IEVM2AnyOnRamp.sol"; +import {INonceManager} from "./interfaces/INonceManager.sol"; + +import {AuthorizedCallers} from "../shared/access/AuthorizedCallers.sol"; + +/// @title NonceManager +/// @notice NonceManager contract that manages sender nonces for the on/off ramps +contract NonceManager is INonceManager, AuthorizedCallers { + error PreviousRampAlreadySet(); + + event PreviousRampsUpdated(uint64 indexed remoteChainSelector, PreviousRamps prevRamp); + event SkippedIncorrectNonce(uint64 sourceChainSelector, uint64 nonce, bytes sender); + + /// @dev Struct that contains the previous on/off ramp addresses + struct PreviousRamps { + address prevOnRamp; // Previous onRamp + address prevOffRamp; // Previous offRamp + } + + /// @dev Struct that contains the chain selector and the previous on/off ramps, same as PreviousRamps but with the chain selector + /// so that an array of these can be passed to the applyPreviousRampsUpdates function + struct PreviousRampsArgs { + uint64 remoteChainSelector; // Chain selector + PreviousRamps prevRamps; // Previous on/off ramps + } + + /// @dev previous ramps + mapping(uint64 chainSelector => PreviousRamps previousRamps) private s_previousRamps; + /// @dev The current outbound nonce per sender used on the onramp + mapping(uint64 destChainSelector => mapping(address sender => uint64 outboundNonce)) private s_outboundNonces; + /// @dev The current inbound nonce per sender used on the offramp + /// Eventually in sync with the outbound nonce in the remote source chain NonceManager, used to enforce that messages are + /// executed in the same order they are sent (assuming they are DON) + mapping(uint64 sourceChainSelector => mapping(bytes sender => uint64 inboundNonce)) private s_inboundNonces; + + constructor(address[] memory authorizedCallers) AuthorizedCallers(authorizedCallers) {} + + /// @inheritdoc INonceManager + function getIncrementedOutboundNonce( + uint64 destChainSelector, + address sender + ) external onlyAuthorizedCallers returns (uint64) { + uint64 outboundNonce = _getOutboundNonce(destChainSelector, sender) + 1; + s_outboundNonces[destChainSelector][sender] = outboundNonce; + + return outboundNonce; + } + + /// @notice Returns the outbound nonce for a given sender on a given destination chain + /// @param destChainSelector The destination chain selector + /// @param sender The sender address + /// @return The outbound nonce + function getOutboundNonce(uint64 destChainSelector, address sender) external view returns (uint64) { + return _getOutboundNonce(destChainSelector, sender); + } + + function _getOutboundNonce(uint64 destChainSelector, address sender) private view returns (uint64) { + uint64 outboundNonce = s_outboundNonces[destChainSelector][sender]; + + // When introducing the NonceManager with existing lanes, we still want to have sequential nonces. + // Referencing the old onRamp preserves sequencing between updates. + if (outboundNonce == 0) { + address prevOnRamp = s_previousRamps[destChainSelector].prevOnRamp; + if (prevOnRamp != address(0)) { + return IEVM2AnyOnRamp(prevOnRamp).getSenderNonce(sender); + } + } + + return outboundNonce; + } + + /// @inheritdoc INonceManager + function incrementInboundNonce( + uint64 sourceChainSelector, + uint64 expectedNonce, + bytes calldata sender + ) external onlyAuthorizedCallers returns (bool) { + uint64 inboundNonce = _getInboundNonce(sourceChainSelector, sender) + 1; + + if (inboundNonce != expectedNonce) { + // If the nonce is not the expected one, this means that there are still messages in flight so we skip + // the nonce increment + emit SkippedIncorrectNonce(sourceChainSelector, expectedNonce, sender); + return false; + } + + s_inboundNonces[sourceChainSelector][sender] = inboundNonce; + + return true; + } + + /// @notice Returns the inbound nonce for a given sender on a given source chain + /// @param sourceChainSelector The source chain selector + /// @param sender The encoded sender address + /// @return The inbound nonce + function getInboundNonce(uint64 sourceChainSelector, bytes calldata sender) external view returns (uint64) { + return _getInboundNonce(sourceChainSelector, sender); + } + + function _getInboundNonce(uint64 sourceChainSelector, bytes calldata sender) private view returns (uint64) { + uint64 inboundNonce = s_inboundNonces[sourceChainSelector][sender]; + + // When introducing the NonceManager with existing lanes, we still want to have sequential nonces. + // Referencing the old offRamp to check the expected nonce if none is set for a + // given sender allows us to skip the current message in the current offRamp if it would not be the next according + // to the old offRamp. This preserves sequencing between updates. + if (inboundNonce == 0) { + address prevOffRamp = s_previousRamps[sourceChainSelector].prevOffRamp; + if (prevOffRamp != address(0)) { + // We only expect EVM previous offRamps here so we can safely decode the sender + return IEVM2AnyOnRamp(prevOffRamp).getSenderNonce(abi.decode(sender, (address))); + } + } + + return inboundNonce; + } + + /// @notice Updates the previous ramps addresses + /// @param previousRampsArgs The previous on/off ramps addresses + function applyPreviousRampsUpdates(PreviousRampsArgs[] calldata previousRampsArgs) external onlyOwner { + for (uint256 i = 0; i < previousRampsArgs.length; ++i) { + PreviousRampsArgs calldata previousRampsArg = previousRampsArgs[i]; + + PreviousRamps storage prevRamps = s_previousRamps[previousRampsArg.remoteChainSelector]; + + // If the previous ramps are already set then they should not be updated + if (prevRamps.prevOnRamp != address(0) || prevRamps.prevOffRamp != address(0)) { + revert PreviousRampAlreadySet(); + } + + prevRamps.prevOnRamp = previousRampsArg.prevRamps.prevOnRamp; + prevRamps.prevOffRamp = previousRampsArg.prevRamps.prevOffRamp; + + emit PreviousRampsUpdated(previousRampsArg.remoteChainSelector, previousRampsArg.prevRamps); + } + } + + /// @notice Gets the previous onRamp address for the given chain selector + /// @param chainSelector The chain selector + /// @return The previous onRamp address + function getPreviousRamps(uint64 chainSelector) external view returns (PreviousRamps memory) { + return s_previousRamps[chainSelector]; + } +} diff --git a/contracts/src/v0.8/ccip/PriceRegistry.sol b/contracts/src/v0.8/ccip/PriceRegistry.sol new file mode 100644 index 00000000000..f15232271e9 --- /dev/null +++ b/contracts/src/v0.8/ccip/PriceRegistry.sol @@ -0,0 +1,888 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; +import {IPriceRegistry} from "./interfaces/IPriceRegistry.sol"; + +import {AuthorizedCallers} from "../shared/access/AuthorizedCallers.sol"; +import {AggregatorV3Interface} from "./../shared/interfaces/AggregatorV3Interface.sol"; +import {Client} from "./libraries/Client.sol"; +import {Internal} from "./libraries/Internal.sol"; +import {Pool} from "./libraries/Pool.sol"; +import {USDPriceWith18Decimals} from "./libraries/USDPriceWith18Decimals.sol"; + +import {EnumerableSet} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice The PriceRegistry contract responsibility is to store the current gas price in USD for a given destination chain, +/// and the price of a token in USD allowing the owner or priceUpdater to update this value. +/// The authorized callers in the contract represent the fee price updaters. +contract PriceRegistry is AuthorizedCallers, IPriceRegistry, ITypeAndVersion { + using EnumerableSet for EnumerableSet.AddressSet; + using USDPriceWith18Decimals for uint224; + + /// @notice Token price data feed update + struct TokenPriceFeedUpdate { + address sourceToken; // Source token to update feed for + IPriceRegistry.TokenPriceFeedConfig feedConfig; // Feed config update data + } + + /// @dev Struct that contains the static configuration + /// RMN depends on this struct, if changing, please notify the RMN maintainers. + // solhint-disable-next-line gas-struct-packing + struct StaticConfig { + uint96 maxFeeJuelsPerMsg; // ─╮ Maximum fee that can be charged for a message + address linkToken; // ────────╯ LINK token address + uint32 stalenessThreshold; // The amount of time a gas price can be stale before it is considered invalid. + } + + error TokenNotSupported(address token); + error ChainNotSupported(uint64 chain); + error StaleGasPrice(uint64 destChainSelector, uint256 threshold, uint256 timePassed); + error DataFeedValueOutOfUint224Range(); + error InvalidDestBytesOverhead(address token, uint32 destBytesOverhead); + error MessageGasLimitTooHigh(); + error DestinationChainNotEnabled(uint64 destChainSelector); + error ExtraArgOutOfOrderExecutionMustBeTrue(); + error InvalidExtraArgsTag(); + error SourceTokenDataTooLarge(address token); + error InvalidDestChainConfig(uint64 destChainSelector); + error MessageFeeTooHigh(uint256 msgFeeJuels, uint256 maxFeeJuelsPerMsg); + error InvalidStaticConfig(); + error MessageTooLarge(uint256 maxSize, uint256 actualSize); + error UnsupportedNumberOfTokens(); + + event PriceUpdaterSet(address indexed priceUpdater); + event PriceUpdaterRemoved(address indexed priceUpdater); + event FeeTokenAdded(address indexed feeToken); + event FeeTokenRemoved(address indexed feeToken); + event UsdPerUnitGasUpdated(uint64 indexed destChain, uint256 value, uint256 timestamp); + event UsdPerTokenUpdated(address indexed token, uint256 value, uint256 timestamp); + event PriceFeedPerTokenUpdated(address indexed token, IPriceRegistry.TokenPriceFeedConfig priceFeedConfig); + + event TokenTransferFeeConfigUpdated( + uint64 indexed destChainSelector, address indexed token, TokenTransferFeeConfig tokenTransferFeeConfig + ); + event TokenTransferFeeConfigDeleted(uint64 indexed destChainSelector, address indexed token); + event PremiumMultiplierWeiPerEthUpdated(address indexed token, uint64 premiumMultiplierWeiPerEth); + event DestChainConfigUpdated(uint64 indexed destChainSelector, DestChainConfig destChainConfig); + event DestChainAdded(uint64 indexed destChainSelector, DestChainConfig destChainConfig); + + /// @dev Struct to hold the fee & validation configs for a destination chain + struct DestChainConfig { + bool isEnabled; // ──────────────────────────╮ Whether this destination chain is enabled + uint16 maxNumberOfTokensPerMsg; // │ Maximum number of distinct ERC20 token transferred per message + uint32 maxDataBytes; // │ Maximum payload data size in bytes + uint32 maxPerMsgGasLimit; // │ Maximum gas limit for messages targeting EVMs + uint32 destGasOverhead; // │ Gas charged on top of the gasLimit to cover destination chain costs + uint16 destGasPerPayloadByte; // │ Destination chain gas charged for passing each byte of `data` payload to receiver + uint32 destDataAvailabilityOverheadGas; // | Extra data availability gas charged on top of the message, e.g. for OCR + uint16 destGasPerDataAvailabilityByte; // | Amount of gas to charge per byte of message data that needs availability + uint16 destDataAvailabilityMultiplierBps; // │ Multiplier for data availability gas, multiples of bps, or 0.0001 + // The following three properties are defaults, they can be overridden by setting the TokenTransferFeeConfig for a token + uint16 defaultTokenFeeUSDCents; // │ Default token fee charged per token transfer + uint32 defaultTokenDestGasOverhead; // ──────╯ Default gas charged to execute the token transfer on the destination chain + uint32 defaultTokenDestBytesOverhead; // ────╮ Default extra data availability bytes charged per token transfer + uint32 defaultTxGasLimit; // │ Default gas limit for a tx + uint64 gasMultiplierWeiPerEth; // │ Multiplier for gas costs, 1e18 based so 11e17 = 10% extra cost. + uint32 networkFeeUSDCents; // │ Flat network fee to charge for messages, multiples of 0.01 USD + bool enforceOutOfOrder; // │ Whether to enforce the allowOutOfOrderExecution extraArg value to be true. + bytes4 chainFamilySelector; // ──────────────╯ Selector that identifies the destination chain's family. Used to determine the correct validations to perform for the dest chain. + } + + /// @dev Struct to hold the configs and its destination chain selector + /// Same as DestChainConfig but with the destChainSelector so that an array of these + /// can be passed in the constructor and the applyDestChainConfigUpdates function + //solhint-disable gas-struct-packing + struct DestChainConfigArgs { + uint64 destChainSelector; // Destination chain selector + DestChainConfig destChainConfig; // Config to update for the chain selector + } + + /// @dev Struct to hold the transfer fee configuration for token transfers + struct TokenTransferFeeConfig { + uint32 minFeeUSDCents; // ──────────╮ Minimum fee to charge per token transfer, multiples of 0.01 USD + uint32 maxFeeUSDCents; // │ Maximum fee to charge per token transfer, multiples of 0.01 USD + uint16 deciBps; // │ Basis points charged on token transfers, multiples of 0.1bps, or 1e-5 + uint32 destGasOverhead; // │ Gas charged to execute the token transfer on the destination chain + // │ Extra data availability bytes that are returned from the source pool and sent + uint32 destBytesOverhead; // │ to the destination pool. Must be >= Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + bool isEnabled; // ─────────────────╯ Whether this token has custom transfer fees + } + + /// @dev Struct to hold the token transfer fee configurations for a token, same as TokenTransferFeeConfig but with the token address included so + /// that an array of these can be passed in the TokenTransferFeeConfigArgs struct to set the mapping + struct TokenTransferFeeConfigSingleTokenArgs { + address token; // Token address + TokenTransferFeeConfig tokenTransferFeeConfig; // struct to hold the transfer fee configuration for token transfers + } + + /// @dev Struct to hold the token transfer fee configurations for a destination chain and a set of tokens. Same as TokenTransferFeeConfigSingleTokenArgs + /// but with the destChainSelector and an array of TokenTransferFeeConfigSingleTokenArgs included so that an array of these can be passed in the constructor + /// and the applyTokenTransferFeeConfigUpdates function + struct TokenTransferFeeConfigArgs { + uint64 destChainSelector; // Destination chain selector + TokenTransferFeeConfigSingleTokenArgs[] tokenTransferFeeConfigs; // Array of token transfer fee configurations + } + + /// @dev Struct to hold a pair of destination chain selector and token address so that an array of these can be passed in the + /// applyTokenTransferFeeConfigUpdates function to remove the token transfer fee configuration for a token + struct TokenTransferFeeConfigRemoveArgs { + uint64 destChainSelector; // ─╮ Destination chain selector + address token; // ────────────╯ Token address + } + + /// @dev Struct to hold the fee token configuration for a token, same as the s_premiumMultiplierWeiPerEth but with + /// the token address included so that an array of these can be passed in the constructor and + /// applyPremiumMultiplierWeiPerEthUpdates to set the mapping + struct PremiumMultiplierWeiPerEthArgs { + address token; // // ───────────────────╮ Token address + uint64 premiumMultiplierWeiPerEth; // ──╯ Multiplier for destination chain specific premiums. Should never be 0 so can be used as an isEnabled flag + } + + string public constant override typeAndVersion = "PriceRegistry 1.6.0-dev"; + + /// @dev The gas price per unit of gas for a given destination chain, in USD with 18 decimals. + /// Multiple gas prices can be encoded into the same value. Each price takes {Internal.GAS_PRICE_BITS} bits. + /// For example, if Optimism is the destination chain, gas price can include L1 base fee and L2 gas price. + /// Logic to parse the price components is chain-specific, and should live in OnRamp. + /// @dev Price of 1e18 is 1 USD. Examples: + /// Very Expensive: 1 unit of gas costs 1 USD -> 1e18 + /// Expensive: 1 unit of gas costs 0.1 USD -> 1e17 + /// Cheap: 1 unit of gas costs 0.000001 USD -> 1e12 + mapping(uint64 destChainSelector => Internal.TimestampedPackedUint224 price) private + s_usdPerUnitGasByDestChainSelector; + + /// @dev The price, in USD with 18 decimals, per 1e18 of the smallest token denomination. + /// @dev Price of 1e18 represents 1 USD per 1e18 token amount. + /// 1 USDC = 1.00 USD per full token, each full token is 1e6 units -> 1 * 1e18 * 1e18 / 1e6 = 1e30 + /// 1 ETH = 2,000 USD per full token, each full token is 1e18 units -> 2000 * 1e18 * 1e18 / 1e18 = 2_000e18 + /// 1 LINK = 5.00 USD per full token, each full token is 1e18 units -> 5 * 1e18 * 1e18 / 1e18 = 5e18 + mapping(address token => Internal.TimestampedPackedUint224 price) private s_usdPerToken; + + /// @dev Stores the price data feed configurations per token. + mapping(address token => IPriceRegistry.TokenPriceFeedConfig dataFeedAddress) private s_usdPriceFeedsPerToken; + + /// @dev The multiplier for destination chain specific premiums that can be set by the owner or fee admin + /// This should never be 0 once set, so it can be used as an isEnabled flag + mapping(address token => uint64 premiumMultiplierWeiPerEth) internal s_premiumMultiplierWeiPerEth; + + /// @dev The destination chain specific fee configs + mapping(uint64 destChainSelector => DestChainConfig destChainConfig) internal s_destChainConfigs; + + /// @dev The token transfer fee config that can be set by the owner or fee admin + mapping(uint64 destChainSelector => mapping(address token => TokenTransferFeeConfig tranferFeeConfig)) internal + s_tokenTransferFeeConfig; + + /// @dev Maximum fee that can be charged for a message. This is a guard to prevent massively overcharging due to misconfiguation. + uint96 internal immutable i_maxFeeJuelsPerMsg; + /// @dev The link token address + address internal immutable i_linkToken; + + // Price updaters are allowed to update the prices. + EnumerableSet.AddressSet private s_priceUpdaters; + // Subset of tokens which prices tracked by this registry which are fee tokens. + EnumerableSet.AddressSet private s_feeTokens; + // The amount of time a gas price can be stale before it is considered invalid. + uint32 private immutable i_stalenessThreshold; + + constructor( + StaticConfig memory staticConfig, + address[] memory priceUpdaters, + address[] memory feeTokens, + TokenPriceFeedUpdate[] memory tokenPriceFeeds, + TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs, + PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs, + DestChainConfigArgs[] memory destChainConfigArgs + ) AuthorizedCallers(priceUpdaters) { + if ( + staticConfig.linkToken == address(0) || staticConfig.maxFeeJuelsPerMsg == 0 + || staticConfig.stalenessThreshold == 0 + ) { + revert InvalidStaticConfig(); + } + + i_linkToken = staticConfig.linkToken; + i_maxFeeJuelsPerMsg = staticConfig.maxFeeJuelsPerMsg; + i_stalenessThreshold = staticConfig.stalenessThreshold; + + _applyFeeTokensUpdates(feeTokens, new address[](0)); + _updateTokenPriceFeeds(tokenPriceFeeds); + _applyDestChainConfigUpdates(destChainConfigArgs); + _applyPremiumMultiplierWeiPerEthUpdates(premiumMultiplierWeiPerEthArgs); + _applyTokenTransferFeeConfigUpdates(tokenTransferFeeConfigArgs, new TokenTransferFeeConfigRemoveArgs[](0)); + } + + // ================================================================ + // │ Price calculations │ + // ================================================================ + + /// @inheritdoc IPriceRegistry + function getTokenPrice(address token) public view override returns (Internal.TimestampedPackedUint224 memory) { + IPriceRegistry.TokenPriceFeedConfig memory priceFeedConfig = s_usdPriceFeedsPerToken[token]; + if (priceFeedConfig.dataFeedAddress == address(0)) { + return s_usdPerToken[token]; + } + + return _getTokenPriceFromDataFeed(priceFeedConfig); + } + + /// @inheritdoc IPriceRegistry + function getValidatedTokenPrice(address token) external view override returns (uint224) { + return _getValidatedTokenPrice(token); + } + + /// @inheritdoc IPriceRegistry + function getTokenPrices(address[] calldata tokens) + external + view + override + returns (Internal.TimestampedPackedUint224[] memory) + { + uint256 length = tokens.length; + Internal.TimestampedPackedUint224[] memory tokenPrices = new Internal.TimestampedPackedUint224[](length); + for (uint256 i = 0; i < length; ++i) { + tokenPrices[i] = getTokenPrice(tokens[i]); + } + return tokenPrices; + } + + /// @inheritdoc IPriceRegistry + function getTokenPriceFeedConfig(address token) + external + view + override + returns (IPriceRegistry.TokenPriceFeedConfig memory) + { + return s_usdPriceFeedsPerToken[token]; + } + + /// @inheritdoc IPriceRegistry + function getDestinationChainGasPrice(uint64 destChainSelector) + external + view + override + returns (Internal.TimestampedPackedUint224 memory) + { + return s_usdPerUnitGasByDestChainSelector[destChainSelector]; + } + + /// @inheritdoc IPriceRegistry + function getTokenAndGasPrices( + address token, + uint64 destChainSelector + ) public view override returns (uint224 tokenPrice, uint224 gasPriceValue) { + Internal.TimestampedPackedUint224 memory gasPrice = s_usdPerUnitGasByDestChainSelector[destChainSelector]; + // We do allow a gas price of 0, but no stale or unset gas prices + if (gasPrice.timestamp == 0) revert ChainNotSupported(destChainSelector); + uint256 timePassed = block.timestamp - gasPrice.timestamp; + if (timePassed > i_stalenessThreshold) revert StaleGasPrice(destChainSelector, i_stalenessThreshold, timePassed); + + return (_getValidatedTokenPrice(token), gasPrice.value); + } + + /// @inheritdoc IPriceRegistry + /// @dev this function assumes that no more than 1e59 dollars are sent as payment. + /// If more is sent, the multiplication of feeTokenAmount and feeTokenValue will overflow. + /// Since there isn't even close to 1e59 dollars in the world economy this is safe. + function convertTokenAmount( + address fromToken, + uint256 fromTokenAmount, + address toToken + ) public view override returns (uint256) { + /// Example: + /// fromTokenAmount: 1e18 // 1 ETH + /// ETH: 2_000e18 + /// LINK: 5e18 + /// return: 1e18 * 2_000e18 / 5e18 = 400e18 (400 LINK) + return (fromTokenAmount * _getValidatedTokenPrice(fromToken)) / _getValidatedTokenPrice(toToken); + } + + /// @notice Gets the token price for a given token and revert if the token is not supported + /// @param token The address of the token to get the price for + /// @return the token price + function _getValidatedTokenPrice(address token) internal view returns (uint224) { + Internal.TimestampedPackedUint224 memory tokenPrice = getTokenPrice(token); + // Token price must be set at least once + if (tokenPrice.timestamp == 0 || tokenPrice.value == 0) revert TokenNotSupported(token); + return tokenPrice.value; + } + + /// @notice Gets the token price from a data feed address, rebased to the same units as s_usdPerToken + /// @param priceFeedConfig token data feed configuration with valid data feed address (used to retrieve price & timestamp) + /// @return tokenPrice data feed price answer rebased to s_usdPerToken units, with latest block timestamp + function _getTokenPriceFromDataFeed(IPriceRegistry.TokenPriceFeedConfig memory priceFeedConfig) + internal + view + returns (Internal.TimestampedPackedUint224 memory tokenPrice) + { + AggregatorV3Interface dataFeedContract = AggregatorV3Interface(priceFeedConfig.dataFeedAddress); + ( + /* uint80 roundID */ + , + int256 dataFeedAnswer, + /* uint startedAt */ + , + /* uint256 updatedAt */ + , + /* uint80 answeredInRound */ + ) = dataFeedContract.latestRoundData(); + + if (dataFeedAnswer < 0) { + revert DataFeedValueOutOfUint224Range(); + } + uint256 rebasedValue = uint256(dataFeedAnswer); + + // Rebase formula for units in smallest token denomination: usdValue * (1e18 * 1e18) / 1eTokenDecimals + // feedValue * (10 ** (18 - feedDecimals)) * (10 ** (18 - erc20Decimals)) + // feedValue * (10 ** ((18 - feedDecimals) + (18 - erc20Decimals))) + // feedValue * (10 ** (36 - feedDecimals - erc20Decimals)) + // feedValue * (10 ** (36 - (feedDecimals + erc20Decimals))) + // feedValue * (10 ** (36 - excessDecimals)) + // If excessDecimals > 36 => flip it to feedValue / (10 ** (excessDecimals - 36)) + + uint8 excessDecimals = dataFeedContract.decimals() + priceFeedConfig.tokenDecimals; + + if (excessDecimals > 36) { + rebasedValue /= 10 ** (excessDecimals - 36); + } else { + rebasedValue *= 10 ** (36 - excessDecimals); + } + + if (rebasedValue > type(uint224).max) { + revert DataFeedValueOutOfUint224Range(); + } + + // Data feed staleness is unchecked to decouple the PriceRegistry from data feed delay issues + return Internal.TimestampedPackedUint224({value: uint224(rebasedValue), timestamp: uint32(block.timestamp)}); + } + + // ================================================================ + // │ Fee tokens │ + // ================================================================ + + /// @inheritdoc IPriceRegistry + function getFeeTokens() external view returns (address[] memory) { + return s_feeTokens.values(); + } + + /// @notice Add and remove tokens from feeTokens set. + /// @param feeTokensToAdd The addresses of the tokens which are now considered fee tokens + /// and can be used to calculate fees. + /// @param feeTokensToRemove The addresses of the tokens which are no longer considered feeTokens. + function applyFeeTokensUpdates( + address[] memory feeTokensToAdd, + address[] memory feeTokensToRemove + ) external onlyOwner { + _applyFeeTokensUpdates(feeTokensToAdd, feeTokensToRemove); + } + + /// @notice Add and remove tokens from feeTokens set. + /// @param feeTokensToAdd The addresses of the tokens which are now considered fee tokens + /// and can be used to calculate fees. + /// @param feeTokensToRemove The addresses of the tokens which are no longer considered feeTokens. + function _applyFeeTokensUpdates(address[] memory feeTokensToAdd, address[] memory feeTokensToRemove) private { + for (uint256 i = 0; i < feeTokensToAdd.length; ++i) { + if (s_feeTokens.add(feeTokensToAdd[i])) { + emit FeeTokenAdded(feeTokensToAdd[i]); + } + } + for (uint256 i = 0; i < feeTokensToRemove.length; ++i) { + if (s_feeTokens.remove(feeTokensToRemove[i])) { + emit FeeTokenRemoved(feeTokensToRemove[i]); + } + } + } + + // ================================================================ + // │ Price updates │ + // ================================================================ + + /// @inheritdoc IPriceRegistry + function updatePrices(Internal.PriceUpdates calldata priceUpdates) external override { + // The caller must be the fee updater + _validateCaller(); + + uint256 tokenUpdatesLength = priceUpdates.tokenPriceUpdates.length; + + for (uint256 i = 0; i < tokenUpdatesLength; ++i) { + Internal.TokenPriceUpdate memory update = priceUpdates.tokenPriceUpdates[i]; + s_usdPerToken[update.sourceToken] = + Internal.TimestampedPackedUint224({value: update.usdPerToken, timestamp: uint32(block.timestamp)}); + emit UsdPerTokenUpdated(update.sourceToken, update.usdPerToken, block.timestamp); + } + + uint256 gasUpdatesLength = priceUpdates.gasPriceUpdates.length; + + for (uint256 i = 0; i < gasUpdatesLength; ++i) { + Internal.GasPriceUpdate memory update = priceUpdates.gasPriceUpdates[i]; + s_usdPerUnitGasByDestChainSelector[update.destChainSelector] = + Internal.TimestampedPackedUint224({value: update.usdPerUnitGas, timestamp: uint32(block.timestamp)}); + emit UsdPerUnitGasUpdated(update.destChainSelector, update.usdPerUnitGas, block.timestamp); + } + } + + /// @notice Updates the USD token price feeds for given tokens + /// @param tokenPriceFeedUpdates Token price feed updates to apply + function updateTokenPriceFeeds(TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates) external onlyOwner { + _updateTokenPriceFeeds(tokenPriceFeedUpdates); + } + + /// @notice Updates the USD token price feeds for given tokens + /// @param tokenPriceFeedUpdates Token price feed updates to apply + function _updateTokenPriceFeeds(TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates) private { + for (uint256 i; i < tokenPriceFeedUpdates.length; ++i) { + TokenPriceFeedUpdate memory update = tokenPriceFeedUpdates[i]; + address sourceToken = update.sourceToken; + IPriceRegistry.TokenPriceFeedConfig memory tokenPriceFeedConfig = update.feedConfig; + + s_usdPriceFeedsPerToken[sourceToken] = tokenPriceFeedConfig; + emit PriceFeedPerTokenUpdated(sourceToken, tokenPriceFeedConfig); + } + } + + // ================================================================ + // │ Fee quoting │ + // ================================================================ + + /// @inheritdoc IPriceRegistry + /// @dev The function should always validate message.extraArgs, message.receiver and family-specific configs + function getValidatedFee( + uint64 destChainSelector, + Client.EVM2AnyMessage calldata message + ) external view returns (uint256 feeTokenAmount) { + DestChainConfig memory destChainConfig = s_destChainConfigs[destChainSelector]; + if (!destChainConfig.isEnabled) revert DestinationChainNotEnabled(destChainSelector); + + uint256 numberOfTokens = message.tokenAmounts.length; + _validateMessage(destChainConfig, message.data.length, numberOfTokens, message.receiver); + + uint64 premiumMultiplierWeiPerEth = s_premiumMultiplierWeiPerEth[message.feeToken]; + + // The below call asserts that feeToken is a supported token + (uint224 feeTokenPrice, uint224 packedGasPrice) = getTokenAndGasPrices(message.feeToken, destChainSelector); + + // Calculate premiumFee in USD with 18 decimals precision first. + // If message-only and no token transfers, a flat network fee is charged. + // If there are token transfers, premiumFee is calculated from token transfer fee. + // If there are both token transfers and message, premiumFee is only calculated from token transfer fee. + uint256 premiumFee = 0; + uint32 tokenTransferGas = 0; + uint32 tokenTransferBytesOverhead = 0; + if (numberOfTokens > 0) { + (premiumFee, tokenTransferGas, tokenTransferBytesOverhead) = + _getTokenTransferCost(destChainConfig, destChainSelector, message.feeToken, feeTokenPrice, message.tokenAmounts); + } else { + // Convert USD cents with 2 decimals to 18 decimals. + premiumFee = uint256(destChainConfig.networkFeeUSDCents) * 1e16; + } + + // Calculate data availability cost in USD with 36 decimals. Data availability cost exists on rollups that need to post + // transaction calldata onto another storage layer, e.g. Eth mainnet, incurring additional storage gas costs. + uint256 dataAvailabilityCost = 0; + + // Only calculate data availability cost if data availability multiplier is non-zero. + // The multiplier should be set to 0 if destination chain does not charge data availability cost. + if (destChainConfig.destDataAvailabilityMultiplierBps > 0) { + dataAvailabilityCost = _getDataAvailabilityCost( + destChainConfig, + // Parse the data availability gas price stored in the higher-order 112 bits of the encoded gas price. + uint112(packedGasPrice >> Internal.GAS_PRICE_BITS), + message.data.length, + numberOfTokens, + tokenTransferBytesOverhead + ); + } + + // Calculate execution gas fee on destination chain in USD with 36 decimals. + // We add the message gas limit, the overhead gas, the gas of passing message data to receiver, and token transfer gas together. + // We then multiply this gas total with the gas multiplier and gas price, converting it into USD with 36 decimals. + // uint112(packedGasPrice) = executionGasPrice + + // NOTE: when supporting non-EVM chains, revisit how generic this fee logic can be + // NOTE: revisit parsing non-EVM args + + uint256 executionCost = uint112(packedGasPrice) + * ( + destChainConfig.destGasOverhead + (message.data.length * destChainConfig.destGasPerPayloadByte) + tokenTransferGas + + _parseEVMExtraArgsFromBytes(message.extraArgs, destChainConfig).gasLimit + ) * destChainConfig.gasMultiplierWeiPerEth; + + // Calculate number of fee tokens to charge. + // Total USD fee is in 36 decimals, feeTokenPrice is in 18 decimals USD for 1e18 smallest token denominations. + // Result of the division is the number of smallest token denominations. + return ((premiumFee * premiumMultiplierWeiPerEth) + executionCost + dataAvailabilityCost) / feeTokenPrice; + } + + /// @notice Sets the fee configuration for a token + /// @param premiumMultiplierWeiPerEthArgs Array of PremiumMultiplierWeiPerEthArgs structs. + function applyPremiumMultiplierWeiPerEthUpdates( + PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs + ) external onlyOwner { + _applyPremiumMultiplierWeiPerEthUpdates(premiumMultiplierWeiPerEthArgs); + } + + /// @dev Set the fee config. + /// @param premiumMultiplierWeiPerEthArgs The multiplier for destination chain specific premiums. + function _applyPremiumMultiplierWeiPerEthUpdates( + PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs + ) internal { + for (uint256 i = 0; i < premiumMultiplierWeiPerEthArgs.length; ++i) { + address token = premiumMultiplierWeiPerEthArgs[i].token; + uint64 premiumMultiplierWeiPerEth = premiumMultiplierWeiPerEthArgs[i].premiumMultiplierWeiPerEth; + s_premiumMultiplierWeiPerEth[token] = premiumMultiplierWeiPerEth; + + emit PremiumMultiplierWeiPerEthUpdated(token, premiumMultiplierWeiPerEth); + } + } + + /// @notice Gets the fee configuration for a token. + /// @param token The token to get the fee configuration for. + /// @return premiumMultiplierWeiPerEth The multiplier for destination chain specific premiums. + function getPremiumMultiplierWeiPerEth(address token) external view returns (uint64 premiumMultiplierWeiPerEth) { + return s_premiumMultiplierWeiPerEth[token]; + } + + /// @notice Returns the token transfer cost parameters. + /// A basis point fee is calculated from the USD value of each token transfer. + /// For each individual transfer, this fee is between [minFeeUSD, maxFeeUSD]. + /// Total transfer fee is the sum of each individual token transfer fee. + /// @dev Assumes that tokenAmounts are validated to be listed tokens elsewhere. + /// @dev Splitting one token transfer into multiple transfers is discouraged, + /// as it will result in a transferFee equal or greater than the same amount aggregated/de-duped. + /// @param destChainConfig the config configured for the destination chain selector. + /// @param destChainSelector the destination chain selector. + /// @param feeToken address of the feeToken. + /// @param feeTokenPrice price of feeToken in USD with 18 decimals. + /// @param tokenAmounts token transfers in the message. + /// @return tokenTransferFeeUSDWei total token transfer bps fee in USD with 18 decimals. + /// @return tokenTransferGas total execution gas of the token transfers. + /// @return tokenTransferBytesOverhead additional token transfer data passed to destination, e.g. USDC attestation. + function _getTokenTransferCost( + DestChainConfig memory destChainConfig, + uint64 destChainSelector, + address feeToken, + uint224 feeTokenPrice, + Client.EVMTokenAmount[] calldata tokenAmounts + ) internal view returns (uint256 tokenTransferFeeUSDWei, uint32 tokenTransferGas, uint32 tokenTransferBytesOverhead) { + uint256 numberOfTokens = tokenAmounts.length; + + for (uint256 i = 0; i < numberOfTokens; ++i) { + Client.EVMTokenAmount memory tokenAmount = tokenAmounts[i]; + TokenTransferFeeConfig memory transferFeeConfig = s_tokenTransferFeeConfig[destChainSelector][tokenAmount.token]; + + // If the token has no specific overrides configured, we use the global defaults. + if (!transferFeeConfig.isEnabled) { + tokenTransferFeeUSDWei += uint256(destChainConfig.defaultTokenFeeUSDCents) * 1e16; + tokenTransferGas += destChainConfig.defaultTokenDestGasOverhead; + tokenTransferBytesOverhead += destChainConfig.defaultTokenDestBytesOverhead; + continue; + } + + uint256 bpsFeeUSDWei = 0; + // Only calculate bps fee if ratio is greater than 0. Ratio of 0 means no bps fee for a token. + // Useful for when the PriceRegistry cannot return a valid price for the token. + if (transferFeeConfig.deciBps > 0) { + uint224 tokenPrice = 0; + if (tokenAmount.token != feeToken) { + tokenPrice = _getValidatedTokenPrice(tokenAmount.token); + } else { + tokenPrice = feeTokenPrice; + } + + // Calculate token transfer value, then apply fee ratio + // ratio represents multiples of 0.1bps, or 1e-5 + bpsFeeUSDWei = (tokenPrice._calcUSDValueFromTokenAmount(tokenAmount.amount) * transferFeeConfig.deciBps) / 1e5; + } + + tokenTransferGas += transferFeeConfig.destGasOverhead; + tokenTransferBytesOverhead += transferFeeConfig.destBytesOverhead; + + // Bps fees should be kept within range of [minFeeUSD, maxFeeUSD]. + // Convert USD values with 2 decimals to 18 decimals. + uint256 minFeeUSDWei = uint256(transferFeeConfig.minFeeUSDCents) * 1e16; + if (bpsFeeUSDWei < minFeeUSDWei) { + tokenTransferFeeUSDWei += minFeeUSDWei; + continue; + } + + uint256 maxFeeUSDWei = uint256(transferFeeConfig.maxFeeUSDCents) * 1e16; + if (bpsFeeUSDWei > maxFeeUSDWei) { + tokenTransferFeeUSDWei += maxFeeUSDWei; + continue; + } + + tokenTransferFeeUSDWei += bpsFeeUSDWei; + } + + return (tokenTransferFeeUSDWei, tokenTransferGas, tokenTransferBytesOverhead); + } + + /// @notice Returns the estimated data availability cost of the message. + /// @dev To save on gas, we use a single destGasPerDataAvailabilityByte value for both zero and non-zero bytes. + /// @param destChainConfig the config configured for the destination chain selector. + /// @param dataAvailabilityGasPrice USD per data availability gas in 18 decimals. + /// @param messageDataLength length of the data field in the message. + /// @param numberOfTokens number of distinct token transfers in the message. + /// @param tokenTransferBytesOverhead additional token transfer data passed to destination, e.g. USDC attestation. + /// @return dataAvailabilityCostUSD36Decimal total data availability cost in USD with 36 decimals. + function _getDataAvailabilityCost( + DestChainConfig memory destChainConfig, + uint112 dataAvailabilityGasPrice, + uint256 messageDataLength, + uint256 numberOfTokens, + uint32 tokenTransferBytesOverhead + ) internal pure returns (uint256 dataAvailabilityCostUSD36Decimal) { + // dataAvailabilityLengthBytes sums up byte lengths of fixed message fields and dynamic message fields. + // Fixed message fields do account for the offset and length slot of the dynamic fields. + uint256 dataAvailabilityLengthBytes = Internal.ANY_2_EVM_MESSAGE_FIXED_BYTES + messageDataLength + + (numberOfTokens * Internal.ANY_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN) + tokenTransferBytesOverhead; + + // destDataAvailabilityOverheadGas is a separate config value for flexibility to be updated independently of message cost. + // Its value is determined by CCIP lane implementation, e.g. the overhead data posted for OCR. + uint256 dataAvailabilityGas = (dataAvailabilityLengthBytes * destChainConfig.destGasPerDataAvailabilityByte) + + destChainConfig.destDataAvailabilityOverheadGas; + + // dataAvailabilityGasPrice is in 18 decimals, destDataAvailabilityMultiplierBps is in 4 decimals + // We pad 14 decimals to bring the result to 36 decimals, in line with token bps and execution fee. + return ((dataAvailabilityGas * dataAvailabilityGasPrice) * destChainConfig.destDataAvailabilityMultiplierBps) * 1e14; + } + + /// @notice Gets the transfer fee config for a given token. + /// @param destChainSelector The destination chain selector. + /// @param token The token address. + function getTokenTransferFeeConfig( + uint64 destChainSelector, + address token + ) external view returns (TokenTransferFeeConfig memory tokenTransferFeeConfig) { + return s_tokenTransferFeeConfig[destChainSelector][token]; + } + + /// @notice Sets the transfer fee config. + /// @dev only callable by the owner or admin. + function applyTokenTransferFeeConfigUpdates( + TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs, + TokenTransferFeeConfigRemoveArgs[] memory tokensToUseDefaultFeeConfigs + ) external onlyOwner { + _applyTokenTransferFeeConfigUpdates(tokenTransferFeeConfigArgs, tokensToUseDefaultFeeConfigs); + } + + /// @notice internal helper to set the token transfer fee config. + function _applyTokenTransferFeeConfigUpdates( + TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs, + TokenTransferFeeConfigRemoveArgs[] memory tokensToUseDefaultFeeConfigs + ) internal { + for (uint256 i = 0; i < tokenTransferFeeConfigArgs.length; ++i) { + TokenTransferFeeConfigArgs memory tokenTransferFeeConfigArg = tokenTransferFeeConfigArgs[i]; + uint64 destChainSelector = tokenTransferFeeConfigArg.destChainSelector; + + for (uint256 j = 0; j < tokenTransferFeeConfigArg.tokenTransferFeeConfigs.length; ++j) { + TokenTransferFeeConfig memory tokenTransferFeeConfig = + tokenTransferFeeConfigArg.tokenTransferFeeConfigs[j].tokenTransferFeeConfig; + address token = tokenTransferFeeConfigArg.tokenTransferFeeConfigs[j].token; + + if (tokenTransferFeeConfig.destBytesOverhead < Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) { + revert InvalidDestBytesOverhead(token, tokenTransferFeeConfig.destBytesOverhead); + } + + s_tokenTransferFeeConfig[destChainSelector][token] = tokenTransferFeeConfig; + + emit TokenTransferFeeConfigUpdated(destChainSelector, token, tokenTransferFeeConfig); + } + } + + // Remove the custom fee configs for the tokens that are in the tokensToUseDefaultFeeConfigs array + for (uint256 i = 0; i < tokensToUseDefaultFeeConfigs.length; ++i) { + uint64 destChainSelector = tokensToUseDefaultFeeConfigs[i].destChainSelector; + address token = tokensToUseDefaultFeeConfigs[i].token; + delete s_tokenTransferFeeConfig[destChainSelector][token]; + emit TokenTransferFeeConfigDeleted(destChainSelector, token); + } + } + + // ================================================================ + // │ Validations & message processing │ + // ================================================================ + + /// @notice Validates that the destAddress matches the expected format of the family. + /// @param chainFamilySelector Tag to identify the target family + /// @param destAddress Dest address to validate + /// @dev precondition - assumes the family tag is correct and validated + function _validateDestFamilyAddress(bytes4 chainFamilySelector, bytes memory destAddress) internal pure { + if (chainFamilySelector == Internal.CHAIN_FAMILY_SELECTOR_EVM) { + Internal._validateEVMAddress(destAddress); + } + } + + /// @dev Convert the extra args bytes into a struct with validations against the dest chain config + /// @param extraArgs The extra args bytes + /// @param destChainConfig Dest chain config to validate against + /// @return EVMExtraArgs the extra args struct (latest version) + function _parseEVMExtraArgsFromBytes( + bytes calldata extraArgs, + DestChainConfig memory destChainConfig + ) internal pure returns (Client.EVMExtraArgsV2 memory) { + Client.EVMExtraArgsV2 memory evmExtraArgs = + _parseUnvalidatedEVMExtraArgsFromBytes(extraArgs, destChainConfig.defaultTxGasLimit); + + if (evmExtraArgs.gasLimit > uint256(destChainConfig.maxPerMsgGasLimit)) revert MessageGasLimitTooHigh(); + if (destChainConfig.enforceOutOfOrder && !evmExtraArgs.allowOutOfOrderExecution) { + revert ExtraArgOutOfOrderExecutionMustBeTrue(); + } + + return evmExtraArgs; + } + + /// @dev Convert the extra args bytes into a struct + /// @param extraArgs The extra args bytes + /// @param defaultTxGasLimit default tx gas limit to use in the absence of extra args + /// @return EVMExtraArgs the extra args struct (latest version) + function _parseUnvalidatedEVMExtraArgsFromBytes( + bytes calldata extraArgs, + uint64 defaultTxGasLimit + ) private pure returns (Client.EVMExtraArgsV2 memory) { + if (extraArgs.length == 0) { + // If extra args are empty, generate default values + return Client.EVMExtraArgsV2({gasLimit: defaultTxGasLimit, allowOutOfOrderExecution: false}); + } + + bytes4 extraArgsTag = bytes4(extraArgs); + bytes memory argsData = extraArgs[4:]; + + if (extraArgsTag == Client.EVM_EXTRA_ARGS_V2_TAG) { + return abi.decode(argsData, (Client.EVMExtraArgsV2)); + } else if (extraArgsTag == Client.EVM_EXTRA_ARGS_V1_TAG) { + // EVMExtraArgsV1 originally included a second boolean (strict) field which has been deprecated. + // Clients may still include it but it will be ignored. + return Client.EVMExtraArgsV2({gasLimit: abi.decode(argsData, (uint256)), allowOutOfOrderExecution: false}); + } + + revert InvalidExtraArgsTag(); + } + + /// @notice Validate the forwarded message to ensure it matches the configuration limits (message length, number of tokens) + /// and family-specific expectations (address format) + /// @param destChainConfig Dest chain config + /// @param dataLength The length of the data field of the message. + /// @param numberOfTokens The number of tokens to be sent. + /// @param receiver Message receiver on the dest chain + function _validateMessage( + DestChainConfig memory destChainConfig, + uint256 dataLength, + uint256 numberOfTokens, + bytes memory receiver + ) internal pure { + // Check that payload is formed correctly + if (dataLength > uint256(destChainConfig.maxDataBytes)) { + revert MessageTooLarge(uint256(destChainConfig.maxDataBytes), dataLength); + } + if (numberOfTokens > uint256(destChainConfig.maxNumberOfTokensPerMsg)) revert UnsupportedNumberOfTokens(); + _validateDestFamilyAddress(destChainConfig.chainFamilySelector, receiver); + } + + /// @inheritdoc IPriceRegistry + function processMessageArgs( + uint64 destChainSelector, + address feeToken, + uint256 feeTokenAmount, + bytes calldata extraArgs + ) external view returns (uint256 msgFeeJuels, bool isOutOfOrderExecution, bytes memory convertedExtraArgs) { + // Convert feeToken to link if not already in link + if (feeToken == i_linkToken) { + msgFeeJuels = feeTokenAmount; + } else { + msgFeeJuels = convertTokenAmount(feeToken, feeTokenAmount, i_linkToken); + } + + if (msgFeeJuels > i_maxFeeJuelsPerMsg) revert MessageFeeTooHigh(msgFeeJuels, i_maxFeeJuelsPerMsg); + + uint64 defaultTxGasLimit = s_destChainConfigs[destChainSelector].defaultTxGasLimit; + // NOTE: when supporting non-EVM chains, revisit this and parse non-EVM args. + // We can parse unvalidated args since this message is called after getFee (which will already validate the params) + Client.EVMExtraArgsV2 memory parsedExtraArgs = _parseUnvalidatedEVMExtraArgsFromBytes(extraArgs, defaultTxGasLimit); + isOutOfOrderExecution = parsedExtraArgs.allowOutOfOrderExecution; + + return (msgFeeJuels, isOutOfOrderExecution, Client._argsToBytes(parsedExtraArgs)); + } + + /// @inheritdoc IPriceRegistry + /// @dev precondition - rampTokenAmounts and sourceTokenAmounts lengths must be equal + function validatePoolReturnData( + uint64 destChainSelector, + Internal.RampTokenAmount[] calldata rampTokenAmounts, + Client.EVMTokenAmount[] calldata sourceTokenAmounts + ) external view { + bytes4 chainFamilySelector = s_destChainConfigs[destChainSelector].chainFamilySelector; + + for (uint256 i = 0; i < rampTokenAmounts.length; ++i) { + address sourceToken = sourceTokenAmounts[i].token; + + // Since the DON has to pay for the extraData to be included on the destination chain, we cap the length of the + // extraData. This prevents gas bomb attacks on the NOPs. As destBytesOverhead accounts for both + // extraData and offchainData, this caps the worst case abuse to the number of bytes reserved for offchainData. + uint256 destPoolDataLength = rampTokenAmounts[i].extraData.length; + if (destPoolDataLength > Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) { + if (destPoolDataLength > s_tokenTransferFeeConfig[destChainSelector][sourceToken].destBytesOverhead) { + revert SourceTokenDataTooLarge(sourceToken); + } + } + + _validateDestFamilyAddress(chainFamilySelector, rampTokenAmounts[i].destTokenAddress); + } + } + + // ================================================================ + // │ Configs │ + // ================================================================ + + /// @notice Returns the configured config for the dest chain selector + /// @param destChainSelector destination chain selector to fetch config for + /// @return destChainConfig config for the dest chain + function getDestChainConfig(uint64 destChainSelector) external view returns (DestChainConfig memory) { + return s_destChainConfigs[destChainSelector]; + } + + /// @notice Updates the destination chain specific config. + /// @param destChainConfigArgs Array of source chain specific configs. + function applyDestChainConfigUpdates(DestChainConfigArgs[] memory destChainConfigArgs) external onlyOwner { + _applyDestChainConfigUpdates(destChainConfigArgs); + } + + /// @notice Internal version of applyDestChainConfigUpdates. + function _applyDestChainConfigUpdates(DestChainConfigArgs[] memory destChainConfigArgs) internal { + for (uint256 i = 0; i < destChainConfigArgs.length; ++i) { + DestChainConfigArgs memory destChainConfigArg = destChainConfigArgs[i]; + uint64 destChainSelector = destChainConfigArgs[i].destChainSelector; + DestChainConfig memory destChainConfig = destChainConfigArg.destChainConfig; + + // NOTE: when supporting non-EVM chains, update chainFamilySelector validations + if ( + destChainSelector == 0 || destChainConfig.defaultTxGasLimit == 0 + || destChainConfig.chainFamilySelector != Internal.CHAIN_FAMILY_SELECTOR_EVM + || destChainConfig.defaultTokenDestBytesOverhead < Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + || destChainConfig.defaultTxGasLimit > destChainConfig.maxPerMsgGasLimit + ) { + revert InvalidDestChainConfig(destChainSelector); + } + + // The chain family selector cannot be zero - indicates that it is a new chain + if (s_destChainConfigs[destChainSelector].chainFamilySelector == 0) { + emit DestChainAdded(destChainSelector, destChainConfig); + } else { + emit DestChainConfigUpdated(destChainSelector, destChainConfig); + } + + s_destChainConfigs[destChainSelector] = destChainConfig; + } + } + + /// @notice Returns the static PriceRegistry config. + /// @dev RMN depends on this function, if changing, please notify the RMN maintainers. + /// @return the configuration. + function getStaticConfig() external view returns (StaticConfig memory) { + return StaticConfig({ + maxFeeJuelsPerMsg: i_maxFeeJuelsPerMsg, + linkToken: i_linkToken, + stalenessThreshold: i_stalenessThreshold + }); + } +} diff --git a/contracts/src/v0.8/ccip/RMN.sol b/contracts/src/v0.8/ccip/RMN.sol new file mode 100644 index 00000000000..424aad8fa57 --- /dev/null +++ b/contracts/src/v0.8/ccip/RMN.sol @@ -0,0 +1,964 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; +import {IRMN} from "./interfaces/IRMN.sol"; + +import {OwnerIsCreator} from "./../shared/access/OwnerIsCreator.sol"; + +import {EnumerableSet} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +// An active curse on this subject will cause isCursed() to return true. Use this subject if there is an issue with a +// remote chain, for which there exists a legacy lane contract deployed on the same chain as this RMN contract is +// deployed, relying on isCursed(). +bytes16 constant LEGACY_CURSE_SUBJECT = 0x01000000000000000000000000000000; + +// An active curse on this subject will cause isCursed() and isCursed(bytes32) to return true. Use this subject for +// issues affecting all of CCIP chains, or pertaining to the chain that this contract is deployed on, instead of using +// the local chain selector as a subject. +bytes16 constant GLOBAL_CURSE_SUBJECT = 0x01000000000000000000000000000001; + +// The curse vote address representing the owner in data structures, events and recorded votes. Remains constant, even +// if the owner changes. +address constant OWNER_CURSE_VOTE_ADDR = address(~uint160(0)); // 0xff...ff + +// The curse vote address used in an OwnerUnvoteToCurseRequest to lift a curse, if there is no active curse votes for +// the subject that we are able to unvote, but the conditions for an active curse no longer hold. +address constant LIFT_CURSE_VOTE_ADDR = address(0); + +/// @dev This contract is owned by RMN, if changing, please notify the RMN maintainers. +// solhint-disable chainlink-solidity/explicit-returns +contract RMN is IRMN, OwnerIsCreator, ITypeAndVersion { + using EnumerableSet for EnumerableSet.AddressSet; + + // STATIC CONFIG + string public constant override typeAndVersion = "RMN 1.5.0-dev"; + + uint256 private constant MAX_NUM_VOTERS = 16; + + // MAGIC VALUES + bytes28 private constant NO_VOTES_CURSES_HASH = bytes28(0); + + // DYNAMIC CONFIG + /// @notice blessVoteAddr and curseVoteAddr can't be 0. Additionally curseVoteAddr can't be LIFT_CURSE_VOTE_ADDR or + /// OWNER_CURSE_VOTE_ADDR. At least one of blessWeight & curseWeight must be non-zero, i.e., a voter could only vote + /// to bless, or only vote to curse, or both vote to bless and vote to curse. + struct Voter { + // This is the address the voter should use to call voteToBless. + address blessVoteAddr; + // This is the address the voter should use to call voteToCurse. + address curseVoteAddr; + // The weight of this voter's vote for blessing. + uint8 blessWeight; + // The weight of this voter's vote for cursing. + uint8 curseWeight; + } + + struct Config { + Voter[] voters; + // When the total weight of voters that have voted to bless a tagged root reaches + // or exceeds blessWeightThreshold, the tagged root becomes blessed. + uint16 blessWeightThreshold; + // When the total weight of voters that have voted to curse a subject reaches or + // exceeds curseWeightThreshold, the subject becomes cursed. + uint16 curseWeightThreshold; + } + + struct VersionedConfig { + Config config; + // The version is incremented every time the config changes. + // The initial configuration on the contract will have configVersion == 1. + uint32 configVersion; + // The block number at which the config was last set. Helps the offchain + // code check that the config was set in a stable block or double-check + // that it has the correct config by querying logs at that block number. + uint32 blockNumber; + } + + VersionedConfig private s_versionedConfig; + + // STATE + struct BlesserRecord { + // The config version at which this BlesserRecord was last set. A blesser + // is considered active iff this configVersion equals + // s_versionedConfig.configVersion. + uint32 configVersion; + uint8 weight; + uint8 index; + } + + mapping(address blessVoteAddr => BlesserRecord blesserRecord) private s_blesserRecords; + + struct BlessVoteProgress { + // This particular ordering saves us ~400 gas per voteToBless call, compared to the bool being at the bottom, even + // though the size of the struct is the same. + bool weightThresholdMet; + // A BlessVoteProgress is considered invalid if weightThresholdMet is false when + // s_versionedConfig.configVersion changes. we don't want old in-progress + // votes to continue when we set a new config! + // The config version at which the bless vote for a tagged root was initiated. + uint32 configVersion; + uint16 accumulatedWeight; + // Care must be taken that the bitmap has at least as many bits as MAX_NUM_VOTERS. + // uint200 is much larger than we need, but it saves us ~100 gas per voteToBless call to fill the word instead of + // using a smaller type. + // _bitmapGet(voterBitmap, i) = true indicates that the i-th voter has voted to bless + uint200 voterBitmap; + } + + mapping(bytes32 taggedRootHash => BlessVoteProgress blessVoteProgress) private s_blessVoteProgressByTaggedRootHash; + + // Any tagged root with a commit store included in s_permaBlessedCommitStores will be considered automatically + // blessed. + EnumerableSet.AddressSet private s_permaBlessedCommitStores; + + struct CurserRecord { + bool active; + uint8 weight; + mapping(bytes16 curseId => bool used) usedCurseIds; // retained across config changes + } + + mapping(address curseVoteAddr => CurserRecord curserRecord) private s_curserRecords; + + struct ConfigVersionAndCursesHash { + uint32 configVersion; // configVersion != s_versionedConfig.configVersion means no active vote + bytes28 cursesHash; // bytes28(0) means no active vote; truncated so that ConfigVersionAndCursesHash fits in a word + } + + struct CurseVoteProgress { + uint32 configVersion; // upon config change, lazy set to new config version + uint16 curseWeightThreshold; // upon config change, lazy set to new config value + uint16 accumulatedWeight; // upon config change, lazy set to 0 + // A curse becomes active after either: + // - sum([voter.weight for voter who voted in current config]) >= curseWeightThreshold + // - ownerCurse is invoked + // Once a curse is active, only the owner can lift it. + bool curseActive; // retained across config changes + mapping(address => ConfigVersionAndCursesHash) latestVoteToCurseByCurseVoteAddr; // retained across config changes + } + + mapping(bytes16 subject => CurseVoteProgress curseVoteProgress) private + s_potentiallyOutdatedCurseVoteProgressBySubject; + + // We intentionally use a struct here, even though it contains a single field, to make it obvious to future editors + // that there is space for more fields. + struct CurseHotVars { + uint64 numSubjectsCursed; // incremented by voteToCurse, ownerCurse; decremented by ownerUnvoteToCurse + } + + CurseHotVars private s_curseHotVars; + + enum RecordedCurseRelatedOpTag { + // A vote to curse, through either voteToCurse or ownerCurse. + VoteToCurse, + // An unvote to curse, through unvoteToCurse. + UnvoteToCurse, + // An unvote to curse, through ownerUnvoteToCurse, which was not forced (forceUnvote=false). + OwnerUnvoteToCurseUnforced, + // An unvote to curse, through ownerUnvoteToCurse, which was forced (forceUnvote=true). + OwnerUnvoteToCurseForced, + // A configuration change. + // + // For subjects that are not cursed when this happens, past votes do not get accounted for in the new configuration. + // If a voter votes during the new configuration, their curses hash will restart from NO_VOTES_CURSES_HASH. + // + // For subjects that are cursed when this happens, past votes get accounted for. + // If a voter votes during the new configuration, their curses hash will continue from its old value. + SetConfig + } + + /// @notice Provides the ability to quickly reconstruct the curse-related state of the contract offchain, without + /// having to replay all past events. Replaying past events often takes long, and in some cases might even be + /// infeasible due to log pruning. + /// + /// @dev We could save some gas by omitting some fields and instead using them as mapping keys, but we would lose the + /// cross-voter ordering, or cross-subject ordering, or cross-vote/unvote ordering. + struct RecordedCurseRelatedOp { + RecordedCurseRelatedOpTag tag; + uint64 blockTimestamp; + bool cursed; // whether the subject is cursed after this op; if tag in {SetConfig}, will be false + address curseVoteAddr; // if tag in {SetConfig}, will be address(0) + bytes16 subject; // if tag in {SetConfig}, will be bytes16(0) + bytes16 curseId; // if tag in {SetConfig, UnvoteToCurse, OwnerUnvoteToCurseUnforced, OwnerUnvoteToCurseForced}, will be bytes16(0) + } + + RecordedCurseRelatedOp[] private s_recordedCurseRelatedOps; + + /// @dev This function is to _ONLY_ be called in order to determine if a curse should become active upon a + /// vote-to-curse, or a curse should be deactivated upon an owner-unvote-to-curse. + /// Other reasons for a curse to be active, which are not covered here: + /// 1. Cursedness is retained from a prior config. + /// 2. The curse weight threshold was met at some point, which activated a curse, and enough voters unvoted to curse + /// such that the curse weight threshold is no longer met. + function _shouldCurseBeActive(CurseVoteProgress storage sptr_upToDateCurseVoteProgress) internal view returns (bool) { + return sptr_upToDateCurseVoteProgress.latestVoteToCurseByCurseVoteAddr[OWNER_CURSE_VOTE_ADDR].cursesHash + != NO_VOTES_CURSES_HASH + || sptr_upToDateCurseVoteProgress.accumulatedWeight >= sptr_upToDateCurseVoteProgress.curseWeightThreshold; + } + + /// @dev It might be the case that due to the lazy update of curseVoteProgress, a curse is active even though + /// _shouldCurseBeActive(curseVoteProgress) is false, i.e., the owner has no active vote to curse and the curse + /// weight threshold has not been met. + function _getUpToDateCurseVoteProgress( + uint32 configVersion, + bytes16 subject + ) internal returns (CurseVoteProgress storage) { + CurseVoteProgress storage sptr_curseVoteProgress = s_potentiallyOutdatedCurseVoteProgressBySubject[subject]; + if (configVersion != sptr_curseVoteProgress.configVersion) { + sptr_curseVoteProgress.configVersion = configVersion; + sptr_curseVoteProgress.curseWeightThreshold = s_versionedConfig.config.curseWeightThreshold; + sptr_curseVoteProgress.accumulatedWeight = 0; + + if (sptr_curseVoteProgress.curseActive) { + // If a curse was active, count past votes to curse and retain the curses hash for cursers who are part of the + // new config. + Config storage sptr_config = s_versionedConfig.config; + for (uint256 i = 0; i < sptr_config.voters.length; ++i) { + Voter storage sptr_voter = sptr_config.voters[i]; + ConfigVersionAndCursesHash storage sptr_cvch = + sptr_curseVoteProgress.latestVoteToCurseByCurseVoteAddr[sptr_voter.curseVoteAddr]; + if (sptr_cvch.configVersion < configVersion && sptr_cvch.cursesHash != NO_VOTES_CURSES_HASH) { + // `< configVersion` instead of `== configVersion-1`, because there might have been multiple config changes + // without a lazy update of our subject. This has the side effect of retaining votes from very old configs + // that we might not really intend to retain, but these can be removed by the owner later. + sptr_cvch.configVersion = configVersion; + sptr_curseVoteProgress.accumulatedWeight += sptr_voter.curseWeight; + } + } + // We don't need to think about OWNER_CURSE_VOTE_ADDR here, because its ConfigVersionAndCursesHash counts even + // if the configVersion is not the current config version, in contrast to regular voters. + // It's an irregularity, but it saves us > 5k gas (if the owner had previously voted) for the unlucky voter who + // enters this branch. + } else { + // If a curse was not active, we don't count past votes to curse for voters who are part of the new config. + // Their curses hash will be restart from NO_VOTES_CURSES_HASH when they vote to curse again. + // We expect that the offchain code will revote to curse in case it voted to curse, and the vote to curse was + // lost due to any reason, including a config change when the curse was not yet active. + } + } + return sptr_curseVoteProgress; + } + + // EVENTS, ERRORS + + event ConfigSet(uint32 indexed configVersion, Config config); + + error InvalidConfig(); + + event TaggedRootBlessed(uint32 indexed configVersion, IRMN.TaggedRoot taggedRoot, uint16 accumulatedWeight); + event TaggedRootBlessVotesReset(uint32 indexed configVersion, IRMN.TaggedRoot taggedRoot, bool wasBlessed); + event VotedToBless(uint32 indexed configVersion, address indexed voter, IRMN.TaggedRoot taggedRoot, uint8 weight); + + event VotedToCurse( + uint32 indexed configVersion, + address indexed voter, + bytes16 subject, + bytes16 curseId, + uint8 weight, + uint64 blockTimestamp, + bytes28 cursesHash, + uint16 accumulatedWeight + ); + event UnvotedToCurse( + uint32 indexed configVersion, + address indexed voter, + bytes16 subject, + uint8 weight, + bytes28 cursesHash, + uint16 remainingAccumulatedWeight + ); + event SkippedUnvoteToCurse(address indexed voter, bytes16 subject, bytes28 onchainCursesHash, bytes28 cursesHash); + event Cursed(uint32 indexed configVersion, bytes16 subject, uint64 blockTimestamp); + event CurseLifted(bytes16 subject); + + // These events make it easier for offchain logic to discover that it performs + // the same actions multiple times. + event AlreadyVotedToBless(uint32 indexed configVersion, address indexed voter, IRMN.TaggedRoot taggedRoot); + event AlreadyBlessed(uint32 indexed configVersion, address indexed voter, IRMN.TaggedRoot taggedRoot); + + // Emitted by ownerRemoveThenAddPermaBlessedCommitStores. + event PermaBlessedCommitStoreAdded(address commitStore); + event PermaBlessedCommitStoreRemoved(address commitStore); + + error ReusedCurseId(address voter, bytes16 curseId); + error UnauthorizedVoter(address voter); + error VoteToBlessNoop(); + error VoteToCurseNoop(); + error UnvoteToCurseNoop(); + error VoteToBlessForbiddenDuringActiveGlobalCurse(); + + /// @notice Thrown when subjects are not a strictly increasing monotone sequence. + // Prevents a subject from receiving multiple votes to curse with the same curse id. + error SubjectsMustBeStrictlyIncreasing(); + + constructor(Config memory config) { + { + // Ensure that the bitmap is large enough to hold MAX_NUM_VOTERS. + // We do this in the constructor because MAX_NUM_VOTERS is constant. + BlessVoteProgress memory vp = BlessVoteProgress({ + configVersion: 0, + voterBitmap: type(uint200).max, // will not compile if it doesn't fit + accumulatedWeight: 0, + weightThresholdMet: false + }); + assert(vp.voterBitmap >> (MAX_NUM_VOTERS - 1) >= 1); + } + _setConfig(config); + } + + function _bitmapGet(uint200 bitmap, uint8 index) internal pure returns (bool) { + assert(index < MAX_NUM_VOTERS); + return bitmap & (uint200(1) << index) != 0; + } + + function _bitmapSet(uint200 bitmap, uint8 index) internal pure returns (uint200) { + assert(index < MAX_NUM_VOTERS); + return bitmap | (uint200(1) << index); + } + + function _bitmapCount(uint200 bitmap) internal pure returns (uint8 oneBits) { + assert(bitmap < 1 << MAX_NUM_VOTERS); + // https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan + for (; bitmap != 0; ++oneBits) { + bitmap &= bitmap - 1; + } + } + + function _taggedRootHash(IRMN.TaggedRoot memory taggedRoot) internal pure returns (bytes32) { + return keccak256(abi.encode(taggedRoot.commitStore, taggedRoot.root)); + } + + function _cursesHash(bytes28 prevCursesHash, bytes16 curseId) internal pure returns (bytes28) { + return bytes28(keccak256(abi.encode(prevCursesHash, curseId))); + } + + function _blockTimestamp() internal view returns (uint64) { + return uint64(block.timestamp); + } + + /// @param taggedRoots A tagged root is hashed as `keccak256(abi.encode(taggedRoot.commitStore + /// /* address */, taggedRoot.root /* bytes32 */))`. + /// @notice Tagged roots which are already (voted to be) blessed are skipped and emit corresponding events. In case + /// the call has no effect, i.e., all passed tagged roots are skipped, the function reverts with a `VoteToBlessNoop`. + function voteToBless(IRMN.TaggedRoot[] calldata taggedRoots) external { + // If we have an active global curse, something is really wrong. Let's err on the + // side of caution and not accept further blessings during this time of + // uncertainty. + if (isCursed(GLOBAL_CURSE_SUBJECT)) revert VoteToBlessForbiddenDuringActiveGlobalCurse(); + + uint32 configVersion = s_versionedConfig.configVersion; + BlesserRecord memory blesserRecord = s_blesserRecords[msg.sender]; + if (blesserRecord.configVersion != configVersion) revert UnauthorizedVoter(msg.sender); + + bool noop = true; + for (uint256 i = 0; i < taggedRoots.length; ++i) { + IRMN.TaggedRoot memory taggedRoot = taggedRoots[i]; + bytes32 taggedRootHash = _taggedRootHash(taggedRoot); + BlessVoteProgress memory voteProgress = s_blessVoteProgressByTaggedRootHash[taggedRootHash]; + if (voteProgress.weightThresholdMet) { + // We don't revert here because it's unreasonable to expect from the + // voter to know exactly when to stop voting. Most likely when they + // voted they didn't realize the threshold would be reached by the time + // their vote was counted. + // Additionally, there might be other tagged roots for which votes might + // count, and we want to allow that to happen. + emit AlreadyBlessed(configVersion, msg.sender, taggedRoot); + continue; + } else if (voteProgress.configVersion != configVersion) { + // Note that voteProgress.weightThresholdMet must be false at this point + + // If votes were received while an older config was in effect, + // invalidate them and start from scratch. + // If votes were never received, set the current config version. + voteProgress = BlessVoteProgress({ + configVersion: configVersion, + voterBitmap: 0, + accumulatedWeight: 0, + weightThresholdMet: false + }); + } else if (_bitmapGet(voteProgress.voterBitmap, blesserRecord.index)) { + // We don't revert here because there might be other tagged roots for + // which votes might count, and we want to allow that to happen. + emit AlreadyVotedToBless(configVersion, msg.sender, taggedRoot); + continue; + } + noop = false; + voteProgress.voterBitmap = _bitmapSet(voteProgress.voterBitmap, blesserRecord.index); + voteProgress.accumulatedWeight += blesserRecord.weight; + emit VotedToBless(configVersion, msg.sender, taggedRoot, blesserRecord.weight); + if (voteProgress.accumulatedWeight >= s_versionedConfig.config.blessWeightThreshold) { + voteProgress.weightThresholdMet = true; + emit TaggedRootBlessed(configVersion, taggedRoot, voteProgress.accumulatedWeight); + } + s_blessVoteProgressByTaggedRootHash[taggedRootHash] = voteProgress; + } + + if (noop) { + revert VoteToBlessNoop(); + } + } + + /// @notice Can be called by the owner to remove unintentionally voted or even blessed tagged roots in a recovery + /// scenario. The owner must ensure that there are no in-flight transactions by RMN nodes voting for any of the + /// taggedRoots before calling this function, as such in-flight transactions could lead to the roots becoming + /// re-blessed shortly after the call to this function, contrary to the original intention. + function ownerResetBlessVotes(IRMN.TaggedRoot[] calldata taggedRoots) external onlyOwner { + uint32 configVersion = s_versionedConfig.configVersion; + for (uint256 i = 0; i < taggedRoots.length; ++i) { + IRMN.TaggedRoot memory taggedRoot = taggedRoots[i]; + bytes32 taggedRootHash = _taggedRootHash(taggedRoot); + BlessVoteProgress memory voteProgress = s_blessVoteProgressByTaggedRootHash[taggedRootHash]; + delete s_blessVoteProgressByTaggedRootHash[taggedRootHash]; + bool wasBlessed = voteProgress.weightThresholdMet; + if (voteProgress.configVersion == configVersion || wasBlessed) { + emit TaggedRootBlessVotesReset(configVersion, taggedRoot, wasBlessed); + } + } + } + + struct UnvoteToCurseRequest { + bytes16 subject; + bytes28 cursesHash; + } + + // For use in internal calls. + enum Privilege { + Owner, + Voter + } + + function _authorizedUnvoteToCurse( + Privilege priv, // Privilege.Owner during an ownerUnvoteToCurse call, Privilege.Voter during a unvoteToCurse call + uint32 configVersion, + address curseVoteAddr, + UnvoteToCurseRequest memory req, + bool forceUnvote, // true only during an ownerUnvoteToCurse call, when OwnerUnvoteToCurseRequest.forceUnvote is true + CurserRecord storage sptr_curserRecord, + CurseVoteProgress storage sptr_curseVoteProgress + ) internal returns (bool unvoted, bool curseLifted) { + { + assert(priv == Privilege.Voter || priv == Privilege.Owner); // sanity check + // Check that the supplied arguments are feasible for our privilege. + if (forceUnvote || curseVoteAddr == OWNER_CURSE_VOTE_ADDR || curseVoteAddr == LIFT_CURSE_VOTE_ADDR) { + assert(priv == Privilege.Owner); + } + } + + ConfigVersionAndCursesHash memory cvch = sptr_curseVoteProgress.latestVoteToCurseByCurseVoteAddr[curseVoteAddr]; + + // First, try to unvote. + if ( + sptr_curserRecord.active && (curseVoteAddr == OWNER_CURSE_VOTE_ADDR || cvch.configVersion == configVersion) + && cvch.cursesHash != NO_VOTES_CURSES_HASH && (cvch.cursesHash == req.cursesHash || forceUnvote) + ) { + unvoted = true; + delete sptr_curseVoteProgress.latestVoteToCurseByCurseVoteAddr[curseVoteAddr]; + // Assumes: s_curserRecords[OWNER_CURSE_VOTE_ADDR].weight == 0, enforced by _setConfig + sptr_curseVoteProgress.accumulatedWeight -= sptr_curserRecord.weight; + + emit UnvotedToCurse( + configVersion, + curseVoteAddr, + req.subject, + sptr_curserRecord.weight, + req.cursesHash, + sptr_curseVoteProgress.accumulatedWeight + ); + } + + // If we have owner privilege, and the conditions for the curse to be active no longer hold, we are able to lift the + // curse. + bool shouldTryToLiftCurse = priv == Privilege.Owner && (unvoted || curseVoteAddr == LIFT_CURSE_VOTE_ADDR); + + if (shouldTryToLiftCurse && sptr_curseVoteProgress.curseActive && !_shouldCurseBeActive(sptr_curseVoteProgress)) { + curseLifted = true; + sptr_curseVoteProgress.curseActive = false; + --s_curseHotVars.numSubjectsCursed; + emit CurseLifted(req.subject); + } + + if (unvoted || curseLifted) { + RecordedCurseRelatedOpTag tag; + if (priv == Privilege.Owner) { + if (forceUnvote) { + tag = RecordedCurseRelatedOpTag.OwnerUnvoteToCurseForced; + } else { + tag = RecordedCurseRelatedOpTag.OwnerUnvoteToCurseUnforced; + } + } else if (priv == Privilege.Voter) { + tag = RecordedCurseRelatedOpTag.UnvoteToCurse; + } else { + // solhint-disable-next-line gas-custom-errors, reason-string + revert(); // assumption violation + } + s_recordedCurseRelatedOps.push( + RecordedCurseRelatedOp({ + tag: tag, + cursed: sptr_curseVoteProgress.curseActive, + curseVoteAddr: curseVoteAddr, + curseId: bytes16(0), + subject: req.subject, + blockTimestamp: _blockTimestamp() + }) + ); + } else { + emit SkippedUnvoteToCurse(curseVoteAddr, req.subject, cvch.cursesHash, req.cursesHash); + } + } + + /// @notice Can be called by a curser to remove unintentional votes to curse. + /// We expect this to be called very rarely, e.g. in case of a bug in the + /// offchain code causing false voteToCurse calls. + /// @notice Should be called from curser's corresponding curseVoteAddr. + function unvoteToCurse(UnvoteToCurseRequest[] memory unvoteToCurseRequests) external { + address curseVoteAddr = msg.sender; + CurserRecord storage sptr_curserRecord = s_curserRecords[curseVoteAddr]; + + if (!sptr_curserRecord.active) revert UnauthorizedVoter(curseVoteAddr); + + uint32 configVersion = s_versionedConfig.configVersion; + bool anyVoteWasUnvoted = false; + for (uint256 i = 0; i < unvoteToCurseRequests.length; ++i) { + UnvoteToCurseRequest memory req = unvoteToCurseRequests[i]; + CurseVoteProgress storage sptr_curseVoteProgress = _getUpToDateCurseVoteProgress(configVersion, req.subject); + (bool unvoted, bool curseLifted) = _authorizedUnvoteToCurse( + Privilege.Voter, configVersion, curseVoteAddr, req, false, sptr_curserRecord, sptr_curseVoteProgress + ); + assert(!curseLifted); // assumption violation: voters can't lift curses + anyVoteWasUnvoted = anyVoteWasUnvoted || unvoted; + } + + if (!anyVoteWasUnvoted) { + revert UnvoteToCurseNoop(); + } + } + + /// @notice A vote to curse is appropriate during unhealthy blockchain conditions + /// (eg. finality violations). + function voteToCurse(bytes16 curseId, bytes16[] memory subjects) external { + address curseVoteAddr = msg.sender; + assert(curseVoteAddr != OWNER_CURSE_VOTE_ADDR); + CurserRecord storage sptr_curserRecord = s_curserRecords[curseVoteAddr]; + if (!sptr_curserRecord.active) revert UnauthorizedVoter(curseVoteAddr); + _authorizedVoteToCurse(curseVoteAddr, curseId, subjects, sptr_curserRecord); + } + + function _authorizedVoteToCurse( + address curseVoteAddr, + bytes16 curseId, + bytes16[] memory subjects, + CurserRecord storage sptr_curserRecord + ) internal { + if (subjects.length == 0) revert VoteToCurseNoop(); + + if (sptr_curserRecord.usedCurseIds[curseId]) revert ReusedCurseId(curseVoteAddr, curseId); + sptr_curserRecord.usedCurseIds[curseId] = true; + + // NOTE: We could pack configVersion into CurserRecord that we already load in the beginning of this function to + // avoid the following extra storage read for it, but since voteToCurse is not on the hot path we'd rather keep + // things simple. + uint32 configVersion = s_versionedConfig.configVersion; + for (uint256 i = 0; i < subjects.length; ++i) { + if (i >= 1 && !(subjects[i - 1] < subjects[i])) { + // Prevents a subject from receiving multiple votes to curse with the same curse id. + revert SubjectsMustBeStrictlyIncreasing(); + } + + bytes16 subject = subjects[i]; + CurseVoteProgress storage sptr_curseVoteProgress = _getUpToDateCurseVoteProgress(configVersion, subject); + ConfigVersionAndCursesHash memory cvch = sptr_curseVoteProgress.latestVoteToCurseByCurseVoteAddr[curseVoteAddr]; + bytes28 prevCursesHash; + if ( + (curseVoteAddr != OWNER_CURSE_VOTE_ADDR && cvch.configVersion < configVersion) + || cvch.cursesHash == NO_VOTES_CURSES_HASH + ) { + // if owner's first vote, or if voter's first vote in this config version + prevCursesHash = NO_VOTES_CURSES_HASH; // start hashchain from scratch, explicit + sptr_curseVoteProgress.accumulatedWeight += sptr_curserRecord.weight; + } else { + // we've already accounted for the weight + prevCursesHash = cvch.cursesHash; + } + sptr_curseVoteProgress.latestVoteToCurseByCurseVoteAddr[curseVoteAddr] = cvch = + ConfigVersionAndCursesHash({configVersion: configVersion, cursesHash: _cursesHash(prevCursesHash, curseId)}); + emit VotedToCurse( + configVersion, + curseVoteAddr, + subject, + curseId, + sptr_curserRecord.weight, + _blockTimestamp(), + cvch.cursesHash, + sptr_curseVoteProgress.accumulatedWeight + ); + + if ( + prevCursesHash == NO_VOTES_CURSES_HASH && !sptr_curseVoteProgress.curseActive + && _shouldCurseBeActive(sptr_curseVoteProgress) + ) { + sptr_curseVoteProgress.curseActive = true; + ++s_curseHotVars.numSubjectsCursed; + emit Cursed(configVersion, subject, _blockTimestamp()); + } + + s_recordedCurseRelatedOps.push( + RecordedCurseRelatedOp({ + tag: RecordedCurseRelatedOpTag.VoteToCurse, + cursed: sptr_curseVoteProgress.curseActive, + curseVoteAddr: curseVoteAddr, + curseId: curseId, + subject: subject, + blockTimestamp: _blockTimestamp() + }) + ); + } + } + + /// @notice Enables the owner to immediately have the system enter the cursed state. + function ownerCurse(bytes16 curseId, bytes16[] memory subjects) external onlyOwner { + address curseVoteAddr = OWNER_CURSE_VOTE_ADDR; + CurserRecord storage sptr_curserRecord = s_curserRecords[curseVoteAddr]; + // no need to check if sptr_curserRecord.active, we must have the onlyOwner modifier + _authorizedVoteToCurse(curseVoteAddr, curseId, subjects, sptr_curserRecord); + } + + // Set curseVoteAddr=LIFT_CURSE_VOTE_ADDR, cursesHash=bytes28(0), to reset curseActive if it can be reset. Useful if + // all voters have unvoted to curse on their own and the curse can now be lifted without any individual votes that can + // be unvoted. + // solhint-disable-next-line gas-struct-packing + struct OwnerUnvoteToCurseRequest { + address curseVoteAddr; + UnvoteToCurseRequest unit; + bool forceUnvote; + } + + /// @notice Enables the owner to remove curse votes. After the curse votes are removed, + /// this function will check whether the curse is still valid and restore the uncursed state if possible. + /// This function also enables the owner to lift a curse created through ownerCurse. + function ownerUnvoteToCurse(OwnerUnvoteToCurseRequest[] memory ownerUnvoteToCurseRequests) external onlyOwner { + bool anyCurseWasLifted = false; + bool anyVoteWasUnvoted = false; + uint32 configVersion = s_versionedConfig.configVersion; + for (uint256 i = 0; i < ownerUnvoteToCurseRequests.length; ++i) { + OwnerUnvoteToCurseRequest memory req = ownerUnvoteToCurseRequests[i]; + CurseVoteProgress storage sptr_curseVoteProgress = _getUpToDateCurseVoteProgress(configVersion, req.unit.subject); + (bool unvoted, bool curseLifted) = _authorizedUnvoteToCurse( + Privilege.Owner, + configVersion, + req.curseVoteAddr, + req.unit, + req.forceUnvote, + s_curserRecords[req.curseVoteAddr], + sptr_curseVoteProgress + ); + anyVoteWasUnvoted = anyVoteWasUnvoted || unvoted; + anyCurseWasLifted = anyCurseWasLifted || curseLifted; + } + + if (anyCurseWasLifted) { + // Invalidate all in-progress votes to bless or curse by bumping the config version. + // They might have been based on false information about the source chain + // (e.g. in case of a finality violation). + _setConfig(s_versionedConfig.config); + } + + if (!(anyVoteWasUnvoted || anyCurseWasLifted)) { + revert UnvoteToCurseNoop(); + } + } + + function setConfig(Config memory config) external onlyOwner { + _setConfig(config); + } + + /// @notice Any tagged root with a commit store included in this array will be considered automatically blessed. + function getPermaBlessedCommitStores() external view returns (address[] memory) { + return s_permaBlessedCommitStores.values(); + } + + /// @notice The ordering of parameters is important. First come the commit stores to remove, then the commit stores to + /// add. + function ownerRemoveThenAddPermaBlessedCommitStores( + address[] memory removes, + address[] memory adds + ) external onlyOwner { + for (uint256 i = 0; i < removes.length; ++i) { + if (s_permaBlessedCommitStores.remove(removes[i])) { + emit PermaBlessedCommitStoreRemoved(removes[i]); + } + } + for (uint256 i = 0; i < adds.length; ++i) { + if (s_permaBlessedCommitStores.add(adds[i])) { + emit PermaBlessedCommitStoreAdded(adds[i]); + } + } + } + + /// @inheritdoc IRMN + function isBlessed(IRMN.TaggedRoot calldata taggedRoot) external view returns (bool) { + return s_blessVoteProgressByTaggedRootHash[_taggedRootHash(taggedRoot)].weightThresholdMet + || s_permaBlessedCommitStores.contains(taggedRoot.commitStore); + } + + /// @inheritdoc IRMN + function isCursed() external view returns (bool) { + if (s_curseHotVars.numSubjectsCursed == 0) { + return false; // happy path costs a single SLOAD + } else { + return s_potentiallyOutdatedCurseVoteProgressBySubject[GLOBAL_CURSE_SUBJECT].curseActive + || s_potentiallyOutdatedCurseVoteProgressBySubject[LEGACY_CURSE_SUBJECT].curseActive; + } + } + + /// @inheritdoc IRMN + function isCursed(bytes16 subject) public view returns (bool) { + if (s_curseHotVars.numSubjectsCursed == 0) { + return false; // happy path costs a single SLOAD + } else { + return s_potentiallyOutdatedCurseVoteProgressBySubject[GLOBAL_CURSE_SUBJECT].curseActive + || s_potentiallyOutdatedCurseVoteProgressBySubject[subject].curseActive; + } + } + + /// @notice Config version might be incremented for many reasons, including + /// the lifting of a curse, or a regular config change. + function getConfigDetails() external view returns (uint32 version, uint32 blockNumber, Config memory config) { + version = s_versionedConfig.configVersion; + blockNumber = s_versionedConfig.blockNumber; + config = s_versionedConfig.config; + } + + /// @return blessVoteAddrs addresses of voters, will be empty if voting took place with an older config version + /// @return accumulatedWeight sum of weights of voters, will be zero if voting took place with an older config version + /// @return blessed will be accurate regardless of when voting took place + /// @dev This is a helper method for offchain code so efficiency is not really a concern. + function getBlessProgress(IRMN.TaggedRoot calldata taggedRoot) + external + view + returns (address[] memory blessVoteAddrs, uint16 accumulatedWeight, bool blessed) + { + bytes32 taggedRootHash = _taggedRootHash(taggedRoot); + BlessVoteProgress memory progress = s_blessVoteProgressByTaggedRootHash[taggedRootHash]; + blessed = progress.weightThresholdMet; + if (progress.configVersion == s_versionedConfig.configVersion) { + accumulatedWeight = progress.accumulatedWeight; + uint200 bitmap = progress.voterBitmap; + blessVoteAddrs = new address[](_bitmapCount(bitmap)); + Voter[] memory voters = s_versionedConfig.config.voters; + uint256 j = 0; + for (uint8 i = 0; i < voters.length; ++i) { + if (_bitmapGet(bitmap, i)) { + blessVoteAddrs[j] = voters[i].blessVoteAddr; + ++j; + } + } + } + } + + /// @return curseVoteAddrs the curseVoteAddr of each voter with an active vote to curse + /// @return cursesHashes the i-th value is the curses hash of curseVoteAddrs[i] + /// @return accumulatedWeight the accumulated weight of all voters with an active vote to curse who are part of the + /// current config + /// @return cursed might be true even if the owner has no active vote and accumulatedWeight < curseWeightThreshold, + /// due to a retained curse from a prior config + /// @dev This is a helper method for offchain code so efficiency is not really a concern. + function getCurseProgress(bytes16 subject) + external + view + returns (address[] memory curseVoteAddrs, bytes28[] memory cursesHashes, uint16 accumulatedWeight, bool cursed) + { + uint32 configVersion = s_versionedConfig.configVersion; + Config memory config = s_versionedConfig.config; + // Can't use _getUpToDateCurseVoteProgress here because we can't call a non-view function from within a view. + // So we get to repeat some accounting. + CurseVoteProgress storage outdatedCurseVoteProgress = s_potentiallyOutdatedCurseVoteProgressBySubject[subject]; + + cursed = outdatedCurseVoteProgress.curseActive; + + // See _getUpToDateCurseVoteProgress for more context. + bool shouldCountVotesFromOlderConfigs = outdatedCurseVoteProgress.configVersion < configVersion && cursed; + + // A play in two acts, because we can't push to arrays in memory, so we need to precompute the array's length. + // First act: we count the number of cursers, i.e., voters with active vote. + // Second act: push the cursers to the arrays, sum their weights. + + uint256 numCursers = 0; // we reuse this variable for writing to perserve stack space + accumulatedWeight = 0; + for (uint256 act = 1; act <= 2; ++act) { + uint256 i = config.voters.length; // not config.voters.length-1 to account for the owner + while (true) { + address curseVoteAddr; + uint8 weight; + if (i < config.voters.length) { + curseVoteAddr = config.voters[i].curseVoteAddr; + weight = config.voters[i].curseWeight; + } else { + // Allows us to include the owner's vote and curses hash in the result. + curseVoteAddr = OWNER_CURSE_VOTE_ADDR; + weight = 0; + } + + ConfigVersionAndCursesHash memory cvch = + outdatedCurseVoteProgress.latestVoteToCurseByCurseVoteAddr[curseVoteAddr]; + bool hasActiveVote = ( + shouldCountVotesFromOlderConfigs || cvch.configVersion == configVersion + || curseVoteAddr == OWNER_CURSE_VOTE_ADDR + ) && cvch.cursesHash != NO_VOTES_CURSES_HASH; + if (hasActiveVote) { + if (act == 1) { + ++numCursers; + } else if (act == 2) { + accumulatedWeight += weight; + --numCursers; + curseVoteAddrs[numCursers] = curseVoteAddr; + cursesHashes[numCursers] = cvch.cursesHash; + } else { + // solhint-disable-next-line gas-custom-errors, reason-string + revert(); // assumption violation + } + } + + if (i > 0) { + --i; + } else { + break; + } + } + + if (act == 1) { + // We are done counting at this point, initialize the arrays for the second act that follows immediately after. + curseVoteAddrs = new address[](numCursers); + cursesHashes = new bytes28[](numCursers); + } + } + } + + /// @notice Returns the number of subjects that are currently cursed. + function getCursedSubjectsCount() external view returns (uint256) { + return s_curseHotVars.numSubjectsCursed; + } + + /// @dev This is a helper method for offchain code to know what arguments to use for getRecordedCurseRelatedOps. + function getRecordedCurseRelatedOpsCount() external view returns (uint256) { + return s_recordedCurseRelatedOps.length; + } + + /// @dev This is a helper method for offchain code so efficiency is not really a concern. + /// @dev Returns s_recordedCurseRelatedOps[offset:offset+limit]. + function getRecordedCurseRelatedOps( + uint256 offset, + uint256 limit + ) external view returns (RecordedCurseRelatedOp[] memory) { + uint256 pageLen; + if (offset + limit <= s_recordedCurseRelatedOps.length) { + pageLen = limit; + } else if (offset < s_recordedCurseRelatedOps.length) { + pageLen = s_recordedCurseRelatedOps.length - offset; + } else { + pageLen = 0; + } + RecordedCurseRelatedOp[] memory page = new RecordedCurseRelatedOp[](pageLen); + for (uint256 i = 0; i < pageLen; ++i) { + page[i] = s_recordedCurseRelatedOps[offset + i]; + } + return page; + } + + function _validateConfig(Config memory config) internal pure returns (bool) { + if ( + config.voters.length == 0 || config.voters.length > MAX_NUM_VOTERS || config.blessWeightThreshold == 0 + || config.curseWeightThreshold == 0 + ) { + return false; + } + + uint256 totalBlessWeight = 0; + uint256 totalCurseWeight = 0; + address[] memory allAddrs = new address[](2 * config.voters.length); + for (uint256 i = 0; i < config.voters.length; ++i) { + Voter memory voter = config.voters[i]; + // The owner can always curse using the ownerCurse method, and is not supposed to be included in the voters list. + // Even though the intent is for the actual owner address to NOT be included in the voters list, we don't + // explicitly disallow curseVoteAddr == owner() here. Even if we did, the owner could transfer ownership of the + // contract, and so we couldn't guarantee that the owner is not eventually included in the voters list. + if ( + voter.blessVoteAddr == address(0) || voter.curseVoteAddr == address(0) + || voter.curseVoteAddr == LIFT_CURSE_VOTE_ADDR || voter.curseVoteAddr == OWNER_CURSE_VOTE_ADDR + || (voter.blessWeight == 0 && voter.curseWeight == 0) + ) { + return false; + } + allAddrs[2 * i + 0] = voter.blessVoteAddr; + allAddrs[2 * i + 1] = voter.curseVoteAddr; + totalBlessWeight += voter.blessWeight; + totalCurseWeight += voter.curseWeight; + } + for (uint256 i = 0; i < allAddrs.length; ++i) { + address allAddrs_i = allAddrs[i]; + for (uint256 j = i + 1; j < allAddrs.length; ++j) { + if (allAddrs_i == allAddrs[j]) { + return false; + } + } + } + + return totalBlessWeight >= config.blessWeightThreshold && totalCurseWeight >= config.curseWeightThreshold; + } + + function _setConfig(Config memory config) private { + if (!_validateConfig(config)) revert InvalidConfig(); + + // We can't directly assign s_versionedConfig.config to config + // because copying a memory array into storage is not supported. + { + s_versionedConfig.config.blessWeightThreshold = config.blessWeightThreshold; + s_versionedConfig.config.curseWeightThreshold = config.curseWeightThreshold; + while (s_versionedConfig.config.voters.length != 0) { + Voter memory voter = s_versionedConfig.config.voters[s_versionedConfig.config.voters.length - 1]; + delete s_blesserRecords[voter.blessVoteAddr]; + delete s_curserRecords[voter.curseVoteAddr]; // usedCurseIds mapping is retained, as intended + s_versionedConfig.config.voters.pop(); + } + for (uint256 i = 0; i < config.voters.length; ++i) { + s_versionedConfig.config.voters.push(config.voters[i]); + } + } + + ++s_versionedConfig.configVersion; + uint32 configVersion = s_versionedConfig.configVersion; + + for (uint8 i = 0; i < config.voters.length; ++i) { + Voter memory voter = config.voters[i]; + s_blesserRecords[voter.blessVoteAddr] = + BlesserRecord({configVersion: configVersion, index: i, weight: voter.blessWeight}); + { + CurserRecord storage sptr_curserRecord = s_curserRecords[voter.curseVoteAddr]; + // Solidity will not let us initialize as CurserRecord({...}) due to the nested mapping + sptr_curserRecord.active = true; + sptr_curserRecord.weight = voter.curseWeight; + } + } + { + // Initialize the owner's CurserRecord + // We could in principle perform this initialization once in the constructor instead, and save a small bit of gas. + // But configuration changes are relatively infrequent, and keeping the initialization here makes the contract's + // correctness easier to reason about. + CurserRecord storage sptr_ownerCurserRecord = s_curserRecords[OWNER_CURSE_VOTE_ADDR]; + sptr_ownerCurserRecord.active = true; // Assumed by vote/unvote-to-curse logic + sptr_ownerCurserRecord.weight = 0; // Assumed by vote/unvote-to-curse logic + } + s_versionedConfig.blockNumber = uint32(block.number); + emit ConfigSet(configVersion, config); + + s_recordedCurseRelatedOps.push( + RecordedCurseRelatedOp({ + tag: RecordedCurseRelatedOpTag.SetConfig, + blockTimestamp: _blockTimestamp(), + cursed: false, + curseVoteAddr: address(0), + curseId: bytes16(0), + subject: bytes16(0) + }) + ); + } +} diff --git a/contracts/src/v0.8/ccip/Router.sol b/contracts/src/v0.8/ccip/Router.sol new file mode 100644 index 00000000000..e50651bc5ba --- /dev/null +++ b/contracts/src/v0.8/ccip/Router.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../shared/interfaces/ITypeAndVersion.sol"; +import {IAny2EVMMessageReceiver} from "./interfaces/IAny2EVMMessageReceiver.sol"; +import {IEVM2AnyOnRamp} from "./interfaces/IEVM2AnyOnRamp.sol"; +import {IRMN} from "./interfaces/IRMN.sol"; +import {IRouter} from "./interfaces/IRouter.sol"; +import {IRouterClient} from "./interfaces/IRouterClient.sol"; +import {IWrappedNative} from "./interfaces/IWrappedNative.sol"; + +import {OwnerIsCreator} from "../shared/access/OwnerIsCreator.sol"; +import {CallWithExactGas} from "../shared/call/CallWithExactGas.sol"; +import {Client} from "./libraries/Client.sol"; +import {Internal} from "./libraries/Internal.sol"; + +import {IERC20} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @title Router +/// @notice This is the entry point for the end user wishing to send data across chains. +/// @dev This contract is used as a router for both on-ramps and off-ramps +contract Router is IRouter, IRouterClient, ITypeAndVersion, OwnerIsCreator { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.UintSet; + + error FailedToSendValue(); + error InvalidRecipientAddress(address to); + error OffRampMismatch(uint64 chainSelector, address offRamp); + error BadARMSignal(); + + event OnRampSet(uint64 indexed destChainSelector, address onRamp); + event OffRampAdded(uint64 indexed sourceChainSelector, address offRamp); + event OffRampRemoved(uint64 indexed sourceChainSelector, address offRamp); + event MessageExecuted(bytes32 messageId, uint64 sourceChainSelector, address offRamp, bytes32 calldataHash); + + struct OnRamp { + uint64 destChainSelector; + address onRamp; + } + + struct OffRamp { + uint64 sourceChainSelector; + address offRamp; + } + + string public constant override typeAndVersion = "Router 1.2.0"; + // We limit return data to a selector plus 4 words. This is to avoid + // malicious contracts from returning large amounts of data and causing + // repeated out-of-gas scenarios. + uint16 public constant MAX_RET_BYTES = 4 + 4 * 32; + // STATIC CONFIG + // Address of RMN proxy contract (formerly known as ARM) + address private immutable i_armProxy; + + // DYNAMIC CONFIG + address private s_wrappedNative; + // destChainSelector => onRamp address + // Only ever one onRamp enabled at a time for a given destChainSelector. + mapping(uint256 destChainSelector => address onRamp) private s_onRamps; + // Stores [sourceChainSelector << 160 + offramp] as a pair to allow for + // lookups for specific chain/offramp pairs. + EnumerableSet.UintSet private s_chainSelectorAndOffRamps; + + constructor(address wrappedNative, address armProxy) { + // Zero address indicates unsupported auto-wrapping, therefore, unsupported + // native fee token payments. + s_wrappedNative = wrappedNative; + i_armProxy = armProxy; + } + + // ================================================================ + // │ Message sending │ + // ================================================================ + + /// @inheritdoc IRouterClient + function getFee( + uint64 destinationChainSelector, + Client.EVM2AnyMessage memory message + ) external view returns (uint256 fee) { + if (message.feeToken == address(0)) { + // For empty feeToken return native quote. + message.feeToken = address(s_wrappedNative); + } + address onRamp = s_onRamps[destinationChainSelector]; + if (onRamp == address(0)) revert UnsupportedDestinationChain(destinationChainSelector); + return IEVM2AnyOnRamp(onRamp).getFee(destinationChainSelector, message); + } + + /// @notice This functionality has been removed and will revert when called. + function getSupportedTokens(uint64 chainSelector) external view returns (address[] memory) { + if (!isChainSupported(chainSelector)) { + return new address[](0); + } + return IEVM2AnyOnRamp(s_onRamps[uint256(chainSelector)]).getSupportedTokens(chainSelector); + } + + /// @inheritdoc IRouterClient + function isChainSupported(uint64 chainSelector) public view returns (bool) { + return s_onRamps[chainSelector] != address(0); + } + + /// @inheritdoc IRouterClient + function ccipSend( + uint64 destinationChainSelector, + Client.EVM2AnyMessage memory message + ) external payable whenNotCursed returns (bytes32) { + address onRamp = s_onRamps[destinationChainSelector]; + if (onRamp == address(0)) revert UnsupportedDestinationChain(destinationChainSelector); + uint256 feeTokenAmount; + // address(0) signals payment in true native + if (message.feeToken == address(0)) { + // for fee calculation we check the wrapped native price as we wrap + // as part of the native fee coin payment. + message.feeToken = s_wrappedNative; + // We rely on getFee to validate that the feeToken is whitelisted. + feeTokenAmount = IEVM2AnyOnRamp(onRamp).getFee(destinationChainSelector, message); + // Ensure sufficient native. + if (msg.value < feeTokenAmount) revert InsufficientFeeTokenAmount(); + // Wrap and send native payment. + // Note we take the whole msg.value regardless if its larger. + feeTokenAmount = msg.value; + IWrappedNative(message.feeToken).deposit{value: feeTokenAmount}(); + IERC20(message.feeToken).safeTransfer(onRamp, feeTokenAmount); + } else { + if (msg.value > 0) revert InvalidMsgValue(); + // We rely on getFee to validate that the feeToken is whitelisted. + feeTokenAmount = IEVM2AnyOnRamp(onRamp).getFee(destinationChainSelector, message); + IERC20(message.feeToken).safeTransferFrom(msg.sender, onRamp, feeTokenAmount); + } + + // Transfer the tokens to the token pools. + for (uint256 i = 0; i < message.tokenAmounts.length; ++i) { + IERC20 token = IERC20(message.tokenAmounts[i].token); + // We rely on getPoolBySourceToken to validate that the token is whitelisted. + token.safeTransferFrom( + msg.sender, + address(IEVM2AnyOnRamp(onRamp).getPoolBySourceToken(destinationChainSelector, token)), + message.tokenAmounts[i].amount + ); + } + + return IEVM2AnyOnRamp(onRamp).forwardFromRouter(destinationChainSelector, message, feeTokenAmount, msg.sender); + } + + // ================================================================ + // │ Message execution │ + // ================================================================ + + /// @inheritdoc IRouter + /// @dev _callWithExactGas protects against return data bombs by capping the return data size at MAX_RET_BYTES. + function routeMessage( + Client.Any2EVMMessage calldata message, + uint16 gasForCallExactCheck, + uint256 gasLimit, + address receiver + ) external override whenNotCursed returns (bool success, bytes memory retData, uint256 gasUsed) { + // We only permit offRamps to call this function. + if (!isOffRamp(message.sourceChainSelector, msg.sender)) revert OnlyOffRamp(); + + // We encode here instead of the offRamps to constrain specifically what functions + // can be called from the router. + bytes memory data = abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message); + + (success, retData, gasUsed) = CallWithExactGas._callWithExactGasSafeReturnData( + data, receiver, gasLimit, gasForCallExactCheck, Internal.MAX_RET_BYTES + ); + + emit MessageExecuted(message.messageId, message.sourceChainSelector, msg.sender, keccak256(data)); + return (success, retData, gasUsed); + } + + // @notice Merges a chain selector and offRamp address into a single uint256 by shifting the + // chain selector 160 bits to the left. + function _mergeChainSelectorAndOffRamp( + uint64 sourceChainSelector, + address offRampAddress + ) internal pure returns (uint256) { + return (uint256(sourceChainSelector) << 160) + uint160(offRampAddress); + } + + // ================================================================ + // │ Config │ + // ================================================================ + + /// @notice Gets the wrapped representation of the native fee coin. + /// @return The address of the ERC20 wrapped native. + function getWrappedNative() external view returns (address) { + return s_wrappedNative; + } + + /// @notice Sets a new wrapped native token. + /// @param wrappedNative The address of the new wrapped native ERC20 token. + function setWrappedNative(address wrappedNative) external onlyOwner { + s_wrappedNative = wrappedNative; + } + + /// @notice Gets the RMN address, formerly known as ARM + /// @return The address of the RMN proxy contract, formerly known as ARM + function getArmProxy() external view returns (address) { + return i_armProxy; + } + + /// @inheritdoc IRouter + function getOnRamp(uint64 destChainSelector) external view returns (address) { + return s_onRamps[destChainSelector]; + } + + function getOffRamps() external view returns (OffRamp[] memory) { + uint256[] memory encodedOffRamps = s_chainSelectorAndOffRamps.values(); + OffRamp[] memory offRamps = new OffRamp[](encodedOffRamps.length); + for (uint256 i = 0; i < encodedOffRamps.length; ++i) { + uint256 encodedOffRamp = encodedOffRamps[i]; + offRamps[i] = + OffRamp({sourceChainSelector: uint64(encodedOffRamp >> 160), offRamp: address(uint160(encodedOffRamp))}); + } + return offRamps; + } + + /// @inheritdoc IRouter + function isOffRamp(uint64 sourceChainSelector, address offRamp) public view returns (bool) { + // We have to encode the sourceChainSelector and offRamp into a uint256 to use as a key in the set. + return s_chainSelectorAndOffRamps.contains(_mergeChainSelectorAndOffRamp(sourceChainSelector, offRamp)); + } + + /// @notice applyRampUpdates applies a set of ramp changes which provides + /// the ability to add new chains and upgrade ramps. + function applyRampUpdates( + OnRamp[] calldata onRampUpdates, + OffRamp[] calldata offRampRemoves, + OffRamp[] calldata offRampAdds + ) external onlyOwner { + // Apply egress updates. + // We permit zero address as way to disable egress. + for (uint256 i = 0; i < onRampUpdates.length; ++i) { + OnRamp memory onRampUpdate = onRampUpdates[i]; + s_onRamps[onRampUpdate.destChainSelector] = onRampUpdate.onRamp; + emit OnRampSet(onRampUpdate.destChainSelector, onRampUpdate.onRamp); + } + + // Apply ingress updates. + for (uint256 i = 0; i < offRampRemoves.length; ++i) { + uint64 sourceChainSelector = offRampRemoves[i].sourceChainSelector; + address offRampAddress = offRampRemoves[i].offRamp; + + // If the selector-offRamp pair does not exist, revert. + if (!s_chainSelectorAndOffRamps.remove(_mergeChainSelectorAndOffRamp(sourceChainSelector, offRampAddress))) { + revert OffRampMismatch(sourceChainSelector, offRampAddress); + } + + emit OffRampRemoved(sourceChainSelector, offRampAddress); + } + + for (uint256 i = 0; i < offRampAdds.length; ++i) { + uint64 sourceChainSelector = offRampAdds[i].sourceChainSelector; + address offRampAddress = offRampAdds[i].offRamp; + + if (s_chainSelectorAndOffRamps.add(_mergeChainSelectorAndOffRamp(sourceChainSelector, offRampAddress))) { + emit OffRampAdded(sourceChainSelector, offRampAddress); + } + } + } + + /// @notice Provides the ability for the owner to recover any tokens accidentally + /// sent to this contract. + /// @dev Must be onlyOwner to avoid malicious token contract calls. + /// @param tokenAddress ERC20-token to recover + /// @param to Destination address to send the tokens to. + function recoverTokens(address tokenAddress, address to, uint256 amount) external onlyOwner { + if (to == address(0)) revert InvalidRecipientAddress(to); + + if (tokenAddress == address(0)) { + (bool success,) = to.call{value: amount}(""); + if (!success) revert FailedToSendValue(); + return; + } + IERC20(tokenAddress).safeTransfer(to, amount); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Ensure that the RMN has not cursed the network. + modifier whenNotCursed() { + if (IRMN(i_armProxy).isCursed()) revert BadARMSignal(); + _; + } +} diff --git a/contracts/src/v0.8/ccip/applications/CCIPClientExample.sol b/contracts/src/v0.8/ccip/applications/CCIPClientExample.sol new file mode 100644 index 00000000000..b105cf8b00f --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/CCIPClientExample.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IRouterClient} from "../interfaces/IRouterClient.sol"; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {Client} from "../libraries/Client.sol"; +import {CCIPReceiver} from "./CCIPReceiver.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +// @notice Example of a client which supports EVM/non-EVM chains +// @dev If chain specific logic is required for different chain families (e.g. particular +// decoding the bytes sender for authorization checks), it may be required to point to a helper +// authorization contract unless all chain families are known up front. +// @dev If contract does not implement IAny2EVMMessageReceiver and IERC165, +// and tokens are sent to it, ccipReceive will not be called but tokens will be transferred. +// @dev If the client is upgradeable you have significantly more flexibility and +// can avoid storage based options like the below contract uses. However it's +// worth carefully considering how the trust assumptions of your client dapp will +// change if you introduce upgradeability. An immutable dapp building on top of CCIP +// like the example below will inherit the trust properties of CCIP (i.e. the oracle network). +// @dev The receiver's are encoded offchain and passed as direct arguments to permit supporting +// new chain family receivers (e.g. a Solana encoded receiver address) without upgrading. +contract CCIPClientExample is CCIPReceiver, OwnerIsCreator { + error InvalidChain(uint64 chainSelector); + + event MessageSent(bytes32 messageId); + event MessageReceived(bytes32 messageId); + + // Current feeToken + IERC20 public s_feeToken; + // Below is a simplistic example (same params for all messages) of using storage to allow for new options without + // upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.). + // and will always be backwards compatible i.e. upgrades are opt-in. + // Offchain we can compute the V1 extraArgs: + // Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000}); + // bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs); + // Then later compute V2 extraArgs, for example if a refund feature was added: + // Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234}); + // bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs); + // and update storage with the new args. + // If different options are required for different messages, for example different gas limits, + // one can simply key based on (chainSelector, messageType) instead of only chainSelector. + mapping(uint64 destChainSelector => bytes extraArgsBytes) public s_chains; + + constructor(IRouterClient router, IERC20 feeToken) CCIPReceiver(address(router)) { + s_feeToken = feeToken; + s_feeToken.approve(address(router), type(uint256).max); + } + + function enableChain(uint64 chainSelector, bytes memory extraArgs) external onlyOwner { + s_chains[chainSelector] = extraArgs; + } + + function disableChain(uint64 chainSelector) external onlyOwner { + delete s_chains[chainSelector]; + } + + function ccipReceive(Client.Any2EVMMessage calldata message) + external + virtual + override + onlyRouter + validChain(message.sourceChainSelector) + { + // Extremely important to ensure only router calls this. + // Tokens in message if any will be transferred to this contract + // TODO: Validate sender/origin chain and process message and/or tokens. + _ccipReceive(message); + } + + function _ccipReceive(Client.Any2EVMMessage memory message) internal override { + emit MessageReceived(message.messageId); + } + + /// @notice sends data to receiver on dest chain. Assumes address(this) has sufficient native asset. + function sendDataPayNative( + uint64 destChainSelector, + bytes memory receiver, + bytes memory data + ) external validChain(destChainSelector) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](0); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: receiver, + data: data, + tokenAmounts: tokenAmounts, + extraArgs: s_chains[destChainSelector], + feeToken: address(0) // We leave the feeToken empty indicating we'll pay raw native. + }); + bytes32 messageId = IRouterClient(i_ccipRouter).ccipSend{ + value: IRouterClient(i_ccipRouter).getFee(destChainSelector, message) + }(destChainSelector, message); + emit MessageSent(messageId); + } + + /// @notice sends data to receiver on dest chain. Assumes address(this) has sufficient feeToken. + function sendDataPayFeeToken( + uint64 destChainSelector, + bytes memory receiver, + bytes memory data + ) external validChain(destChainSelector) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](0); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: receiver, + data: data, + tokenAmounts: tokenAmounts, + extraArgs: s_chains[destChainSelector], + feeToken: address(s_feeToken) + }); + // Optional uint256 fee = i_ccipRouter.getFee(destChainSelector, message); + // Can decide if fee is acceptable. + // address(this) must have sufficient feeToken or the send will revert. + bytes32 messageId = IRouterClient(i_ccipRouter).ccipSend(destChainSelector, message); + emit MessageSent(messageId); + } + + /// @notice sends data to receiver on dest chain. Assumes address(this) has sufficient native token. + function sendDataAndTokens( + uint64 destChainSelector, + bytes memory receiver, + bytes memory data, + Client.EVMTokenAmount[] memory tokenAmounts + ) external validChain(destChainSelector) { + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + IERC20(tokenAmounts[i].token).transferFrom(msg.sender, address(this), tokenAmounts[i].amount); + IERC20(tokenAmounts[i].token).approve(i_ccipRouter, tokenAmounts[i].amount); + } + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: receiver, + data: data, + tokenAmounts: tokenAmounts, + extraArgs: s_chains[destChainSelector], + feeToken: address(s_feeToken) + }); + // Optional uint256 fee = i_ccipRouter.getFee(destChainSelector, message); + // Can decide if fee is acceptable. + // address(this) must have sufficient feeToken or the send will revert. + bytes32 messageId = IRouterClient(i_ccipRouter).ccipSend(destChainSelector, message); + emit MessageSent(messageId); + } + + // @notice user sends tokens to a receiver + // Approvals can be optimized with a whitelist of tokens and inf approvals if desired. + function sendTokens( + uint64 destChainSelector, + bytes memory receiver, + Client.EVMTokenAmount[] memory tokenAmounts + ) external validChain(destChainSelector) { + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + IERC20(tokenAmounts[i].token).transferFrom(msg.sender, address(this), tokenAmounts[i].amount); + IERC20(tokenAmounts[i].token).approve(i_ccipRouter, tokenAmounts[i].amount); + } + bytes memory data; + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: receiver, + data: data, + tokenAmounts: tokenAmounts, + extraArgs: s_chains[destChainSelector], + feeToken: address(s_feeToken) + }); + // Optional uint256 fee = i_ccipRouter.getFee(destChainSelector, message); + // Can decide if fee is acceptable. + // address(this) must have sufficient feeToken or the send will revert. + bytes32 messageId = IRouterClient(i_ccipRouter).ccipSend(destChainSelector, message); + emit MessageSent(messageId); + } + + modifier validChain(uint64 chainSelector) { + if (s_chains[chainSelector].length == 0) revert InvalidChain(chainSelector); + _; + } +} diff --git a/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol b/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol new file mode 100644 index 00000000000..7011f814de7 --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IAny2EVMMessageReceiver} from "../interfaces/IAny2EVMMessageReceiver.sol"; + +import {Client} from "../libraries/Client.sol"; + +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; + +/// @title CCIPReceiver - Base contract for CCIP applications that can receive messages. +abstract contract CCIPReceiver is IAny2EVMMessageReceiver, IERC165 { + address internal immutable i_ccipRouter; + + constructor(address router) { + if (router == address(0)) revert InvalidRouter(address(0)); + i_ccipRouter = router; + } + + /// @notice IERC165 supports an interfaceId + /// @param interfaceId The interfaceId to check + /// @return true if the interfaceId is supported + /// @dev Should indicate whether the contract implements IAny2EVMMessageReceiver + /// e.g. return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || interfaceId == type(IERC165).interfaceId + /// This allows CCIP to check if ccipReceive is available before calling it. + /// If this returns false or reverts, only tokens are transferred to the receiver. + /// If this returns true, tokens are transferred and ccipReceive is called atomically. + /// Additionally, if the receiver address does not have code associated with + /// it at the time of execution (EXTCODESIZE returns 0), only tokens will be transferred. + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; + } + + /// @inheritdoc IAny2EVMMessageReceiver + function ccipReceive(Client.Any2EVMMessage calldata message) external virtual override onlyRouter { + _ccipReceive(message); + } + + /// @notice Override this function in your implementation. + /// @param message Any2EVMMessage + function _ccipReceive(Client.Any2EVMMessage memory message) internal virtual; + + ///////////////////////////////////////////////////////////////////// + // Plumbing + ///////////////////////////////////////////////////////////////////// + + /// @notice Return the current router + /// @return CCIP router address + function getRouter() public view virtual returns (address) { + return address(i_ccipRouter); + } + + error InvalidRouter(address router); + + /// @dev only calls from the set router are accepted. + modifier onlyRouter() { + if (msg.sender != getRouter()) revert InvalidRouter(msg.sender); + _; + } +} diff --git a/contracts/src/v0.8/ccip/applications/DefensiveExample.sol b/contracts/src/v0.8/ccip/applications/DefensiveExample.sol new file mode 100644 index 00000000000..54e1e809465 --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/DefensiveExample.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IRouterClient} from "../interfaces/IRouterClient.sol"; + +import {Client} from "../libraries/Client.sol"; +import {CCIPClientExample} from "./CCIPClientExample.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableMap} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableMap.sol"; + +contract DefensiveExample is CCIPClientExample { + using EnumerableMap for EnumerableMap.Bytes32ToUintMap; + using SafeERC20 for IERC20; + + error OnlySelf(); + error ErrorCase(); + error MessageNotFailed(bytes32 messageId); + + event MessageFailed(bytes32 indexed messageId, bytes reason); + event MessageSucceeded(bytes32 indexed messageId); + event MessageRecovered(bytes32 indexed messageId); + + // Example error code, could have many different error codes. + enum ErrorCode { + // RESOLVED is first so that the default value is resolved. + RESOLVED, + // Could have any number of error codes here. + BASIC + } + + // The message contents of failed messages are stored here. + mapping(bytes32 messageId => Client.Any2EVMMessage contents) public s_messageContents; + + // Contains failed messages and their state. + EnumerableMap.Bytes32ToUintMap internal s_failedMessages; + + // This is used to simulate a revert in the processMessage function. + bool internal s_simRevert = false; + + constructor(IRouterClient router, IERC20 feeToken) CCIPClientExample(router, feeToken) {} + + /// @notice The entrypoint for the CCIP router to call. This function should + /// never revert, all errors should be handled internally in this contract. + /// @param message The message to process. + /// @dev Extremely important to ensure only router calls this. + function ccipReceive(Client.Any2EVMMessage calldata message) + external + override + onlyRouter + validChain(message.sourceChainSelector) + { + try this.processMessage(message) {} + catch (bytes memory err) { + // Could set different error codes based on the caught error. Each could be + // handled differently. + s_failedMessages.set(message.messageId, uint256(ErrorCode.BASIC)); + s_messageContents[message.messageId] = message; + // Don't revert so CCIP doesn't revert. Emit event instead. + // The message can be retried later without having to do manual execution of CCIP. + emit MessageFailed(message.messageId, err); + return; + } + emit MessageSucceeded(message.messageId); + } + + /// @notice This function the entrypoint for this contract to process messages. + /// @param message The message to process. + /// @dev This example just sends the tokens to the owner of this contracts. More + /// interesting functions could be implemented. + /// @dev It has to be external because of the try/catch. + function processMessage(Client.Any2EVMMessage calldata message) + external + onlySelf + validChain(message.sourceChainSelector) + { + // Simulate a revert + if (s_simRevert) revert ErrorCase(); + + // Send tokens to the owner + for (uint256 i = 0; i < message.destTokenAmounts.length; ++i) { + IERC20(message.destTokenAmounts[i].token).safeTransfer(owner(), message.destTokenAmounts[i].amount); + } + // Do other things that might revert + } + + /// @notice This function is callable by the owner when a message has failed + /// to unblock the tokens that are associated with that message. + /// @dev This function is only callable by the owner. + function retryFailedMessage(bytes32 messageId, address tokenReceiver) external onlyOwner { + if (s_failedMessages.get(messageId) != uint256(ErrorCode.BASIC)) revert MessageNotFailed(messageId); + // Set the error code to 0 to disallow reentry and retry the same failed message + // multiple times. + s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED)); + + // Do stuff to retry message, potentially just releasing the associated tokens + Client.Any2EVMMessage memory message = s_messageContents[messageId]; + + // send the tokens to the receiver as escape hatch + for (uint256 i = 0; i < message.destTokenAmounts.length; ++i) { + IERC20(message.destTokenAmounts[i].token).safeTransfer(tokenReceiver, message.destTokenAmounts[i].amount); + } + + emit MessageRecovered(messageId); + } + + // An example function to demonstrate recovery + function setSimRevert(bool simRevert) external onlyOwner { + s_simRevert = simRevert; + } + + modifier onlySelf() { + if (msg.sender != address(this)) revert OnlySelf(); + _; + } +} diff --git a/contracts/src/v0.8/ccip/applications/EtherSenderReceiver.sol b/contracts/src/v0.8/ccip/applications/EtherSenderReceiver.sol new file mode 100644 index 00000000000..ce8ed1ff7a0 --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/EtherSenderReceiver.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +import {IRouterClient} from "../interfaces/IRouterClient.sol"; +import {IWrappedNative} from "../interfaces/IWrappedNative.sol"; + +import {Client} from "./../libraries/Client.sol"; +import {CCIPReceiver} from "./CCIPReceiver.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +//solhint-disable interface-starts-with-i +interface CCIPRouter { + function getWrappedNative() external view returns (address); +} + +/// @notice A contract that can send raw ether cross-chain using CCIP. +/// Since CCIP only supports ERC-20 token transfers, this contract accepts +/// normal ether, wraps it, and uses CCIP to send it cross-chain. +/// On the receiving side, the wrapped ether is unwrapped and sent to the final receiver. +/// @notice This contract only supports chains where the wrapped native contract +/// is the WETH contract (i.e not WMATIC, or WAVAX, etc.). This is because the +/// receiving contract will always unwrap the ether using it's local wrapped native contract. +/// @dev This contract is both a sender and a receiver. This same contract can be +/// deployed on source and destination chains to facilitate cross-chain ether transfers +/// and act as a sender and a receiver. +/// @dev This contract is intentionally ownerless and permissionless. This contract +/// will never hold any excess funds, native or otherwise, when used correctly. +contract EtherSenderReceiver is CCIPReceiver, ITypeAndVersion { + using SafeERC20 for IERC20; + + error InvalidTokenAmounts(uint256 gotAmounts); + error InvalidToken(address gotToken, address expectedToken); + error TokenAmountNotEqualToMsgValue(uint256 gotAmount, uint256 msgValue); + + string public constant override typeAndVersion = "EtherSenderReceiver 1.5.0"; + + /// @notice The wrapped native token address. + /// @dev If the wrapped native token address changes on the router, this contract will need to be redeployed. + IWrappedNative public immutable i_weth; + + /// @param router The CCIP router address. + constructor(address router) CCIPReceiver(router) { + i_weth = IWrappedNative(CCIPRouter(router).getWrappedNative()); + i_weth.approve(router, type(uint256).max); + } + + /// @notice Need this in order to unwrap correctly. + receive() external payable {} + + /// @notice Get the fee for sending a message to a destination chain. + /// This is mirrored from the router for convenience, construct the appropriate + /// message and get it's fee. + /// @param destinationChainSelector The destination chainSelector + /// @param message The cross-chain CCIP message including data and/or tokens + /// @return fee returns execution fee for the message + /// delivery to destination chain, denominated in the feeToken specified in the message. + /// @dev Reverts with appropriate reason upon invalid message. + function getFee( + uint64 destinationChainSelector, + Client.EVM2AnyMessage calldata message + ) external view returns (uint256 fee) { + Client.EVM2AnyMessage memory validatedMessage = _validatedMessage(message); + + return IRouterClient(getRouter()).getFee(destinationChainSelector, validatedMessage); + } + + /// @notice Send raw native tokens cross-chain. + /// @param destinationChainSelector The destination chain selector. + /// @param message The CCIP message with the following fields correctly set: + /// - bytes receiver: The _contract_ address on the destination chain that will receive the wrapped ether. + /// The caller must ensure that this contract address is correct, otherwise funds may be lost forever. + /// - address feeToken: The fee token address. Must be address(0) for native tokens, or a supported CCIP fee token otherwise (i.e, LINK token). + /// In the event a feeToken is set, we will transferFrom the caller the fee amount before sending the message, in order to forward them to the router. + /// - EVMTokenAmount[] tokenAmounts: The tokenAmounts array must contain a single element with the following fields: + /// - uint256 amount: The amount of ether to send. + /// There are a couple of cases here that depend on the fee token specified: + /// 1. If feeToken == address(0), the fee must be included in msg.value. Therefore tokenAmounts[0].amount must be less than msg.value, + /// and the difference will be used as the fee. + /// 2. If feeToken != address(0), the fee is not included in msg.value, and tokenAmounts[0].amount must be equal to msg.value. + /// these fees to the CCIP router. + /// @return messageId The CCIP message ID. + function ccipSend( + uint64 destinationChainSelector, + Client.EVM2AnyMessage calldata message + ) external payable returns (bytes32) { + _validateFeeToken(message); + Client.EVM2AnyMessage memory validatedMessage = _validatedMessage(message); + + i_weth.deposit{value: validatedMessage.tokenAmounts[0].amount}(); + + uint256 fee = IRouterClient(getRouter()).getFee(destinationChainSelector, validatedMessage); + if (validatedMessage.feeToken != address(0)) { + // If the fee token is not native, we need to transfer the fee to this contract and re-approve it to the router. + // Its not possible to have any leftover tokens in this path because we transferFrom the exact fee that CCIP + // requires from the caller. + IERC20(validatedMessage.feeToken).safeTransferFrom(msg.sender, address(this), fee); + + // We gave an infinite approval of weth to the router in the constructor. + if (validatedMessage.feeToken != address(i_weth)) { + IERC20(validatedMessage.feeToken).approve(getRouter(), fee); + } + + return IRouterClient(getRouter()).ccipSend(destinationChainSelector, validatedMessage); + } + + // We don't want to keep any excess ether in this contract, so we send over the entire address(this).balance as the fee. + // CCIP will revert if the fee is insufficient, so we don't need to check here. + return IRouterClient(getRouter()).ccipSend{value: address(this).balance}(destinationChainSelector, validatedMessage); + } + + /// @notice Validate the message content. + /// @dev Only allows a single token to be sent. Always overwritten to be address(i_weth) + /// and receiver is always msg.sender. + function _validatedMessage(Client.EVM2AnyMessage calldata message) + internal + view + returns (Client.EVM2AnyMessage memory) + { + Client.EVM2AnyMessage memory validatedMessage = message; + + if (validatedMessage.tokenAmounts.length != 1) { + revert InvalidTokenAmounts(validatedMessage.tokenAmounts.length); + } + + validatedMessage.data = abi.encode(msg.sender); + validatedMessage.tokenAmounts[0].token = address(i_weth); + + return validatedMessage; + } + + function _validateFeeToken(Client.EVM2AnyMessage calldata message) internal view { + uint256 tokenAmount = message.tokenAmounts[0].amount; + + if (message.feeToken != address(0)) { + // If the fee token is NOT native, then the token amount must be equal to msg.value. + // This is done to ensure that there is no leftover ether in this contract. + if (msg.value != tokenAmount) { + revert TokenAmountNotEqualToMsgValue(tokenAmount, msg.value); + } + } + } + + /// @notice Receive the wrapped ether, unwrap it, and send it to the specified EOA in the data field. + /// @param message The CCIP message containing the wrapped ether amount and the final receiver. + /// @dev The code below should never revert if the message being is valid according + /// to the above _validatedMessage and _validateFeeToken functions. + function _ccipReceive(Client.Any2EVMMessage memory message) internal override { + address receiver = abi.decode(message.data, (address)); + + if (message.destTokenAmounts.length != 1) { + revert InvalidTokenAmounts(message.destTokenAmounts.length); + } + + if (message.destTokenAmounts[0].token != address(i_weth)) { + revert InvalidToken(message.destTokenAmounts[0].token, address(i_weth)); + } + + uint256 tokenAmount = message.destTokenAmounts[0].amount; + i_weth.withdraw(tokenAmount); + + // it is possible that the below call may fail if receiver.code.length > 0 and the contract + // doesn't e.g have a receive() or a fallback() function. + (bool success,) = payable(receiver).call{value: tokenAmount}(""); + if (!success) { + // We have a few options here: + // 1. Revert: this is bad generally because it may mean that these tokens are stuck. + // 2. Store the tokens in a mapping and allow the user to withdraw them with another tx. + // 3. Send WETH to the receiver address. + // We opt for (3) here because at least the receiver will have the funds and can unwrap them if needed. + // However it is worth noting that if receiver is actually a contract AND the contract _cannot_ withdraw + // the WETH, then the WETH will be stuck in this contract. + i_weth.deposit{value: tokenAmount}(); + i_weth.transfer(receiver, tokenAmount); + } + } +} diff --git a/contracts/src/v0.8/ccip/applications/PingPongDemo.sol b/contracts/src/v0.8/ccip/applications/PingPongDemo.sol new file mode 100644 index 00000000000..423fdc45467 --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/PingPongDemo.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IRouterClient} from "../interfaces/IRouterClient.sol"; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {Client} from "../libraries/Client.sol"; +import {CCIPReceiver} from "./CCIPReceiver.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +/// @title PingPongDemo - A simple ping-pong contract for demonstrating cross-chain communication +contract PingPongDemo is CCIPReceiver, OwnerIsCreator, ITypeAndVersion { + event Ping(uint256 pingPongCount); + event Pong(uint256 pingPongCount); + + // The chain ID of the counterpart ping pong contract + uint64 internal s_counterpartChainSelector; + // The contract address of the counterpart ping pong contract + address internal s_counterpartAddress; + // Pause ping-ponging + bool private s_isPaused; + // The fee token used to pay for CCIP transactions + IERC20 internal s_feeToken; + + constructor(address router, IERC20 feeToken) CCIPReceiver(router) { + s_isPaused = false; + s_feeToken = feeToken; + s_feeToken.approve(address(router), type(uint256).max); + } + + function typeAndVersion() external pure virtual returns (string memory) { + return "PingPongDemo 1.2.0"; + } + + function setCounterpart(uint64 counterpartChainSelector, address counterpartAddress) external onlyOwner { + s_counterpartChainSelector = counterpartChainSelector; + s_counterpartAddress = counterpartAddress; + } + + function startPingPong() external onlyOwner { + s_isPaused = false; + _respond(1); + } + + function _respond(uint256 pingPongCount) internal virtual { + if (pingPongCount & 1 == 1) { + emit Ping(pingPongCount); + } else { + emit Pong(pingPongCount); + } + bytes memory data = abi.encode(pingPongCount); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(s_counterpartAddress), + data: data, + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: "", + feeToken: address(s_feeToken) + }); + IRouterClient(getRouter()).ccipSend(s_counterpartChainSelector, message); + } + + function _ccipReceive(Client.Any2EVMMessage memory message) internal override { + uint256 pingPongCount = abi.decode(message.data, (uint256)); + if (!s_isPaused) { + _respond(pingPongCount + 1); + } + } + + ///////////////////////////////////////////////////////////////////// + // Plumbing + ///////////////////////////////////////////////////////////////////// + + function getCounterpartChainSelector() external view returns (uint64) { + return s_counterpartChainSelector; + } + + function setCounterpartChainSelector(uint64 chainSelector) external onlyOwner { + s_counterpartChainSelector = chainSelector; + } + + function getCounterpartAddress() external view returns (address) { + return s_counterpartAddress; + } + + function getFeeToken() external view returns (IERC20) { + return s_feeToken; + } + + function setCounterpartAddress(address addr) external onlyOwner { + s_counterpartAddress = addr; + } + + function isPaused() external view returns (bool) { + return s_isPaused; + } + + function setPaused(bool pause) external onlyOwner { + s_isPaused = pause; + } +} diff --git a/contracts/src/v0.8/ccip/applications/SelfFundedPingPong.sol b/contracts/src/v0.8/ccip/applications/SelfFundedPingPong.sol new file mode 100644 index 00000000000..80bc7bb24ab --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/SelfFundedPingPong.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Router} from "../Router.sol"; +import {Client} from "../libraries/Client.sol"; +import {EVM2EVMOnRamp} from "../onRamp/EVM2EVMOnRamp.sol"; +import {PingPongDemo} from "./PingPongDemo.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract SelfFundedPingPong is PingPongDemo { + string public constant override typeAndVersion = "SelfFundedPingPong 1.2.0"; + + event Funded(); + event CountIncrBeforeFundingSet(uint8 countIncrBeforeFunding); + + // Defines the increase in ping pong count before self-funding is attempted. + // Set to 0 to disable auto-funding, auto-funding only works for ping-pongs that are set as NOPs in the onRamp. + uint8 private s_countIncrBeforeFunding; + + constructor(address router, IERC20 feeToken, uint8 roundTripsBeforeFunding) PingPongDemo(router, feeToken) { + // PingPong count increases by 2 for each round trip. + s_countIncrBeforeFunding = roundTripsBeforeFunding * 2; + } + + function _respond(uint256 pingPongCount) internal override { + if (pingPongCount & 1 == 1) { + emit Ping(pingPongCount); + } else { + emit Pong(pingPongCount); + } + + fundPingPong(pingPongCount); + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(s_counterpartAddress), + data: abi.encode(pingPongCount), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: "", + feeToken: address(s_feeToken) + }); + Router(getRouter()).ccipSend(s_counterpartChainSelector, message); + } + + /// @notice A function that is responsible for funding this contract. + /// The contract can only be funded if it is set as a nop in the target onRamp. + /// In case your contract is not a nop you can prevent this function from being called by setting s_countIncrBeforeFunding=0. + function fundPingPong(uint256 pingPongCount) public { + // If selfFunding is disabled, or ping pong count has not reached s_countIncrPerFunding, do not attempt funding. + if (s_countIncrBeforeFunding == 0 || pingPongCount < s_countIncrBeforeFunding) return; + + // Ping pong on one side will always be even, one side will always to odd. + if (pingPongCount % s_countIncrBeforeFunding <= 1) { + EVM2EVMOnRamp(Router(getRouter()).getOnRamp(s_counterpartChainSelector)).payNops(); + emit Funded(); + } + } + + function getCountIncrBeforeFunding() external view returns (uint8) { + return s_countIncrBeforeFunding; + } + + function setCountIncrBeforeFunding(uint8 countIncrBeforeFunding) external onlyOwner { + s_countIncrBeforeFunding = countIncrBeforeFunding; + emit CountIncrBeforeFundingSet(countIncrBeforeFunding); + } +} diff --git a/contracts/src/v0.8/ccip/applications/TokenProxy.sol b/contracts/src/v0.8/ccip/applications/TokenProxy.sol new file mode 100644 index 00000000000..6fd26c076bc --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/TokenProxy.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {IRouterClient} from "../interfaces/IRouterClient.sol"; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {Client} from "../libraries/Client.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract TokenProxy is OwnerIsCreator { + using SafeERC20 for IERC20; + + error InvalidToken(); + error NoDataAllowed(); + error GasShouldBeZero(); + + /// @notice The CCIP router contract + IRouterClient internal immutable i_ccipRouter; + /// @notice Only this token is allowed to be sent using this proxy + address internal immutable i_token; + + constructor(address router, address token) OwnerIsCreator() { + i_ccipRouter = IRouterClient(router); + i_token = token; + // Approve the router to spend an unlimited amount of tokens to reduce + // gas cost per tx. + IERC20(token).approve(router, type(uint256).max); + } + + /// @notice Simply forwards the request to the CCIP router and returns the result. + /// @param destinationChainSelector The destination chainSelector + /// @param message The cross-chain CCIP message including data and/or tokens + /// @return fee returns execution fee for the message delivery to destination chain, + /// denominated in the feeToken specified in the message. + /// @dev Reverts with appropriate reason upon invalid message. + function getFee( + uint64 destinationChainSelector, + Client.EVM2AnyMessage calldata message + ) external view returns (uint256 fee) { + _validateMessage(message); + return i_ccipRouter.getFee(destinationChainSelector, message); + } + + /// @notice Validates the message content, forwards it to the CCIP router and returns the result. + function ccipSend( + uint64 destinationChainSelector, + Client.EVM2AnyMessage calldata message + ) external payable returns (bytes32 messageId) { + _validateMessage(message); + if (message.feeToken != address(0)) { + // This path is probably warmed up already so the extra cost isn't too bad. + uint256 feeAmount = i_ccipRouter.getFee(destinationChainSelector, message); + IERC20(message.feeToken).safeTransferFrom(msg.sender, address(this), feeAmount); + IERC20(message.feeToken).approve(address(i_ccipRouter), feeAmount); + } + + // Transfer the tokens from the sender to this contract. + IERC20(message.tokenAmounts[0].token).transferFrom(msg.sender, address(this), message.tokenAmounts[0].amount); + + return i_ccipRouter.ccipSend{value: msg.value}(destinationChainSelector, message); + } + + /// @notice Validates the message content. + /// @dev Only allows a single token to be sent, and no data. + function _validateMessage(Client.EVM2AnyMessage calldata message) internal view { + if (message.tokenAmounts.length != 1 || message.tokenAmounts[0].token != i_token) revert InvalidToken(); + if (message.data.length > 0) revert NoDataAllowed(); + + if (message.extraArgs.length == 0 || bytes4(message.extraArgs) != Client.EVM_EXTRA_ARGS_V1_TAG) { + revert GasShouldBeZero(); + } + + if (abi.decode(message.extraArgs[4:], (Client.EVMExtraArgsV1)).gasLimit != 0) revert GasShouldBeZero(); + } + + /// @notice Returns the CCIP router contract. + function getRouter() external view returns (IRouterClient) { + return i_ccipRouter; + } + + /// @notice Returns the token that this proxy is allowed to send. + function getToken() external view returns (address) { + return i_token; + } +} diff --git a/contracts/src/v0.8/ccip/capability/CCIPConfig.sol b/contracts/src/v0.8/ccip/capability/CCIPConfig.sol new file mode 100644 index 00000000000..40b7a4a2f93 --- /dev/null +++ b/contracts/src/v0.8/ccip/capability/CCIPConfig.sol @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ICapabilityConfiguration} from "../../keystone/interfaces/ICapabilityConfiguration.sol"; +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {ICapabilitiesRegistry} from "./interfaces/ICapabilitiesRegistry.sol"; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; + +import {SortedSetValidationUtil} from "../../shared/util/SortedSetValidationUtil.sol"; +import {Internal} from "../libraries/Internal.sol"; +import {CCIPConfigTypes} from "./libraries/CCIPConfigTypes.sol"; + +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol"; +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice CCIPConfig stores the configuration for the CCIP capability. +/// We have two classes of configuration: chain configuration and DON (in the CapabilitiesRegistry sense) configuration. +/// Each chain will have a single configuration which includes information like the router address. +/// Each CR DON will have up to four configurations: for each of (commit, exec), one blue and one green configuration. +/// This is done in order to achieve "blue-green" deployments. +contract CCIPConfig is ITypeAndVersion, ICapabilityConfiguration, OwnerIsCreator, IERC165 { + using EnumerableSet for EnumerableSet.UintSet; + + /// @notice Emitted when a chain's configuration is set. + /// @param chainSelector The chain selector. + /// @param chainConfig The chain configuration. + event ChainConfigSet(uint64 chainSelector, CCIPConfigTypes.ChainConfig chainConfig); + + /// @notice Emitted when a chain's configuration is removed. + /// @param chainSelector The chain selector. + event ChainConfigRemoved(uint64 chainSelector); + + error ChainConfigNotSetForChain(uint64 chainSelector); + error NodeNotInRegistry(bytes32 p2pId); + error OnlyCapabilitiesRegistryCanCall(); + error ChainSelectorNotFound(uint64 chainSelector); + error ChainSelectorNotSet(); + error TooManyOCR3Configs(); + error TooManySigners(); + error TooManyTransmitters(); + error TooManyBootstrapP2PIds(); + error P2PIdsLengthNotMatching(uint256 p2pIdsLength, uint256 signersLength, uint256 transmittersLength); + error NotEnoughTransmitters(uint256 got, uint256 minimum); + error FMustBePositive(); + error FChainMustBePositive(); + error FTooHigh(); + error InvalidPluginType(); + error OfframpAddressCannotBeZero(); + error InvalidConfigLength(uint256 length); + error InvalidConfigStateTransition( + CCIPConfigTypes.ConfigState currentState, CCIPConfigTypes.ConfigState proposedState + ); + error NonExistentConfigTransition(); + error WrongConfigCount(uint64 got, uint64 expected); + error WrongConfigDigest(bytes32 got, bytes32 expected); + error WrongConfigDigestBlueGreen(bytes32 got, bytes32 expected); + + /// @notice Type and version override. + string public constant override typeAndVersion = "CCIPConfig 1.6.0-dev"; + + /// @notice The canonical capabilities registry address. + address internal immutable i_capabilitiesRegistry; + + /// @notice chain configuration for each chain that CCIP is deployed on. + mapping(uint64 chainSelector => CCIPConfigTypes.ChainConfig chainConfig) internal s_chainConfigurations; + + /// @notice All chains that are configured. + EnumerableSet.UintSet internal s_remoteChainSelectors; + + /// @notice OCR3 configurations for each DON. + /// Each CR DON will have a commit and execution configuration. + /// This means that a DON can have up to 4 configurations, since we are implementing blue/green deployments. + mapping( + uint32 donId => mapping(Internal.OCRPluginType pluginType => CCIPConfigTypes.OCR3ConfigWithMeta[] ocr3Configs) + ) internal s_ocr3Configs; + + /// @notice The DONs that have been configured. + EnumerableSet.UintSet internal s_donIds; + + uint8 internal constant MAX_OCR3_CONFIGS_PER_PLUGIN = 2; + uint8 internal constant MAX_OCR3_CONFIGS_PER_DON = 4; + uint8 internal constant MAX_NUM_ORACLES = 31; + + /// @param capabilitiesRegistry the canonical capabilities registry address. + constructor(address capabilitiesRegistry) { + i_capabilitiesRegistry = capabilitiesRegistry; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == type(ICapabilityConfiguration).interfaceId || interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ + // │ Config Getters │ + // ================================================================ + + /// @notice Returns all the chain configurations. + /// @return The chain configurations. + // TODO: will this eventually hit the RPC max response size limit? + function getAllChainConfigs() external view returns (CCIPConfigTypes.ChainConfigInfo[] memory) { + uint256[] memory chainSelectors = s_remoteChainSelectors.values(); + CCIPConfigTypes.ChainConfigInfo[] memory chainConfigs = + new CCIPConfigTypes.ChainConfigInfo[](s_remoteChainSelectors.length()); + for (uint256 i = 0; i < chainSelectors.length; ++i) { + uint64 chainSelector = uint64(chainSelectors[i]); + chainConfigs[i] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: chainSelector, + chainConfig: s_chainConfigurations[chainSelector] + }); + } + return chainConfigs; + } + + /// @notice Returns the OCR configuration for the given don ID and plugin type. + /// @param donId The DON ID. + /// @param pluginType The plugin type. + /// @return The OCR3 configurations, up to 2 (blue and green). + function getOCRConfig( + uint32 donId, + Internal.OCRPluginType pluginType + ) external view returns (CCIPConfigTypes.OCR3ConfigWithMeta[] memory) { + return s_ocr3Configs[donId][pluginType]; + } + + // ================================================================ + // │ Capability Configuration │ + // ================================================================ + + /// @inheritdoc ICapabilityConfiguration + /// @dev The CCIP capability will fetch the configuration needed directly from this contract. + /// The offchain syncer will call this function, however, so its important that it doesn't revert. + function getCapabilityConfiguration(uint32 /* donId */ ) external pure override returns (bytes memory configuration) { + return bytes(""); + } + + /// @notice Called by the registry prior to the config being set for a particular DON. + function beforeCapabilityConfigSet( + bytes32[] calldata, /* nodes */ + bytes calldata config, + uint64, /* configCount */ + uint32 donId + ) external override { + if (msg.sender != i_capabilitiesRegistry) { + revert OnlyCapabilitiesRegistryCanCall(); + } + + CCIPConfigTypes.OCR3Config[] memory ocr3Configs = abi.decode(config, (CCIPConfigTypes.OCR3Config[])); + (CCIPConfigTypes.OCR3Config[] memory commitConfigs, CCIPConfigTypes.OCR3Config[] memory execConfigs) = + _groupByPluginType(ocr3Configs); + if (commitConfigs.length > 0) { + _updatePluginConfig(donId, Internal.OCRPluginType.Commit, commitConfigs); + } + if (execConfigs.length > 0) { + _updatePluginConfig(donId, Internal.OCRPluginType.Execution, execConfigs); + } + } + + function _updatePluginConfig( + uint32 donId, + Internal.OCRPluginType pluginType, + CCIPConfigTypes.OCR3Config[] memory newConfig + ) internal { + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = s_ocr3Configs[donId][pluginType]; + + // Validate the state transition being proposed, which is implicitly defined by the combination + // of lengths of the current and new configurations. + CCIPConfigTypes.ConfigState currentState = _stateFromConfigLength(currentConfig.length); + CCIPConfigTypes.ConfigState proposedState = _stateFromConfigLength(newConfig.length); + _validateConfigStateTransition(currentState, proposedState); + + // Build the new configuration with metadata and validate that the transition is valid. + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfigWithMeta = + _computeNewConfigWithMeta(donId, currentConfig, newConfig, currentState, proposedState); + _validateConfigTransition(currentConfig, newConfigWithMeta); + + // Update contract state with new configuration if its valid. + // We won't run out of gas from this delete since the array is at most 2 elements long. + delete s_ocr3Configs[donId][pluginType]; + for (uint256 i = 0; i < newConfigWithMeta.length; ++i) { + s_ocr3Configs[donId][pluginType].push(newConfigWithMeta[i]); + } + } + + // ================================================================ + // │ Config State Machine │ + // ================================================================ + + /// @notice Determine the config state of the configuration from the length of the config. + /// @param configLen The length of the configuration. + /// @return The config state. + function _stateFromConfigLength(uint256 configLen) internal pure returns (CCIPConfigTypes.ConfigState) { + if (configLen > 2) { + revert InvalidConfigLength(configLen); + } + return CCIPConfigTypes.ConfigState(configLen); + } + + // the only valid state transitions are the following: + // init -> running (first ever config) + // running -> staging (blue/green proposal) + // staging -> running (promotion) + // everything else is invalid and should revert. + function _validateConfigStateTransition( + CCIPConfigTypes.ConfigState currentState, + CCIPConfigTypes.ConfigState newState + ) internal pure { + // Calculate the difference between the new state and the current state + int256 stateDiff = int256(uint256(newState)) - int256(uint256(currentState)); + + // Check if the state transition is valid: + // Valid transitions: + // 1. currentState -> newState (where stateDiff == 1) + // e.g., init -> running or running -> staging + // 2. staging -> running (where stateDiff == -1) + if (stateDiff == 1 || (stateDiff == -1 && currentState == CCIPConfigTypes.ConfigState.Staging)) { + return; + } + revert InvalidConfigStateTransition(currentState, newState); + } + + function _validateConfigTransition( + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig, + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfigWithMeta + ) internal pure { + uint256 currentConfigLen = currentConfig.length; + uint256 newConfigLen = newConfigWithMeta.length; + if (currentConfigLen == 0 && newConfigLen == 1) { + // Config counts always must start at 1 for the first ever config. + if (newConfigWithMeta[0].configCount != 1) { + revert WrongConfigCount(newConfigWithMeta[0].configCount, 1); + } + return; + } + + if (currentConfigLen == 1 && newConfigLen == 2) { + // On a blue/green proposal: + // * the config digest of the blue config must remain unchanged. + // * the green config count must be the blue config count + 1. + if (newConfigWithMeta[0].configDigest != currentConfig[0].configDigest) { + revert WrongConfigDigestBlueGreen(newConfigWithMeta[0].configDigest, currentConfig[0].configDigest); + } + if (newConfigWithMeta[1].configCount != currentConfig[0].configCount + 1) { + revert WrongConfigCount(newConfigWithMeta[1].configCount, currentConfig[0].configCount + 1); + } + return; + } + + if (currentConfigLen == 2 && newConfigLen == 1) { + // On a promotion, the green config digest must become the blue config digest. + if (newConfigWithMeta[0].configDigest != currentConfig[1].configDigest) { + revert WrongConfigDigest(newConfigWithMeta[0].configDigest, currentConfig[1].configDigest); + } + return; + } + + revert NonExistentConfigTransition(); + } + + /// @notice Computes a new configuration with metadata based on the current configuration and the new configuration. + /// @param donId The DON ID. + /// @param currentConfig The current configuration, including metadata. + /// @param newConfig The new configuration, without metadata. + /// @param currentState The current state of the configuration. + /// @param newState The new state of the configuration. + /// @return The new configuration with metadata. + function _computeNewConfigWithMeta( + uint32 donId, + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig, + CCIPConfigTypes.OCR3Config[] memory newConfig, + CCIPConfigTypes.ConfigState currentState, + CCIPConfigTypes.ConfigState newState + ) internal view returns (CCIPConfigTypes.OCR3ConfigWithMeta[] memory) { + uint64[] memory configCounts = new uint64[](newConfig.length); + + // Set config counts based on the only valid state transitions. + // Init -> Running (first ever config) + // Running -> Staging (blue/green proposal) + // Staging -> Running (promotion) + if (currentState == CCIPConfigTypes.ConfigState.Init && newState == CCIPConfigTypes.ConfigState.Running) { + // First ever config starts with config count == 1. + configCounts[0] = 1; + } else if (currentState == CCIPConfigTypes.ConfigState.Running && newState == CCIPConfigTypes.ConfigState.Staging) { + // On a blue/green proposal, the config count of the green config is the blue config count + 1. + configCounts[0] = currentConfig[0].configCount; + configCounts[1] = currentConfig[0].configCount + 1; + } else if (currentState == CCIPConfigTypes.ConfigState.Staging && newState == CCIPConfigTypes.ConfigState.Running) { + // On a promotion, the config count of the green config becomes the blue config count. + configCounts[0] = currentConfig[1].configCount; + } else { + revert InvalidConfigStateTransition(currentState, newState); + } + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfigWithMeta = + new CCIPConfigTypes.OCR3ConfigWithMeta[](newConfig.length); + for (uint256 i = 0; i < configCounts.length; ++i) { + _validateConfig(newConfig[i]); + newConfigWithMeta[i] = CCIPConfigTypes.OCR3ConfigWithMeta({ + config: newConfig[i], + configCount: configCounts[i], + configDigest: _computeConfigDigest(donId, configCounts[i], newConfig[i]) + }); + } + + return newConfigWithMeta; + } + + /// @notice Group the OCR3 configurations by plugin type for further processing. + /// @param ocr3Configs The OCR3 configurations to group. + function _groupByPluginType(CCIPConfigTypes.OCR3Config[] memory ocr3Configs) + internal + pure + returns (CCIPConfigTypes.OCR3Config[] memory commitConfigs, CCIPConfigTypes.OCR3Config[] memory execConfigs) + { + if (ocr3Configs.length > MAX_OCR3_CONFIGS_PER_DON) { + revert TooManyOCR3Configs(); + } + + // Declare with size 2 since we have a maximum of two configs per plugin type (blue, green). + // If we have less we will adjust the length later using mstore. + // If the caller provides more than 2 configs per plugin type, we will revert due to out of bounds + // access in the for loop below. + commitConfigs = new CCIPConfigTypes.OCR3Config[](MAX_OCR3_CONFIGS_PER_PLUGIN); + execConfigs = new CCIPConfigTypes.OCR3Config[](MAX_OCR3_CONFIGS_PER_PLUGIN); + uint256 commitCount; + uint256 execCount; + for (uint256 i = 0; i < ocr3Configs.length; ++i) { + if (ocr3Configs[i].pluginType == Internal.OCRPluginType.Commit) { + commitConfigs[commitCount] = ocr3Configs[i]; + ++commitCount; + } else { + execConfigs[execCount] = ocr3Configs[i]; + ++execCount; + } + } + + // Adjust the length of the arrays to the actual number of configs. + assembly { + mstore(commitConfigs, commitCount) + mstore(execConfigs, execCount) + } + + return (commitConfigs, execConfigs); + } + + function _validateConfig(CCIPConfigTypes.OCR3Config memory cfg) internal view { + if (cfg.chainSelector == 0) revert ChainSelectorNotSet(); + if (cfg.pluginType != Internal.OCRPluginType.Commit && cfg.pluginType != Internal.OCRPluginType.Execution) { + revert InvalidPluginType(); + } + // TODO: can we do more sophisticated validation than this? + if (cfg.offrampAddress.length == 0) revert OfframpAddressCannotBeZero(); + if (!s_remoteChainSelectors.contains(cfg.chainSelector)) revert ChainSelectorNotFound(cfg.chainSelector); + + // Some of these checks below are done in OCR2/3Base config validation, so we do them again here. + // Role DON OCR configs will have all the Role DON signers but only a subset of transmitters. + if (cfg.signers.length > MAX_NUM_ORACLES) revert TooManySigners(); + if (cfg.transmitters.length > MAX_NUM_ORACLES) revert TooManyTransmitters(); + + // We check for chain config presence above, so fChain here must be non-zero. + uint256 minTransmittersLength = 3 * s_chainConfigurations[cfg.chainSelector].fChain + 1; + if (cfg.transmitters.length < minTransmittersLength) { + revert NotEnoughTransmitters(cfg.transmitters.length, minTransmittersLength); + } + if (cfg.F == 0) revert FMustBePositive(); + if (cfg.signers.length <= 3 * cfg.F) revert FTooHigh(); + + if (cfg.p2pIds.length != cfg.signers.length || cfg.p2pIds.length != cfg.transmitters.length) { + revert P2PIdsLengthNotMatching(cfg.p2pIds.length, cfg.signers.length, cfg.transmitters.length); + } + if (cfg.bootstrapP2PIds.length > cfg.p2pIds.length) revert TooManyBootstrapP2PIds(); + + // check for duplicate p2p ids and bootstrapP2PIds. + // check that p2p ids in cfg.bootstrapP2PIds are included in cfg.p2pIds. + SortedSetValidationUtil._checkIsValidUniqueSubset(cfg.bootstrapP2PIds, cfg.p2pIds); + + // Check that the readers are in the capabilities registry. + for (uint256 i = 0; i < cfg.signers.length; ++i) { + _ensureInRegistry(cfg.p2pIds[i]); + } + } + + /// @notice Computes the digest of the provided configuration. + /// @dev In traditional OCR config digest computation, block.chainid and address(this) are used + /// in order to further domain separate the digest. We can't do that here since the digest will + /// be used on remote chains; so we use the chain selector instead of block.chainid. The don ID + /// replaces the address(this) in the traditional computation. + /// @param donId The DON ID. + /// @param configCount The configuration count. + /// @param ocr3Config The OCR3 configuration. + /// @return The computed digest. + function _computeConfigDigest( + uint32 donId, + uint64 configCount, + CCIPConfigTypes.OCR3Config memory ocr3Config + ) internal pure returns (bytes32) { + uint256 h = uint256( + keccak256( + abi.encode( + ocr3Config.chainSelector, + donId, + ocr3Config.pluginType, + ocr3Config.offrampAddress, + configCount, + ocr3Config.bootstrapP2PIds, + ocr3Config.p2pIds, + ocr3Config.signers, + ocr3Config.transmitters, + ocr3Config.F, + ocr3Config.offchainConfigVersion, + ocr3Config.offchainConfig + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x000a << (256 - 16); // 0x000a00..00 + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } + + // ================================================================ + // │ Chain Configuration │ + // ================================================================ + + /// @notice Sets and/or removes chain configurations. + /// @param chainSelectorRemoves The chain configurations to remove. + /// @param chainConfigAdds The chain configurations to add. + function applyChainConfigUpdates( + uint64[] calldata chainSelectorRemoves, + CCIPConfigTypes.ChainConfigInfo[] calldata chainConfigAdds + ) external onlyOwner { + // Process removals first. + for (uint256 i = 0; i < chainSelectorRemoves.length; ++i) { + // check if the chain selector is in s_remoteChainSelectors first. + if (!s_remoteChainSelectors.contains(chainSelectorRemoves[i])) { + revert ChainSelectorNotFound(chainSelectorRemoves[i]); + } + + delete s_chainConfigurations[chainSelectorRemoves[i]]; + s_remoteChainSelectors.remove(chainSelectorRemoves[i]); + + emit ChainConfigRemoved(chainSelectorRemoves[i]); + } + + // Process additions next. + for (uint256 i = 0; i < chainConfigAdds.length; ++i) { + CCIPConfigTypes.ChainConfig memory chainConfig = chainConfigAdds[i].chainConfig; + bytes32[] memory readers = chainConfig.readers; + uint64 chainSelector = chainConfigAdds[i].chainSelector; + + // Verify that the provided readers are present in the capabilities registry. + for (uint256 j = 0; j < readers.length; j++) { + _ensureInRegistry(readers[j]); + } + + // Verify that fChain is positive. + if (chainConfig.fChain == 0) { + revert FChainMustBePositive(); + } + + s_chainConfigurations[chainSelector] = chainConfig; + s_remoteChainSelectors.add(chainSelector); + + emit ChainConfigSet(chainSelector, chainConfig); + } + } + + /// @notice Helper function to ensure that a node is in the capabilities registry. + /// @param p2pId The P2P ID of the node to check. + function _ensureInRegistry(bytes32 p2pId) internal view { + ICapabilitiesRegistry.NodeInfo memory node = ICapabilitiesRegistry(i_capabilitiesRegistry).getNode(p2pId); + if (node.p2pId == bytes32("")) { + revert NodeNotInRegistry(p2pId); + } + } +} diff --git a/contracts/src/v0.8/ccip/capability/interfaces/ICapabilitiesRegistry.sol b/contracts/src/v0.8/ccip/capability/interfaces/ICapabilitiesRegistry.sol new file mode 100644 index 00000000000..621c3686cfa --- /dev/null +++ b/contracts/src/v0.8/ccip/capability/interfaces/ICapabilitiesRegistry.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +interface ICapabilitiesRegistry { + struct NodeInfo { + /// @notice The id of the node operator that manages this node + uint32 nodeOperatorId; + /// @notice The number of times the node's configuration has been updated + uint32 configCount; + /// @notice The ID of the Workflow DON that the node belongs to. A node can + /// only belong to one DON that accepts Workflows. + uint32 workflowDONId; + /// @notice The signer address for application-layer message verification. + bytes32 signer; + /// @notice This is an Ed25519 public key that is used to identify a node. + /// This key is guaranteed to be unique in the CapabilitiesRegistry. It is + /// used to identify a node in the the P2P network. + bytes32 p2pId; + /// @notice The list of hashed capability IDs supported by the node + bytes32[] hashedCapabilityIds; + /// @notice The list of capabilities DON Ids supported by the node. A node + /// can belong to multiple capabilities DONs. This list does not include a + /// Workflow DON id if the node belongs to one. + uint256[] capabilitiesDONIds; + } + + /// @notice Gets a node's data + /// @param p2pId The P2P ID of the node to query for + /// @return NodeInfo The node data + function getNode(bytes32 p2pId) external view returns (NodeInfo memory); +} diff --git a/contracts/src/v0.8/ccip/capability/interfaces/IOCR3ConfigEncoder.sol b/contracts/src/v0.8/ccip/capability/interfaces/IOCR3ConfigEncoder.sol new file mode 100644 index 00000000000..6d0b0f72a5a --- /dev/null +++ b/contracts/src/v0.8/ccip/capability/interfaces/IOCR3ConfigEncoder.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {CCIPConfigTypes} from "../libraries/CCIPConfigTypes.sol"; + +/// @dev This is so that we can generate gethwrappers and easily encode/decode OCR3Config +/// in the offchain integration tests. +interface IOCR3ConfigEncoder { + /// @dev Encodes an array of OCR3Config into a bytes array. For test usage only. + function exposeOCR3Config(CCIPConfigTypes.OCR3Config[] calldata config) external view returns (bytes memory); +} diff --git a/contracts/src/v0.8/ccip/capability/libraries/CCIPConfigTypes.sol b/contracts/src/v0.8/ccip/capability/libraries/CCIPConfigTypes.sol new file mode 100644 index 00000000000..99adef84b10 --- /dev/null +++ b/contracts/src/v0.8/ccip/capability/libraries/CCIPConfigTypes.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Internal} from "../../libraries/Internal.sol"; + +library CCIPConfigTypes { + /// @notice ConfigState indicates the state of the configuration. + /// A DON's configuration always starts out in the "Init" state - this is the starting state. + /// The only valid transition from "Init" is to the "Running" state - this is the first ever configuration. + /// The only valid transition from "Running" is to the "Staging" state - this is a blue/green proposal. + /// The only valid transition from "Staging" is back to the "Running" state - this is a promotion. + /// TODO: explain rollbacks? + enum ConfigState { + Init, + Running, + Staging + } + + /// @notice Chain configuration. + /// Changes to chain configuration are detected out-of-band in plugins and decoded offchain. + struct ChainConfig { + bytes32[] readers; // The P2P IDs of the readers for the chain. These IDs must be registered in the capabilities registry. + uint8 fChain; // The fault tolerance parameter of the chain. + bytes config; // The chain configuration. This is kept intentionally opaque so as to add fields in the future if needed. + } + + /// @notice Chain configuration information struct used in applyChainConfigUpdates and getAllChainConfigs. + struct ChainConfigInfo { + uint64 chainSelector; + ChainConfig chainConfig; + } + + /// @notice OCR3 configuration. + struct OCR3Config { + Internal.OCRPluginType pluginType; // ────────╮ The plugin that the configuration is for. + uint64 chainSelector; // | The (remote) chain that the configuration is for. + uint8 F; // | The "big F" parameter for the role DON. + uint64 offchainConfigVersion; // ─────────────╯ The version of the offchain configuration. + bytes offrampAddress; // The remote chain offramp address. + // NOTE: bootstrapP2PIds and p2pIds should be sent as sorted sets + bytes32[] bootstrapP2PIds; // The bootstrap P2P IDs of the oracles that are part of the role DON. + // len(p2pIds) == len(signers) == len(transmitters) == 3 * F + 1 + // NOTE: indexes matter here! The p2p ID at index i corresponds to the signer at index i and the transmitter at index i. + // This is crucial in order to build the oracle ID <-> peer ID mapping offchain. + bytes32[] p2pIds; // The P2P IDs of the oracles that are part of the role DON. + bytes[] signers; // The onchain signing keys of nodes in the don. + bytes[] transmitters; // The onchain transmitter keys of nodes in the don. + bytes offchainConfig; // The offchain configuration for the OCR3 protocol. Protobuf encoded. + } + + /// @notice OCR3 configuration with metadata, specifically the config count and the config digest. + struct OCR3ConfigWithMeta { + OCR3Config config; // The OCR3 configuration. + uint64 configCount; // The config count used to compute the config digest. + bytes32 configDigest; // The config digest of the OCR3 configuration. + } +} diff --git a/contracts/src/v0.8/ccip/docs/multi-chain-overview-ocr3.png b/contracts/src/v0.8/ccip/docs/multi-chain-overview-ocr3.png new file mode 100644 index 0000000000000000000000000000000000000000..39302619cb445c67ea075ded0124745a15fe7d03 GIT binary patch literal 818615 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

f`CP(pD`&2Ter2^98|-S%l_*Q0 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 i_LkF&KDp6Cdt;_a>dFJl*yy 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%0AaY>sguW89sxn#7QQeUrT)XqrA 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

PCm9!XMdVF%gIAbsVIomzd5K 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�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

  • HInsX*zj@~VtlZOaD@tpb{rrr+l;wl`M;bUd%quf^+^ z5~v3VuL+SKIHYbYQ)ktJw6gazt|cD>{0Os?jf_&ciU1k64a^=6Srx`xOrUW)W-;>C z6kb7vLER6~s7w<^C*E02FV0YgjyWhy{=7bT!eNvno6MQ3P{kNqq?rdK1GuyO7v83c z#dgO@T$>j?W#DX$o0ZZ!olx}i-GB_|XY@2cXqUQStRYjnlpakid@G-n$<=BP`CjX!+2Y-p zo2>Q^_WS2UvR2r({^{lGSh=d6YDgygYTX)WI-kV`5kPod zPq3cH07U2;R#Y5>0=@YF1on~3EG8xH0lALkzhjjO`wg&%Wo4{7buRj)HQ8ekG>Fts zzU?lPLsL*kkhD`Y!XvyL$0+bPPOfeY&7)7!wr>Ty*uBlwAvTUlX1BDo#6tVHc?o2; z<=2>GT;k=DEGDAmuFyr_zsH$9acvptmAdSS^te39 zFc|v|_n2m#qCG~DYz1OW0l-fNSJ>io$?L|^jYuu^T@3j@+6_C#N zXFuMfVLQfndMrk>Lezr~z~tA;-JE+5dh(ongCgZZe#i^)Rh!IhxJNVx(HZco?$;Onn&^UZ%w=A`PCf0IwWzk^94;HHhIj|e=- z`&bHoqCAP-Sbzrdn2+XWjC6ed2FbCe6anMn^)H7$!0h_IY$hOQ|5Zz}#OC<*gK zL?Sq0t58A`qy4?}${WaED~%LXK~LYkxwb*MtB&*_BS}e_taHb=r}gav&X^ni{5oJ< zU!tA)>G*kEzwX!F@2BCr+}@Wj^Y6?EB6b&S4i!sQK25qxTywe~Kd-GYf01{0-i}{f z{OFxbLcUY{D|dEoM{jhXofHj)J5+ps!&Oh*EOKG2)_51Zzpwn=l7K@5=pihU2;?&gjdR_Pn(n9C+^<#z8LO7VQn5p?W zp3f;kr8`ZL!W&wk3Zy7GF8~Q5>t*yByg2gat z2sm7^@++L4Ut3?H+%P*Ax)rDuO`i51+<7KDf{3+ZZ>~b8BY%9nnH2DY@oC40z!FgIQ7*((11d?IsH0NY_RW*3;&|2Tv`f%rn!- zda9)GGMsL&Y67Hwgy@h&0Pw@F5O1P`QEy!7;GJ0b} zHu*Jh<;ZAdnqlYiEk9{%|GduKSC8v)0Y_-YX`Wh8lpOAX!FGj|_cbot&iIFzn3#MX zr%jB&v%PFMjd9B>wN2T(zQc83|*zEUu@ zkO}1LP&y+FnZ0S207SS6$<1#Lk^9^fA7J1!MtEfG0IgGf;djBxK#h|A%4`Ul74Jnr za&mJ26QDL(myY)%#b=22?2ToL9Mfdq(axL8&c=F0z+;AGU}$*c1wP}DI_nI%(+4)|b{w1!uiW6%e z27AKa`(Ks8AKxUqMnc)X#k{-TAa(RTiDMF8S??Qjz{7$lsdj{wkR5nq0fDs-1R@eU zRZly!|K)|g-vBTCgp*mm(@G{eJ!NkM?2kJ#gy;?cH-dEp1jCUI{|ZR1DLi7_55>jI z>Q1({J~0&)%@!!i=I{zVz)(9to{$0B=ces!v*0_xeMZw>}49Zb|^dq}5+qOI%QmVj0Zs*V#d(ebK4L8&s>{XoE3CAPw! z#vr11qJYh%K8;LL&6MUlt~c@Bq~|dVFE1aePHM5Dd*2@Tee)LyUFzNR6!g6re0b0E zo)bWY%FxLW1Iar{=PheX?@=JhXjZ%qb+S_E?t~|~Jl-ll3iw}1^shy((;Z&-D1YHJ zkJf593mW~BC$2aSXvB)qlR*#zO+yTH06_k9T{9=$e;{8DkW^M(<7+wo$ap?A2u5RM zJy5#39>aTlr!1(zx%gC+r!YV&9?bH33=sD&_7a{hfbbXFo>|_M+kVWywz%iVd67j} zED!%ueh@7^EP)fMK6)@Pje?u|=^8T5+eF#j}W+Ti;_brs#5Fz z1h7L?fQ!Ja(WyuuU+hAt|KD)`*N48lG|YAPk8bK+%e1B+83#oRQHUFL8YfuFprn5Z z`}7#X72Z@r=%f)!`t$4hR^(+MboNpffRPD5r%75bNcOn4~7>L@G(``Dp%H=S4RBvsb3X~b$ z7y}WFcZxe$cnRgEzFeW+J{93IEmSH0k{17_HP?@vCmey{_@POVc-RgwmLhzA6`yV zBse1E^`+m3RaR_pz%Gw7o|BUk_kit)O`~DbajriiddW!8G@$^O%%1LO6TGjdZ}79Nqg_^TYW-I{Y6=A4DZ{M2HOLlHK2PjLH-h zy5uU_+BpKcC-?_o#V6DeL?5Bz>G7Giem*FvgdDQj5!C<4X#f0>ncDaM?h@YL&HH(r zR*?b)gHG9WQfz8|1z;=&Mkff9&{Kq_|fU9C~zVi zn2tlX6^|}=k06Q%Lf@wb(Wsp)WTD>OCEE@4G6w?r6*G727n50$sXn+;v76%`TZ(z$2 zcUa6UO~Qjlj`F!t5DHRCR$5Q`C_IMmwg8`Qe<;e5VdJz>0M0o`iHM5zIKhjHyQ1%L z+Z_Z1sTCpAT3)QFPQCrrhDj|Z9Qd~y`+I$QD$vXB_RH=L%NGY1EG}-Y@nct?Yt`Ts zKGC&%L5wfW1krS90rc{A$eaL?n_}HtY zf7c4Rg6-~gyYgl9vB^NnmMj^r$v;|48RbB0DP+in_Q6GsC?O7s znHKuJJw)oA;Sx%TgXDFGj4dz@eEQK+;5`N`a52fiXGH)9-yR(`Z@)-H)v2@{w`54- z)2#HonaaLcz}M}vHwQ$TN2yqp&zvYQJC9Fy-VG$w!%v^;HmdN?W~g(zGd*YS?>qT58?ePfVj!m^5Ee z($haSCKao=;k-Yu4HTn0r~f-)@eiE)A*wgC-glvU4`p8iD3n5xk#32-!=Ec;0TZ{k z_dl4J4lpqbC^WY+E|tj%^mF2 z`3{^-)6;0NDqMQ{W|Jk8n6Z2JT>u4jRqQV+dSRP?(#khR=_7bV3_YmKO z0M7?Bm|e=ZwEN2Yt&iUqDox)%ttLo-<(NU0U-QC-F^d;MQ!XUTk+2c-S_KEPK=m5C7?N_HkRuk z(!q3XO^}ZS7!FlAdq`KV4L;R_{Mz)sL~BdHVR@odlE*Yd`+lof{|To7P~Mah;XDB) zWcGHwbQT#}(f_x+{wt1s;*nnEXc62+lJp*6h8*A?4+2C`bRmEZV*Oz$AVa&D!6^*y zyojs44iM84x4kenKK~4Md(5?Z9FXAv#kt;tew}owml+`A0szB0HCYRdelt4e)6JkL z{PXBR$pAFJ3T4LCa;ESkG-EyEt(=jL2c7Z(y`l(!61f{i&e!1q_kuEM{_>U5X9p9hfu*BH_M_xLmtS~4!X%ND{t0;9s2ya0B{7~3e``wwpZE}j2M zbxE)tkxzmhj~1MHTu%b~;d{T7g`Q0lhzyPEL7-Id3etK%0BF@!!_g=As;|Xm2uM7U z0Ah2;t-pT}qE$GLQ8dmjasN{&SqQ>rjVyczo30VuoRltWBf3=6xGnm&kswI3g1HjR;wm zJjJ@Geju-0)N~LFL@4s&a|=lyGW^{hPUbhL!Y%=KC!&?C1W#UyTz_HFC->5+Qv>(t z)#xp?K&G1dYf+tyDE~^)f6Vp^q<;u5mLET3oSP|+p@&R9lv^&$qo^d;Q3837m+9;w z?@2=P9vhJN)@FL%8wNj;)Z3&L5pBF%v(toEt*(v}+ONyhPi}2~&jC?r1S|0okg9|6 z@8V+Qdr*@r08XAnoY&OBUG=}FLjX{lQZ9wpy?t zNn@jtgT@_4{BuH9SF8{^o)}Zo-!l?M$^bkX&PLNt_nFr~8cY3D@Ju>_*7*1pGC0Yg%k$ap8~||#lo2_chwvV5N9C8)yK#1-@)HatMHG`I(+nnhk#Ok&s^Q>bUWsi z&heZ_J-du(@7dnBvh29HtdRfc&%d3O+1*#=biSitXJQEK?H%^yK4!}OISf!TrXf6} z&u^&^E;|lrr3;CSw9ZK3)e#%dI%A0Cp-1;+Ymi&U%KT=bL<@`{LtuhPFgk&af=w9? zE@y`*7=(Edi@RzBjIv|;`udsUC4YyPaEGGp(Ja@h z#m>KZc28(h0cF5br7q5J1AC^}B&ZsypIl*&tLcT0Xu+#xPtx zJrrCrn9tEFNI2%hMb4Ojr{O6{=5|5>3W(Sa#-4pkoexN9)uq-4Sd?Kpy<&gyqrVpd ziMTHrO(ymHZTrMLgSCd@Kh&z5h!DdHJ-(A!!3{VJ4t5Ty<&xC{{rv*JVWtbwi>>1@4H{RdD4Vl>a zlrmk1{}4J~B*}PS6r_zmPI3aZPw=TRWPa-Zy>@|m#_ZIEadUL7F`AtJxf4hb&ANTug*} zck!c^`%><&pS2OI*S{mvOS%7P2ScJciUcxFVCUe_ioN#vhm2!DP|%9sg2HZ1xdPf$ zk4brM;5z58Y|#x&Zn&O2Ho#cx6WbN~Kb{_G6A9=(VG1zmgc;`?e+t~b*xUETS`mhYH(jdbB)iG8Wv$26aKLgy zkwDBnpRcJ$r#76v!{Jj0uDr$g%SqlSLG6)A=4IwddodlI>&iR;TD9{*io`)Pv4V6@ zcQeFCj0k|;pTEh0ED6*7|HxuK(6!Kw1!}=X;*qg2+13kw1qB7x@T>wW5j5}a;1-W* z1$|L)0bc4cZln>|PswL(-3;g^%+z}%Li+ftvNQ?vuE=lw#g{Y^lo}w)RhK+e2~Uqd zAsF-WB|nXcDvE1zVpB^Co$y9Gq(b@S54Vus*l>;l1q=*K7qPi%D7NawyD8H3HY;A~(#N5B+m+9U#h4-#9YD?t(ZZsni9l3U4)*%GXSTzmP!6QYRhH1Hm>^LnGKzs@X zn07@8S~$JOAhMO}ZyWD;ykC>pGEHX!HxO3!M`k1AK?VvU?jjwVu-u>4`^G035WKtk&<^yj|F zn3`G_j38nUJ~Ms{Sg{fs-Da<~3_g2J#xg%?X@8iAE5GcZv1(Z5bH<+yKBR(p-t#u6 zU+g0cQk-{Ww{ZQt)AB180nb#|UfgW<2PV!gu{-KW{1T79KfvB1(jg z*ICS6kl?NL6zm`A8O9`Ge4%ggnPX%i5gP*f4~Vn_*~8Nm0u$+n$OjUs2mb#VWjM_B zzz=lD0CZ-*RRv-fGg?R$%IHsHcF7(}cx|?5O&MS>k;1t_%P34^<~{MVQCLqN;R-bg zbeiZywYAQbOjNSmNozXu-B>50`n@3!=BZ;oIuv==CCC|hVQwKN?}qJR2q@cm9I z?|{xvNIah{V;m%9uww#DA(8hD;~(Jk&8Np8_MMD_0)=}-Ose_{zGXJZXz|&!UE85F zFr@IEe6mK*zEk*-^-okZ{e;HD^Z9_wz+m-AO0#Ji3U553$3j+Z)mEiJOHV&he9O@4 z%H2T(2nh+mOP-h*V6<1GYeFjfZ^{csuh~WQw?IY4aJ9&3^@nDZeiO9(hV>E=obACUXxS#*7AT+sO(S^`@Oxewz{)oi zP?%yR#l7&z?s96JF3rPfWO|t}s*rlk3>ftcu`_^E2)izhfozoqB)b>&0+MZgpH4th z(@7M?FNU?{MsNwv2wxM65jyj0{rP5Rp`AbgOrlT>JUxR~_& z=;KqQAq7kTpM_WR208Y>y<3uh6e~Ml+@&tvfCX6Q4IuzUVPF7yXM+kJ&{7dn8*2lk zblnOM%L`Wydc_IN_Q%?X^fhi8O}2LfHr6EE_Hd^OKNX*;G9>$v%!LJ6_v=;%O0q2| z7hO8ELcg-nP)z|wnMIRvkR|ZCwuK@9zj-b|gYD?C?Ti&^O&XQKHI3`o|Q7EYkp2r)pc3|LtE$@`4C$bs|4PkwJ)c0bjn- zON!>FgjR&}?Gl~P=~@>%+I}eTv|cTCh=szb@6Z3+?%g;b4}vgNOkWp*J0hl5Xbj9N zoTX=S7#RUZr)c!9YGMGFJSQPz5o9j_q82=1jM4tn zrcCvt+~MLB+?r-O%Q0cf{2EU)cTmQbO*EnyPAZywqnQm6uZKo|md03>t?BvpbILw1t4%GLdX@14Cz_VlKn>c%fg*2i~zAMa9X>g=a)6>vp>r zuAr|XUITSB+C7Bz3pPx>!QrlJ(^k7@d?6zyj4Q`uGq#T1A0%<5GE`1OT(a$6_{Syl zIpc0b+eUFtSC}DCi^CJ~d7*X6=|mT?kB?&vRJx7X=j2ygM09o0ScF zmxliMVRJ{_`yydXdH*0xD&)Rf3Ey#>-TDsSAbgqN#f4K+e?r@}`9w;FJu!#%oUrYi zC%k;KI13)8XR=nKhq+(lU#AyI97%28uyJ4>gpW1e<<_NIFJJY!cdcxP+1tc4X&Q&5 zIfh;pmz0dZ?>j5{m5uH0XCHWC_V$!lpBCAl8o`VyDn$qffF)kHpIt_wkON>3_+*wr zR2DN}`Z>JM_g5}+fJ{f!q@tUMKjbn1P{uJqh_&BgDCSAn1aEVe{E*{~L^}Ws>xmqTiurT*F!D^@gTUgw$OCLt zp65wNA3`T$Y$aw3Swur!mTvd0jPQY?F-M>D84V}Q0+V*)p}p(f5Ipf=>kX5>SuUwq z{3|OKD%s2VMaKk5OfSr5Pxgeb?}Xvb+VXQ-$JP^2-4-u`k-N55LCDnif}Q7qT@Drg zDoag;F%n49rrPA8Ld|KL9Jc8wgU>BTk_J-Pq5TDR(zs%Z$m@<;BX^ihCpoxJr>U(p z*#?n4L1#=`A)|ce`Wc? zX9Fwdc~oF|cW^*jVtZtRA%xTSj6o`fhFtEJ5zW?JsE)@=H;C&tJskHz;AVv65gz9NWa8BBVSx36`D-3(Q=I?Zd(5#*oj zT&YEkFZyR|uOP(5YrGEoVe7ug0=@VVb^GUkGSd%vvUS|_RAS*DW!NB3g7XvQaKHM} z6^VY{`K~QRm^0b^1oH>3|A0Hd{5eK?fP!D@f{K^(a#pFoWK@400}*m7og5p19ut>`fUIj3TS7NS<8o$A5Wf=a*0 z2HHW+gvEjYQV)b#-tQ@+LY`0E0nRFhHU&68uhwjojF}Xd^)iI zs6ywCz^28`g~Mz%lLcKnBb90)jrYCf5CL(V6tMZ9*N-YwtNd@ zxz2FbMqx_{3*)UvOjU(Fd7kVV&*J`c!Uf$RQ9Qp~NBV1HUjrMfL}QQg*T%|_vA=x6 zB8UF$YD=GtN>RHZ+)8$}UlXg}no@rw=ib-IT>P&Z^Q#~gHUes0WK;IU;c_=_n@Ho` zJz*#jWAXbuDEcZ5pAUENd+$n(ZMku}-a?q0HxcmPxjsGcr+v$D0N$t*2e?&z-)jo% zmiu-AdqMoj(nGrW$@l&y!ymnL>IppI!s5#4bqk-%62Gi-kg(4xB8C~#;yCIZIK0fB zGp-g6cpey@6ug#q7uB~McTc)7DL2)?XJIM1aGrQpzW+*|*kXD$?FoN%9B^>5 zWpzu6S^t>4VkVUk?`!i6f3cF^6DDYXmcSV$cT?wsA6auIR!=?g`mUjY z{DDqLXko0w!s)t=HQm7T26YLPY!u57=5B592r`qiu0)O6yeQ}1#R71vaHnMWrS-k* z(wdr59TpBdP_o+V)|QVvx>l}?(Bvi&)=|dcHBK0!>@?n>IdzSy{765YOZg&M+?tM5 z70b6y{+Aj~nBbI~26L*q&^9aOr+9c!CH?(#LAgTltm5M<1*Pq5!sL`*67k+Ev7U?I zgX+4004DzSVjG%IETTlK&-@4$S74$(~rRYO<*N%8AtBOww zdj?b;)?9BLNR!ci&3->%jk*XKJJr*HTc)MU6=VeTBUKYC{XH0|^V;H`>nmZCF)@g@ z`9r+NNP(U}M1l5U_A_c9_)6Pn;8a(_xsIhJ%D%uYtiq5`m;OfBatye2czF7izsa&f z`C}A=Ae0g%F>~}iGSCx?@zEs=UyMEB-6Vx#5o|U!yZsBQXj86U0`5x0ZE}*lGcENDCyuDOo63Ni++vq?mUnI)#b9V0|ow7b{ z(Rtw;%w2Dov$M+W8XD7?FWae)lL?KEKX*E#EqC8y+s8r_nC=E6s#KQjzq0!~Tu24u z56Ig<|97=F4jrA8{}q9#iwh^?1#x?dl-)hkEFj@^cdQFOaMGw6fEjkr4g{5D{7LvH z2qM=9qCX9Fjp3tJx4+qg+XX0tS89B91U4hRjr1@ zC)2`M23HplmqZ}5mM~Ne3ICl3Gf1dX7d#Y#3GCPd$>&JZuiBro_cg_P;Os3neTAAk z+6()bn^NM0Arip1uQm0|dF>5n@g%`YWC7Ty;vixzM~Qx)$&@_*=yrI)*fTjHW~f>} zJRpu7I3YaOoB@EdweEc3*-%4c30V*NN#pBObe``MW=P;OBB2rX+f}pHtFe-k0m=lb zj3R@eI63}gvg_w~UopcU2YLCEdWFrnt`~(PD|`dt&wn4*(V>PWCr?VBDiBsNza47m z)w`*YxrA%9HyzOB0Uhha;i;XDye-IE4w+seR=_^Gfypl@Ffz3FqanYsZ*vYOtr4^R za~A;CXlWRs+Rgw_^)D~45!#)kW8+1M?ISJ-ed8kS>y6q)z6Y7;ZO)=KS z^4@T+9(_E=@P2}X5Uj1-7D2PjrYcMQmd3_QC~PO&TNDsO)ik-%d^FM52s-LH-g5o} z=doahxk1Z2JE7wRniS8U2LO}Wyy#B9PH=pKP=#02XX(Fpy~zXC0Gf&HYRDTD?vhRp zbYXlGRdFY|tG_$`Jp|~xONa5)g;efVmRyZK%3y01m41M#*p}vtc3hvM0@)ZO;Hi!N zO98(D6p-$i_9mqr_Hi4BOnr1R_m8zFCu>?ilCI0Ntcysjjz-XRv)#6)g`%=#fZMQT zLQx{7&dY+;D(yd8&YX3g1Jr#sf+&zX^lMxFb)@aVJjHCCCu!M6VN_ANjif(~Nu;&l z)XVmt*3|n-<c3K8KfS)tW=P`cm*=SGwkncyb8b0b z@XVbMS9<1zg~<~FGMwBX)$54XFU==|irB)2o#0L+C&Uj;YMzq;>uaoyy+ zV0YS3VWIK~>WiD>_w@JO>?8pn2q?dVKPPlG#`=9{rG{t4zB)Rhx;9{ z5YVb&@7+5wNRHLCD`VXGD5Hko@|gfX;PRpgIIxT?ObJZTEOleI<1OIn;XH<60Upf8 zx$ZMSt-p1QTG}U%VZ7jrt#;a*I(f7pMA>zC#bk>JFJ*?Yc6aLiaiI0ZlC7leGA$BL zBr*jhU{sx=k7`wRK}2%->iK-Mo##>$27|>mMiX0~gO+H7KGe{vOCnv}2a@b_li}3%W5bHo_gGqWl z>fh+QiNFP=>5gBGUx>KD+s^v#bsdCl{bKc9BG3aQNzE(ZAPj6`S`%aa4Ji7eSPbwt zh$^E4@WSKi7WrTy9AUbA2!coPLvkz+AR^atXkFx8yWn6{A^V7@C3NL^_(gINHx|qU zrjUkzl)`I_x?fx`e9m)WjQH8TU?q0>nEPO}iw^VNaVr#zi|PcjHp zPu!MQMc;4pzZzPUpBkPA?xRp?W&RaT?v1n{@+(lX^*dVdeZE#1Zu7IWbjbsN<}*9d z4fNADf^`zP-qwX6w2fig@jO~9XF}!J5L+*{Ed1*TIW{1t8N8N8`&Bb-LFVCW6;8zd zM4S<ySYnUIPYD{X9TB`KGi*%yT*3^hExu?;I#4lKt>!wm1ZdQgm&GP6jN6x2 zGCV_j{+P!egFgEgP91O5$LBHIC+kOEg0Qfj2HGq0MM1 zOrY8Wyn^Op1=h7XV6c_;6#FqHC5YT6=jNFobEj-l(81lxh0Qr8AiaDy`&1M|AJYu* zr0vo8k>CreZl%U*QbyMrM`1JzRR<0Ji9cs`1nB^yNl=n8J?P55C=gv*c+yqrPi6Bm zP0(^vTV!q_Y!bnni-)JP?(kk5=Fodxvg+ zuAi)KIk@0i(d@7S=TW^gObq5**z9E&yw555x8@{l=F<)K0{1T5ulc9gFGfhJH_o3B zgHbyL9O2DNPgT2){=g*IY!Xwqa9gd zk+gRHXqko0Gp=yLpv?3l=QLmdk^50EVcL05RaKSw11fNTG_?Qg!;|lQ80Qm2&PDqW ztt>&Q%q<*fSFaiNxJysP(_{;)SFf4tsqAbR{29D^lMES^)&Smmd2mqP#m#NG*&jYP zv3Pv!ACqtu1s-A+6{WxCZKu?MOtpLSzJ~qJ&V8c8z_x`}IA8X%rYr1TQ5Ql?8cA$^ji!mCH5fM6II7LU+!dOWVu&^WsXAa31b=VUyFffq+ z>Vkbbo$Sph1kg<5xE~lI@m%L7VVCxWNs%Rj;b(;m4^W1C&S?4ybbPYix5=tkmo!nQ zo^hn!Xg!^8ep#=$^1(dI@~fq-vPhd$bm1u8c}z0bt4^qAV?@kU+L6SfTVx|<{7|ILXqJ_Gk*gd!$ zaJ5k^S%>)PB73L?vQu}}2=LF&_BSAmy8Bg)n$Aeg`giQMyYE>Hq|dhHb~Cl; zE=+Y*y7N`DD~p*?yD7Hj&fU{u_~pt-u)r~8K!wKK^@{NOKtTHIquOVG99=9w$>t7h zD|hIn1s#_!_a5Co%2r~p_4t|e9y|MAElK%A`F@Fe+z`4L zOI`!%`fTvJoMsm27+)|7CXr72_ofqd?J0pVH~|MZUj*KKi6)DEY0tx%SD>`x{{ z)mhj1c64s=m*UCCRI22jnrnUPNZ_gbRZ$0!E}ZHUkw9d{yc@oNfIzlN-s2ELuIN&K z%2XVCVF{>5HITAXCq$a`W*DyL?VrN?D=iWrwXbj&k(Qf*%UlI2L@&zt<{>)Y{hxOQ zpx*+GJyB1ftF5%R<8w{VV|l$gx*0rP^Z=$f9^!42Y5v_ZE@j;X-i#owbSol# zl&p}RB$?dGc_7)DZa>d7nkYI~TI&X8>uxCeY<}cWzy}Ku*NSIITPLpZ#|j{Ylr267 zg>5P?R6S~(?aM5CG)fRiXZZ*+VGT#RC~0hV_E6lkp9#$l3Nxb;ay&R}xTO@3y;dwz z>;tsc&n>m^DUyInsrdmay~*yEoNSn+1nBLcgHuH7?03%4kNZ&82X*J8dN&IFRL6^k zYYP=9`mg&hSa0(rKV^ZGVX8v9)Wzl2{JBfid$<>d5Itc{%u+MQxt2!DT=}Qi+=QOL zs{bqy@zXv~YY(Pd5;J~^<1o&DizY(eqzZAkYhY?fz&Vd~{^wtA^3@5{AQQTyGqRqL znFIn(XvKnZZP;W9+VAfTF#Kl)5>7fOdnR+C_Y1BZxtYewYB(n5-I^8*za~cn@sa%# zmweWSd>w`{v>&72ROO4`NC>^8`dU+C&(@!(LDgZzsh2C!-upvWl`oNIAFPYCnvnM*H&#R)h$aFuFWx3*2a&%n3czr?x{ihV$m<{GM zWR4Cgi(Y=o0F!Skbn2YoQBXujbCiqB=j$VpB7U&^>Rhax-PZt0fl^}pyHt+E8RE?A zlBiW7jWQ7XXvBxsi$v@0Q}#oyBj>7iR2Ho#^KoB0^$DB!B|P~>&_R3F#M}ygFR7d# zEwaf26&X&7MLN4w{u4+k{>=KHbPHsJLn!N|YU=#ub*w7lf57{U0m3F@-i1L;xO2U<^b;dDu3nziufK>z$s? zW292x5#EOQbBpX?;ua4S%u$Su>G3Sk4tp7VCyG`NqT<<|ZytlHEh!W?r4;5fT(`lb zk_oJprA&Nw`~AmPL}%$XU1>DmCSqt*;8|J6rnFz`U|U1-9xrdfg*a+91e~LheVI+< z)hi~gmKHHt`BbfkO5Ze4s;oG3V#Gv5mS@htJ;w~6f5FsUr!2V3OS|tpd?nPFDZr-aT|N6}d8~2Eiw4a0fIsSu zeE|keoANgC0ioKYrC8)27u&Ci03*rD(%Q`l`Gt2(B;~$!w!CJi*)I8}uaeVKCgcD1 zF3V5=P4<5q$KOrxQUQZ8OHC#pb;GfwI2~`YTdjyAh(2@iKf7e|rtvXVyK85zffSNv zlb??Nf#DvaB%5Q8TcNe1vyf8XE1msG%z*k#Y+)|SR1$Z!x;9pM4lw)k46bIqHF6{m zAbD;Gn5x{!OiXK?CC(B!eHbSe)&>R+QEZ z0iw6RHQnE{jM)f+=HLEYYJpQXyGeNZ2P0vM2k?*7z4Uv?PFqvGTJXU|sVMo8S7I7c` zQ&v;Ui!)0{@MXF@^zUK{#4wA4_>a=H{oHCb`^VerkZL8FyPjLG3;zM@kcoPJ!T#-e zhw*6GS8|L!)q6FCz1pN)Pr{5-Jwia_<|J?eea%0@3cVmSUN!JFk7R))_+ZZZhRKeM zdFe5bpvvd!jjw3;&Ov^#SDIvDkb}FjNG!j)@Uo+@w3Ow={X20Ogb?;zfvD*--}S|@ z+x(ET`eIPZWH0=zyX8(xJ*klbrq+RRU1?h6-ZYaJ^46BW?^X$>pQ9VBy#07j8P_V~ z2Qu@r7gBJHaerkLZ?HW(W11=*&B)1gVeex{7t+q*V58{7zI6gBAZgH96krKjjsA=B z>0s{NX;Ll_)S?|YsCZ=AU;pDJ$c||9lW&ysIg_P~C#JEV|L-pWjGf@g$(@wJvzP39 zt%~VPovWsduRvt)ffD;)LSSi9H2fR1uyMD0dit-7lktHjrlq@m4pYebuM#WxCQ9%h zV0h%3Y4BIzvF(ToX(x3bW~ycB3F}CD_TJ^I>&^eX6QIYz7IbW1Ofzs&5!heH^dKTc zI>1%UN>N|y{Tf#B1quiC+T-)*+Y)=6@A6uRxE{CcP2M#aUeI1Rs5_WXbTUMA2FoiI z12Abz`HK`%Dr*#H5~1xM5>cQDlAN5#6t|ca<3+!rjJRG;D1KqRt98CZF!$hK7ui!Y zpjbRHHzSzNV(O3E!zF*^Pa-*6ZA)s$M5~%l5JJRb)LR&o(?Td8q^Cy-u}^=66v<~y z-0mmRaroXe{#O1Rw7;C3M<0S8nxyb|A;TNT09LUf%vjSgFN`dH7Z{y7H}Ak0v&Qu$ zN08EJE(;;Lb`K8VXZ(kNX6bBv>spMD+p$W*i6mLom_j1D z|IAlC0`fP(2s0<@-YFHKVGI(yZ^X5GM6v!>;sv2UwMP@VI(^7q*SlOZ%}QbK(e){^5L8~W-W=*Tsfmlsg;V5-4LNw zj6^VkD3-$jhDufwzQI>k-!y01{>f7(2h@|m(<4PX}JBReV7f}4wK+l$>m2l<&B$|;u8Sm z(-fe3({%GPKuq|R4B-FMY=F@8hQ%3UB@o9PMFJI*aUKISz_#=C#z9WnL}_8Mug>B4 z=uraUFqv_6G;2fpI9J&F;VDo+fw3Sb?Qe|yY8ojS2I-zoh9=J zcCAA*fbcJu3WY+rwtT)L0|Ml?yi93!Vk{}XZ6a)jxtDTotS^sy!rE>7KzcV zwq=D~kUXrzl{!^xgV*4`5{`1>hr~mvpjTS&k{Yr7@R88z$%+^@NL-iSIhU_}k z_9v^oJv;`rT7i6W8O8)P@8*vOU8gqQB;W@a9^PaKmYG{FMTRo>Q`?{7&umkX$_j2T z%XXF%;KTHi?(y#7nQ5!hvX~|h5r$Nqzc5|1co#ngcv6wcL;=jV#L#+y1)W_h2HS z8PJ8%??>A2&Up%8W=6e&=;Oh}XZokvs^%sdC8GKLBmvS*e>Dm!(V<;fylB3bl}sN{ zFTAgi(Umt8YcYXYn8bg`%J;fut?uBS56Ta(MemxH!9jugES@T-Ym`mqO-2~SR-RK( zlDp`Xbc90&6&x}$GK>8oLtA5`9kmv4xR8VOH)@;TyC};4MJZ`!r-xpB==N=7Nqx54k|%%uuTc*n*8e z#}8a^g~Mq78dKqc0Mq=yocw4E%5cT-xg(jBjyYDpm(UjvTbPi(a>M%~wOA;B@rCkT4d^;YXm$$E z2r=CW4k{_(l4T_mWdKoDqYS|5`({EeDI7cKN-AZJrz)6y>*=w#+%skYwa8HZxuwcaW z3v=+oW znWY&0Ia$Z@<>h0VM0|v_nfjl;ne^pLn`K-MmnVdh4p_qeOCRMj6=*XgW4Xub=NfK3 zZ1GPQW}g z=f3|qKc6vf-I%|WY0d2*h^arZAz*9rxly|cJXTK(e?LL&>iUvZ@nwTPyIVJ0mW*y| zP0DF!I0p|m!iqrFNpR-j#1!T-#xnP=HCBCGV<>wqH)2ZmTEn|w(VKU=F>2L0oD7Hf~;MDz<{Nf;pb64Lm5j>)Iog{8>+Yw%znoR8zUWn%t3wlb9Y= zv)8jWu|gq`CMHJ^mjcY*sXUS?aaCdA#Dtk zxX%%Q3LZ*rfMe$X?h7~5)lZ+~KrKzkDY><>4Q!m|#iaX;12TC23mdbkoGZYKFi@Z! zi5^m>)`#gnGD3fAeGvLS=Q0YE{d#QG~ zA1Ig5aDHEP*ELd*hRP)FB?z_tawPsGga-!JJ_bbl$$`r6aIqf;(NnKUxEhFSYhr|V z8An+b{#6iqRPRycO#_EQz@Nxz3sm|+l2LSv3r*fsLW?0ZROxSoAU3N+{xn5Q)H62$Mk-9yy7Y-!)h-yuY+GSDD`U_?Kov-nD2AGLv^|Kj&56o75ed(-HN_2Q z_EoW+=GJ`vA+z}*hOl!H;kE`_V{L5u!paJb>fp96G?j}?UPMp*ff*?7Dp1J!NExuD z82+puD$^2JU+J84moK-GPvmM(tRI9ntv)6Q9{p+8cF_p-5DsREP2;Zmh9n5|oLuj3 zo%i6rUGn)nFb^0DX`gJ|nZFJ;m@L`l&d~e`-CdX|yufz-Rqt7wcUHfa@N|ebiJ-g> z%Cj${QdvJ>Vvt6cdB8Z3BdAbw;>N%ZUWFE!^O|NxL%U)7W^CG50szTG76D<{$!l83 zkyt~T95Q=A&8J{;Hp?0f9o?tbk&fJSO|PBry;mHWNAd2A^V!_ZE3dH$Xn=$Qp*9pa zQUYS=C9%Zy1~V2}zJ z(t~loKsWup|I!|Zvkcj5w6?K%r2gbz@Fzvb^Ao)y+WtOE_?O-w?xem^F7%KDC>2b! zCf^*}`Uo+!3sx%w%yUr!84$z=iE-|K`qA`AsHTv6a4SXr`%mM#>a@9d4}Pr%N7;f& zqFD`rt%Xz`o6FEAeq@^g3j!vdsPc^MPA6w{EB&#`T90j^>k$QG1M>8P26=S*aZk(h z<7u~>Q%%*jaEDWSKfF`z!3IEx_s&uGH?CdArmk52jQy}W4d+%esehH-I@`!$>rQTkA zORGQa9j+Me*#iOfalCAUufvK(`#4qvxviBmB8Tw8QW&0s?uLRR`A1_?#N!^bJKhb1 zZn=05N^hDddR3nZ(l#YHkjX|-Ohfp96co>A-f;To%_2ju;>*l}%X(s@)`7L?U$fs0 zBZv5sBbeqSV$q58#G$@~kB`YdYBF9U)B@F1&?B|VH{|uV^WtCg;dv6c%Dc1IBk49^ z1xk-Wqz`>Y?qmq{Fm`WWA6~ejn#(SMy%32~%H`s;@?_rd=Bn7J=BRSw_)Yr=sdy5@ zSDWjlxA#=?X8m>ef+O-pzAvq&w+szX_~BPy(}T&$+UTOOr{gpudCKTZ^Yo`%JU*~} zP61>a-*@2C>TYJ%3mW>`vUSNDwq!0?A6y~%7a+_ac(kV)!iFkmz$SxS#GOA=rsbQow6&s|SE zl=(6^Ye46HZM*(#dnciZaAM;+^~yRq8JS$Z5tpO==MMW5i2?uQLp0x=5Sk(6KBgtt zjgcE;zaq?I1`meV;pMsOj!wUYqVW2dQI&W@3k%x5%0_VU3+WP=WnI6&aG5d_d&f4 zmAs=jG6~Eg3n={-WP$GMpbOlX567XZ)Hq9y>qkO-1F7{Zuz{d3q0zeGg@e;>_DHIJ z5=xk=!vR%*}vimi4v6B_WU z5ChHv{lk+u7S&1UwG#MDeT}b>)Ibhes0@0!IPA~wC!IHh!7EUdiT&K9{lrxgAtKI5 z6w7!V!I(+q7k)IX>!O)qZTCmf?O_Duk@9^CmMIV$GtpT1K3Df+tUU3}_iRV&NCr(< z@=MfD`FO%b8>-$BzkBYol}MmSW2nf;nB@>s{7CM)F3g}>L|=A%pgqj=#b?X)Vd-%T z)fD2C1!mZ4y6=69uX3HLNPeJXzaBU_EVghN@^Y0ENMw6z%(&exEAjGNBZ2$W{bO&{x9(G-xeHXh7a384kH{EEQHoH$MN9kC)!pD6!PGwd zV87jeutV8!J4RgR?l;3i0tTGUFu6Wm=#a7ETUxokL=_K9&2^FO)joS4iZEK==C8;* zv>H)!d90NNC*O*TGeC^0I^IbVR303xd6KIBlQ^@-lh3w&Vz91EY6fZh%@iM8{cwMD6(EbS za!PP7{CDaNRMXK&X-Z!eU@lL0+1NuWXI4UJEajJ9K5UbGGU}`Wt>$uV-;5sCCqKz`leVVF&K24GhR&y{K6AaW5=$~>+ex$VZ?~$82@N56 z8}R0!>_m9WXZXu9|Ay7~f%(O8`MM12%>D86>Y8^I@yiNzNwbBPVz@@pHl*n{(u zc|tU_UjLcFjnP+=OCR-Y^|}NtS=!E2eBtwOi;(wMtKaVRRr9pDHgr@jIBQQ`i%fqt zLwkGb-XT}9f18mu!0|qL{95U8!-VO(mCvzOwu5fIC%WmPqs25jxg-gRi7U^O0B{_` zs&umA6%_J%m7MdGz`Cq>=hXxk%o0Rc_MG5fDY_CRwAHQSSGjY<=i6vIRWH&s98s6y zy6$+0Ahhqu=>WZm`f}0+JJ9VY1E#ZvF!^^4dLkT><17s{tae9v!v}*Jl9W02`dx%# zhDy{ZZS-#u%V?jn+X+h5caQ1^CmzhC`KP|ocmTCn;EL|VbO2XijhP!7X?URKhPD+3 zp%g0SZ=1e{xl6vtQLBeO*}bBkD|x{RHpL&?mHy;b+Z>QXTdlgKzq;ulGnpwy9XFp- zBv94zqH$w8B~fhuT;s&;xEX44ZX|x~DuI@1>w90V&Mf9==4jq`!Ckvvo}i>$ZmU(k zR_o0yXBnMd&lKKN8g4R^4lId&Ki$D~=zW9CL>?P36SE`KFoMh;DU4n;@o4)N|6Q*R zqjnpM-%zIFi{1-shodj6KMM;j{K~W&Hu(HzWm_-Nrom~VgFTj!=wgRdEJaK65=A`k z_UaYTM1oS?B8U!9I0y0H&lEm>Jo1g~IG$U5=to;E7&XK|v$xeG!Cm7nxHsezmyBWvTE2{VoKR^t4?JREv{|$L ztQf9~%nW8~EF$mt$|oUkm<7oMA5*^zXnE7ZMOcH9 zE>My*lqa)|uRo`oZWDOiFC^6RsdamJKpz(&4Qc0;r#&dhM0WRe-$$@qK(eN;*6A7+ zIqswt1na??==?S=m62gW=M$osd|PiCGBipr`6XPABX?Zh?06fqEhb3;%)xd*s+<|` zNcN6ETV4_IvP_r+taKHcwJG^&>q5HV$I#JgQvI~$TNgjolMxh;l=t&b(8DZ&sr>=Z ziT=A|d%HVHvc*eCft(|7oYooL)pvHk7&6w{lL=F;;*bE~Gl6O)l(VYLbGL@qU`#!Z z&**RBj}=mS3TEWaf($7p@kBVAt?`}j2yO{^8NZ6IlB0rb9nBb4(Du-q#)+U7#NoTd zf}au!y-60`>WNjBM{gB9sVO#7R8T@o27fbFrSmQN0T$>J-D}K;$(GBnbjh@tlGjm6S?rM)_V~Q45*n+M{(lI zcVaeYYfF)D^fo~cc7>KYQ9PD;a8IW7pRgIP(29iAqB$W>_X_ z$G)B#VHH3=6+*DXl8LyOhJ5=2OuYp~!OLA({qvmdgR{13e{G!r!)EF#=AY5LiTHpF z^BRiMkg^50G=imO9_cZway2o}X9~7#i-RhXfSvT1g1PplmaL-N1=_6{S0xl?!=Ez^ z_4w+~t#M%U;P_}?8G8D%oh*!&(0k}VXU8FWv#~Qt46_6p4FaB%{wGg3M}+LaNpv_JvS9JHOlZXAc|oxJvbM`6+W*;HwIP$fkZU-Ab0KM zp?53TtdiU{QAG}F#FLP@U-!QuZp~ImYt$QVJ!`gwHcikQ+!su|_2c`O zNA8)^wni%a->_`~W9dbMj*gC|kKtPqp%`ROOF@BIbHf#2mk8Uldl=wOo$46T*%*36 za))>6_81#c?HTi9E^Ky>L_N;{LzBkP`Hvz@i@E7Pdpnzc+ zLLyXE_O}bXU}DD1L5a#ODV5={M(}4YKdbE~F@~8N(Kwy!oTSbq%HXmu-EQFO;XhI< zI>*#$co+`B)_Nh{0+`m>#USH{94-G6aCvDX$rTeT7?|!i#|HsdKC0&C<|fnSeHpG= zP7aSQ3xwV1-&_w}y?Yk1RQwF`&)#9PgZ1rpCUZfFako144yogsqZ%yFUX9SK^d@TV z)}MF@VCF&d{l)i3@OMVdqwnuyDP%EunCz5|Fx!MgK|R!X)qgemW6(aATNFr%;W=ms z^{INBeiW6g(2;SI3Sut#nWJUuL=YDm56FMbn}E76n{+|0S^rppaK9%wP%|@8p|#5F zHhKpA?aBPN?}r_hdhpqI5jW^67Gnc!fh22;C=lo2b|yN9fl_5M5v}K*T#T;rBJcv` zLfyzG+DfO-&TFkt7tibsS5Q8a``C2&HO*!ge?Su+oZS$nl{;Ma@c+zf9LsDrO9JK{ zvMn4dBRb~axcPqKRPJ?$!+wp0u7TchHQA6NG%Y&hXd)1aU$K*(qWXrgNT}!W%fhbj z&rXwB&Qb>o6b3=^^lUGy-zR6E2rz4;p7`t0OG4kJCbHoU=NuHSzh|KGlTP)mluB5UQ^nI6HKTCE)ZgAH=p@+k6 zd4tlgT^&l%Z-+bi%ybKDSsu*I=iWP`fq7Ka7_mOEzf#4QbSCC%)DM}PT>fNlZ9|{C z!B$bQbH32O>zU4bw?c=Ltx1KNDi%KJnJ+K#BTn|OwuNR~^Cy>dtXBp!1~W`c?hFCb zuc5J~!qegU0t-r~PSj)VuZBhj<-_J%5?>f%i>FC{zwQJj%^I)IQYBOCA@xP)Crxes3@r zo0y#JI&GBzU6!y&*M!6G7{Yc#iHB8lzSX7TL;p&ALT4z2HS@2vik!HnY>61OQ3$`o zr2C$J>YsLJ!|hQujp<+S#r53fWFF-ua}Ah%(z=7CSx)?Em-+*t{FQF4=ndKNDu34( zmSOnx@C1Xv)eV;pE*pkwZ7a{<^m#vt}X{bd0=6#OxwcBY6M#< zxf-kqz^@j-EV7pPUAg(X<3Y?NQ0rSQc2pSfgWNb8NdXIxMLTDA2q7>nH;nGB z)wzcAbC)BepyaCxn<}RbOlCtRZOv9$kS`%Xx|v!_E~zt;5(D464OoineUvYhLTH7E z%HOj6lZ#=+5gxn}7+wgW!C}(%Chd&iAv98EG`aH5 zg_Nd9h@ei1>srfQ>lF}PH7iNeSKJrLT| zGzbIJWA`$@#TyW>kk*UbmSxp$RUYGO@N0uYA4yu)8c-}k`omH z1zUZsX(fjFEaVp%!`J%NMw)0IfHuh{N`J)iZFWD;mM=zud+U)+XQ{?g8y9Uo_g3h& zWKoLz2$kIkCox12);j`Y3M{WagqBVRe2!F zvq>YUEUba#{^-WBwl-e+X!d1f`+RlFyVUmp}%71>E zL*Y9u*cFZL-6(5W0S)S}U_2L6a4H=ctxhR`B;?gDc9nH4mQOPy?Oq#12odqjFKw&k zj>KQX1d-v$_-tb!|H}`>6(%sEf72*nO|KL^=42aWQj2G8Y`t~l-IbI~`>Zqslz}V4 z8+ujs4lXWtfkg`%%A5C9*TwQs`uv!TzZXa1@=(yy3-B8Ug`c4ORro*1vhL3MZeOA< z%%zL@+a)F7{_zW-cFGA2+z+X?50W_3P0wcm>m>OL%<(0+*aOSy)XxH#wz+&_zpA7l zho<)Y^i2_cf00tbV2{82iiHmgvb*(ZY-5=I`uDFtEkKpiKq;P#VsCnaIE-6eq3)tu z;8W$6A6e2V!>E$EFLoUYGtOl3J7L@1VzpA62rUY)7g;DzVwwreV(GX~LD8Ok4Qza7@Wx``B7F&-X3r5) z?Fs%bj2Tc}xcBuE3gdAO5HH5L8q-Dhhm!562NB~MLo^M3zWkMIY)Mr;sFF{Zzl9TI zB7AnkR{^T3B7T=hC_2o~Pn{Xd3E4dmRrA|%UE35W!$b~++|-%*&Zt2EdGS-L!@+|M zz`*!Ndo>m)G4`f0Ycnogk0S=IJ=RXC0FwAR8^>a-H=f^VzVkvvYzd#7k%!y=T6IjK zZ9>#tU&SiqMB?npDIUexu#Z{-l8rqP zsHP_3r>1L0(QOT$d%izhD?i=+nQz((^bRr$8+a{Rpw7( z`o)koLeRZydXDq8YP@A;eHM>mr4D$u{oyTpKXYP&rmJkVZA}jLbmX)jC%fvcgzF)B zL%adtl$U<&9}XSIXLW=z8}_>EyX@Q|#9FIHZPEnmnlVrLViuF=EvaIuh?4-OrS){b(EDl2PdaKMn(9Y>| z_P;W_$urs=Z0s+yJ6^Z(%#sUTFn?x0p)NI5)@35e(DVgE5k20*B+c+3-1-{agTrCR z!FxB^KZ*Z8CWbHU(cbQ9xf?!>>ZweZyXkF54f>T`D84!wdZ2*#l8#a=*6Z1AID1l` zw?O0>!hs_~wov2kDm_vHGoEqz?kBheDrGt)I&B0NdKLZ_Bjqs8)hu(Y2>QDv9Q=_$ zdEd+!Q zvx{3>6yr!PI3Esi@|OU0PP34N&rxg;-jnP4=+UEqZ*fJJTDA98RdGO@i}Q^ZYztGh zJ~A?LL@$$0HL2^nf#`=K2y5QHVrCRln>m`JE9+#!hd=hMxm_=yx)dp*+P79#&f2d1 zSY?vyK4?}Dli26y(kOOgs6WpdH(qbgyR6JSALx5RSsUQg*w&_Ap!Gqcb%z_rC12ua zJhgp>?i6!TgL^W50}Y?`&U2vp^5Z%sTB>^o7f-Uvm1Yo8>a!#Hh{~80tx=Kmuu7qxtqv*AE&*CBXoiW!)D*09K&~vITx^$Y(dM zW7j~S5cbqx?@w0`)p5YVq(CmPZGnBSRQK3Og$9*6OZN6t6k13BGoU$mDApJ*5DEGl zU$6HkQpIzFlHhOBG=&p5E2WAz1i$sfK=LNI%P8c~h(+--3=gOuD-023{_c^R2ye{P zFF`{mLhzLyUHmjS9sQ-hDhEWXqmSG640X>!Vix-S(ajozlUn*ze0>y z>x=PadD}_ia{ChTJnoJX3If%JHZ|I@yK1)Q2goX6K@H@PKWnu;2OhfAG84fg3tpH| z0;R?IfU?2g%QI5=$SV19)*wmtO(nbtoL(eB9=TpNNDMFy!A19*=BXyC)qAsO01xUdme zqDjbQ_gUJP3$j)P86u^WO9WRWNAo$#pPrc#K%5MqS(#Qd-zLD|lZeF4uo^e{|EXZ` z-`B)&u)Bp$;$eTtUfEdH`BXEWcl{e=SwHUk~bi*uf>7{jZLt3Ttxe4+n@qT zDtxbc{i6qN@ifR;(nV<#b?*fP@g8>otx`4^*@zj%NZ<=$dm7t8t-5F*$7A?Ul}()F zYSi(=oGyLvM0&41P%`DD4O(a4aScEb2lA4Abcf4L#vScB29`yo6r8Q_H-56#F40if z5K&$%7$E&0J|QXmnO2>TWZsmxkVF^*-c<;JGP3aw7{?AKax3wKqlJ74JmMTZS$qM?ghcFN(Q#2V)~P!Uw; z-ARClfOY;?32+d&^Ldbht6U5)r%er}kZ7neY+PBsHgiEn)GYd2J!e45|` zwGZ(-k*8(_W4Y<{yXPE$_eqt;ZSx1MSTDcOf-pwohi|%- zo8A$!TmE?onpfYT02$IzbC2&=LY!wB<$0(Q1|Fe15bRU=7fC8;j+hI8gcw_hJ~97z z={@bMm-lZ~3*^$|F`#Q*15bh*+JofDb2$i3fCW-Kzb^$Vzd8ENH&3%7PWqDoW`uk( zDdhKaAnGbxsrLK7Z-$8?2{$IOtC*DaKwQD&ndjK$FX8(C7S~S{Hl}M{?j3ZAOi*^g zXF>6ps(C4@211^c{{P`gJu~B3mwnOGGe{F=D~TMf*9zAxCfL9i?S$NO4vU3*wRLrr zEMWZjWKYWmxzhPZ`a zCTyaS%4 z+*8Ftb8-)6ju+Im^{(@_CX2<(`lGf4+~=#@Mqrp=fQ(Mg&dMOZ9pgr@YveJ_D=dhw zocvdL6{Pc5vj`y76h`nzBV{@SPhfH>pBisGM~Y(>*}U27Ufp9@Fw09V1np2HVUSA3 zP>R3Px1_y{@QY=o-(Cpv4}!4Z&vE^Xv#`ZS z39T!FA84F(v znM{>-y-EAOe8yk?yp4tBAm+V!1Z)EH-~Qaz6Ns%UU0gz17d{{2Ph_2x+CJ*%^Mr77 zwfz4N4+{1JqndI6`Yh8C|4 ziRX|c2+hE=&{q>}%^gZ2JX46bV9e6Y7ZsAV{mziGRY}Om2x3C|BdHcX*FD@Cj+EPd z!)_H1dKN_-WB+r@B(>N+A&fMf(%N*I9HGtD9i4vM@+ui}b#Ne;Ts~cNqS7+GOZb1j zUe!a#ZVZB_eV4kv?FNWq?$1 z-zH2V!i+E!8co~T;60bVm`_$Ukp1dNN5BQl*#ic=wc$+Vv#|nQ^XEXUE-l*S1Nr>f zkkP24Pw?P=H`*3TfHyO-n&kM~WuXB)6NyVv8|HCm4+Q_T2f$2nxZk*Z;1JL^=2~c0 z=bkQ8rpq677dIJIvYf~Eq7gaFY^rIj*w+ZvIDV}E<40F#G><{;)F7|RPVYnJ0$q7$ ze^$b3yAZ8~&M;V! zYMg#nuXa&v%!cKnH>TYwoUXa0>&r(mz(Lura+o9FGCeh%bGhO?WQwHKz^u8|e)XFL zUOkUalz}0O<4+&Kl0cP{o?w_UrKB*h>?NdY5rFGdm9&*U8EVwPx z6;{5~Y5%MgG*(pwD2GvgTb!j86cpS$XtUO;TJgrY8PMyDexeH#sJi>X+gm88n5io4 zGk9nXw{PO4K}@>bp#HY@?RD~3e)=XExG@5H#XFF;~;d1BIXx?LY_FqkzD zf3#zeWIi3uCEo;$fKH79B@9Xh8^VpX4D&oVU*7eryN$YYG*}tNrY@(t*6% zqY6lvbav(7`W+_qAj6l6aw*pTMi@(A+=1lP)miCV$R#*=_kUtDkT>73l(dm_S8t2S z=Q7W8YLvFDY#>Gwmxc|ULCcp4j~D2K2idmJ*gj@%>updq`nGrY z0$33)EZBokh`0+vWio(~=Wm)`fE?5X{GZPQ(NFGxK6rYNLL@)YgzU$eR7X2P zXgdvVL0RTk`xn>vwWtq89iZL475kv9QZS#Ectq5Zhu2UDO}+UV#-R+mV$q|wMigug zhS`BMYxn{@5lzAU|0eRR9;zr(hcpMxcXVSNR^hbvd-EQYvB(Gt@{&A3_jQuKJ5y^^*2C5&!J%fK1@nj1GG6`}0g>cz`Prv+LH{^{uJW?l%3U zll2e)2s6;(R7GMB?QvX`X&w%JvGLO+f9JLted~J0TkXTeMD)3eniX;xW`9gE#N@E zd#O8IAAD4zmgTh{xxK*OaOsG)RV&d$cj-@#wo_KGwqvW$SqL64(ADc$v$cxfA^He9 zIyufcFC>J8q3ukxeHA-UmuEJX6`ZFy!L=au!cVL@{>aX@hW6Sw-S=cEeAn?zhy4Lq z;L6ez9V(toNpqdarQ6PI;{{S_To$f041zb%rp4DjPdjm64l!$vFKo zSn>F(1}gP6sl)9vG;^;j(tg{}A&$T_y(ER|3I-?8Qz=q4sm={~X4rpMiXx@C%t=DC zPE};*sqQZ&-ZNx-wB7eyq*zh_!}5unpZiI3*^l{eFUrj{^76=BZ%RyFF?uz-5!J2L7OO|k| zvMb%}=YDAwND$c;5&$z`;dce_FA<19mqK=P^>cNVGPuU^a80dF<92{#KNihi*gupg z8Pac^Vg|zHNSzzO@6BwDDv%~w4y%2haspTCh%b^G2%+4uVQgZY8Pbb%^Xm^G?pg>6 zHMzDqxMslOcnqV8-I!AhQoD*B=IlPrwY2KreRML~KMRCw{$^;IT5UPvQByC?*B_d$G9jmFjcut7iIm_6+QlH5 za!Dp1~lF@Is2II`Zh5ee3s9S zQZ=(IF6s-8O1mTcg zswP5|yK$o1eh2%@a3eV@{&1&9LQH!0L7d2OB<`Ng4rdEQV}^EBAm@@Upf9Qkn0PC4 z%5Hr?Q*WpwavE*J=!Nw$5L;|vnvLCc2_A%?5P5@(+EuigJYha*bYaKyC5}y{{h@fF z2GRyeu2A9K0Rn4Cu!23g=56{r~)cI2)8g#%T69TXbqJ}WYQDARpke3LtY}**QrpTT9XK2(pi?7K< zq&m6Eh3C;lH2E$J_QT(1&MLrOPr!VOVTufl5|l{`$5pfpMRL;DDF!>;*n1wsdDD!^ z86^Lc^}XxB$fNJDgutajQ*cR5!00FEUnmv``Fin$CtW$=od;kgF!6>n{}*j<9aUAg zwv7v-f}*hLj!kVO1u1FRbO}h8fV8xLG%CWTQ(BOXph$OvN_U8q0@B?`3*THk&vVZC z{l4?Q|9xZZfx|6pxz?O_T=#XyM1FU<7X83Nqhi1*4d4Py;%c9{dw0_7IV1Wy2k#hB zyq7w_O_(HKSbke3=zLgz|9aIt`OKRScVk%;-b%CzvIEMP1M@K5D5!t()sPc4A4U~P z>l$3MYoCM?#4IFPmU7Gxt3e*~KOhByy6*thCh_=HPpML)54P_$Wtk@$d*QW_AYn5v z58nDq_#H~gzh%Cyk?j(fPMG;(@i$wSJ!$}j9komoM%wfB*FN~jfwpYbnZ0!F-72v*ui|`YYovMa`bA-sTD{Kr(}Fc)15DAufrO_s6&YS32l%AzjxoC2g_Gv_a>6J7$65JbkP?2M7E% zJqkdga!9M!mZGR{a4OvXptcc^uAihYpp z6!?{=62Ch0>IB`)gYSR4ub`Q!l3O%0|2R2=6!eu|H!eormw{-Lxc@32%7t z)DpO?zSw?I?NMh=rz39px@T3!5|YED!YZhM;3G-j$>xXn*`xi<18W~lot#j|qyibY z&?qN`*90}c-4l+q39ZS6Ht{AM7F=O8xyFs}d^l89jeQW;Fj|g-_>{6GUI~)Q0moi^ zTFe#8tVH9WpU0b8bavxL_(}YtAYZa;PhkgDmA>z3n+)c{0 z{MxGgH}m;DS|~hih!^j@7p&)h9@IG()aeV7%VS@f$H!$FKX~)_?O!<)Rr2=(ree6z zkVv4&BqmkNdn1`jBi2#&EsEMUWtf#^x)4A+687o4CSh?PRB90x^Ha?7btbZDI8kbTSs7BBDR|#13OVYk7S#HtC3igw5(GhsWK8)PEI@_1C z{?Z;I^*&#{uoLtb-tD#yv-~aLcDPHT4)z@0v)+P)0w`$!J^sao{5x@N0OG(R!m`CQ z8F13Odo$TJP^}LiD0Z3iYj~^ljP}G3e_{HV_nw!v$uX=%skPE~$?Uc~Ds=Jz|F>K2 zJ;ue?`PTd*P$=y9-#s~`%tuz^mXC6^J>td=HCk0Jwzz04UjmXLgn7E9~$4i;C^L0&m;QvcfPUU721FCdI zo-WepW`YRicSI07IY`cMK>j+(l;LU87b-c~ce`6iclgpDd2_~GB5tdzrMuDTl!9mggJ&D(M zS8o>-8M$$;@;!CfezE)ovm{RzkU6(Uhy}z+k>r1)6mhPXHyTcFCl=j)i+an843P1@ zZ!>s@_^wX-H}P?H*B9`#-;Q5vs#y1(O!T6NJWn)c7?RyyFtevoH9 z%oV-YPL(d^hXMqVOfb-p{QR}78fRwsr1o`_HIpAuQa zW^zU4;1_VsBJ?#bfG6qr>(Bv;kS48sbsi&nnEdy2g@CH6s`nr|?=rq~=kmlzOfc7+ z34`lSukhR;NYdtbxz8<6vwTPU+-B-X7d~`G+xd{`8tQp7Wh%6NC zX*5vlbyL%WzKZl{hI*0Oh}CGR>mgZsjoC#k@|Yg#uOdQaNm!4`t=AM2(Wg)+-F(vV z%IkH*+*s^R)L+?seG`EE7Wjat{jcdkgh5C^0QL>sKNbW$7JVdk3(J!6WuxizT6e2- z*ZetALxr9kRKip>TG`f5KpcTEE`6H6!AhJa=8VPBbdZpT3h3Fd>7KXCj=O8|^S66- zwTt*eM;#K)>*_6nfjjm7qwQld%~Y=cpsRGhy2#0UUSFRbKd6kePaS=ylQ`ny3^WKt z*v-}Ie-`-5S>Ws6-pd&HXnmnj7WTr0=cr=+)s9YR`VlsrK+tCXDDF$ zI$B-%>>f2}Uj?1C;WR@xF3$H7eH?#n?<`MbW%$@f0Mau*vrmlmKazICcOP%kb)Ek? zRj;spf_*+};@>)uDXk*g3eE}spIL6r8|nuc{!wpPrWFzjDBnnaA}Zux>4N-T0-@l7 zE2)*wmIC!1hs5iwaTBT8dzyznPDckexf`}J1`K+!k1SV$BcLqLz)b*qU#~^=$tN0h zS4r`Z{^1h-KEnH*zhdnE*Lc-f8@F;0hwd{n%cG&72t`sdJl&HN7> z!=N2zJG8&I!%=A{LGI$r5MQQBbZ0$RDJq)T{$#nA%&ozFp^0o@NjPiOuW$&*MEt!{ zhI%21C>VOq7^dwI*M0jf){Un2)PT|I_v1F^E9mj(PFs=26=>t^V!!r#4c?1M&A=J% zxb{c7h&Q*NMMfHVbplEOX;KHYYpMxP>M*j`>F9I%SZ&N^n3371k^Eqa3Q9D{V-%N_ z@#=~fO2(JOS!QaTZr!fT^>&>y(d3>A2_Wtms<3* zj+V3mR`&k}upoD3u|8Hp#`6T#36638AB&HeuPIX_kL{Z6M4pp`Qt5qrmtOTjCB+5s zTc8kb3ahcbG=e2tHT98VB+s?DB#SHE{ZHHC1o~s{x!s9(F0x7x8@rr%V)Vj;cIxrg z>kh8Y0`+&8B4tDa9Q?;Z;Alz~i&vJHJQpab`Y${e*er|5gDix0eff^O?C|jINR$x= zz!e{l9_@2W|4l9o@xniXpk1+HspzyJ7>o>^5;f;yj-U=5|HeXLU{o?SQ&trtq~uW`=Epq#I6(#Z8l>cm>7r&5< zbDZb~d-Su>jhc_m=O#}pICNlw#FVHctYdZImie0EmY>++OEMl?I3PZr9hqu7l1fC>$gbRYK~@NWB)V;A{wTI+GV4l%eP7Ue*gFYp#(}0ZvlWj;Y!;Jr~RgjH*qoD%K zmGP1ID{HmG>f4MOVa~IA+Nh13jet4Uv ze?GO2HJEFLOGH*iZZ3&;^S5JXL6pjgkQz7xc0{iZ)8}mDt7; zQWJqq6oHAP7*vBV(EyOuM~@9bm%jr(4xA(&l*^U^wx#3E=%bYHtS|O$R4$r-RhILo zfk``$>py=X9CbckG=K8URbraN0Zf#6(kq9dR9m4Gz zZ+sB#$xb|RII2{RJK{S?{s2;^xcFg&P^o8EH^4Rj9WYEO=H{@zf?BOQ636w5>b@n? zHgl*vXz*?`X4ey>WVrD=HqYazk>&awS}+LA+sw-9hLsg*!z$oJ0$;z*eEwqpIq8wM zgRmE(frElTBLCF}QrN2QWgKkS7s3N>o^2?n{GkZR_swxLtqr z-R~co0?R@FYHcER3hFbIC=B&}GGNfOJQsJvVFox6cHF*A#5*YvGC!)ersBEw2=Ta+ zt}76SklX2(L>w!SqW`H%3IH;&$}=N3@{hQ7r$0UL*Tm>kjpVoa%Xg3p`X6INX4Ks# zWf2ZvjUSo6T79t2UuiVKRblYqiIt$x@&)_LPT?o7{$gSs#KfZ5@FJVXzu<`8trEz) z^$KCRU@^xGFoq0y{C~AX{}ynhc!X`j;16o^wW+dg#C-03te;+o>XH z$D=BHZM%GZejMKjr#cLVkXKLc-1ey>!-5%5s(INgRfp-?$kvOq?5ZCqg#KSdKDE}Z zVzAtFzlaryI_M6Q**$Nf0)~&vSO#mI`HuY>f_m@yPPrUUq(J6HGs9O?f*KND^S5BU z%ts^0yzpA}y#vD@2nd0B@i!3kgJc>98bKUbMnZK-C_>+q+=3Q=)s}mN8aMvI+Ek_D z`rFb#$XtY6Ej}>z|KDmRMLreagKDhgp6Zr8+5|KFOO~hX z0(|V3pRBg1C+(c-)+`qt=RK_RBec=Ey;Zuo`v>Iye|6LdJGp3_=Mcg~j%wuOfPgFZ zFS>G}GP78^H&BF{*VF-Z8v&40hkcj?3OmReuY?zsyR`l59CYe*7HweG>@BwUy=ruF za*Ey}e~@cZfK_OG@FuMBK4_ZZyXZ+~7N4%0#wOy@({AK{#sf@TFK>nvkF=}`XnH>F z(&7M{{Z_{0!?o_pU*Mj)+=ptO?5-q*G7%3V=I!j#0TZm^3+nIOppwH#w^;=1J9iA% z8|tbVVi7~xN=nL2e-0?G<32ohZBBd+1^{ynni)Y`5dzu%mWuSWzimHV$)#fKDsqp| z-V6lE$=ZtCG~G@?E|6NrxM|o34zoz(_Q!9%<8J*h#{iS)S2SoyZUOzqie^T7eQb?D z9uT+wdzc8-H*0*d0o_|)J-^+vJW_K>gp0VWpNR?zQcPVJ5j+z8l*ksy67^@^s?XRv z=uz%Zd=iSAhffQ4eVuuBqK>Ucb`2W52f3J|D1$(}&OE(2Tp@s5mOA9t!nYWRGAi~u zma197Xl~0i=s(}B5L(2iAb5#69@piIiqpHumo4y$_`SJe{B*jMM6}5<+gphT#$PM< zVdOVN^Bj--?bTNvcv-h3z16LJsv9CU`J;2=HpM+K@Z(6YF>=b1R;4=IqiM#a=HLl% z7|Wxl->Q_)_%|E@l_hC#Hr@{mSe8L>j*IBdosA2}8^CwPZQ6J!IJ>*Ytm*Cn(${Xa z%n_91L!@s$A!w|rfOvbC)nV3nR!v#66hUYB$>GKRMb?~M`@d^Ze?x)YRK;A!-(`N4 zplmvnqkw-&@kI+%vpQ>J&=(Fyfq`!enlRG*pMp(jVJ7s<_+Ks04fU5Vr)=EUoD%&% zo9s{SeaJj={)~9lOeXGi1!Oa{IN6LU;o47svY7P06(R==H@k^de==pB04)+QZ1di= zEN1O^C40e}C6;S5WWpXvcTu}*sEpt4-$5TH-I>VqaEC+gb}HsG9_+pI6QV*GXPC}L z;8D0AR4|ZW;yq{d%b)?xH3?f-iIYQYz0q`#4ixA!^h8%n_in!sEq*#^c$}PV=P{Re zb86RZpH30@*?Xp|A~lBI1k-d~fP+LB2`b@UwLcB|syM-8=L6;(-F7=r_>Vf(ciK=b zDzi#TAgG>94iEibYEl1PB@_Z`Y2uMp@6$ljH9p|GZ<<`YNd}#QRUCEz=2Gh8_(2gv zaeYAVHL!v&;kfrqn*69YH#dVqm0h;ZzxZw&okb067o1Bg59L%)09bH;H|Gu-rCy*9 zCcpZxeiD$_d38R0u_gDUD@~=A7JvIDp+EIY;;@U6JOPU~srwF7+Kq?*V#qf;4qG_c z%1HAicQi~CeuRf%mMC?EX7Al|dB|0hE|?3*r*pkZnZgeo_qFgB4yR?!7KfcDEKL#e zrjbJRzJvh)Rjyp~Lvqh2;(Sv_KN5P<6-yjNdt&;nb^$`AB4U$qAb*0gvim!9>m~Yq z_cbdoDZQHUpXsWXQ`gL<@~3x}p8RHE4$xf?LrzO>Z|;0 z3XwuAauY>3O&4mdTTiJ((l@fEf`5m0724AddiYc!Gp6u-wqEWm+j?gpei^#}i%UjpSc7gk&BzfuJ+ z1kYNj58B-XajP&W?=(*uDLQj#BUDRfzg2oPb#i8dNRf^t zA+~c;aSR%9uK;R;v!?A2_JU4F$op=NMI2w_+VLI=stq7G(dwN;#P_T^WosHZknv5b zR51+A%gl5ZJWsk_z76IiBN4u@25Zec5)#Cz#=;0iaF(c~G2&E77RMV13{b^|2+n6gj(E;7~?@+Tm2CX5n;BVj~QOzs?V={CUCso5V4Z0!| z&t3M$5F7j=2Dt5Yi6o{MS1^gxd!}!KAA6=8&^2Rm64iw2=W%z;CHgbOqYP_Y$n$PG zkAG0h_>4j%n6Hbj)%>j1eWBcuaxZy-bfVaz4|iv0=8teiyW5W_xK8?+?Gy=)Hws~9 zY&K zo(Y`ZSAySATmGo_O~3I~*>_r}o3%VKCBG?<8Q$(sr7~zXp2esmYi44a9>c1ub|i!k zAJ~3>C(e?ou;*@T%$OqZF;M!{BiBOA%7I>Q_YwH2{?z>ox<=E@LcA-!K$rU4V&P4K zWLd*ekl#3>`bqdcY&N?~Y49n3_d+GU`uU$j6QbTk;{=oPSLAc=3OtZ0@KA=2uF3>X z9AKpk?V1D)2%NMq^XY&leGficM9rXJ)yBS|}Fc!*TrzIH)H z0|%K5n56CAre;S9gU5Czc}*$ui?Z#r*!P_pvmN6*J1OcXf(L8ADNxOxydN4Ey`mrjPU=PTa(hK~7r3 z|D-b7xMT}a2iQ0i0`hXgb5i|A8Y93Jb7AkJ-c7P z(b#l%EE8W{$HBM>P1J1#yPab>$-KQy6%f~c=SzbrafRP(I1`UYg@j_cgZ@=-4}0}6 zkfRB1QFs}u+lEE;8?GW1K5aFCcvLWc1-$z3FcvedSanz=$J*T2nkLL!+40)Q@lsPJ zkqLh7wRTm#fYl00=z$1jy}R?vH!f<=bVKK39~#VW-jr8!hCYlZCZfBF15mQdCnxYFK5F#(k+)S8xKAfHZtv zsB|4<#9zdIh{SO<=(+|}5DO^aSA!Q=;e=oV)50_qVKVezc%h2|#lS5vddOj|yQy3v zK$X5ZZ6-`N`L|CBH&*-Xu+#i(Vv}nHsXI|W)wg%Zd8{OdKMt}R2SgiePB%n=l;*G9 z?7i&sc-l|>?T z_NHS5TDLEhLqP(6d2Rd-8%e*q=@`S;$cdUb=82loFNOA$!{Y@%pI>PrVb5QD%OnYF z)pLKjM~AV*BmrynJPhJxcu|b}JO%GM*!ZDdVkSdA4&v8eGc%^VKESL7#Zv!eR)M<>pJyWAUX=nsm6HmkEIu{N~`q$@nV`ES=y|qmwox z{s{J%0dl<3p7O5NCr0yBfNLQ~!5jGh{*4AId^MlyR~Z^5c~j!;|E9@WtO#-R^Ru4J zXTG!fBa>v*evHaLGBta#S=y}SF;`DIzqZk82aYQU_;~rYlyO24edUyCw{QemzykKw zaHSFB1oA*Uwk-@8HHKR8h^ zh8)9l;MEKppV5RZDz{el+C8(^aTE)zKlUGe^SYu3*q(W&Kl@Ebf~#e;pw3PwF^SI9 zj7n@H%z6-=XVDiAo=UmyOHFiuE2I@^r(gAeRxmtvgtfM}-;fC-c~hd2avulGoh~Jp zl0}w~E4m{gL(eG(>{7QqIg2v%H}IK>1@h%ovtf82Cr}7_tDgxjlHhv`u;1I-bQp8s zPqHO+yarNTe*5yGOkmm+ued^Q2!#0WH$3%lR4>xUF>LU;*DwAHfmI`!tX^OTl z66@suAxJ>pegh>$PXq$>fLR> zgjS&Ifjr=1W&ABT_?}Jz_PPoahl~a6l^|($JUmqDrPbvXzTg&^Nt(!#vqfM-gxD%^ zQ?brH53s;=^X5NwRUF2TH4hJ#=R_&g(a4G;3LSfEMP#Q*F?K@!`0EaJwBW`N(29)_ zCo{%h_aO!8)pmk2;^*K5=Ad{ix`>`F4}7r{VMUMOG6{nRp~FaI0FSUx8BWN2`NVWD zoFH+j{^`z{g7p=00;Ud1HFN}Ym$>OUTXcXkc))O1bwyS*g7GMA0T0TF1<0&y0K`R7@zeE>+RZ<0eu!YmIyrq+*ce zz4b+vYZgekod?6HG-f5VCG}qrz?Z$FDdx~o}20U z&*#K{`zK&cNOF#C?gk$CZS?3dkhqCz}*m7imTNU=SlW*Ddg#ci72CZ80AyAtOC9v);+D{e0n&|`4 zsUGoju4H`7hPmT%dJArBZ!jk(70L$>#eW#M4DUdTpTL6z?^W9g=7v(I??xgNR!Y2igGW^MYFv2wc*bEaywCucF}j zXU<9JuKWjt4972CEYsN$0ukVy9gPgk*Eh)}U)|RGXBNQ9U=t$W=fL(htsOGH_-p(8 z?aw!%8)I8QniK$OQr};i#0WI>-o0LiqPLx{>v4T5e%Ha)ErJG)L@6Hmp9F8w$+{;#l+5E`e|tAfWcJ- z7HtyddIh}PL!jwQM0Bp$;E{r%SOtHbYFH90|9D;N$4ESiU_{LOd!gRbn410^4w(8^ z3fdc!l~k8z`z82of`CA{VzhVj6AT%mIBQ)=urssJ&KzBLIbp=d#8iAlWKY_LMc@h# z%?v>ATjbMbM_!r{Zg55gyesH7<-;u5+n&qhyq51&Qu*6*VEH_@KscnBf~i)b+TK;J&&aChyzJ%BBe1IAFBajSXW5&g_H#N z7b3Z#rLjo5$17Gv6YilX0vc!=emMD;XQ)tu$G=J|&D;a+_vI_&yi)}j7Q}j?3;AQQ z;J>bb)}i3f>{Er$BUs@wGJ}!y?_;rt#HI9EckRUX9e&8F-h#Cb1MEdao&au>1bIP= zj%a!^?#BTlul3465;RYHF~?ksT81dkspQ@z;>N`=cTE4LYJ4`t!8Nxgs=t3tr%oMaZs+xr{5lYD>)8rCvx~ zd%tABIo}Z{?bT=@{qwnV!Dc%-7rqD<>4No0EvpyafkvrFFaasT2w!3Mpwxb2ju}n#Q}SU}9pgn_>wUyigC_c)+cBiQ;+N+01=xG6&mT}` z$42u1&8pd9E~3$7k;Gi8RP~;ReZ{(o9|FoO9MBl!W4`8yT)cgEX5ie&R_C9n zd`gR8A#|aocTMLhj}g5c;76k5V;FwC7ngH$IoQV;E_{TfZZs_$A>(oRlu{)(`@MS! z0OMCb-R9+EVi@&qdgIP%_y&RXU#v*|XjpHHI?Q(aplv-jsPEO-dehki{VL(MezR3m z)WqmH@9F!c)U^=zH(^>a>5h%752Rx8OJ&UuWry}SG~{cYk1CebGUOT*Zx4G8J8I*- zlD+e2??ArZ>-c7dm|tvNBoS-uzK@bwl&Rq-H4p?=7^US$U~iY{?9p|$i_nfhI5Z#& z#3(ppV2cu_L;r$YK;;T$fZ?;-AhlA$JfPdo`iK|_SAexr=syJ!{W~VeORSHV4VGFc zZEVvk^1>BS+dF2EJUiK9ihJ#)@x@u)C$h6HxnyMkri})+0YpM7dx`l%#emua&hq;+ zhFf+3{@u{ZQZ)}xzmErlBe|zOUsI%}N0Z6s)VOSkqGcl%G0WElWI_=mCB12Uyd&#| zOrq0hU60I|5dd8zVnRQC&kcpeoj+SyN@YM2HFw-6-FzFD?nr;JRlH;55}^@_C@fJ+ z%ih3&URwFzFUFEWjWmWp@`#~L{cf`N!%^n}@wz8#(cnw*nMv6X=IHMe50C)4eH7~PT zp4&TnyfiKjI?3|)efFXrJ{i+R;=$CBhQFPz!xqja%YYpDgt-H#``ZzJiCP2mZ2h~4 z*>y-kgT+GSHZ&jf7I?LJ~kdabc?WwcJo8Vv<`^9FhxaQHl~>^&4hJC z?a% z3&m>AnhkpU`|%7$>?Iwe8YL$jMC-4u@aEjoa8P-%6=CrF`0Vus6Id$8a}myCpFG3- zP_UX>iDKqE_u~Cwo^1X%_szs`jt?`LG`z>4|({%tcn&rLVf<>Fa<*)f?yYDV$(GMXCZQ z2&saPL^ZtxOs45OXr&GCZoXUl6*gy;rFvDsNbh-XAQB1dfjC6m^+}v=lD;AOpS4@K zew``OI&I-LKoDM*W}*nd^CVUNb#=?0z0(%GZi!R&otZY@ zwHP5gOCxgL97xx?g5Zh|nb~o~v5v;`jvJV1|6vCs;ZVpS0)-rsmm!A|up^`3%hrm6 zurekLZvt$EM_{WCev%)kV9vaP_^!b$xh$*xbe^8$y`|>f-d@Yc{}Bd#V8V|T=)Xr1 z`ptojMfRUa6e5t{85q@Rz|us0r3#!J=Uy~UnGe@95WrW1Q11yuy>R7liz3N2U^34b z(arH}pAuCU*~d|_*=(QY0ux*OFa88`nTQ!Q6oG}9q6a^C?tw~SjjL(mzQzSEcHeM~ zryBmc-F3Lci8uZvtU1p-=n)b|9kR^*CRAJdlqiH;DJKiLGFw}^9lhh<=v(4t!ivUf z5C;Cu;%n@-Oh86M%CzIZwGSI}MiqGsh$#Oy2`E*R%wxf$Z4p=M9egl1JL~CUVj+4u z_FhfSO$yekJ}<-x%xnW#>kL! z%8aSo?0mOi*9{aMv^Af4{5g#djHPg+px3|u&7{_ku41y*nV2)}^wx*S(+=OTFhWM< zaOI{a(lv^F^9YWFP*xAe)7fDhTEkkW>q~uUEuax^z6R92;j}8gk$yw*J7#t1zDshA z_){o_a!#GBu{kfbi~F5ff*vP=EsY?pM{F}>DKT}{lm+-) z6y+96z29$4|F?o@W9^S)v?y}kgcScjCrs{7@6^+_oTJuMsNt)9;9QaST=^!g*=wdSfX4U#o3lP!H5b;CXl%x*E< z&QdGn`vknqJ)b+XJ~(ac@)3byX|4&Ua{Lcmexde#yqEnW_I>iSKYRkFtcmt}v(UTl! z^<-x`sJzI}UQK@gk-OQ2 zZnGb6OZ=n##PjlmbVv0CpFcUjqv_N!g?u0POBz1#SAaI*T7hsy>^B`Qqvo_+*9Euf zCU07!dJzC>v5t;?!~;JwaiZ?1@Y z^EL6#o2TR1T?R8SNYOxYkLup*ht#~7GV#%Sl~pL7$5XeCd_m;AnV;D>Zx6t~w_j2++&>N`zd+|1p`Nu|I7xyGaVS15QgwGGF!N_5&yYh2y7v1g=BM z##Im(C(s!pRG8J$!;KrxrqASHOySeYhBT0w9}AIini6TZMz~Mlx?iSXp#KOA!wE%5 zI!Dm*VQaSC)@0iyMg(f5-uZM4H5STQdGIw^=HZF?o&^BDaBcnyJxTtwv$Dfzy(AOW z1ntp+Lk4eEG`r{B3OswW{=VMjk<8>mF8hX9lHIQZ>YD16W^UPt4!!yqzs{HBWW#l? z(GQwm$~xHp8n+^wZHchb&0e&pN2B^&EKx-U)5Ah+>WwZcZdCSPN9^!@FHX0bl7uYo zx8ZJmpuQ{bwH|+sCu&6HY7?^2c@*bKf3l7}8+r+ac+fRitY?QyE~M4@$!a=vlzSjA z?rz*iVp#$yhgE^+>}D;f+!0kOUb470T=l&if?2sV=Qf`83xfs6IaVzE_Z1#XV++VvNI*CDdfQ zP2k2&|-=xO4VSY8PJ4wJ^O!hWSK|)9% z{v*&k8zJ22F}$mqPCc0E?ak+Scd*#jTyIf*weDc^?{#A;4{;T-V*K21+`D!2_#dVY z+aB_2F@D9z{bcidOZ^NwoEH}_1hi>mOVu*OUcl7ZSp?k|*hKvIsD*ewNX(e<>iW?e zK=4`u8@t};4@O?)In&`lsf);aH5w+Y2J$9AxjJ*QGhtXUJ+^;Dq=@LNjY}iNsiwgw zKxue)QlxS{{`L6h9Q<+DW_YC4&SymFY&$FbvV_ zii0T3EH^BhC*TN4J%ap+MuLzyX8w;m{S#717);i$TSI=dNjrT&d(8Z{<8-~WW7O1G zoq2{kBF+XjF3SeHCWnNrebh&%0MBFVLe(7Fqw$Og79tnoV0H_ZJzd;LHX-m3uGK~a zS6mk?d1j`XvTsv7*TJ71GFYao79w)8%u=Ve9-r>m#-x$;cI+bv+9D)MDHE9?>2jp~ zPw4{bZMipKs*JJE1(FIB8p!gLFI@SuA`Y2i#}^F@X!fb!B#B!-GNs6!hXqdu@LGaz&+v9aj{_Bz%ORLdsBw!##8)Y#uwax z3xjtBU|i*Xg1Vq&BIVEz1O-E5;f((g_v~Zkd+t$XWeWarcXq$5@~qjhuwk7{ z0EN`r$+VGx!Vn~ntu?LDG^wu%y=DFwRG`pK;q}ZG^p__tw-Kx>#@~$++?kyLwYAkw zeHSwJwXzt=Ko~cEt}P#^gfESJitEvTcKU0Ino8US9|RHux5vi{IO^q;6S|S_(?GJI zYX$V&tWIcs(!ZgOmVMJ*>*>iL(>3fe@$l1-(fiJ4%M&-;R_iiBf7&PoPe{LZ%1c4+!I(bjxt;ZGX}MXskbOs0Ta9`%t( zsu(|*2?VqAH|f8S(KHdWV$(~bmVUIBk(PL;o+mRk?l+lkJy@tk%$6sRUVbjfAK1fJOiU@)9?+rU_@+AS}ZrgQjG-8Sw%;@}dUJ~K%gZ|%O? zsd*9>8>I1O6RwhDG+OylujzUh5&H+$Yn4<#196j8ny{a*oLSUb3kMCpdiHi)NE+7K z7ckz?eHlSk{X2pTWCo5Nqa4cC|L6lEI&&+dc;o1Xr<6m*Eqbr}7qq498Gl*SfZtdL zG&L_NJ<{oS)M5rIHZStGKEGrGq}i1a75UGd*t!(NuL(lq?5RFTV{)p}-Is}P4I|A0 zRR;VF|50HeD2icsi8oZ6-loz0oy7^Hgt&vMlZ7yUaHJon>f7x^x%d>7IyE?`HBuy5 zM8;2Dm6P-#H^HVIM9U;jJzTk1n4=9rWQh49ch1P&G~|Pn^@?rxBwNBhTu}GUT&<=l1Cp4ls=kYg8$1*fX z31BivpYd|PYk}BkQT_6bmtwE>=fb?a^Cjh(wYuguXOvB}|Lq5SS34x~t*PPukj50lfA6OZHvOFLwc~#942NPv zj=a72&vpr~dQnWB2K6T^km+@9^4TGHJ1M$78uG0+XwAJS1|EskB@33z_^K#nk} z6iCw2dxbB%n1~aoi}{~~i|;0mDA1GhWVe_0D#3JO8OqyZuF4Id{n+eb%>Ai>Jj6#4kO1Bf*ULZOU&Vz%izRPc^Sm*FlAlKE&LBTi_ns+b%qF852}Nv8H?$X# z-}*SoC{ISac9_pg;U+6DE2Te3!%|2iU7s;)6ZW2Oc%*j&Fi*RGa_kr_ltU2kU6rcF zNk$kmVQjWMXMt$#zE9Xo0I%kHI(cmcq-gw+{yOq8csKGS6a6Dn>9;D^`*VUXkPH}-8m`d&1TLwwG6)#Zt*>G3>7`v+jX zl>F5_>Zn`1h-kU-tktDe;`PfHw2e2igyRwcPZ<(K%=l{L zd=`9dam(tOjSj?kCX1NpI4C1w>I#dTli~a1JZlqolY#sPsY@YS!h7q~=s}T;B57@z z)e?zrm2Hgv;#AvJ--h-MPw}Rrj}PGj4yRKO*KR~50hFE7~YK9gk}_>&V|eq?}9Jf zU%}D1q62vs8MQ*J$$;y~QturS^gapHV9Y2WRu3Gb54^oFu=z$KNA%VUd3nJudh{x4 z`N0GOBU}-x33OPMQn+%Y@FP3Txa0}v$$@oVLfD^i&*hgXqs-}w{*E^n6tXzJuCS?X zf4jiUrZmca%L_EAv%M+@f$3dYRhmpT0cJGnBO`C3 zs$4DZ$}{^nJyC$>MbN{|+D<0zc3dYi-f%v%D(dqsHr0AO&r-$T`)grR$zg73Dflka zrfb3Z7_Ps2Vc-i2RPcGa@;l4E&&FCPnVq0>ZH*N`s!K(mUpD>h1p#_URp^36 zeER%LoSa&*>z-sv3))Y}wu!lvO2;CK(ekms47O@@=$W@l&^%15^0{e%o z*gvHpQh*VRzcK{TgbRR~MkV)*pImRm`-9J=ihKVf#gQyVw^e7tW!xw9qKR{=)r54j zgY;S9Z^Bq6Y#-r-g@&_gCYg9zUb9;SPO_Ndp`lVZt>hjNu5gS|W{BU9Wl|dWsLqgz zVGeNeKRhl07W*ZhsczABiTyX80of!b##GP!b!tY19Djv(_h1%ZLhTXyps6^sCjV1 z<(%2K#z6iWSGu@nsMG$>Nji05T!4<8xe4GdJ@1p1=+UlZGzYq`Oz#uXpHjt*Ao_A5 z&Baa!tKIdCDk|WiGkxGYA?CK}?<1_n5#rma>uV43a)A$jJ5`o7?W<_q_MF$2ReknTz z(3rr8v<(?w!)BS~6!Nv!eIi^4B0X+CIal#&^nJ)h&S$~+X`~Rnc$*Ne5gZMRmuAC- zLaE5W%we1m)A1Z8@#l@caqnLe)S}pf`>3%ADw;ttWE*y$9x{?#z!lK--}Z=o$IWjC z4L%H9XSjU3IWd-B+6J_lUHY)rZNK9JdC49V(f;|pjrqYi-A1~H>9(U496T1C?^Dg3 zWQa;ejM_aE`vrWlShM%Q6t=l4_Sfm3P~5L46RXMyqHhxtdZk_0&&M163;S9f8oG|GE0cE zCE!;51*{f0$SR)dHj`YfwYBcV{*CQ6@8Wt8pk1b0(;@RIqPt|mIWKB}SA>3v5+zLBXI3y_bDp6Ymh zN3YCEZoKOawGeS?#D@2u2Y1$|7%pO~{K_oF*LB7!8TlDr-3J0qD#+(P3Vr5}tJL-jnwil;;hn&mRxg+pJc=(P60d-7NDXI<=m+_ayBt z*B^jaa!q&|i0U(4FK(x(bTE9~XOFo+1EZHkh8_>9XPs?nw4^l+TLRa))SoWZd-H@# zeAMRGb-aEI`2>+S6GeO>zd{i-qSxQKMx9#*+b$X3sH?o@cRoMvK{nyONK(7-Y5L$r9Su=t?Z3AeTQC(w9gnhdGq)1nhaX| zRm$@6xVl&4N;ba{18*{TFKX{+0Y{@h2Rj>h-5q~q@vn?jvFUr*W-q=$&e^I!07#J} zZb=oHte9h=S&{jc0Ob9#5+-ZeFJ*M?KV%fDuaHQFWYhr2s2P8ldt>XHI+KMEEsnRh zI^)is95aqzx_x%WVz=pQKmE@X9qjw95oRC@L*j5VH70|nrV<4kWeB=p_1cZb{Ge`hqQN0xI+@`soccAE*?e?% z%nNGgQIBkfs!v=5{Ru!=`ExI2@L@RIoc?bx5gl+9(E5YTq=Q1B6Prs3b1;k2UoRjN zHT22X9AEXL1x88i~dPYcyd`v#Ok|bjba9Lk8wRlcGsgJcVfl{ z*EO$lM|B;J3nsfePbWQOd&63wQ4k0ZTch!8VMo)|t?(7Zez@AmrH_VXrHnYw zq7UxY4&K6*i6_~ZoDq5#JXooi32duRvWZWwiBRa9yP_$pzAOsy%L5G!DWo_+2hrjH7Yw+*vx;6y6&>NW8)6Tqq(Km< zCW+E4Un{*# zc@;4HE9I35l}TN(fHLk5nM`Pi>WUj_44SUGz-N|4X&p#}1#z>tD+6Oida3tDms-o*)YyELmNja0wa zMytp*yh9K+L)p@2SU4|x_9ou29c`aC=%*Jwmh3TTxCtr`)w|O`Y4J7eD7az_mC(-a zvFnNw?w`SWBVnr=Y9Ty=Vj-xQ1>}KJ%`JofcKH&R{B-y4Y!A zxXV1m@0NXtXhnT7wuRh%wbS24{S(0@$ow(qdF*>kN;(pqJj5`rsf%#FxbByfFpH(k zTSH;=rAg~Lk9%Uoo3@-^}rOtS1Co))z^X|k~0^nW3Y>W zBN%?XiJ}voj{TqBXO{=OQ1W9#q?S7O+qFt>$*Nj z{0(+Kz~7V+>VUl(eYnV`)q0U3$VZiXtHO&(ovYRQ76~OtLboeuGS#2|729T zLY?eYnT?tRP7gQP48TQ)eKbOrXQO5@qU2DFvXCa$s4Flh(Rg716H-Yvm{#fDGk!*fesca3^&-h<|A1IZ(nx>AIy&}xc+IE*)y}$mDkl)y~kSJ)a1108N|adt;MZsKa(Z{NkCz&D?Gb~wU^K}WyDcHp_i}slJbF@gviRi2?cm6& z7ezzgq}buk{EXdlO;o@3|DJzWqf{!36|PdBcuacsVQD0^D}i~h_7RSYBs5DU>R+%} zuP!^eHly5#9tDH&i-1NUjop*pyQT|;sIO&~YYXV)E&QPS_hnHE{ofl{{us+gbAQ%Z z+~Xfm47;R;AQ+^^Qf+xXPV?e=a$GQ#zr@(=3)HGH;o+;4qV{eq2mCu+!~gGx3j~=d z6a7qCRdD;I=eC$@vf<4$es$iR$g?GiLW+Ww+1z>bs{Zgc5#dCun_6h8H+iZn{W>|A zR|bc+8-3wTEyEaq4jOd6abxk=;;E$+2o8KLlpV5Wql_HM~B^-zbv-fH1N)a|QEZTDm`vag3MjeQJVY!WfJzUH4rot^Pc6ne%@5 z*k~eOw5Y?ts!+F?BQD-) z>>Pj~Rd6#T)xwSs4uCm}A5LTK)tEj~_Mb_xJFr+F!4Wn{(5F{1&) zz6;s?8vF05756i$y#A28%r!qeHU_EGhXn&GORZXtpXq|RQPmgYwt3Qt0F)zJYS8%T zt_e%|zo%~XyNU8?I+xpBjBF7&-_D6Dd|-(ywx3dMoOIhzlg~R1T2BNjiZ29$=(tjg z#KZbHAOR7)aF1)OunjO!2$v=zT6`0W|Mp4}M`E6pPFhq7;5ByaQTt<*uxXh8;h&X} zNcMYNAOJsT_(iDFJp?sP;6hvMI{4#v3|;LB_22ufMro4T4I^g>TaUK@X%nE(Ex~h> zzdN3A*v|Wvb26lG@fQ`wzQNC4gaN^$ zL0DH94AKo-tTs)Y8n1cFM6VLAggIy@cio(?-foY5`=U~+&Zu6Zi}q*I zu6^gLoJy^$O+*UWWl$Pr1}eU@)fi#D-Gb;UAxkW`$)i%88m;TE8_%|rQx8;{o!_e` zUo&fdeGC_NdloMh9oq}Y;GbW*d{8gcAjt4K<9zuRoy2sZX8SjrmC}F@MrfXNC@z?{ zN9B9>muKFYRsZ`(U;QT8Ht&cCGu6D$GSpK2^3dEaqMxhN%UyI53Djs<#zRXTj(#M5 zR}XXZ6@x&42oo+LWb57FvU1oMXS`}W{mi*spChp}nICyNxJJ=7p{z$^nsJ@@a`ooI zj+oEvaW`r3#V%Wg%gP)d1NGkq-h~z)reQnu^ZmKdk<=xO5 zEIT4TYc*Mu?9VlRoirPceK7x(LA;HKsW@pkQ}E#*?$VI&(cL&quGV?Cd<1heGQ~O> z{2v79q$!zkezkf;jVgEk{^`i_e{2DEce&0s$7YLKi@G!qa zEP^k8k9Z%9?*`Fe9~2;vbimlJSw2qWZC)bGC~@H(E?fgsnbG+U}* zL#vJd5Odk8?wV4mHK5TU(kwsXdGJy0qoTK^i?li& z1-JcC3UVQoHU`$0$U0KiMUeF|0NlBdqbcf16_3BK@*cH5-JLODv72fj@;oIe-guD+ zj^^{@Izkn~VS|X}JA^v@=43uebF){GhZWux=UQ^69mV+IGr2g?xh!Tsj7Z09#0kq` z5MRz5A!OjUha5+q(mT|Iy#BbQ_L;kZ%HtVwtF4KC%;%bRsz0yE5>vQs z`RO84OEp5EP!)k#Trt@O*Y(uXnG-`^*Dc~_dl(5%Yya|}Z{0hpIInbr;vac4(e=}@ zWwxn@%=;J@fCwlkFgd!;)psxS6TL~xgs6M)gN%h7L{d}9BO z6;!yNZ9E`HoIJ`BzA4&`#(M=BAcBQ9e+GQ2>{yVVk|x-Q^tQ!4xH`RaYlfe`Fr=YU zZLD<x22!IeaYVY(IMpZD_7r38>&1|v&kQs$Abdr(<5SB zory0fRIe%Qx9UE7AbA^lJ)BykAG!sZ*Qxg(@qh5)GdC3!l`2;2WsW+d0ZQvsg?JzF ziuIno-J$`3_>n9U9?ahRA=NidFuQ5%;r5wfZm4}~#^ zg}omuteVpp^6N;Qkd9%9mBCqG4Z%9x87J|ov(vnpqC;(JVA7!tR_+ghA?I^(!0<>Pio z#MH@?c-U|asACZGh&~t*PpbE`Ed1{u{zKtgfWAS`wSHAC41yY%6j@S)$7t9!0cT!TfK-igJI1y+9+f zedJ4}r4Cx*;HvcmjxOwcSKBBR4H~E&Z!Us%rG9l-*?Lnu8&mL(Wut`bXOsraB zX%E113$L^<{3Yhgt3N%tvN>zKeHi|(r}qvp!I5{9qZd9yF-8}KEpxT?%JMa6W}%3a z>t1AZuxUFn43oKn82$MvxR!w*f7Lk%xHy_ICHLg<;eihg#6|%f2#BUsH2Dh~L7l;E z&qPvbwkEy`19Jw&LV^zIX)!3D*A6qO`un^nawb>i^ww=e0Lj;1_-26!IRT1 zR=RbB^J-Qp$*>Pu_wGRAO7A^K`eF*HP=QW6xyiyE7e-6T1Bv|q$U*I7{_a(f z9MPmLRoDzM43{yWA>XwKaEypbUAIqf+S;#v7}}qGrr2*jju)<}9Aw`cm%y025QJcQegb zT5Z1(;jDj~T*ReD$X^COi*v5>o3iE0>7U*nivfAOcZ_Lh+ensFa#ERTHjT_T(`mnr z8qeWA|4)_nNeH&n0nM^7^Dvyc$*;Ms8XOPE{miyU$OimtNvT6t;-1}*mJf_@5myN8c5i0$y53b zRrN5%cxH*muZuMFBO4((V*qyj2oMI^8`|IjFer5%9#z|(ps#$!%{Ye%hs&N;!yw^{ z+1%KsrlHl@uX5O46;KE5O4p~*@pQmm5rys)>9z*UWoc<<&_%|I8eiseF`T$=oMd>} zIp|(rESoTTmSvraj0j3oe!Pb;{9GTvfg&b(#b;)nC-(yD2tm}y_K<^{BI!R=b)X&S zbeAsr>4r1ZdspPjFI(?G15+GsJADAZU5@3Ie6!chqlQfvMgBQPN;)#|5cX!YLTCf0Ak>e%B z<06!VPR(X``{{Nju4D=pSPPMzI#)Qlp1^E~&QdG*bOCGMde9j4fKw?;P%L|{$mQ)6 zg|CLl2sS}BAXe(K-W3XtLc1wnttRgb&(Ii1AlXr_IEb_;`7yQ8+Io00 zLjsF(4jz#%Q_iX7qMB!GOVW=!?Jx$;cThi`&WddptB>q z-k0%Wj7UK|Hr+qYG+2JSYWpF-|O{Elr+0J_(7c zuy{Of2+#cVNDLiy z)?}gP{kQK_1B!Z0VZ08jE9)nl3~|&2bES^Mq#!g7W9DXQePv*UE zRJh&@uB!C1kJ!Hk(h@uJ-b>k8;jHgYv0ZXmamvhM-L5Rbj;d7({L}jT%d`a1e1snO z<$o9(#S;S3$bUvMIPQ0$c@$64DJ0>BD9*?D^=e{1#T`L-gate@T*2d%2edvH79>*E zl7jZJ`R_BYlOxUf%}1cq0>rLV>OVNlf0r7Wmx#;r9EJXAZ;D1E98Sw;`KANqNRae> zolh)(I4UX80AGr?MLfA__P%%l=0d#Wy*Rb>2U;3MCyQREkALS4e%bI#C{%yR27vj6 z+;|r=LyZ1e_fIJ}pV^k(iY%%;=VJh{SMDWX4qzi+eMfY(ko7})Uh7-s@ef}Rw=!i3 zxFv)Yo%cX!tx^)jGTLIu!zbd`ixNThTDD9sl^NqabC&w6MPXPC^+biU}|M7Ij&RG95_pASP zqI6AbLCGxg)>57$Whpl@%yHYFCdcx8q;llMZ3GZ&EE_$-jR+^eF^XtOrj{TkjhOCyu>T{<3eKaMUeBx|Kt^wd(tnIg0J>1S}X zfT}>N_gyg0f$;K7c>VW_L)v{I&mu<{l?=l=XAkr|FDj980)t~&71Ura?WX#3rIt0A zlwV$r(uG+PsSyoH0eOwJe4XsPS}6A%x z;dBEoVg{c%J)fA%r%)0x;_%lT|IowDaSZi_!~cT1B?CH@CqOa4ESbZ3P~mYZ z&b&5rydLC`GV~V^mME|KgD=havEpQ}l38POKU%Jb%`he9+B@74sX+gXE_~wf>wOa2 z-O%KQ&GkXuvL}{ftRUC;?1N*=afk+#gndPcPbZ>qFjIhMLwc215)Q8Tkk|H{2k~S@ z*4%pk?i99wBH353GriSlkvzi$1Ao{Wr$x`>+Bq#TrkruBS^Ni5QNOsJug&0KFq^iVj=pP*@&MU3gZZCmglL|fcTI~An z#meUEH*MIbm!m$6oHDE#-@iHkwY}B&9_W3S##4dRLecj*@X!7AO<%Lt&qnMvVxh_F zc}Asjv%1HS?gpY0%|;ho63{IX_uqrFH7j4YZrwcQFzM0w2AUz=o1|O=h$uk(EnZDv zH6#FPHk_nB=77SA#O2Rw$Y?#s=}|Vv!BHV?de|x|zOBh8;_0rmAVQl3w5Lj^zZ>=s zScv1Sw}#^#mv$V%|ted)#cW$Zp(-)7PDnZZWp( zw z@^#FkxnP^R8OLY#SH|y76GaA+J5%20MoMPQG`nI73Y@BM=FyZ!0?SYhgE2gvCuNv_ z$S2}^skI>Tb!94G<>8y~#8Pi=d==NMX+{94Szw|fP$3-BgW){5GgSPVCMvpdc=bs- zfhM-9SeY#shP%_JZ`|H?u-`pz;g3+lW094oxiQpXY<%;$J2DZQBd0gcK*LJsw^OLZ zKllqb_2Gfv;?edl+V;uzraSzcioC5clFgT50zDYT421fR#SAiw zD}Ctyv6w|sbbtq`0YS8nf#=Qi#-P6)y=>Y`Dn~OJS_}q<46B^>5X6LK zzuimik|{AJDdoV({}^=Uf{`s0gBDMtaRvm>c3C_`XS`6QD|P#`jnx#4LDdxNFG$EN z=2{)_rponVeMxo^>+U#wW|PNej<0O+!viG3q*XYMKd>c_*%`DXQRl-VjGUhw^(26b z-SW=Ywxgv5pY27j@2ruurnu|M&;*CBV56T2MUHEUYCPpk6OGllJ>39ptju* zA$Mup>dkQ(8l}&w^f`Mm-CwfC@?&=sMeuPX`k5?DoCsY2%7&`t=G|4m!{e`F;B%}O zD^;Sn<7l)w_e7j#weV`?nR1?_v6FM{OsMTb#rhP)969T;e43mSeT~{w=SQ|zf0!H> zj(%%iaXJbp7on|oIQwwAzeihvWYsn$q;TIyc zf_b3HN`bjK>Ictrh6D6XO08u}oXaJ^UD(vMlS2lQ*$nfx+Sb2r{!w#@!@%wRajCtU zH$v~!Kg!yDGHijsad$b7%e3=n`z{+V{FM9eB4rz;WP|(aj>6W23?d){pu4kmv G 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
    TVQ-14{{- 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

    #I#=ET6*`j1(B(^G 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 z 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 N + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contracts/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol b/contracts/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol new file mode 100644 index 00000000000..6305311050d --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../libraries/Client.sol"; + +/// @notice Application contracts that intend to receive messages from +/// the router should implement this interface. +interface IAny2EVMMessageReceiver { + /// @notice Called by the Router to deliver a message. + /// If this reverts, any token transfers also revert. The message + /// will move to a FAILED state and become available for manual execution. + /// @param message CCIP Message + /// @dev Note ensure you check the msg.sender is the OffRampRouter + function ccipReceive(Client.Any2EVMMessage calldata message) external; +} diff --git a/contracts/src/v0.8/ccip/interfaces/IAny2EVMOffRamp.sol b/contracts/src/v0.8/ccip/interfaces/IAny2EVMOffRamp.sol new file mode 100644 index 00000000000..1881dede2ee --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IAny2EVMOffRamp.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IAny2EVMOffRamp { + /// @notice Returns the the current nonce for a receiver. + /// @param sender The sender address + /// @return nonce The nonce value belonging to the sender address. + function getSenderNonce(address sender) external view returns (uint64 nonce); +} diff --git a/contracts/src/v0.8/ccip/interfaces/ICommitStore.sol b/contracts/src/v0.8/ccip/interfaces/ICommitStore.sol new file mode 100644 index 00000000000..1183eb277b8 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/ICommitStore.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ICommitStore { + /// @notice Returns timestamp of when root was accepted or 0 if verification fails. + /// @dev This method uses a merkle tree within a merkle tree, with the hashedLeaves, + /// proofs and proofFlagBits being used to get the root of the inner tree. + /// This root is then used as the singular leaf of the outer tree. + function verify( + bytes32[] calldata hashedLeaves, + bytes32[] calldata proofs, + uint256 proofFlagBits + ) external view returns (uint256 timestamp); + + /// @notice Returns the expected next sequence number + function getExpectedNextSequenceNumber() external view returns (uint64 sequenceNumber); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IEVM2AnyOnRamp.sol b/contracts/src/v0.8/ccip/interfaces/IEVM2AnyOnRamp.sol new file mode 100644 index 00000000000..d657e148cb2 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IEVM2AnyOnRamp.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IEVM2AnyOnRampClient} from "./IEVM2AnyOnRampClient.sol"; + +interface IEVM2AnyOnRamp is IEVM2AnyOnRampClient { + /// @notice Gets the next sequence number to be used in the onRamp + /// @return the next sequence number to be used + function getExpectedNextSequenceNumber() external view returns (uint64); + + /// @notice Get the next nonce for a given sender + /// @param sender The sender to get the nonce for + /// @return nonce The next nonce for the sender + function getSenderNonce(address sender) external view returns (uint64 nonce); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IEVM2AnyOnRampClient.sol b/contracts/src/v0.8/ccip/interfaces/IEVM2AnyOnRampClient.sol new file mode 100644 index 00000000000..1744d6c2295 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IEVM2AnyOnRampClient.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IPoolV1} from "./IPool.sol"; + +import {Client} from "../libraries/Client.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +interface IEVM2AnyOnRampClient { + /// @notice Get the fee for a given ccip message + /// @param destChainSelector The destination chain selector + /// @param message The message to calculate the cost for + /// @return fee The calculated fee + function getFee(uint64 destChainSelector, Client.EVM2AnyMessage calldata message) external view returns (uint256 fee); + + /// @notice Get the pool for a specific token + /// @param destChainSelector The destination chain selector + /// @param sourceToken The source chain token to get the pool for + /// @return pool Token pool + function getPoolBySourceToken(uint64 destChainSelector, IERC20 sourceToken) external view returns (IPoolV1); + + /// @notice Gets a list of all supported source chain tokens. + /// @param destChainSelector The destination chain selector + /// @return tokens The addresses of all tokens that this onRamp supports the given destination chain + function getSupportedTokens(uint64 destChainSelector) external view returns (address[] memory tokens); + + /// @notice Send a message to the remote chain + /// @dev only callable by the Router + /// @dev approve() must have already been called on the token using the this ramp address as the spender. + /// @dev if the contract is paused, this function will revert. + /// @param destChainSelector The destination chain selector + /// @param message Message struct to send + /// @param feeTokenAmount Amount of fee tokens for payment + /// @param originalSender The original initiator of the CCIP request + function forwardFromRouter( + uint64 destChainSelector, + Client.EVM2AnyMessage memory message, + uint256 feeTokenAmount, + address originalSender + ) external returns (bytes32); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IGetCCIPAdmin.sol b/contracts/src/v0.8/ccip/interfaces/IGetCCIPAdmin.sol new file mode 100644 index 00000000000..d83a1f34e89 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IGetCCIPAdmin.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IGetCCIPAdmin { + /// @notice Returns the admin of the token. + /// @dev This method is named to never conflict with existing methods. + function getCCIPAdmin() external view returns (address); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IMessageInterceptor.sol b/contracts/src/v0.8/ccip/interfaces/IMessageInterceptor.sol new file mode 100644 index 00000000000..c2b432426b6 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IMessageInterceptor.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../libraries/Client.sol"; + +/// @notice Interface for plug-in message hook contracts that intercept OffRamp & OnRamp messages +/// and perform validations / state changes on top of the messages. The interceptor functions are expected to +/// revert on validation failures. +interface IMessageInterceptor { + /// @notice Common error that can be thrown on validation failures and used by consumers + /// @param errorReason abi encoded revert reason + error MessageValidationError(bytes errorReason); + + /// @notice Intercepts & validates the given OffRamp message. Reverts on validation failure + /// @param message to validate + function onInboundMessage(Client.Any2EVMMessage memory message) external; + + /// @notice Intercepts & validates the given OnRamp message. Reverts on validation failure + /// @param destChainSelector remote destination chain selector where the message is being sent to + /// @param message to validate + function onOutboundMessage(uint64 destChainSelector, Client.EVM2AnyMessage memory message) external; +} diff --git a/contracts/src/v0.8/ccip/interfaces/INonceManager.sol b/contracts/src/v0.8/ccip/interfaces/INonceManager.sol new file mode 100644 index 00000000000..52408ae4f57 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/INonceManager.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice Contract interface that allows managing sender nonces +interface INonceManager { + /// @notice Increments the outbound nonce for a given sender on a given destination chain + /// @param destChainSelector The destination chain selector + /// @param sender The sender address + /// @return The new outbound nonce + function getIncrementedOutboundNonce(uint64 destChainSelector, address sender) external returns (uint64); + + /// @notice Increments the inbound nonce for a given sender on a given source chain + /// @notice The increment is only applied if the resulting nonce matches the expectedNonce + /// @param sourceChainSelector The destination chain selector + /// @param expectedNonce The expected inbound nonce + /// @param sender The encoded sender address + /// @return True if the nonce was incremented, false otherwise + function incrementInboundNonce( + uint64 sourceChainSelector, + uint64 expectedNonce, + bytes calldata sender + ) external returns (bool); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IOwner.sol b/contracts/src/v0.8/ccip/interfaces/IOwner.sol new file mode 100644 index 00000000000..ccb1039e555 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IOwner.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IOwner { + /// @notice Returns the owner of the contract. + /// @dev This method is named to match with the OpenZeppelin Ownable contract. + function owner() external view returns (address); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IPool.sol b/contracts/src/v0.8/ccip/interfaces/IPool.sol new file mode 100644 index 00000000000..5d5c95e03c7 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IPool.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Pool} from "../libraries/Pool.sol"; + +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; + +/// @notice Shared public interface for multiple V1 pool types. +/// Each pool type handles a different child token model (lock/unlock, mint/burn.) +interface IPoolV1 is IERC165 { + /// @notice Lock tokens into the pool or burn the tokens. + /// @param lockOrBurnIn Encoded data fields for the processing of tokens on the source chain. + /// @return lockOrBurnOut Encoded data fields for the processing of tokens on the destination chain. + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut); + + /// @notice Releases or mints tokens to the receiver address. + /// @param releaseOrMintIn All data required to release or mint tokens. + /// @return releaseOrMintOut The amount of tokens released or minted on the local chain, denominated + /// in the local token's decimals. + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + returns (Pool.ReleaseOrMintOutV1 memory); + + /// @notice Checks whether a remote chain is supported in the token pool. + /// @param remoteChainSelector The selector of the remote chain. + /// @return true if the given chain is a permissioned remote chain. + function isSupportedChain(uint64 remoteChainSelector) external view returns (bool); + + /// @notice Returns if the token pool supports the given token. + /// @param token The address of the token. + /// @return true if the token is supported by the pool. + function isSupportedToken(address token) external view returns (bool); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IPoolPriorTo1_5.sol b/contracts/src/v0.8/ccip/interfaces/IPoolPriorTo1_5.sol new file mode 100644 index 00000000000..d8a2f15fd29 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IPoolPriorTo1_5.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +// Shared public interface for multiple pool types. +// Each pool type handles a different child token model (lock/unlock, mint/burn.) +interface IPoolPriorTo1_5 { + /// @notice Lock tokens into the pool or burn the tokens. + /// @param originalSender Original sender of the tokens. + /// @param receiver Receiver of the tokens on destination chain. + /// @param amount Amount to lock or burn. + /// @param remoteChainSelector Destination chain Id. + /// @param extraArgs Additional data passed in by sender for lockOrBurn processing + /// in custom pools on source chain. + /// @return retData Optional field that contains bytes. Unused for now but already + /// implemented to allow future upgrades while preserving the interface. + function lockOrBurn( + address originalSender, + bytes calldata receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes calldata extraArgs + ) external returns (bytes memory); + + /// @notice Releases or mints tokens to the receiver address. + /// @param originalSender Original sender of the tokens. + /// @param receiver Receiver of the tokens. + /// @param amount Amount to release or mint. + /// @param remoteChainSelector Source chain Id. + /// @param extraData Additional data supplied offchain for releaseOrMint processing in + /// custom pools on dest chain. This could be an attestation that was retrieved through a + /// third party API. + /// @dev offchainData can come from any untrusted source. + function releaseOrMint( + bytes memory originalSender, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory extraData + ) external; + + /// @notice Gets the IERC20 token that this pool can lock or burn. + /// @return token The IERC20 token representation. + function getToken() external view returns (IERC20 token); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IPriceRegistry.sol b/contracts/src/v0.8/ccip/interfaces/IPriceRegistry.sol new file mode 100644 index 00000000000..8a20299371f --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IPriceRegistry.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../libraries/Client.sol"; +import {Internal} from "../libraries/Internal.sol"; + +interface IPriceRegistry { + /// @notice Token price data feed configuration + struct TokenPriceFeedConfig { + address dataFeedAddress; // ──╮ AggregatorV3Interface contract (0 - feed is unset) + uint8 tokenDecimals; // ──────╯ Decimals of the token that the feed represents + } + + /// @notice Update the price for given tokens and gas prices for given chains. + /// @param priceUpdates The price updates to apply. + function updatePrices(Internal.PriceUpdates memory priceUpdates) external; + + /// @notice Get the `tokenPrice` for a given token. + /// @param token The token to get the price for. + /// @return tokenPrice The tokenPrice for the given token. + function getTokenPrice(address token) external view returns (Internal.TimestampedPackedUint224 memory); + + /// @notice Get the `tokenPrice` for a given token, checks if the price is valid. + /// @param token The token to get the price for. + /// @return tokenPrice The tokenPrice for the given token if it exists and is valid. + function getValidatedTokenPrice(address token) external view returns (uint224); + + /// @notice Get the `tokenPrice` for an array of tokens. + /// @param tokens The tokens to get prices for. + /// @return tokenPrices The tokenPrices for the given tokens. + function getTokenPrices(address[] calldata tokens) external view returns (Internal.TimestampedPackedUint224[] memory); + + /// @notice Returns the token price data feed configuration + /// @param token The token to retrieve the feed config for + /// @return dataFeedAddress The token price data feed config (if feed address is 0, the feed config is disabled) + function getTokenPriceFeedConfig(address token) external view returns (TokenPriceFeedConfig memory); + + /// @notice Get an encoded `gasPrice` for a given destination chain ID. + /// The 224-bit result encodes necessary gas price components. + /// On L1 chains like Ethereum or Avax, the only component is the gas price. + /// On Optimistic Rollups, there are two components - the L2 gas price, and L1 base fee for data availability. + /// On future chains, there could be more or differing price components. + /// PriceRegistry does not contain chain-specific logic to parse destination chain price components. + /// @param destChainSelector The destination chain to get the price for. + /// @return gasPrice The encoded gasPrice for the given destination chain ID. + function getDestinationChainGasPrice(uint64 destChainSelector) + external + view + returns (Internal.TimestampedPackedUint224 memory); + + /// @notice Gets the fee token price and the gas price, both denominated in dollars. + /// @param token The source token to get the price for. + /// @param destChainSelector The destination chain to get the gas price for. + /// @return tokenPrice The price of the feeToken in 1e18 dollars per base unit. + /// @return gasPrice The price of gas in 1e18 dollars per base unit. + function getTokenAndGasPrices( + address token, + uint64 destChainSelector + ) external view returns (uint224 tokenPrice, uint224 gasPrice); + + /// @notice Convert a given token amount to target token amount. + /// @param fromToken The given token address. + /// @param fromTokenAmount The given token amount. + /// @param toToken The target token address. + /// @return toTokenAmount The target token amount. + function convertTokenAmount( + address fromToken, + uint256 fromTokenAmount, + address toToken + ) external view returns (uint256 toTokenAmount); + + /// @notice Get the list of fee tokens. + /// @return The tokens set as fee tokens. + function getFeeTokens() external view returns (address[] memory); + + /// @notice Validates the ccip message & returns the fee + /// @param destChainSelector The destination chain selector. + /// @param message The message to get quote for. + /// @return feeTokenAmount The amount of fee token needed for the fee, in smallest denomination of the fee token. + function getValidatedFee( + uint64 destChainSelector, + Client.EVM2AnyMessage calldata message + ) external view returns (uint256 feeTokenAmount); + + /// @notice Converts the extraArgs to the latest version and returns the converted message fee in juels + /// @param destChainSelector destination chain selector to process + /// @param feeToken Fee token address used to pay for message fees + /// @param feeTokenAmount Fee token amount + /// @param extraArgs Message extra args that were passed in by the client + /// @return msgFeeJuels message fee in juels + /// @return isOutOfOrderExecution true if the message should be executed out of order + /// @return convertedExtraArgs extra args converted to the latest family-specific args version + function processMessageArgs( + uint64 destChainSelector, + address feeToken, + uint256 feeTokenAmount, + bytes memory extraArgs + ) external view returns (uint256 msgFeeJuels, bool isOutOfOrderExecution, bytes memory convertedExtraArgs); + + /// @notice Validates pool return data + /// @param destChainSelector Destination chain selector to which the token amounts are sent to + /// @param rampTokenAmounts Token amounts with populated pool return data + /// @param sourceTokenAmounts Token amounts originally sent in a Client.EVM2AnyMessage message + function validatePoolReturnData( + uint64 destChainSelector, + Internal.RampTokenAmount[] calldata rampTokenAmounts, + Client.EVMTokenAmount[] calldata sourceTokenAmounts + ) external view; +} diff --git a/contracts/src/v0.8/ccip/interfaces/IRMN.sol b/contracts/src/v0.8/ccip/interfaces/IRMN.sol new file mode 100644 index 00000000000..a409731549f --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IRMN.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice This interface contains the only RMN-related functions that might be used on-chain by other CCIP contracts. +interface IRMN { + /// @notice A Merkle root tagged with the address of the commit store contract it is destined for. + struct TaggedRoot { + address commitStore; + bytes32 root; + } + + /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed. + function isBlessed(TaggedRoot calldata taggedRoot) external view returns (bool); + + /// @notice Iff there is an active global or legacy curse, this function returns true. + function isCursed() external view returns (bool); + + /// @notice Iff there is an active global curse, or an active curse for `subject`, this function returns true. + /// @param subject To check whether a particular chain is cursed, set to bytes16(uint128(chainSelector)). + function isCursed(bytes16 subject) external view returns (bool); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IRouter.sol b/contracts/src/v0.8/ccip/interfaces/IRouter.sol new file mode 100644 index 00000000000..7f4544fd0fa --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IRouter.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../libraries/Client.sol"; + +interface IRouter { + error OnlyOffRamp(); + + /// @notice Route the message to its intended receiver contract. + /// @param message Client.Any2EVMMessage struct. + /// @param gasForCallExactCheck of params for exec + /// @param gasLimit set of params for exec + /// @param receiver set of params for exec + /// @dev if the receiver is a contracts that signals support for CCIP execution through EIP-165. + /// the contract is called. If not, only tokens are transferred. + /// @return success A boolean value indicating whether the ccip message was received without errors. + /// @return retBytes A bytes array containing return data form CCIP receiver. + /// @return gasUsed the gas used by the external customer call. Does not include any overhead. + function routeMessage( + Client.Any2EVMMessage calldata message, + uint16 gasForCallExactCheck, + uint256 gasLimit, + address receiver + ) external returns (bool success, bytes memory retBytes, uint256 gasUsed); + + /// @notice Returns the configured onramp for a specific destination chain. + /// @param destChainSelector The destination chain Id to get the onRamp for. + /// @return onRampAddress The address of the onRamp. + function getOnRamp(uint64 destChainSelector) external view returns (address onRampAddress); + + /// @notice Return true if the given offRamp is a configured offRamp for the given source chain. + /// @param sourceChainSelector The source chain selector to check. + /// @param offRamp The address of the offRamp to check. + function isOffRamp(uint64 sourceChainSelector, address offRamp) external view returns (bool isOffRamp); +} diff --git a/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol b/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol new file mode 100644 index 00000000000..9805a41bbdc --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../libraries/Client.sol"; + +interface IRouterClient { + error UnsupportedDestinationChain(uint64 destChainSelector); + error InsufficientFeeTokenAmount(); + error InvalidMsgValue(); + + /// @notice Checks if the given chain ID is supported for sending/receiving. + /// @param destChainSelector The chain to check. + /// @return supported is true if it is supported, false if not. + function isChainSupported(uint64 destChainSelector) external view returns (bool supported); + + /// @param destinationChainSelector The destination chainSelector + /// @param message The cross-chain CCIP message including data and/or tokens + /// @return fee returns execution fee for the message + /// delivery to destination chain, denominated in the feeToken specified in the message. + /// @dev Reverts with appropriate reason upon invalid message. + function getFee( + uint64 destinationChainSelector, + Client.EVM2AnyMessage memory message + ) external view returns (uint256 fee); + + /// @notice Request a message to be sent to the destination chain + /// @param destinationChainSelector The destination chain ID + /// @param message The cross-chain CCIP message including data and/or tokens + /// @return messageId The message ID + /// @dev Note if msg.value is larger than the required fee (from getFee) we accept + /// the overpayment with no refund. + /// @dev Reverts with appropriate reason upon invalid message. + function ccipSend( + uint64 destinationChainSelector, + Client.EVM2AnyMessage calldata message + ) external payable returns (bytes32); +} diff --git a/contracts/src/v0.8/ccip/interfaces/ITokenAdminRegistry.sol b/contracts/src/v0.8/ccip/interfaces/ITokenAdminRegistry.sol new file mode 100644 index 00000000000..0e441229011 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/ITokenAdminRegistry.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +interface ITokenAdminRegistry { + /// @notice Returns the pool for the given token. + function getPool(address token) external view returns (address); + + /// @notice Proposes an administrator for the given token as pending administrator. + /// @param localToken The token to register the administrator for. + /// @param administrator The administrator to register. + function proposeAdministrator(address localToken, address administrator) external; +} diff --git a/contracts/src/v0.8/ccip/interfaces/IWrappedNative.sol b/contracts/src/v0.8/ccip/interfaces/IWrappedNative.sol new file mode 100644 index 00000000000..4225827a612 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/IWrappedNative.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +interface IWrappedNative is IERC20 { + function deposit() external payable; + + function withdraw(uint256 wad) external; +} diff --git a/contracts/src/v0.8/ccip/interfaces/automation/ILinkAvailable.sol b/contracts/src/v0.8/ccip/interfaces/automation/ILinkAvailable.sol new file mode 100644 index 00000000000..b0dad9a5e70 --- /dev/null +++ b/contracts/src/v0.8/ccip/interfaces/automation/ILinkAvailable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice Implement this contract so that a keeper-compatible contract can monitor +/// and fund the implementation contract with LINK if it falls below a defined threshold. +interface ILinkAvailable { + function linkAvailableForPayment() external view returns (int256 availableBalance); +} diff --git a/contracts/src/v0.8/ccip/libraries/Client.sol b/contracts/src/v0.8/ccip/libraries/Client.sol new file mode 100644 index 00000000000..a985371bef1 --- /dev/null +++ b/contracts/src/v0.8/ccip/libraries/Client.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// End consumer library. +library Client { + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct EVMTokenAmount { + address token; // token address on the local chain. + uint256 amount; // Amount of tokens. + } + + struct Any2EVMMessage { + bytes32 messageId; // MessageId corresponding to ccipSend on source. + uint64 sourceChainSelector; // Source chain selector. + bytes sender; // abi.decode(sender) if coming from an EVM chain. + bytes data; // payload sent in original message. + EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation. + } + + // If extraArgs is empty bytes, the default is 200k gas limit. + struct EVM2AnyMessage { + bytes receiver; // abi.encode(receiver address) for dest EVM chains + bytes data; // Data payload + EVMTokenAmount[] tokenAmounts; // Token transfers + address feeToken; // Address of feeToken. address(0) means you will send msg.value. + bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV2) + } + + // bytes4(keccak256("CCIP EVMExtraArgsV1")); + bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; + + struct EVMExtraArgsV1 { + uint256 gasLimit; + } + + function _argsToBytes(EVMExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) { + return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); + } + + // bytes4(keccak256("CCIP EVMExtraArgsV2")); + bytes4 public constant EVM_EXTRA_ARGS_V2_TAG = 0x181dcf10; + + /// @param gasLimit: gas limit for the callback on the destination chain. + /// @param allowOutOfOrderExecution: if true, it indicates that the message can be executed in any order relative to other messages from the same sender. + /// This value's default varies by chain. On some chains, a particular value is enforced, meaning if the expected value + /// is not set, the message request will revert. + struct EVMExtraArgsV2 { + uint256 gasLimit; + bool allowOutOfOrderExecution; + } + + function _argsToBytes(EVMExtraArgsV2 memory extraArgs) internal pure returns (bytes memory bts) { + return abi.encodeWithSelector(EVM_EXTRA_ARGS_V2_TAG, extraArgs); + } +} diff --git a/contracts/src/v0.8/ccip/libraries/Internal.sol b/contracts/src/v0.8/ccip/libraries/Internal.sol new file mode 100644 index 00000000000..db2bc05ee53 --- /dev/null +++ b/contracts/src/v0.8/ccip/libraries/Internal.sol @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {MerkleMultiProof} from "../libraries/MerkleMultiProof.sol"; +import {Client} from "./Client.sol"; + +// Library for CCIP internal definitions common to multiple contracts. +library Internal { + error InvalidEVMAddress(bytes encodedAddress); + + /// @dev The minimum amount of gas to perform the call with exact gas. + /// We include this in the offramp so that we can redeploy to adjust it + /// should a hardfork change the gas costs of relevant opcodes in callWithExactGas. + uint16 internal constant GAS_FOR_CALL_EXACT_CHECK = 5_000; + // @dev We limit return data to a selector plus 4 words. This is to avoid + // malicious contracts from returning large amounts of data and causing + // repeated out-of-gas scenarios. + uint16 internal constant MAX_RET_BYTES = 4 + 4 * 32; + + /// @notice A collection of token price and gas price updates. + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct PriceUpdates { + TokenPriceUpdate[] tokenPriceUpdates; + GasPriceUpdate[] gasPriceUpdates; + } + + /// @notice Token price in USD. + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct TokenPriceUpdate { + address sourceToken; // Source token + uint224 usdPerToken; // 1e18 USD per 1e18 of the smallest token denomination. + } + + /// @notice Gas price for a given chain in USD, its value may contain tightly packed fields. + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct GasPriceUpdate { + uint64 destChainSelector; // Destination chain selector + uint224 usdPerUnitGas; // 1e18 USD per smallest unit (e.g. wei) of destination chain gas + } + + /// @notice A timestamped uint224 value that can contain several tightly packed fields. + struct TimestampedPackedUint224 { + uint224 value; // ───────╮ Value in uint224, packed. + uint32 timestamp; // ────╯ Timestamp of the most recent price update. + } + + /// @dev Gas price is stored in 112-bit unsigned int. uint224 can pack 2 prices. + /// When packing L1 and L2 gas prices, L1 gas price is left-shifted to the higher-order bits. + /// Using uint8 type, which cannot be higher than other bit shift operands, to avoid shift operand type warning. + uint8 public constant GAS_PRICE_BITS = 112; + + struct PoolUpdate { + address token; // The IERC20 token address + address pool; // The token pool address + } + + struct SourceTokenData { + // The source pool address, abi encoded. This value is trusted as it was obtained through the onRamp. It can be + // relied upon by the destination pool to validate the source pool. + bytes sourcePoolAddress; + // The address of the destination token, abi encoded in the case of EVM chains + // This value is UNTRUSTED as any pool owner can return whatever value they want. + bytes destTokenAddress; + // Optional pool data to be transferred to the destination chain. Be default this is capped at + // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead + // has to be set for the specific token. + bytes extraData; + } + + /// @notice Report that is submitted by the execution DON at the execution phase. (including chain selector data) + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct ExecutionReportSingleChain { + uint64 sourceChainSelector; // Source chain selector for which the report is submitted + Any2EVMRampMessage[] messages; + // Contains a bytes array for each message, each inner bytes array contains bytes per transferred token + bytes[][] offchainTokenData; + bytes32[] proofs; + uint256 proofFlagBits; + } + + /// @notice Report that is submitted by the execution DON at the execution phase. + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct ExecutionReport { + EVM2EVMMessage[] messages; + // Contains a bytes array for each message, each inner bytes array contains bytes per transferred token + bytes[][] offchainTokenData; + bytes32[] proofs; + uint256 proofFlagBits; + } + + /// @notice The cross chain message that gets committed to EVM chains. + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct EVM2EVMMessage { + uint64 sourceChainSelector; // ───────────╮ the chain selector of the source chain, note: not chainId + address sender; // ───────────────────────╯ sender address on the source chain + address receiver; // ─────────────────────╮ receiver address on the destination chain + uint64 sequenceNumber; // ────────────────╯ sequence number, not unique across lanes + uint256 gasLimit; // user supplied maximum gas amount available for dest chain execution + bool strict; // ──────────────────────────╮ DEPRECATED + uint64 nonce; // │ nonce for this lane for this sender, not unique across senders/lanes + address feeToken; // ─────────────────────╯ fee token + uint256 feeTokenAmount; // fee token amount + bytes data; // arbitrary data payload supplied by the message sender + Client.EVMTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer + bytes[] sourceTokenData; // array of token data, one per token + bytes32 messageId; // a hash of the message data + } + + /// @dev EVM2EVMMessage struct has 13 fields, including 3 variable arrays. + /// Each variable array takes 1 more slot to store its length. + /// When abi encoded, excluding array contents, + /// EVM2EVMMessage takes up a fixed number of 16 lots, 32 bytes each. + /// For structs that contain arrays, 1 more slot is added to the front, reaching a total of 17. + uint256 public constant MESSAGE_FIXED_BYTES = 32 * 17; + + /// @dev Each token transfer adds 1 EVMTokenAmount and 1 bytes. + /// When abiEncoded, each EVMTokenAmount takes 2 slots, each bytes takes 2 slots, excl bytes contents + uint256 public constant MESSAGE_FIXED_BYTES_PER_TOKEN = 32 * 4; + + /// @dev Any2EVMRampMessage struct has 10 fields, including 3 variable unnested arrays (data, receiver and tokenAmounts). + /// Each variable array takes 1 more slot to store its length. + /// When abi encoded, excluding array contents, + /// Any2EVMMessage takes up a fixed number of 13 slots, 32 bytes each. + /// For structs that contain arrays, 1 more slot is added to the front, reaching a total of 14. + /// The fixed bytes does not cover struct data (this is represented by ANY_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN) + uint256 public constant ANY_2_EVM_MESSAGE_FIXED_BYTES = 32 * 14; + + /// @dev Each token transfer adds 1 RampTokenAmount + /// RampTokenAmount has 4 fields, including 3 bytes. + /// Each bytes takes 1 more slot to store its length. + /// When abi encoded, each token transfer takes up 7 slots, excl bytes contents. + uint256 public constant ANY_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN = 32 * 7; + + bytes32 internal constant EVM_2_EVM_MESSAGE_HASH = keccak256("EVM2EVMMessageHashV2"); + + /// @dev Used to hash messages for single-lane ramps. + /// OnRamp hash(EVM2EVMMessage) = OffRamp hash(EVM2EVMMessage) + /// The EVM2EVMMessage's messageId is expected to be the output of this hash function + /// @param original Message to hash + /// @param metadataHash Immutable metadata hash representing a lane with a fixed OnRamp + /// @return hashedMessage hashed message as a keccak256 + function _hash(EVM2EVMMessage memory original, bytes32 metadataHash) internal pure returns (bytes32) { + // Fixed-size message fields are included in nested hash to reduce stack pressure. + // This hashing scheme is also used by RMN. If changing it, please notify the RMN maintainers. + return keccak256( + abi.encode( + MerkleMultiProof.LEAF_DOMAIN_SEPARATOR, + metadataHash, + keccak256( + abi.encode( + original.sender, + original.receiver, + original.sequenceNumber, + original.gasLimit, + original.strict, + original.nonce, + original.feeToken, + original.feeTokenAmount + ) + ), + keccak256(original.data), + keccak256(abi.encode(original.tokenAmounts)), + keccak256(abi.encode(original.sourceTokenData)) + ) + ); + } + + bytes32 internal constant ANY_2_EVM_MESSAGE_HASH = keccak256("Any2EVMMessageHashV1"); + bytes32 internal constant EVM_2_ANY_MESSAGE_HASH = keccak256("EVM2AnyMessageHashV1"); + + /// @dev Used to hash messages for multi-lane family-agnostic OffRamps. + /// OnRamp hash(EVM2AnyMessage) != Any2EVMRampMessage.messageId + /// OnRamp hash(EVM2AnyMessage) != OffRamp hash(Any2EVMRampMessage) + /// @param original OffRamp message to hash + /// @param onRamp OnRamp to hash the message with - used to compute the metadataHash + /// @return hashedMessage hashed message as a keccak256 + function _hash(Any2EVMRampMessage memory original, bytes memory onRamp) internal pure returns (bytes32) { + // Fixed-size message fields are included in nested hash to reduce stack pressure. + // This hashing scheme is also used by RMN. If changing it, please notify the RMN maintainers. + return keccak256( + abi.encode( + MerkleMultiProof.LEAF_DOMAIN_SEPARATOR, + // Implicit metadata hash + keccak256( + abi.encode( + ANY_2_EVM_MESSAGE_HASH, original.header.sourceChainSelector, original.header.destChainSelector, onRamp + ) + ), + keccak256( + abi.encode( + original.header.messageId, + original.sender, + original.receiver, + original.header.sequenceNumber, + original.gasLimit, + original.header.nonce + ) + ), + keccak256(original.data), + keccak256(abi.encode(original.tokenAmounts)) + ) + ); + } + + function _hash(EVM2AnyRampMessage memory original, bytes32 metadataHash) internal pure returns (bytes32) { + // Fixed-size message fields are included in nested hash to reduce stack pressure. + // This hashing scheme is also used by RMN. If changing it, please notify the RMN maintainers. + return keccak256( + abi.encode( + MerkleMultiProof.LEAF_DOMAIN_SEPARATOR, + metadataHash, + keccak256( + abi.encode( + original.sender, + original.receiver, + original.header.sequenceNumber, + original.header.nonce, + original.feeToken, + original.feeTokenAmount + ) + ), + keccak256(original.data), + keccak256(abi.encode(original.tokenAmounts)), + keccak256(original.extraArgs) + ) + ); + } + + /// @dev We disallow the first 1024 addresses to never allow calling precompiles. It is extremely unlikely that + /// anyone would ever be able to generate an address in this range. + uint256 public constant PRECOMPILE_SPACE = 1024; + + /// @notice This methods provides validation for parsing abi encoded addresses by ensuring the + /// address is within the EVM address space. If it isn't it will revert with an InvalidEVMAddress error, which + /// we can catch and handle more gracefully than a revert from abi.decode. + /// @return The address if it is valid, the function will revert otherwise. + function _validateEVMAddress(bytes memory encodedAddress) internal pure returns (address) { + if (encodedAddress.length != 32) revert InvalidEVMAddress(encodedAddress); + uint256 encodedAddressUint = abi.decode(encodedAddress, (uint256)); + if (encodedAddressUint > type(uint160).max || encodedAddressUint < PRECOMPILE_SPACE) { + revert InvalidEVMAddress(encodedAddress); + } + return address(uint160(encodedAddressUint)); + } + + /// @notice Enum listing the possible message execution states within + /// the offRamp contract. + /// UNTOUCHED never executed + /// IN_PROGRESS currently being executed, used a replay protection + /// SUCCESS successfully executed. End state + /// FAILURE unsuccessfully executed, manual execution is now enabled. + /// @dev RMN depends on this enum, if changing, please notify the RMN maintainers. + enum MessageExecutionState { + UNTOUCHED, + IN_PROGRESS, + SUCCESS, + FAILURE + } + + /// @notice CCIP OCR plugin type, used to separate execution & commit transmissions and configs + enum OCRPluginType { + Commit, + Execution + } + + /// @notice Family-agnostic token amounts used for both OnRamp & OffRamp messages + struct RampTokenAmount { + // The source pool address, abi encoded. This value is trusted as it was obtained through the onRamp. It can be + // relied upon by the destination pool to validate the source pool. + bytes sourcePoolAddress; + // The address of the destination token, abi encoded in the case of EVM chains + // This value is UNTRUSTED as any pool owner can return whatever value they want. + bytes destTokenAddress; + // Optional pool data to be transferred to the destination chain. Be default this is capped at + // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead + // has to be set for the specific token. + bytes extraData; + uint256 amount; // Amount of tokens. + } + + /// @notice Family-agnostic header for OnRamp & OffRamp messages. + /// The messageId is not expected to match hash(message), since it may originate from another ramp family + struct RampMessageHeader { + bytes32 messageId; // Unique identifier for the message, generated with the source chain's encoding scheme (i.e. not necessarily abi.encoded) + uint64 sourceChainSelector; // ───────╮ the chain selector of the source chain, note: not chainId + uint64 destChainSelector; // | the chain selector of the destination chain, note: not chainId + uint64 sequenceNumber; // │ sequence number, not unique across lanes + uint64 nonce; // ─────────────────────╯ nonce for this lane for this sender, not unique across senders/lanes + } + + /// @notice Family-agnostic message routed to an OffRamp + /// Note: hash(Any2EVMRampMessage) != hash(EVM2AnyRampMessage), hash(Any2EVMRampMessage) != messageId + /// due to encoding & parameter differences + struct Any2EVMRampMessage { + RampMessageHeader header; // Message header + bytes sender; // sender address on the source chain + bytes data; // arbitrary data payload supplied by the message sender + address receiver; // receiver address on the destination chain + uint256 gasLimit; // user supplied maximum gas amount available for dest chain execution + RampTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer + } + + /// @notice Family-agnostic message emitted from the OnRamp + /// Note: hash(Any2EVMRampMessage) != hash(EVM2AnyRampMessage) due to encoding & parameter differences + /// messageId = hash(EVM2AnyRampMessage) using the source EVM chain's encoding format + struct EVM2AnyRampMessage { + RampMessageHeader header; // Message header + address sender; // sender address on the source chain + bytes data; // arbitrary data payload supplied by the message sender + bytes receiver; // receiver address on the destination chain + bytes extraArgs; // destination-chain specific extra args, such as the gasLimit for EVM chains + address feeToken; // fee token + uint256 feeTokenAmount; // fee token amount + RampTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer + } + + // bytes4(keccak256("CCIP ChainFamilySelector EVM")) + bytes4 public constant CHAIN_FAMILY_SELECTOR_EVM = 0x2812d52c; +} diff --git a/contracts/src/v0.8/ccip/libraries/MerkleMultiProof.sol b/contracts/src/v0.8/ccip/libraries/MerkleMultiProof.sol new file mode 100644 index 00000000000..fed8a1165bb --- /dev/null +++ b/contracts/src/v0.8/ccip/libraries/MerkleMultiProof.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library MerkleMultiProof { + /// @notice Leaf domain separator, should be used as the first 32 bytes of a leaf's preimage. + bytes32 internal constant LEAF_DOMAIN_SEPARATOR = 0x0000000000000000000000000000000000000000000000000000000000000000; + /// @notice Internal domain separator, should be used as the first 32 bytes of an internal node's preiimage. + bytes32 internal constant INTERNAL_DOMAIN_SEPARATOR = + 0x0000000000000000000000000000000000000000000000000000000000000001; + + uint256 internal constant MAX_NUM_HASHES = 256; + + error InvalidProof(); + error LeavesCannotBeEmpty(); + + /// @notice Computes the root based on provided pre-hashed leaf nodes in + /// leaves, internal nodes in proofs, and using proofFlagBits' i-th bit to + /// determine if an element of proofs or one of the previously computed leafs + /// or internal nodes will be used for the i-th hash. + /// @param leaves Should be pre-hashed and the first 32 bytes of a leaf's + /// preimage should match LEAF_DOMAIN_SEPARATOR. + /// @param proofs The hashes to be used instead of a leaf hash when the proofFlagBits + /// indicates a proof should be used. + /// @param proofFlagBits A single uint256 of which each bit indicates whether a leaf or + /// a proof needs to be used in a hash operation. + /// @dev the maximum number of hash operations it set to 256. Any input that would require + /// more than 256 hashes to get to a root will revert. + /// @dev For given input `leaves` = [a,b,c] `proofs` = [D] and `proofFlagBits` = 5 + /// totalHashes = 3 + 1 - 1 = 3 + /// ** round 1 ** + /// proofFlagBits = (5 >> 0) & 1 = true + /// hashes[0] = hashPair(a, b) + /// (leafPos, hashPos, proofPos) = (2, 0, 0); + /// + /// ** round 2 ** + /// proofFlagBits = (5 >> 1) & 1 = false + /// hashes[1] = hashPair(D, c) + /// (leafPos, hashPos, proofPos) = (3, 0, 1); + /// + /// ** round 3 ** + /// proofFlagBits = (5 >> 2) & 1 = true + /// hashes[2] = hashPair(hashes[0], hashes[1]) + /// (leafPos, hashPos, proofPos) = (3, 2, 1); + /// + /// i = 3 and no longer < totalHashes. The algorithm is done + /// return hashes[totalHashes - 1] = hashes[2]; the last hash we computed. + // We mark this function as internal to force it to be inlined in contracts + // that use it, but semantically it is public. + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function merkleRoot( + bytes32[] memory leaves, + bytes32[] memory proofs, + uint256 proofFlagBits + ) internal pure returns (bytes32) { + unchecked { + uint256 leavesLen = leaves.length; + uint256 proofsLen = proofs.length; + if (leavesLen == 0) revert LeavesCannotBeEmpty(); + if (!(leavesLen <= MAX_NUM_HASHES + 1 && proofsLen <= MAX_NUM_HASHES + 1)) revert InvalidProof(); + uint256 totalHashes = leavesLen + proofsLen - 1; + if (!(totalHashes <= MAX_NUM_HASHES)) revert InvalidProof(); + if (totalHashes == 0) { + return leaves[0]; + } + bytes32[] memory hashes = new bytes32[](totalHashes); + (uint256 leafPos, uint256 hashPos, uint256 proofPos) = (0, 0, 0); + + for (uint256 i = 0; i < totalHashes; ++i) { + // Checks if the bit flag signals the use of a supplied proof or a leaf/previous hash. + bytes32 a; + if (proofFlagBits & (1 << i) == (1 << i)) { + // Use a leaf or a previously computed hash. + if (leafPos < leavesLen) { + a = leaves[leafPos++]; + } else { + a = hashes[hashPos++]; + } + } else { + // Use a supplied proof. + a = proofs[proofPos++]; + } + + // The second part of the hashed pair is never a proof as hashing two proofs would result in a + // hash that can already be computed offchain. + bytes32 b; + if (leafPos < leavesLen) { + b = leaves[leafPos++]; + } else { + b = hashes[hashPos++]; + } + + if (!(hashPos <= i)) revert InvalidProof(); + + hashes[i] = _hashPair(a, b); + } + if (!(hashPos == totalHashes - 1 && leafPos == leavesLen && proofPos == proofsLen)) revert InvalidProof(); + // Return the last hash. + return hashes[totalHashes - 1]; + } + } + + /// @notice Hashes two bytes32 objects in their given order, prepended by the + /// INTERNAL_DOMAIN_SEPARATOR. + function _hashInternalNode(bytes32 left, bytes32 right) private pure returns (bytes32 hash) { + return keccak256(abi.encode(INTERNAL_DOMAIN_SEPARATOR, left, right)); + } + + /// @notice Hashes two bytes32 objects. The order is taken into account, + /// using the lower value first. + function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { + return a < b ? _hashInternalNode(a, b) : _hashInternalNode(b, a); + } +} diff --git a/contracts/src/v0.8/ccip/libraries/Pool.sol b/contracts/src/v0.8/ccip/libraries/Pool.sol new file mode 100644 index 00000000000..3f1895dcf5a --- /dev/null +++ b/contracts/src/v0.8/ccip/libraries/Pool.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice This library contains various token pool functions to aid constructing the return data. +library Pool { + // The tag used to signal support for the pool v1 standard + // bytes4(keccak256("CCIP_POOL_V1")) + bytes4 public constant CCIP_POOL_V1 = 0xaff2afbf; + + // The number of bytes in the return data for a pool v1 releaseOrMint call. + // This should match the size of the ReleaseOrMintOutV1 struct. + uint16 public constant CCIP_POOL_V1_RET_BYTES = 32; + + // The default max number of bytes in the return data for a pool v1 lockOrBurn call. + // This data can be used to send information to the destination chain token pool. Can be overwritten + // in the TokenTransferFeeConfig.destBytesOverhead if more data is required. + uint256 public constant CCIP_LOCK_OR_BURN_V1_RET_BYTES = 32; + + struct LockOrBurnInV1 { + bytes receiver; // The recipient of the tokens on the destination chain, abi encoded + uint64 remoteChainSelector; // ─╮ The chain ID of the destination chain + address originalSender; // ─────╯ The original sender of the tx on the source chain + uint256 amount; // The amount of tokens to lock or burn, denominated in the source token's decimals + address localToken; // The address on this chain of the token to lock or burn + } + + struct LockOrBurnOutV1 { + // The address of the destination token pool, abi encoded in the case of EVM chains + // This value is UNTRUSTED as any pool owner can return whatever value they want. + bytes destTokenAddress; + // Optional pool data to be transferred to the destination chain. Be default this is capped at + // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead + // has to be set for the specific token. + bytes destPoolData; + } + + struct ReleaseOrMintInV1 { + bytes originalSender; // The original sender of the tx on the source chain + uint64 remoteChainSelector; // ─╮ The chain ID of the source chain + address receiver; // ───────────╯ The recipient of the tokens on the destination chain. This is *NOT* the address to + // send the tokens to, but the address that will receive the tokens via the offRamp. + uint256 amount; // The amount of tokens to release or mint, denominated in the source token's decimals + address localToken; // The address on this chain of the token to release or mint + /// @dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the + /// expected pool address for the given remoteChainSelector. + bytes sourcePoolAddress; // The address of the source pool, abi encoded in the case of EVM chains + bytes sourcePoolData; // The data received from the source pool to process the release or mint + /// @dev WARNING: offchainTokenData is untrusted data. + bytes offchainTokenData; // The offchain data to process the release or mint + } + + struct ReleaseOrMintOutV1 { + // The number of tokens released or minted on the destination chain, denominated in the local token's decimals. + // This value is expected to be equal to the ReleaseOrMintInV1.amount in the case where the source and destination + // chain have the same number of decimals. + uint256 destinationAmount; + } +} diff --git a/contracts/src/v0.8/ccip/libraries/RateLimiter.sol b/contracts/src/v0.8/ccip/libraries/RateLimiter.sol new file mode 100644 index 00000000000..40ac3ca213e --- /dev/null +++ b/contracts/src/v0.8/ccip/libraries/RateLimiter.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/// @notice Implements Token Bucket rate limiting. +/// @dev uint128 is safe for rate limiter state. +/// For USD value rate limiting, it can adequately store USD value in 18 decimals. +/// For ERC20 token amount rate limiting, all tokens that will be listed will have at most +/// a supply of uint128.max tokens, and it will therefore not overflow the bucket. +/// In exceptional scenarios where tokens consumed may be larger than uint128, +/// e.g. compromised issuer, an enabled RateLimiter will check and revert. +library RateLimiter { + error BucketOverfilled(); + error OnlyCallableByAdminOrOwner(); + error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress); + error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress); + error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested); + error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available); + error InvalidRateLimitRate(Config rateLimiterConfig); + error DisabledNonZeroRateLimit(Config config); + error RateLimitMustBeDisabled(); + + event TokensConsumed(uint256 tokens); + event ConfigChanged(Config config); + + struct TokenBucket { + uint128 tokens; // ──────╮ Current number of tokens that are in the bucket. + uint32 lastUpdated; // │ Timestamp in seconds of the last token refill, good for 100+ years. + bool isEnabled; // ──────╯ Indication whether the rate limiting is enabled or not + uint128 capacity; // ────╮ Maximum number of tokens that can be in the bucket. + uint128 rate; // ────────╯ Number of tokens per second that the bucket is refilled. + } + + struct Config { + bool isEnabled; // Indication whether the rate limiting should be enabled + uint128 capacity; // ────╮ Specifies the capacity of the rate limiter + uint128 rate; // ───────╯ Specifies the rate of the rate limiter + } + + /// @notice _consume removes the given tokens from the pool, lowering the + /// rate tokens allowed to be consumed for subsequent calls. + /// @param requestTokens The total tokens to be consumed from the bucket. + /// @param tokenAddress The token to consume capacity for, use 0x0 to indicate aggregate value capacity. + /// @dev Reverts when requestTokens exceeds bucket capacity or available tokens in the bucket + /// @dev emits removal of requestTokens if requestTokens is > 0 + function _consume(TokenBucket storage s_bucket, uint256 requestTokens, address tokenAddress) internal { + // If there is no value to remove or rate limiting is turned off, skip this step to reduce gas usage + if (!s_bucket.isEnabled || requestTokens == 0) { + return; + } + + uint256 tokens = s_bucket.tokens; + uint256 capacity = s_bucket.capacity; + uint256 timeDiff = block.timestamp - s_bucket.lastUpdated; + + if (timeDiff != 0) { + if (tokens > capacity) revert BucketOverfilled(); + + // Refill tokens when arriving at a new block time + tokens = _calculateRefill(capacity, tokens, timeDiff, s_bucket.rate); + + s_bucket.lastUpdated = uint32(block.timestamp); + } + + if (capacity < requestTokens) { + // Token address 0 indicates consuming aggregate value rate limit capacity. + if (tokenAddress == address(0)) revert AggregateValueMaxCapacityExceeded(capacity, requestTokens); + revert TokenMaxCapacityExceeded(capacity, requestTokens, tokenAddress); + } + if (tokens < requestTokens) { + uint256 rate = s_bucket.rate; + // Wait required until the bucket is refilled enough to accept this value, round up to next higher second + // Consume is not guaranteed to succeed after wait time passes if there is competing traffic. + // This acts as a lower bound of wait time. + uint256 minWaitInSeconds = ((requestTokens - tokens) + (rate - 1)) / rate; + + if (tokenAddress == address(0)) revert AggregateValueRateLimitReached(minWaitInSeconds, tokens); + revert TokenRateLimitReached(minWaitInSeconds, tokens, tokenAddress); + } + tokens -= requestTokens; + + // Downcast is safe here, as tokens is not larger than capacity + s_bucket.tokens = uint128(tokens); + emit TokensConsumed(requestTokens); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function _currentTokenBucketState(TokenBucket memory bucket) internal view returns (TokenBucket memory) { + // We update the bucket to reflect the status at the exact time of the + // call. This means we might need to refill a part of the bucket based + // on the time that has passed since the last update. + bucket.tokens = + uint128(_calculateRefill(bucket.capacity, bucket.tokens, block.timestamp - bucket.lastUpdated, bucket.rate)); + bucket.lastUpdated = uint32(block.timestamp); + return bucket; + } + + /// @notice Sets the rate limited config. + /// @param s_bucket The token bucket + /// @param config The new config + function _setTokenBucketConfig(TokenBucket storage s_bucket, Config memory config) internal { + // First update the bucket to make sure the proper rate is used for all the time + // up until the config change. + uint256 timeDiff = block.timestamp - s_bucket.lastUpdated; + if (timeDiff != 0) { + s_bucket.tokens = uint128(_calculateRefill(s_bucket.capacity, s_bucket.tokens, timeDiff, s_bucket.rate)); + + s_bucket.lastUpdated = uint32(block.timestamp); + } + + s_bucket.tokens = uint128(_min(config.capacity, s_bucket.tokens)); + s_bucket.isEnabled = config.isEnabled; + s_bucket.capacity = config.capacity; + s_bucket.rate = config.rate; + + emit ConfigChanged(config); + } + + /// @notice Validates the token bucket config + function _validateTokenBucketConfig(Config memory config, bool mustBeDisabled) internal pure { + if (config.isEnabled) { + if (config.rate >= config.capacity || config.rate == 0) { + revert InvalidRateLimitRate(config); + } + if (mustBeDisabled) { + revert RateLimitMustBeDisabled(); + } + } else { + if (config.rate != 0 || config.capacity != 0) { + revert DisabledNonZeroRateLimit(config); + } + } + } + + /// @notice Calculate refilled tokens + /// @param capacity bucket capacity + /// @param tokens current bucket tokens + /// @param timeDiff block time difference since last refill + /// @param rate bucket refill rate + /// @return the value of tokens after refill + function _calculateRefill( + uint256 capacity, + uint256 tokens, + uint256 timeDiff, + uint256 rate + ) private pure returns (uint256) { + return _min(capacity, tokens + timeDiff * rate); + } + + /// @notice Return the smallest of two integers + /// @param a first int + /// @param b second int + /// @return smallest + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} diff --git a/contracts/src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol b/contracts/src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol new file mode 100644 index 00000000000..3508276d769 --- /dev/null +++ b/contracts/src/v0.8/ccip/libraries/USDPriceWith18Decimals.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library USDPriceWith18Decimals { + /// @notice Takes a price in USD, with 18 decimals per 1e18 token amount, + /// and amount of the smallest token denomination, + /// calculates the value in USD with 18 decimals. + /// @param tokenPrice The USD price of the token. + /// @param tokenAmount Amount of the smallest token denomination. + /// @return USD value with 18 decimals. + /// @dev this function assumes that no more than 1e59 US dollar worth of token is passed in. + /// If more is sent, this function will overflow and revert. + /// Since there isn't even close to 1e59 dollars, this is ok for all legit tokens. + function _calcUSDValueFromTokenAmount(uint224 tokenPrice, uint256 tokenAmount) internal pure returns (uint256) { + /// LINK Example: + /// tokenPrice: 8e18 -> $8/LINK, as 1e18 token amount is 1 LINK, worth 8 USD, or 8e18 with 18 decimals + /// tokenAmount: 2e18 -> 2 LINK + /// result: 8e18 * 2e18 / 1e18 -> 16e18 with 18 decimals = $16 + + /// USDC Example: + /// tokenPrice: 1e30 -> $1/USDC, as 1e18 token amount is 1e12 USDC, worth 1e12 USD, or 1e30 with 18 decimals + /// tokenAmount: 5e6 -> 5 USDC + /// result: 1e30 * 5e6 / 1e18 -> 5e18 with 18 decimals = $5 + return (tokenPrice * tokenAmount) / 1e18; + } + + /// @notice Takes a price in USD, with 18 decimals per 1e18 token amount, + /// and USD value with 18 decimals, + /// calculates amount of the smallest token denomination. + /// @param tokenPrice The USD price of the token. + /// @param usdValue USD value with 18 decimals. + /// @return Amount of the smallest token denomination. + function _calcTokenAmountFromUSDValue(uint224 tokenPrice, uint256 usdValue) internal pure returns (uint256) { + /// LINK Example: + /// tokenPrice: 8e18 -> $8/LINK, as 1e18 token amount is 1 LINK, worth 8 USD, or 8e18 with 18 decimals + /// usdValue: 16e18 -> $16 + /// result: 16e18 * 1e18 / 8e18 -> 2e18 = 2 LINK + + /// USDC Example: + /// tokenPrice: 1e30 -> $1/USDC, as 1e18 token amount is 1e12 USDC, worth 1e12 USD, or 1e30 with 18 decimals + /// usdValue: 5e18 -> $5 + /// result: 5e18 * 1e18 / 1e30 -> 5e6 = 5 USDC + return (usdValue * 1e18) / tokenPrice; + } +} diff --git a/contracts/src/v0.8/ccip/ocr/MultiOCR3Base.sol b/contracts/src/v0.8/ccip/ocr/MultiOCR3Base.sol new file mode 100644 index 00000000000..1872ae276ce --- /dev/null +++ b/contracts/src/v0.8/ccip/ocr/MultiOCR3Base.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +/// @notice Onchain verification of reports from the offchain reporting protocol +/// with multiple OCR plugin support. +abstract contract MultiOCR3Base is ITypeAndVersion, OwnerIsCreator { + // Maximum number of oracles the offchain reporting protocol is designed for + uint256 internal constant MAX_NUM_ORACLES = 31; + + /// @notice triggers a new run of the offchain reporting protocol + /// @param ocrPluginType OCR plugin type for which the config was set + /// @param configDigest configDigest of this configuration + /// @param signers ith element is address ith oracle uses to sign a report + /// @param transmitters ith element is address ith oracle uses to transmit a report via the transmit method + /// @param F maximum number of faulty/dishonest oracles the protocol can tolerate while still working correctly + event ConfigSet(uint8 ocrPluginType, bytes32 configDigest, address[] signers, address[] transmitters, uint8 F); + + /// @notice optionally emitted to indicate the latest configDigest and sequence number + /// for which a report was successfully transmitted. Alternatively, the contract may + /// use latestConfigDigestAndEpoch with scanLogs set to false. + event Transmitted(uint8 indexed ocrPluginType, bytes32 configDigest, uint64 sequenceNumber); + + enum InvalidConfigErrorType { + F_MUST_BE_POSITIVE, + TOO_MANY_TRANSMITTERS, + TOO_MANY_SIGNERS, + F_TOO_HIGH, + REPEATED_ORACLE_ADDRESS + } + + error InvalidConfig(InvalidConfigErrorType errorType); + error WrongMessageLength(uint256 expected, uint256 actual); + error ConfigDigestMismatch(bytes32 expected, bytes32 actual); + error ForkedChain(uint256 expected, uint256 actual); + error WrongNumberOfSignatures(); + error SignaturesOutOfRegistration(); + error UnauthorizedTransmitter(); + error UnauthorizedSigner(); + error NonUniqueSignatures(); + error OracleCannotBeZeroAddress(); + error StaticConfigCannotBeChanged(uint8 ocrPluginType); + + /// @dev Packing these fields used on the hot path in a ConfigInfo variable reduces the + /// retrieval of all of them to a minimum number of SLOADs. + struct ConfigInfo { + bytes32 configDigest; + uint8 F; // ──────────────────────────────╮ maximum number of faulty/dishonest oracles the system can tolerate + uint8 n; // │ number of signers / transmitters + bool isSignatureVerificationEnabled; // ──╯ if true, requires signers and verifies signatures on transmission verification + } + + /// @notice Used for s_oracles[a].role, where a is an address, to track the purpose + /// of the address, or to indicate that the address is unset. + enum Role { + // No oracle role has been set for address a + Unset, + // Signing address for the s_oracles[a].index'th oracle. I.e., report + // signatures from this oracle should ecrecover back to address a. + Signer, + // Transmission address for the s_oracles[a].index'th oracle. I.e., if a + // report is received by OCR2Aggregator.transmit in which msg.sender is + // a, it is attributed to the s_oracles[a].index'th oracle. + Transmitter + } + + struct Oracle { + uint8 index; // ───╮ Index of oracle in s_signers/s_transmitters + Role role; // ─────╯ Role of the address which mapped to this struct + } + + /// @notice OCR configuration for a single OCR plugin within a DON + struct OCRConfig { + ConfigInfo configInfo; // latest OCR config + address[] signers; // addresses oracles use to sign the reports + address[] transmitters; // addresses oracles use to transmit the reports + } + + /// @notice Args to update an OCR Config + struct OCRConfigArgs { + bytes32 configDigest; // Config digest to update to + uint8 ocrPluginType; // ──────────────────╮ OCR plugin type to update config for + uint8 F; // │ maximum number of faulty/dishonest oracles + bool isSignatureVerificationEnabled; // ──╯ if true, requires signers and verifies signatures on transmission verification + address[] signers; // signing address of each oracle + address[] transmitters; // transmission address of each oracle (i.e. the address the oracle actually sends transactions to the contract from) + } + + /// @notice mapping of OCR plugin type -> DON config + mapping(uint8 ocrPluginType => OCRConfig config) internal s_ocrConfigs; + + /// @notice OCR plugin type => signer OR transmitter address mapping + mapping(uint8 ocrPluginType => mapping(address signerOrTransmiter => Oracle oracle)) internal s_oracles; + + // Constant-length components of the msg.data sent to transmit. + // See the "If we wanted to call sam" example on for example reasoning + // https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html + + /// @notice constant length component for transmit functions with no signatures. + /// The signatures are expected to match transmitPlugin(reportContext, report) + uint16 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT_NO_SIGNATURES = 4 // function selector + + 3 * 32 // 3 words containing reportContext + + 32 // word containing start location of abiencoded report value + + 32; // word containing length of report + + /// @notice extra constant length component for transmit functions with signatures (relative to no signatures) + /// The signatures are expected to match transmitPlugin(reportContext, report, rs, ss, rawVs) + uint16 private constant TRANSMIT_MSGDATA_EXTRA_CONSTANT_LENGTH_COMPONENT_FOR_SIGNATURES = 32 // word containing location start of abiencoded rs value + + 32 // word containing start location of abiencoded ss value + + 32 // rawVs value + + 32 // word containing length rs + + 32; // word containing length of ss + + uint256 internal immutable i_chainID; + + constructor() { + i_chainID = block.chainid; + } + + /// @notice sets offchain reporting protocol configuration incl. participating oracles + /// NOTE: The OCR3 config must be sanity-checked against the home-chain registry configuration, to ensure + /// home-chain and remote-chain parity! + /// @param ocrConfigArgs OCR config update args + function setOCR3Configs(OCRConfigArgs[] memory ocrConfigArgs) external onlyOwner { + for (uint256 i; i < ocrConfigArgs.length; ++i) { + _setOCR3Config(ocrConfigArgs[i]); + } + } + + /// @notice sets offchain reporting protocol configuration incl. participating oracles for a single OCR plugin type + /// @param ocrConfigArgs OCR config update args + function _setOCR3Config(OCRConfigArgs memory ocrConfigArgs) internal { + if (ocrConfigArgs.F == 0) revert InvalidConfig(InvalidConfigErrorType.F_MUST_BE_POSITIVE); + + uint8 ocrPluginType = ocrConfigArgs.ocrPluginType; + OCRConfig storage ocrConfig = s_ocrConfigs[ocrPluginType]; + ConfigInfo storage configInfo = ocrConfig.configInfo; + + // If F is 0, then the config is not yet set + if (configInfo.F == 0) { + configInfo.isSignatureVerificationEnabled = ocrConfigArgs.isSignatureVerificationEnabled; + } else if (configInfo.isSignatureVerificationEnabled != ocrConfigArgs.isSignatureVerificationEnabled) { + revert StaticConfigCannotBeChanged(ocrPluginType); + } + + address[] memory transmitters = ocrConfigArgs.transmitters; + // Transmitters are expected to never exceed 255 (since this is bounded by MAX_NUM_ORACLES) + uint8 newTransmittersLength = uint8(transmitters.length); + + if (newTransmittersLength > MAX_NUM_ORACLES) revert InvalidConfig(InvalidConfigErrorType.TOO_MANY_TRANSMITTERS); + + _clearOracleRoles(ocrPluginType, ocrConfig.transmitters); + + if (ocrConfigArgs.isSignatureVerificationEnabled) { + _clearOracleRoles(ocrPluginType, ocrConfig.signers); + + address[] memory signers = ocrConfigArgs.signers; + ocrConfig.signers = signers; + + uint8 signersLength = uint8(signers.length); + configInfo.n = signersLength; + + if (signersLength > MAX_NUM_ORACLES) revert InvalidConfig(InvalidConfigErrorType.TOO_MANY_SIGNERS); + if (signersLength <= 3 * ocrConfigArgs.F) revert InvalidConfig(InvalidConfigErrorType.F_TOO_HIGH); + + _assignOracleRoles(ocrPluginType, signers, Role.Signer); + } + + _assignOracleRoles(ocrPluginType, transmitters, Role.Transmitter); + + ocrConfig.transmitters = transmitters; + configInfo.F = ocrConfigArgs.F; + configInfo.configDigest = ocrConfigArgs.configDigest; + + emit ConfigSet( + ocrPluginType, ocrConfigArgs.configDigest, ocrConfig.signers, ocrConfigArgs.transmitters, ocrConfigArgs.F + ); + _afterOCR3ConfigSet(ocrPluginType); + } + + /// @notice Hook that is called after a plugin's OCR3 config changes + /// @param ocrPluginType Plugin type for which the config changed + function _afterOCR3ConfigSet(uint8 ocrPluginType) internal virtual; + + /// @notice Clears oracle roles for the provided oracle addresses + /// @param ocrPluginType OCR plugin type to clear roles for + /// @param oracleAddresses Oracle addresses to clear roles for + function _clearOracleRoles(uint8 ocrPluginType, address[] memory oracleAddresses) internal { + for (uint256 i = 0; i < oracleAddresses.length; ++i) { + delete s_oracles[ocrPluginType][oracleAddresses[i]]; + } + } + + /// @notice Assigns oracles roles for the provided oracle addresses with uniqueness verification + /// @param ocrPluginType OCR plugin type to assign roles for + /// @param oracleAddresses Oracle addresses to assign roles to + /// @param role Role to assign + function _assignOracleRoles(uint8 ocrPluginType, address[] memory oracleAddresses, Role role) internal { + for (uint8 i = 0; i < oracleAddresses.length; ++i) { + address oracle = oracleAddresses[i]; + if (s_oracles[ocrPluginType][oracle].role != Role.Unset) { + revert InvalidConfig(InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS); + } + if (oracle == address(0)) revert OracleCannotBeZeroAddress(); + s_oracles[ocrPluginType][oracle] = Oracle(i, role); + } + } + + /// @notice _transmit is called to post a new report to the contract. + /// The function should be called after the per-DON reporting logic is completed. + /// @param ocrPluginType OCR plugin type to transmit report for + /// @param report serialized report, which the signatures are signing. + /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param rawVs ith element is the the V component of the ith signature + function _transmit( + uint8 ocrPluginType, + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] memory rs, + bytes32[] memory ss, + bytes32 rawVs // signatures + ) internal { + // reportContext consists of: + // reportContext[0]: ConfigDigest + // reportContext[1]: 24 byte padding, 8 byte sequence number + // reportContext[2]: ExtraHash + ConfigInfo memory configInfo = s_ocrConfigs[ocrPluginType].configInfo; + bytes32 configDigest = reportContext[0]; + + // Scoping this reduces stack pressure and gas usage + { + uint256 expectedDataLength = uint256(TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT_NO_SIGNATURES) + report.length; // one byte pure entry in _report + + if (configInfo.isSignatureVerificationEnabled) { + expectedDataLength += TRANSMIT_MSGDATA_EXTRA_CONSTANT_LENGTH_COMPONENT_FOR_SIGNATURES + rs.length * 32 // 32 bytes per entry in _rs + + ss.length * 32; // 32 bytes per entry in _ss) + } + + if (msg.data.length != expectedDataLength) revert WrongMessageLength(expectedDataLength, msg.data.length); + } + + if (configInfo.configDigest != configDigest) { + revert ConfigDigestMismatch(configInfo.configDigest, configDigest); + } + // If the cached chainID at time of deployment doesn't match the current chainID, we reject all signed reports. + // This avoids a (rare) scenario where chain A forks into chain A and A', A' still has configDigest + // calculated from chain A and so OCR reports will be valid on both forks. + _whenChainNotForked(); + + // Scoping this reduces stack pressure and gas usage + { + Oracle memory transmitter = s_oracles[ocrPluginType][msg.sender]; + // Check that sender is authorized to report + if ( + !( + transmitter.role == Role.Transmitter + && msg.sender == s_ocrConfigs[ocrPluginType].transmitters[transmitter.index] + ) + ) { + revert UnauthorizedTransmitter(); + } + } + + if (configInfo.isSignatureVerificationEnabled) { + // Scoping to reduce stack pressure + { + if (rs.length != configInfo.F + 1) revert WrongNumberOfSignatures(); + if (rs.length != ss.length) revert SignaturesOutOfRegistration(); + } + + bytes32 h = keccak256(abi.encodePacked(keccak256(report), reportContext)); + _verifySignatures(ocrPluginType, h, rs, ss, rawVs); + } + + emit Transmitted(ocrPluginType, configDigest, uint64(uint256(reportContext[1]))); + } + + /// @notice verifies the signatures of a hashed report value for one OCR plugin type + /// @param ocrPluginType OCR plugin type to transmit report for + /// @param hashedReport hashed encoded packing of report + reportContext + /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param rawVs ith element is the the V component of the ith signature + function _verifySignatures( + uint8 ocrPluginType, + bytes32 hashedReport, + bytes32[] memory rs, + bytes32[] memory ss, + bytes32 rawVs // signatures + ) internal view { + // Verify signatures attached to report + bool[MAX_NUM_ORACLES] memory signed; + + uint256 numberOfSignatures = rs.length; + for (uint256 i; i < numberOfSignatures; ++i) { + // Safe from ECDSA malleability here since we check for duplicate signers. + address signer = ecrecover(hashedReport, uint8(rawVs[i]) + 27, rs[i], ss[i]); + // Since we disallow address(0) as a valid signer address, it can + // never have a signer role. + Oracle memory oracle = s_oracles[ocrPluginType][signer]; + if (oracle.role != Role.Signer) revert UnauthorizedSigner(); + if (signed[oracle.index]) revert NonUniqueSignatures(); + signed[oracle.index] = true; + } + } + + /// @notice Validates that the chain ID has not diverged after deployment. Reverts if the chain IDs do not match + function _whenChainNotForked() internal view { + if (i_chainID != block.chainid) revert ForkedChain(i_chainID, block.chainid); + } + + /// @notice information about current offchain reporting protocol configuration + /// @param ocrPluginType OCR plugin type to return config details for + /// @return ocrConfig OCR config for the plugin type + function latestConfigDetails(uint8 ocrPluginType) external view returns (OCRConfig memory ocrConfig) { + return s_ocrConfigs[ocrPluginType]; + } +} diff --git a/contracts/src/v0.8/ccip/ocr/OCR2Abstract.sol b/contracts/src/v0.8/ccip/ocr/OCR2Abstract.sol new file mode 100644 index 00000000000..741433bd5ad --- /dev/null +++ b/contracts/src/v0.8/ccip/ocr/OCR2Abstract.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +abstract contract OCR2Abstract is ITypeAndVersion { + // Maximum number of oracles the offchain reporting protocol is designed for + uint256 internal constant MAX_NUM_ORACLES = 31; + + /// @notice triggers a new run of the offchain reporting protocol + /// @param previousConfigBlockNumber block in which the previous config was set, to simplify historic analysis + /// @param configDigest configDigest of this configuration + /// @param configCount ordinal number of this config setting among all config settings over the life of this contract + /// @param signers ith element is address ith oracle uses to sign a report + /// @param transmitters ith element is address ith oracle uses to transmit a report via the transmit method + /// @param f maximum number of faulty/dishonest oracles the protocol can tolerate while still working correctly + /// @param onchainConfig serialized configuration used by the contract (and possibly oracles) + /// @param offchainConfigVersion version of the serialization format used for "offchainConfig" parameter + /// @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract + event ConfigSet( + uint32 previousConfigBlockNumber, + bytes32 configDigest, + uint64 configCount, + address[] signers, + address[] transmitters, + uint8 f, + bytes onchainConfig, + uint64 offchainConfigVersion, + bytes offchainConfig + ); + + /// @notice sets offchain reporting protocol configuration incl. participating oracles + /// @param signers addresses with which oracles sign the reports + /// @param transmitters addresses oracles use to transmit the reports + /// @param f number of faulty oracles the system can tolerate + /// @param onchainConfig serialized configuration used by the contract (and possibly oracles) + /// @param offchainConfigVersion version number for offchainEncoding schema + /// @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract + function setOCR2Config( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external virtual; + + /// @notice information about current offchain reporting protocol configuration + /// @return configCount ordinal number of current config, out of all configs applied to this contract so far + /// @return blockNumber block at which this config was set + /// @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) + function latestConfigDetails() + external + view + virtual + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest); + + function _configDigestFromConfigData( + uint256 chainId, + address contractAddress, + uint64 configCount, + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) internal pure returns (bytes32) { + uint256 h = uint256( + keccak256( + abi.encode( + chainId, + contractAddress, + configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00 + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } + + /// @notice optionally emitted to indicate the latest configDigest and epoch for + /// which a report was successfully transmitted. Alternatively, the contract may + /// use latestConfigDigestAndEpoch with scanLogs set to false. + event Transmitted(bytes32 configDigest, uint32 epoch); + + /// @notice optionally returns the latest configDigest and epoch for which a + /// report was successfully transmitted. Alternatively, the contract may return + /// scanLogs set to true and use Transmitted events to provide this information + /// to offchain watchers. + /// @return scanLogs indicates whether to rely on the configDigest and epoch + /// returned or whether to scan logs for the Transmitted event instead. + /// @return configDigest + /// @return epoch + function latestConfigDigestAndEpoch() + external + view + virtual + returns (bool scanLogs, bytes32 configDigest, uint32 epoch); + + /// @notice transmit is called to post a new report to the contract + /// @param report serialized report, which the signatures are signing. + /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param rawVs ith element is the the V component of the ith signature + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external virtual; +} diff --git a/contracts/src/v0.8/ccip/ocr/OCR2Base.sol b/contracts/src/v0.8/ccip/ocr/OCR2Base.sol new file mode 100644 index 00000000000..52a6df2f3a2 --- /dev/null +++ b/contracts/src/v0.8/ccip/ocr/OCR2Base.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {OCR2Abstract} from "./OCR2Abstract.sol"; + +/// @notice Onchain verification of reports from the offchain reporting protocol +/// @dev For details on its operation, see the offchain reporting protocol design +/// doc, which refers to this contract as simply the "contract". +abstract contract OCR2Base is OwnerIsCreator, OCR2Abstract { + error InvalidConfig(InvalidConfigErrorType errorType); + error WrongMessageLength(uint256 expected, uint256 actual); + error ConfigDigestMismatch(bytes32 expected, bytes32 actual); + error ForkedChain(uint256 expected, uint256 actual); + error WrongNumberOfSignatures(); + error SignaturesOutOfRegistration(); + error UnauthorizedTransmitter(); + error UnauthorizedSigner(); + error NonUniqueSignatures(); + error OracleCannotBeZeroAddress(); + + enum InvalidConfigErrorType { + F_MUST_BE_POSITIVE, + TOO_MANY_SIGNERS, + F_TOO_HIGH, + REPEATED_ORACLE_ADDRESS, + NUM_SIGNERS_NOT_NUM_TRANSMITTERS + } + + // Packing these fields used on the hot path in a ConfigInfo variable reduces the + // retrieval of all of them to a minimum number of SLOADs. + struct ConfigInfo { + bytes32 latestConfigDigest; + uint8 f; + uint8 n; + } + + // Used for s_oracles[a].role, where a is an address, to track the purpose + // of the address, or to indicate that the address is unset. + enum Role { + // No oracle role has been set for address a + Unset, + // Signing address for the s_oracles[a].index'th oracle. I.e., report + // signatures from this oracle should ecrecover back to address a. + Signer, + // Transmission address for the s_oracles[a].index'th oracle. I.e., if a + // report is received by OCR2Aggregator.transmit in which msg.sender is + // a, it is attributed to the s_oracles[a].index'th oracle. + Transmitter + } + + struct Oracle { + uint8 index; // Index of oracle in s_signers/s_transmitters + Role role; // Role of the address which mapped to this struct + } + + // The current config + ConfigInfo internal s_configInfo; + + // incremented each time a new config is posted. This count is incorporated + // into the config digest, to prevent replay attacks. + uint32 internal s_configCount; + // makes it easier for offchain systems to extract config from logs. + uint32 internal s_latestConfigBlockNumber; + + // signer OR transmitter address + mapping(address signerOrTransmitter => Oracle oracle) internal s_oracles; + + // s_signers contains the signing address of each oracle + address[] internal s_signers; + + // s_transmitters contains the transmission address of each oracle, + // i.e. the address the oracle actually sends transactions to the contract from + address[] internal s_transmitters; + + // The constant-length components of the msg.data sent to transmit. + // See the "If we wanted to call sam" example on for example reasoning + // https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html + uint16 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT = 4 // function selector + + 32 * 3 // 3 words containing reportContext + + 32 // word containing start location of abiencoded report value + + 32 // word containing location start of abiencoded rs value + + 32 // word containing start location of abiencoded ss value + + 32 // rawVs value + + 32 // word containing length of report + + 32 // word containing length rs + + 32; // word containing length of ss + + bool internal immutable i_uniqueReports; + uint256 internal immutable i_chainID; + + constructor(bool uniqueReports) { + i_uniqueReports = uniqueReports; + i_chainID = block.chainid; + } + + // Reverts transaction if config args are invalid + modifier checkConfigValid(uint256 numSigners, uint256 numTransmitters, uint256 f) { + if (numSigners > MAX_NUM_ORACLES) revert InvalidConfig(InvalidConfigErrorType.TOO_MANY_SIGNERS); + if (f == 0) revert InvalidConfig(InvalidConfigErrorType.F_MUST_BE_POSITIVE); + if (numSigners != numTransmitters) revert InvalidConfig(InvalidConfigErrorType.NUM_SIGNERS_NOT_NUM_TRANSMITTERS); + if (numSigners <= 3 * f) revert InvalidConfig(InvalidConfigErrorType.F_TOO_HIGH); + _; + } + + /// @notice sets offchain reporting protocol configuration incl. participating oracles + /// @param signers addresses with which oracles sign the reports + /// @param transmitters addresses oracles use to transmit the reports + /// @param f number of faulty oracles the system can tolerate + /// @param onchainConfig encoded on-chain contract configuration + /// @param offchainConfigVersion version number for offchainEncoding schema + /// @param offchainConfig encoded off-chain oracle configuration + function setOCR2Config( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external override checkConfigValid(signers.length, transmitters.length, f) onlyOwner { + _beforeSetConfig(onchainConfig); + uint256 oldSignerLength = s_signers.length; + for (uint256 i = 0; i < oldSignerLength; ++i) { + delete s_oracles[s_signers[i]]; + delete s_oracles[s_transmitters[i]]; + } + + uint256 newSignersLength = signers.length; + for (uint256 i = 0; i < newSignersLength; ++i) { + // add new signer/transmitter addresses + address signer = signers[i]; + if (s_oracles[signer].role != Role.Unset) revert InvalidConfig(InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS); + if (signer == address(0)) revert OracleCannotBeZeroAddress(); + s_oracles[signer] = Oracle(uint8(i), Role.Signer); + + address transmitter = transmitters[i]; + if (s_oracles[transmitter].role != Role.Unset) { + revert InvalidConfig(InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS); + } + if (transmitter == address(0)) revert OracleCannotBeZeroAddress(); + s_oracles[transmitter] = Oracle(uint8(i), Role.Transmitter); + } + + s_signers = signers; + s_transmitters = transmitters; + + s_configInfo.f = f; + s_configInfo.n = uint8(newSignersLength); + s_configInfo.latestConfigDigest = _configDigestFromConfigData( + block.chainid, + address(this), + ++s_configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + + uint32 previousConfigBlockNumber = s_latestConfigBlockNumber; + s_latestConfigBlockNumber = uint32(block.number); + + emit ConfigSet( + previousConfigBlockNumber, + s_configInfo.latestConfigDigest, + s_configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + } + + /// @dev Hook that is run from setOCR2Config() right after validating configuration. + /// Empty by default, please provide an implementation in a child contract if you need additional configuration processing + function _beforeSetConfig(bytes memory _onchainConfig) internal virtual; + + /// @return list of addresses permitted to transmit reports to this contract + /// @dev The list will match the order used to specify the transmitter during setConfig + function getTransmitters() external view returns (address[] memory) { + return s_transmitters; + } + + /// @notice transmit is called to post a new report to the contract + /// @param report serialized report, which the signatures are signing. + /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param rawVs ith element is the the V component of the ith signature + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external override { + // Scoping this reduces stack pressure and gas usage + { + // report and epochAndRound + _report(report, uint40(uint256(reportContext[1]))); + } + + // reportContext consists of: + // reportContext[0]: ConfigDigest + // reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round + // reportContext[2]: ExtraHash + bytes32 configDigest = reportContext[0]; + ConfigInfo memory configInfo = s_configInfo; + + if (configInfo.latestConfigDigest != configDigest) { + revert ConfigDigestMismatch(configInfo.latestConfigDigest, configDigest); + } + // If the cached chainID at time of deployment doesn't match the current chainID, we reject all signed reports. + // This avoids a (rare) scenario where chain A forks into chain A and A', A' still has configDigest + // calculated from chain A and so OCR reports will be valid on both forks. + if (i_chainID != block.chainid) revert ForkedChain(i_chainID, block.chainid); + + emit Transmitted(configDigest, uint32(uint256(reportContext[1]) >> 8)); + + uint256 expectedNumSignatures; + if (i_uniqueReports) { + expectedNumSignatures = (configInfo.n + configInfo.f) / 2 + 1; + } else { + expectedNumSignatures = configInfo.f + 1; + } + if (rs.length != expectedNumSignatures) revert WrongNumberOfSignatures(); + if (rs.length != ss.length) revert SignaturesOutOfRegistration(); + + // Scoping this reduces stack pressure and gas usage + { + Oracle memory transmitter = s_oracles[msg.sender]; + // Check that sender is authorized to report + if (!(transmitter.role == Role.Transmitter && msg.sender == s_transmitters[transmitter.index])) { + revert UnauthorizedTransmitter(); + } + } + // Scoping this reduces stack pressure and gas usage + { + uint256 expectedDataLength = uint256(TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT) + report.length // one byte pure entry in _report + + rs.length * 32 // 32 bytes per entry in _rs + + ss.length * 32; // 32 bytes per entry in _ss) + if (msg.data.length != expectedDataLength) revert WrongMessageLength(expectedDataLength, msg.data.length); + } + + // Verify signatures attached to report + bytes32 h = keccak256(abi.encodePacked(keccak256(report), reportContext)); + bool[MAX_NUM_ORACLES] memory signed; + + uint256 numberOfSignatures = rs.length; + for (uint256 i = 0; i < numberOfSignatures; ++i) { + // Safe from ECDSA malleability here since we check for duplicate signers. + address signer = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]); + // Since we disallow address(0) as a valid signer address, it can + // never have a signer role. + Oracle memory oracle = s_oracles[signer]; + if (oracle.role != Role.Signer) revert UnauthorizedSigner(); + if (signed[oracle.index]) revert NonUniqueSignatures(); + signed[oracle.index] = true; + } + } + + /// @notice information about current offchain reporting protocol configuration + /// @return configCount ordinal number of current config, out of all configs applied to this contract so far + /// @return blockNumber block at which this config was set + /// @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) + function latestConfigDetails() + external + view + override + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) + { + return (s_configCount, s_latestConfigBlockNumber, s_configInfo.latestConfigDigest); + } + + /// @inheritdoc OCR2Abstract + function latestConfigDigestAndEpoch() + external + view + virtual + override + returns (bool scanLogs, bytes32 configDigest, uint32 epoch) + { + return (true, bytes32(0), uint32(0)); + } + + function _report(bytes calldata report, uint40 epochAndRound) internal virtual; +} diff --git a/contracts/src/v0.8/ccip/ocr/OCR2BaseNoChecks.sol b/contracts/src/v0.8/ccip/ocr/OCR2BaseNoChecks.sol new file mode 100644 index 00000000000..a79df8d589a --- /dev/null +++ b/contracts/src/v0.8/ccip/ocr/OCR2BaseNoChecks.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {OCR2Abstract} from "./OCR2Abstract.sol"; + +/// @notice Onchain verification of reports from the offchain reporting protocol +/// @dev For details on its operation, see the offchain reporting protocol design +/// doc, which refers to this contract as simply the "contract". +/// @dev This contract does ***NOT*** check the supplied signatures on `transmit` +/// This is intentional. +abstract contract OCR2BaseNoChecks is OwnerIsCreator, OCR2Abstract { + error InvalidConfig(InvalidConfigErrorType errorType); + error WrongMessageLength(uint256 expected, uint256 actual); + error ConfigDigestMismatch(bytes32 expected, bytes32 actual); + error ForkedChain(uint256 expected, uint256 actual); + error UnauthorizedTransmitter(); + error OracleCannotBeZeroAddress(); + + enum InvalidConfigErrorType { + F_MUST_BE_POSITIVE, + TOO_MANY_TRANSMITTERS, + REPEATED_ORACLE_ADDRESS + } + + // Packing these fields used on the hot path in a ConfigInfo variable reduces the + // retrieval of all of them to a minimum number of SLOADs. + struct ConfigInfo { + bytes32 latestConfigDigest; + uint8 f; + uint8 n; + } + + // Used for s_oracles[a].role, where a is an address, to track the purpose + // of the address, or to indicate that the address is unset. + enum Role { + // No oracle role has been set for address a + Unset, + // Unused + Signer, + // Transmission address for the s_oracles[a].index'th oracle. I.e., if a + // report is received by OCR2Aggregator.transmit in which msg.sender is + // a, it is attributed to the s_oracles[a].index'th oracle. + Transmitter + } + + struct Oracle { + uint8 index; // Index of oracle in s_transmitters + Role role; // Role of the address which mapped to this struct + } + + // The current config + ConfigInfo internal s_configInfo; + + // incremented each time a new config is posted. This count is incorporated + // into the config digest, to prevent replay attacks. + uint32 internal s_configCount; + // makes it easier for offchain systems to extract config from logs. + uint32 internal s_latestConfigBlockNumber; + + // Transmitter address + mapping(address transmitter => Oracle oracle) internal s_oracles; + + // s_transmitters contains the transmission address of each oracle, + // i.e. the address the oracle actually sends transactions to the contract from + address[] internal s_transmitters; + + // The constant-length components of the msg.data sent to transmit. + // See the "If we wanted to call sam" example on for example reasoning + // https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html + uint16 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT = 4 // function selector + + 32 * 3 // 3 words containing reportContext + + 32 // word containing start location of abiencoded report value + + 32 // word containing location start of abiencoded rs value + + 32 // word containing start location of abiencoded ss value + + 32 // rawVs value + + 32 // word containing length of report + + 32 // word containing length rs + + 32; // word containing length of ss + + uint256 internal immutable i_chainID; + + // Reverts transaction if config args are invalid + modifier checkConfigValid(uint256 numTransmitters, uint256 f) { + if (numTransmitters > MAX_NUM_ORACLES) revert InvalidConfig(InvalidConfigErrorType.TOO_MANY_TRANSMITTERS); + if (f == 0) revert InvalidConfig(InvalidConfigErrorType.F_MUST_BE_POSITIVE); + _; + } + + constructor() { + i_chainID = block.chainid; + } + + /// @notice sets offchain reporting protocol configuration incl. participating oracles + /// @param signers addresses with which oracles sign the reports + /// @param transmitters addresses oracles use to transmit the reports + /// @param f number of faulty oracles the system can tolerate + /// @param onchainConfig encoded on-chain contract configuration + /// @param offchainConfigVersion version number for offchainEncoding schema + /// @param offchainConfig encoded off-chain oracle configuration + function setOCR2Config( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external override checkConfigValid(transmitters.length, f) onlyOwner { + _beforeSetConfig(onchainConfig); + // Scoped to reduce contract size + { + uint256 oldTransmitterLength = s_transmitters.length; + for (uint256 i = 0; i < oldTransmitterLength; ++i) { + delete s_oracles[s_transmitters[i]]; + } + } + uint256 newTransmitterLength = transmitters.length; + for (uint256 i = 0; i < newTransmitterLength; ++i) { + address transmitter = transmitters[i]; + if (s_oracles[transmitter].role != Role.Unset) { + revert InvalidConfig(InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS); + } + if (transmitter == address(0)) revert OracleCannotBeZeroAddress(); + s_oracles[transmitter] = Oracle(uint8(i), Role.Transmitter); + } + + s_transmitters = transmitters; + + s_configInfo.f = f; + s_configInfo.n = uint8(newTransmitterLength); + s_configInfo.latestConfigDigest = _configDigestFromConfigData( + block.chainid, + address(this), + ++s_configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + + uint32 previousConfigBlockNumber = s_latestConfigBlockNumber; + s_latestConfigBlockNumber = uint32(block.number); + + emit ConfigSet( + previousConfigBlockNumber, + s_configInfo.latestConfigDigest, + s_configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + } + + /// @dev Hook that is run from setOCR2Config() right after validating configuration. + /// Empty by default, please provide an implementation in a child contract if you need additional configuration processing + function _beforeSetConfig(bytes memory _onchainConfig) internal virtual; + + /// @return list of addresses permitted to transmit reports to this contract + /// @dev The list will match the order used to specify the transmitter during setConfig + function getTransmitters() external view returns (address[] memory) { + return s_transmitters; + } + + /// @notice transmit is called to post a new report to the contract + /// @param report serialized report, which the signatures are signing. + /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 // signatures + ) external override { + _report(report); + + // reportContext consists of: + // reportContext[0]: ConfigDigest + // reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round + // reportContext[2]: ExtraHash + bytes32 configDigest = reportContext[0]; + bytes32 latestConfigDigest = s_configInfo.latestConfigDigest; + if (latestConfigDigest != configDigest) revert ConfigDigestMismatch(latestConfigDigest, configDigest); + _checkChainForked(); + + emit Transmitted(configDigest, uint32(uint256(reportContext[1]) >> 8)); + + // Scoping this reduces stack pressure and gas usage + { + Oracle memory transmitter = s_oracles[msg.sender]; + // Check that sender is authorized to report + if (!(transmitter.role == Role.Transmitter && msg.sender == s_transmitters[transmitter.index])) { + revert UnauthorizedTransmitter(); + } + } + + uint256 expectedDataLength = uint256(TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT) + report.length // one byte pure entry in _report + + rs.length * 32 // 32 bytes per entry in _rs + + ss.length * 32; // 32 bytes per entry in _ss) + if (msg.data.length != expectedDataLength) revert WrongMessageLength(expectedDataLength, msg.data.length); + } + + function _checkChainForked() internal view { + // If the cached chainID at time of deployment doesn't match the current chainID, we reject all signed reports. + // This avoids a (rare) scenario where chain A forks into chain A and A', A' still has configDigest + // calculated from chain A and so OCR reports will be valid on both forks. + if (i_chainID != block.chainid) revert ForkedChain(i_chainID, block.chainid); + } + + /// @notice information about current offchain reporting protocol configuration + /// @return configCount ordinal number of current config, out of all configs applied to this contract so far + /// @return blockNumber block at which this config was set + /// @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) + function latestConfigDetails() + external + view + override + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) + { + return (s_configCount, s_latestConfigBlockNumber, s_configInfo.latestConfigDigest); + } + + /// @inheritdoc OCR2Abstract + function latestConfigDigestAndEpoch() + external + view + virtual + override + returns (bool scanLogs, bytes32 configDigest, uint32 epoch) + { + return (true, bytes32(0), uint32(0)); + } + + function _report(bytes calldata report) internal virtual; +} diff --git a/contracts/src/v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol b/contracts/src/v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol new file mode 100644 index 00000000000..809e4e22a4e --- /dev/null +++ b/contracts/src/v0.8/ccip/offRamp/EVM2EVMMultiOffRamp.sol @@ -0,0 +1,914 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IAny2EVMMessageReceiver} from "../interfaces/IAny2EVMMessageReceiver.sol"; +import {IMessageInterceptor} from "../interfaces/IMessageInterceptor.sol"; +import {INonceManager} from "../interfaces/INonceManager.sol"; +import {IPoolV1} from "../interfaces/IPool.sol"; +import {IPriceRegistry} from "../interfaces/IPriceRegistry.sol"; +import {IRMN} from "../interfaces/IRMN.sol"; +import {IRouter} from "../interfaces/IRouter.sol"; +import {ITokenAdminRegistry} from "../interfaces/ITokenAdminRegistry.sol"; + +import {CallWithExactGas} from "../../shared/call/CallWithExactGas.sol"; +import {EnumerableMapAddresses} from "../../shared/enumerable/EnumerableMapAddresses.sol"; +import {Client} from "../libraries/Client.sol"; +import {Internal} from "../libraries/Internal.sol"; +import {MerkleMultiProof} from "../libraries/MerkleMultiProof.sol"; +import {Pool} from "../libraries/Pool.sol"; +import {MultiOCR3Base} from "../ocr/MultiOCR3Base.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {ERC165Checker} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol"; + +/// @notice EVM2EVMOffRamp enables OCR networks to execute multiple messages +/// in an OffRamp in a single transaction. +/// @dev The EVM2EVMMultiOnRamp and EVM2EVMMultiOffRamp form an xchain upgradeable unit. Any change to one of them +/// results an onchain upgrade of both contracts. +/// @dev MultiOCR3Base is used to store multiple OCR configs for both the OffRamp and the CommitStore. +/// The execution plugin type has to be configured without signature verification, and the commit +/// plugin type with verification. +contract EVM2EVMMultiOffRamp is ITypeAndVersion, MultiOCR3Base { + using ERC165Checker for address; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + error AlreadyAttempted(uint64 sourceChainSelector, uint64 sequenceNumber); + error AlreadyExecuted(uint64 sourceChainSelector, uint64 sequenceNumber); + error ZeroChainSelectorNotAllowed(); + error ExecutionError(bytes32 messageId, bytes err); + error SourceChainNotEnabled(uint64 sourceChainSelector); + error TokenDataMismatch(uint64 sourceChainSelector, uint64 sequenceNumber); + error UnexpectedTokenData(); + error ManualExecutionNotYetEnabled(uint64 sourceChainSelector); + error ManualExecutionGasLimitMismatch(); + error InvalidManualExecutionGasLimit(uint64 sourceChainSelector, uint256 index, uint256 newLimit); + error RootNotCommitted(uint64 sourceChainSelector); + error RootAlreadyCommitted(uint64 sourceChainSelector, bytes32 merkleRoot); + error InvalidRoot(); + error CanOnlySelfCall(); + error ReceiverError(bytes err); + error TokenHandlingError(bytes err); + error EmptyReport(); + error CursedByRMN(uint64 sourceChainSelector); + error NotACompatiblePool(address notPool); + error InvalidDataLength(uint256 expected, uint256 got); + error InvalidNewState(uint64 sourceChainSelector, uint64 sequenceNumber, Internal.MessageExecutionState newState); + error InvalidStaticConfig(uint64 sourceChainSelector); + error StaleCommitReport(); + error InvalidInterval(uint64 sourceChainSelector, Interval interval); + error ZeroAddressNotAllowed(); + error InvalidMessageDestChainSelector(uint64 messageDestChainSelector); + + /// @dev Atlas depends on this event, if changing, please notify Atlas. + event StaticConfigSet(StaticConfig staticConfig); + event DynamicConfigSet(DynamicConfig dynamicConfig); + /// @dev RMN depends on this event, if changing, please notify the RMN maintainers. + event ExecutionStateChanged( + uint64 indexed sourceChainSelector, + uint64 indexed sequenceNumber, + bytes32 indexed messageId, + Internal.MessageExecutionState state, + bytes returnData + ); + event SourceChainSelectorAdded(uint64 sourceChainSelector); + event SourceChainConfigSet(uint64 indexed sourceChainSelector, SourceChainConfig sourceConfig); + event SkippedAlreadyExecutedMessage(uint64 sourceChainSelector, uint64 sequenceNumber); + /// @dev RMN depends on this event, if changing, please notify the RMN maintainers. + event CommitReportAccepted(CommitReport report); + event RootRemoved(bytes32 root); + + /// @notice Static offRamp config + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct StaticConfig { + uint64 chainSelector; // ───╮ Destination chainSelector + address rmnProxy; // ───────╯ RMN proxy address + address tokenAdminRegistry; // Token admin registry address + address nonceManager; // Address of the nonce manager + } + + /// @notice Per-chain source config (defining a lane from a Source Chain -> Dest OffRamp) + struct SourceChainConfig { + bool isEnabled; // ──────────╮ Flag whether the source chain is enabled or not + uint64 minSeqNr; // ─────────╯ The min sequence number expected for future messages + bytes onRamp; // OnRamp address on the source chain + } + + /// @notice SourceChainConfig update args scoped to one source chain + struct SourceChainConfigArgs { + uint64 sourceChainSelector; // ───╮ Source chain selector of the config to update + bool isEnabled; // ────────────────╯ Flag whether the source chain is enabled or not + bytes onRamp; // OnRamp address on the source chain + } + + /// @notice Dynamic offRamp config + /// @dev since OffRampConfig is part of OffRampConfigChanged event, if changing it, we should update the ABI on Atlas + struct DynamicConfig { + address router; // ─────────────────────────────────╮ Router address + uint32 permissionLessExecutionThresholdSeconds; // │ Waiting time before manual execution is enabled + uint32 maxTokenTransferGas; // │ Maximum amount of gas passed on to token `transfer` call + uint32 maxPoolReleaseOrMintGas; // ─────────────────╯ Maximum amount of gas passed on to token pool when calling releaseOrMint + address messageValidator; // Optional message validator to validate incoming messages (zero address = no validator) + address priceRegistry; // Price registry address on the local chain + } + + /// @notice a sequenceNumber interval + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct Interval { + uint64 min; // ───╮ Minimum sequence number, inclusive + uint64 max; // ───╯ Maximum sequence number, inclusive + } + + /// @dev Struct to hold a merkle root and an interval for a source chain so that an array of these can be passed in the CommitReport. + struct MerkleRoot { + uint64 sourceChainSelector; // Remote source chain selector that the Merkle Root is scoped to + Interval interval; // Report interval of the merkle root + bytes32 merkleRoot; // Merkle root covering the interval & source chain messages + } + + /// @notice Report that is committed by the observing DON at the committing phase + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct CommitReport { + Internal.PriceUpdates priceUpdates; // Collection of gas and price updates to commit + MerkleRoot[] merkleRoots; // Collection of merkle roots per source chain to commit + } + + /// @dev Struct to hold a merkle root for a source chain so that an array of these can be passed in the resetUblessedRoots function. + struct UnblessedRoot { + uint64 sourceChainSelector; // Remote source chain selector that the Merkle Root is scoped to + bytes32 merkleRoot; // Merkle root of a single remote source chain + } + + // STATIC CONFIG + string public constant override typeAndVersion = "EVM2EVMMultiOffRamp 1.6.0-dev"; + /// @dev ChainSelector of this chain + uint64 internal immutable i_chainSelector; + /// @dev The address of the RMN proxy + address internal immutable i_rmnProxy; + /// @dev The address of the token admin registry + address internal immutable i_tokenAdminRegistry; + /// @dev The address of the nonce manager + address internal immutable i_nonceManager; + + // DYNAMIC CONFIG + DynamicConfig internal s_dynamicConfig; + + /// @notice SourceConfig per chain + /// (forms lane configurations from sourceChainSelector => StaticConfig.chainSelector) + mapping(uint64 sourceChainSelector => SourceChainConfig sourceChainConfig) internal s_sourceChainConfigs; + + // STATE + /// @dev A mapping of sequence numbers (per source chain) to execution state using a bitmap with each execution + /// state only taking up 2 bits of the uint256, packing 128 states into a single slot. + /// Message state is tracked to ensure message can only be executed successfully once. + mapping(uint64 sourceChainSelector => mapping(uint64 seqNum => uint256 executionStateBitmap)) internal + s_executionStates; + + // sourceChainSelector => merkleRoot => timestamp when received + mapping(uint64 sourceChainSelector => mapping(bytes32 merkleRoot => uint256 timestamp)) internal s_roots; + /// @dev The sequence number of the last price update + uint64 private s_latestPriceSequenceNumber; + + constructor( + StaticConfig memory staticConfig, + DynamicConfig memory dynamicConfig, + SourceChainConfigArgs[] memory sourceChainConfigs + ) MultiOCR3Base() { + if ( + staticConfig.rmnProxy == address(0) || staticConfig.tokenAdminRegistry == address(0) + || staticConfig.nonceManager == address(0) + ) { + revert ZeroAddressNotAllowed(); + } + + if (staticConfig.chainSelector == 0) { + revert ZeroChainSelectorNotAllowed(); + } + + i_chainSelector = staticConfig.chainSelector; + i_rmnProxy = staticConfig.rmnProxy; + i_tokenAdminRegistry = staticConfig.tokenAdminRegistry; + i_nonceManager = staticConfig.nonceManager; + emit StaticConfigSet(staticConfig); + + _setDynamicConfig(dynamicConfig); + _applySourceChainConfigUpdates(sourceChainConfigs); + } + + // ================================================================ + // │ Messaging │ + // ================================================================ + + // The size of the execution state in bits + uint256 private constant MESSAGE_EXECUTION_STATE_BIT_WIDTH = 2; + // The mask for the execution state bits + uint256 private constant MESSAGE_EXECUTION_STATE_MASK = (1 << MESSAGE_EXECUTION_STATE_BIT_WIDTH) - 1; + + // ================================================================ + // │ Execution │ + // ================================================================ + + /// @notice Returns the current execution state of a message based on its sequenceNumber. + /// @param sourceChainSelector The source chain to get the execution state for + /// @param sequenceNumber The sequence number of the message to get the execution state for. + /// @return The current execution state of the message. + /// @dev we use the literal number 128 because using a constant increased gas usage. + function getExecutionState( + uint64 sourceChainSelector, + uint64 sequenceNumber + ) public view returns (Internal.MessageExecutionState) { + return Internal.MessageExecutionState( + ( + _getSequenceNumberBitmap(sourceChainSelector, sequenceNumber) + >> ((sequenceNumber % 128) * MESSAGE_EXECUTION_STATE_BIT_WIDTH) + ) & MESSAGE_EXECUTION_STATE_MASK + ); + } + + /// @notice Sets a new execution state for a given sequence number. It will overwrite any existing state. + /// @param sourceChainSelector The source chain to set the execution state for + /// @param sequenceNumber The sequence number for which the state will be saved. + /// @param newState The new value the state will be in after this function is called. + /// @dev we use the literal number 128 because using a constant increased gas usage. + function _setExecutionState( + uint64 sourceChainSelector, + uint64 sequenceNumber, + Internal.MessageExecutionState newState + ) internal { + uint256 offset = (sequenceNumber % 128) * MESSAGE_EXECUTION_STATE_BIT_WIDTH; + uint256 bitmap = _getSequenceNumberBitmap(sourceChainSelector, sequenceNumber); + // to unset any potential existing state we zero the bits of the section the state occupies, + // then we do an AND operation to blank out any existing state for the section. + bitmap &= ~(MESSAGE_EXECUTION_STATE_MASK << offset); + // Set the new state + bitmap |= uint256(newState) << offset; + + s_executionStates[sourceChainSelector][sequenceNumber / 128] = bitmap; + } + + /// @param sourceChainSelector remote source chain selector to get sequence number bitmap for + /// @param sequenceNumber sequence number to get bitmap for + /// @return bitmap Bitmap of the given sequence number for the provided source chain selector. One bitmap represents 128 sequence numbers + function _getSequenceNumberBitmap( + uint64 sourceChainSelector, + uint64 sequenceNumber + ) internal view returns (uint256 bitmap) { + return s_executionStates[sourceChainSelector][sequenceNumber / 128]; + } + + /// @notice Manually executes a set of reports. + /// @param reports Internal.ExecutionReportSingleChain[] - list of reports to execute + /// @param gasLimitOverrides New gasLimit for each message per report + // The outer array represents each report, inner array represents each message in the report. + // i.e. gasLimitOverrides[report1][report1Message1] -> access message1 from report1 + /// @dev We permit gas limit overrides so that users may manually execute messages which failed due to + /// insufficient gas provided. + /// The reports do not have to contain all the messages (they can be omitted). Multiple reports can be passed in simultaneously. + function manuallyExecute( + Internal.ExecutionReportSingleChain[] memory reports, + uint256[][] memory gasLimitOverrides + ) external { + // We do this here because the other _execute path is already covered by MultiOCR3Base. + _whenChainNotForked(); + + uint256 numReports = reports.length; + if (numReports != gasLimitOverrides.length) revert ManualExecutionGasLimitMismatch(); + + for (uint256 reportIndex = 0; reportIndex < numReports; ++reportIndex) { + Internal.ExecutionReportSingleChain memory report = reports[reportIndex]; + + uint256 numMsgs = report.messages.length; + uint256[] memory msgGasLimitOverrides = gasLimitOverrides[reportIndex]; + if (numMsgs != msgGasLimitOverrides.length) revert ManualExecutionGasLimitMismatch(); + + for (uint256 msgIndex = 0; msgIndex < numMsgs; ++msgIndex) { + uint256 newLimit = msgGasLimitOverrides[msgIndex]; + // Checks to ensure message cannot be executed with less gas than specified. + if (newLimit != 0) { + if (newLimit < report.messages[msgIndex].gasLimit) { + revert InvalidManualExecutionGasLimit(report.sourceChainSelector, msgIndex, newLimit); + } + } + } + } + + _batchExecute(reports, gasLimitOverrides); + } + + /// @notice Transmit function for execution reports. The function takes no signatures, + /// and expects the exec plugin type to be configured with no signatures. + /// @param report serialized execution report + function execute(bytes32[3] calldata reportContext, bytes calldata report) external { + _batchExecute(abi.decode(report, (Internal.ExecutionReportSingleChain[])), new uint256[][](0)); + + bytes32[] memory emptySigs = new bytes32[](0); + _transmit(uint8(Internal.OCRPluginType.Execution), reportContext, report, emptySigs, emptySigs, bytes32("")); + } + + /// @notice Batch executes a set of reports, each report matching one single source chain + /// @param reports Set of execution reports (one per chain) containing the messages and proofs + /// @param manualExecGasLimits An array of gas limits to use for manual execution + // The outer array represents each report, inner array represents each message in the report. + // i.e. gasLimitOverrides[report1][report1Message1] -> access message1 from report1 + /// @dev The manualExecGasLimits array should either be empty, or match the length of the reports array + /// @dev If called from manual execution, each inner array's length has to match the number of messages. + function _batchExecute( + Internal.ExecutionReportSingleChain[] memory reports, + uint256[][] memory manualExecGasLimits + ) internal { + if (reports.length == 0) revert EmptyReport(); + + bool areManualGasLimitsEmpty = manualExecGasLimits.length == 0; + // Cache array for gas savings in the loop's condition + uint256[] memory emptyGasLimits = new uint256[](0); + + for (uint256 i = 0; i < reports.length; ++i) { + _executeSingleReport(reports[i], areManualGasLimitsEmpty ? emptyGasLimits : manualExecGasLimits[i]); + } + } + + /// @notice Executes a report, executing each message in order. + /// @param report The execution report containing the messages and proofs. + /// @param manualExecGasLimits An array of gas limits to use for manual execution. + /// @dev If called from the DON, this array is always empty. + /// @dev If called from manual execution, this array is always same length as messages. + function _executeSingleReport( + Internal.ExecutionReportSingleChain memory report, + uint256[] memory manualExecGasLimits + ) internal { + uint64 sourceChainSelector = report.sourceChainSelector; + _whenNotCursed(sourceChainSelector); + + SourceChainConfig storage sourceChainConfig = _getEnabledSourceChainConfig(sourceChainSelector); + + uint256 numMsgs = report.messages.length; + if (numMsgs == 0) revert EmptyReport(); + if (numMsgs != report.offchainTokenData.length) revert UnexpectedTokenData(); + + bytes32[] memory hashedLeaves = new bytes32[](numMsgs); + + for (uint256 i = 0; i < numMsgs; ++i) { + Internal.Any2EVMRampMessage memory message = report.messages[i]; + + // Commits do not verify the destChainSelector in the message, since only the root is committed, + // so we have to check it explicitly + if (message.header.destChainSelector != i_chainSelector) { + revert InvalidMessageDestChainSelector(message.header.destChainSelector); + } + + // We do this hash here instead of in _verifyMessages to avoid two separate loops + // over the same data, which increases gas cost. + // Hashing all of the message fields ensures that the message being executed is correct and not tampered with. + // Including the known OnRamp ensures that the message originates from the correct on ramp version + hashedLeaves[i] = Internal._hash(message, sourceChainConfig.onRamp); + } + + // SECURITY CRITICAL CHECK + // NOTE: This check also verifies that all messages match the report's sourceChainSelector + uint256 timestampCommitted = _verify(sourceChainSelector, hashedLeaves, report.proofs, report.proofFlagBits); + if (timestampCommitted == 0) revert RootNotCommitted(sourceChainSelector); + + // Execute messages + bool manualExecution = manualExecGasLimits.length != 0; + for (uint256 i = 0; i < numMsgs; ++i) { + Internal.Any2EVMRampMessage memory message = report.messages[i]; + + Internal.MessageExecutionState originalState = + getExecutionState(sourceChainSelector, message.header.sequenceNumber); + if (originalState == Internal.MessageExecutionState.SUCCESS) { + // If the message has already been executed, we skip it. We want to not revert on race conditions between + // executing parties. This will allow us to open up manual exec while also attempting with the DON, without + // reverting an entire DON batch when a user manually executes while the tx is inflight. + emit SkippedAlreadyExecutedMessage(sourceChainSelector, message.header.sequenceNumber); + continue; + } + // Two valid cases here, we either have never touched this message before, or we tried to execute + // and failed. This check protects against reentry and re-execution because the other state is + // IN_PROGRESS which should not be allowed to execute. + if ( + !( + originalState == Internal.MessageExecutionState.UNTOUCHED + || originalState == Internal.MessageExecutionState.FAILURE + ) + ) revert AlreadyExecuted(sourceChainSelector, message.header.sequenceNumber); + + if (manualExecution) { + bool isOldCommitReport = + (block.timestamp - timestampCommitted) > s_dynamicConfig.permissionLessExecutionThresholdSeconds; + // Manually execution is fine if we previously failed or if the commit report is just too old + // Acceptable state transitions: FAILURE->SUCCESS, UNTOUCHED->SUCCESS, FAILURE->FAILURE + if (!(isOldCommitReport || originalState == Internal.MessageExecutionState.FAILURE)) { + revert ManualExecutionNotYetEnabled(sourceChainSelector); + } + + // Manual execution gas limit can override gas limit specified in the message. Value of 0 indicates no override. + if (manualExecGasLimits[i] != 0) { + message.gasLimit = manualExecGasLimits[i]; + } + } else { + // DON can only execute a message once + // Acceptable state transitions: UNTOUCHED->SUCCESS, UNTOUCHED->FAILURE + if (originalState != Internal.MessageExecutionState.UNTOUCHED) { + revert AlreadyAttempted(sourceChainSelector, message.header.sequenceNumber); + } + } + + // Nonce changes per state transition (these only apply for ordered messages): + // UNTOUCHED -> FAILURE nonce bump + // UNTOUCHED -> SUCCESS nonce bump + // FAILURE -> FAILURE no nonce bump + // FAILURE -> SUCCESS no nonce bump + // UNTOUCHED messages MUST be executed in order always + if (message.header.nonce != 0) { + if (originalState == Internal.MessageExecutionState.UNTOUCHED) { + // If a nonce is not incremented, that means it was skipped, and we can ignore the message + if ( + !INonceManager(i_nonceManager).incrementInboundNonce( + sourceChainSelector, message.header.nonce, message.sender + ) + ) continue; + } + } + + // Although we expect only valid messages will be committed, we check again + // when executing as a defense in depth measure. + bytes[] memory offchainTokenData = report.offchainTokenData[i]; + if (message.tokenAmounts.length != offchainTokenData.length) { + revert TokenDataMismatch(sourceChainSelector, message.header.sequenceNumber); + } + + _setExecutionState(sourceChainSelector, message.header.sequenceNumber, Internal.MessageExecutionState.IN_PROGRESS); + + (Internal.MessageExecutionState newState, bytes memory returnData) = _trialExecute(message, offchainTokenData); + _setExecutionState(sourceChainSelector, message.header.sequenceNumber, newState); + + // Since it's hard to estimate whether manual execution will succeed, we + // revert the entire transaction if it fails. This will show the user if + // their manual exec will fail before they submit it. + if (manualExecution) { + if (newState == Internal.MessageExecutionState.FAILURE) { + if (originalState != Internal.MessageExecutionState.UNTOUCHED) { + // If manual execution fails, we revert the entire transaction, unless the originalState is UNTOUCHED as we + // would still be making progress by changing the state from UNTOUCHED to FAILURE. + revert ExecutionError(message.header.messageId, returnData); + } + } + } + + // The only valid prior states are UNTOUCHED and FAILURE (checked above) + // The only valid post states are FAILURE and SUCCESS (checked below) + if (newState != Internal.MessageExecutionState.SUCCESS) { + if (newState != Internal.MessageExecutionState.FAILURE) { + revert InvalidNewState(sourceChainSelector, message.header.sequenceNumber, newState); + } + } + + emit ExecutionStateChanged( + sourceChainSelector, message.header.sequenceNumber, message.header.messageId, newState, returnData + ); + } + } + + /// @notice Try executing a message. + /// @param message Internal.Any2EVMRampMessage memory message. + /// @param offchainTokenData Data provided by the DON for token transfers. + /// @return the new state of the message, being either SUCCESS or FAILURE. + /// @return revert data in bytes if CCIP receiver reverted during execution. + function _trialExecute( + Internal.Any2EVMRampMessage memory message, + bytes[] memory offchainTokenData + ) internal returns (Internal.MessageExecutionState, bytes memory) { + try this.executeSingleMessage(message, offchainTokenData) {} + catch (bytes memory err) { + // return the message execution state as FAILURE and the revert data + // Max length of revert data is Router.MAX_RET_BYTES, max length of err is 4 + Router.MAX_RET_BYTES + return (Internal.MessageExecutionState.FAILURE, err); + } + // If message execution succeeded, no CCIP receiver return data is expected, return with empty bytes. + return (Internal.MessageExecutionState.SUCCESS, ""); + } + + /// @notice Execute a single message. + /// @param message The message that will be executed. + /// @param offchainTokenData Token transfer data to be passed to TokenPool. + /// @dev We make this external and callable by the contract itself, in order to try/catch + /// its execution and enforce atomicity among successful message processing and token transfer. + /// @dev We use ERC-165 to check for the ccipReceive interface to permit sending tokens to contracts + /// (for example smart contract wallets) without an associated message. + function executeSingleMessage(Internal.Any2EVMRampMessage memory message, bytes[] memory offchainTokenData) external { + if (msg.sender != address(this)) revert CanOnlySelfCall(); + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](0); + if (message.tokenAmounts.length > 0) { + destTokenAmounts = _releaseOrMintTokens( + message.tokenAmounts, message.sender, message.receiver, message.header.sourceChainSelector, offchainTokenData + ); + } + + Client.Any2EVMMessage memory any2EvmMessage = Client.Any2EVMMessage({ + messageId: message.header.messageId, + sourceChainSelector: message.header.sourceChainSelector, + sender: abi.encode(message.sender), + data: message.data, + destTokenAmounts: destTokenAmounts + }); + + address messageValidator = s_dynamicConfig.messageValidator; + if (messageValidator != address(0)) { + try IMessageInterceptor(messageValidator).onInboundMessage(any2EvmMessage) {} + catch (bytes memory err) { + revert IMessageInterceptor.MessageValidationError(err); + } + } + + // There are three cases in which we skip calling the receiver: + // 1. If the message data is empty AND the gas limit is 0. + // This indicates a message that only transfers tokens. It is valid to only send tokens to a contract + // that supports the IAny2EVMMessageReceiver interface, but without this first check we would call the + // receiver without any gas, which would revert the transaction. + // 2. If the receiver is not a contract. + // 3. If the receiver is a contract but it does not support the IAny2EVMMessageReceiver interface. + // + // The ordering of these checks is important, as the first check is the cheapest to execute. + if ( + (message.data.length == 0 && message.gasLimit == 0) || message.receiver.code.length == 0 + || !message.receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId) + ) return; + + (bool success, bytes memory returnData,) = IRouter(s_dynamicConfig.router).routeMessage( + any2EvmMessage, Internal.GAS_FOR_CALL_EXACT_CHECK, message.gasLimit, message.receiver + ); + // If CCIP receiver execution is not successful, revert the call including token transfers + if (!success) revert ReceiverError(returnData); + } + + // ================================================================ + // │ Commit │ + // ================================================================ + + /// @notice Transmit function for commit reports. The function requires signatures, + /// and expects the commit plugin type to be configured with signatures. + /// @param report serialized commit report + /// @dev A commitReport can have two distinct parts (batched together to amortize the cost of checking sigs): + /// 1. Price updates + /// 2. A batch of merkle root and sequence number intervals (per-source) + /// Both have their own, separate, staleness checks, with price updates using the epoch and round + /// number of the latest price update. The merkle root checks for staleness based on the seqNums. + /// They need to be separate because a price report for round t+2 might be included before a report + /// containing a merkle root for round t+1. This merkle root report for round t+1 is still valid + /// and should not be rejected. When a report with a stale root but valid price updates is submitted, + /// we are OK to revert to preserve the invariant that we always revert on invalid sequence number ranges. + /// If that happens, prices will be updates in later rounds. + function commit( + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external { + CommitReport memory commitReport = abi.decode(report, (CommitReport)); + + // Check if the report contains price updates + if (commitReport.priceUpdates.tokenPriceUpdates.length > 0 || commitReport.priceUpdates.gasPriceUpdates.length > 0) + { + uint64 sequenceNumber = uint64(uint256(reportContext[1])); + + // Check for price staleness based on the epoch and round + if (s_latestPriceSequenceNumber < sequenceNumber) { + // If prices are not stale, update the latest epoch and round + s_latestPriceSequenceNumber = sequenceNumber; + // And update the prices in the price registry + IPriceRegistry(s_dynamicConfig.priceRegistry).updatePrices(commitReport.priceUpdates); + } else { + // If prices are stale and the report doesn't contain a root, this report + // does not have any valid information and we revert. + // If it does contain a merkle root, continue to the root checking section. + if (commitReport.merkleRoots.length == 0) revert StaleCommitReport(); + } + } + + for (uint256 i = 0; i < commitReport.merkleRoots.length; ++i) { + MerkleRoot memory root = commitReport.merkleRoots[i]; + uint64 sourceChainSelector = root.sourceChainSelector; + + _whenNotCursed(sourceChainSelector); + SourceChainConfig storage sourceChainConfig = _getEnabledSourceChainConfig(sourceChainSelector); + + // If we reached this section, the report should contain a valid root + if (sourceChainConfig.minSeqNr != root.interval.min || root.interval.min > root.interval.max) { + revert InvalidInterval(root.sourceChainSelector, root.interval); + } + + // TODO: confirm how RMN offchain blessing impacts commit report + bytes32 merkleRoot = root.merkleRoot; + if (merkleRoot == bytes32(0)) revert InvalidRoot(); + // Disallow duplicate roots as that would reset the timestamp and + // delay potential manual execution. + if (s_roots[root.sourceChainSelector][merkleRoot] != 0) { + revert RootAlreadyCommitted(root.sourceChainSelector, merkleRoot); + } + + sourceChainConfig.minSeqNr = root.interval.max + 1; + s_roots[root.sourceChainSelector][merkleRoot] = block.timestamp; + } + + emit CommitReportAccepted(commitReport); + + _transmit(uint8(Internal.OCRPluginType.Commit), reportContext, report, rs, ss, rawVs); + } + + /// @notice Returns the sequence number of the last price update. + /// @return the latest price update sequence number. + function getLatestPriceSequenceNumber() public view returns (uint64) { + return s_latestPriceSequenceNumber; + } + + /// @notice Returns the timestamp of a potentially previously committed merkle root. + /// If the root was never committed 0 will be returned. + /// @param sourceChainSelector The source chain selector. + /// @param root The merkle root to check the commit status for. + /// @return the timestamp of the committed root or zero in the case that it was never + /// committed. + function getMerkleRoot(uint64 sourceChainSelector, bytes32 root) external view returns (uint256) { + return s_roots[sourceChainSelector][root]; + } + + /// @notice Returns if a root is blessed or not. + /// @param root The merkle root to check the blessing status for. + /// @return whether the root is blessed or not. + function isBlessed(bytes32 root) public view returns (bool) { + // TODO: update RMN to also consider the source chain selector for blessing + return IRMN(i_rmnProxy).isBlessed(IRMN.TaggedRoot({commitStore: address(this), root: root})); + } + + /// @notice Used by the owner in case an invalid sequence of roots has been + /// posted and needs to be removed. The interval in the report is trusted. + /// @param rootToReset The roots that will be reset. This function will only + /// reset roots that are not blessed. + function resetUnblessedRoots(UnblessedRoot[] calldata rootToReset) external onlyOwner { + for (uint256 i = 0; i < rootToReset.length; ++i) { + UnblessedRoot memory root = rootToReset[i]; + if (!isBlessed(root.merkleRoot)) { + delete s_roots[root.sourceChainSelector][root.merkleRoot]; + emit RootRemoved(root.merkleRoot); + } + } + } + + /// @notice Returns timestamp of when root was accepted or 0 if verification fails. + /// @dev This method uses a merkle tree within a merkle tree, with the hashedLeaves, + /// proofs and proofFlagBits being used to get the root of the inner tree. + /// This root is then used as the singular leaf of the outer tree. + function _verify( + uint64 sourceChainSelector, + bytes32[] memory hashedLeaves, + bytes32[] memory proofs, + uint256 proofFlagBits + ) internal view virtual returns (uint256 timestamp) { + bytes32 root = MerkleMultiProof.merkleRoot(hashedLeaves, proofs, proofFlagBits); + // Only return non-zero if present and blessed. + if (!isBlessed(root)) { + return 0; + } + return s_roots[sourceChainSelector][root]; + } + + /// @inheritdoc MultiOCR3Base + function _afterOCR3ConfigSet(uint8 ocrPluginType) internal override { + if (ocrPluginType == uint8(Internal.OCRPluginType.Commit)) { + // When the OCR config changes, we reset the sequence number + // since it is scoped per config digest. + // Note that s_minSeqNr/roots do not need to be reset as the roots persist + // across reconfigurations and are de-duplicated separately. + s_latestPriceSequenceNumber = 0; + } + } + + // ================================================================ + // │ Config │ + // ================================================================ + + /// @notice Returns the static config. + /// @dev This function will always return the same struct as the contents is static and can never change. + /// RMN depends on this function, if changing, please notify the RMN maintainers. + function getStaticConfig() external view returns (StaticConfig memory) { + return StaticConfig({ + chainSelector: i_chainSelector, + rmnProxy: i_rmnProxy, + tokenAdminRegistry: i_tokenAdminRegistry, + nonceManager: i_nonceManager + }); + } + + /// @notice Returns the current dynamic config. + /// @return The current config. + function getDynamicConfig() external view returns (DynamicConfig memory) { + return s_dynamicConfig; + } + + /// @notice Returns the source chain config for the provided source chain selector + /// @param sourceChainSelector chain to retrieve configuration for + /// @return SourceChainConfig config for the source chain + function getSourceChainConfig(uint64 sourceChainSelector) external view returns (SourceChainConfig memory) { + return s_sourceChainConfigs[sourceChainSelector]; + } + + /// @notice Updates source configs + /// @param sourceChainConfigUpdates Source chain configs + function applySourceChainConfigUpdates(SourceChainConfigArgs[] memory sourceChainConfigUpdates) external onlyOwner { + _applySourceChainConfigUpdates(sourceChainConfigUpdates); + } + + /// @notice Updates source configs + /// @param sourceChainConfigUpdates Source chain configs + function _applySourceChainConfigUpdates(SourceChainConfigArgs[] memory sourceChainConfigUpdates) internal { + for (uint256 i = 0; i < sourceChainConfigUpdates.length; ++i) { + SourceChainConfigArgs memory sourceConfigUpdate = sourceChainConfigUpdates[i]; + uint64 sourceChainSelector = sourceConfigUpdate.sourceChainSelector; + + if (sourceChainSelector == 0) { + revert ZeroChainSelectorNotAllowed(); + } + + SourceChainConfig storage currentConfig = s_sourceChainConfigs[sourceChainSelector]; + bytes memory currentOnRamp = currentConfig.onRamp; + bytes memory newOnRamp = sourceConfigUpdate.onRamp; + + // OnRamp can never be zero - if it is, then the source chain has been added for the first time + if (currentOnRamp.length == 0) { + if (newOnRamp.length == 0) { + revert ZeroAddressNotAllowed(); + } + + currentConfig.onRamp = newOnRamp; + currentConfig.minSeqNr = 1; + emit SourceChainSelectorAdded(sourceChainSelector); + } else if (keccak256(currentOnRamp) != keccak256(newOnRamp)) { + revert InvalidStaticConfig(sourceChainSelector); + } + + // The only dynamic config is the isEnabled flag + currentConfig.isEnabled = sourceConfigUpdate.isEnabled; + emit SourceChainConfigSet(sourceChainSelector, currentConfig); + } + } + + /// @notice Sets the dynamic config. + /// @param dynamicConfig The new dynamic config. + function setDynamicConfig(DynamicConfig memory dynamicConfig) external onlyOwner { + _setDynamicConfig(dynamicConfig); + } + + /// @notice Sets the dynamic config. + /// @param dynamicConfig The dynamic config. + function _setDynamicConfig(DynamicConfig memory dynamicConfig) internal { + if (dynamicConfig.priceRegistry == address(0) || dynamicConfig.router == address(0)) { + revert ZeroAddressNotAllowed(); + } + + s_dynamicConfig = dynamicConfig; + + emit DynamicConfigSet(dynamicConfig); + } + + /// @notice Returns a source chain config with a check that the config is enabled + /// @param sourceChainSelector Source chain selector to check for cursing + /// @return sourceChainConfig Source chain config + function _getEnabledSourceChainConfig(uint64 sourceChainSelector) internal view returns (SourceChainConfig storage) { + SourceChainConfig storage sourceChainConfig = s_sourceChainConfigs[sourceChainSelector]; + if (!sourceChainConfig.isEnabled) { + revert SourceChainNotEnabled(sourceChainSelector); + } + + return sourceChainConfig; + } + + // ================================================================ + // │ Tokens and pools │ + // ================================================================ + + /// @notice Uses a pool to release or mint a token to a receiver address in two steps. First, the pool is called + /// to release the tokens to the offRamp, then the offRamp calls the token contract to transfer the tokens to the + /// receiver. This is done to ensure the exact number of tokens, the pool claims to release are actually transferred. + /// @dev The local token address is validated through the TokenAdminRegistry. If, due to some misconfiguration, the + /// token is unknown to the registry, the offRamp will revert. The tx, and the tokens, can be retrieved by + /// registering the token on this chain, and re-trying the msg. + /// @param sourceTokenAmount Amount and source data of the token to be released/minted. + /// @param originalSender The message sender on the source chain. + /// @param receiver The address that will receive the tokens. + /// @param sourceChainSelector The remote source chain selector + /// @param offchainTokenData Data fetched offchain by the DON. + /// @return destTokenAmount local token address with amount + function _releaseOrMintSingleToken( + Internal.RampTokenAmount memory sourceTokenAmount, + bytes memory originalSender, + address receiver, + uint64 sourceChainSelector, + bytes memory offchainTokenData + ) internal returns (Client.EVMTokenAmount memory destTokenAmount) { + // We need to safely decode the token address from the sourceTokenData, as it could be wrong, + // in which case it doesn't have to be a valid EVM address. + address localToken = Internal._validateEVMAddress(sourceTokenAmount.destTokenAddress); + // We check with the token admin registry if the token has a pool on this chain. + address localPoolAddress = ITokenAdminRegistry(i_tokenAdminRegistry).getPool(localToken); + // This will call the supportsInterface through the ERC165Checker, and not directly on the pool address. + // This is done to prevent a pool from reverting the entire transaction if it doesn't support the interface. + // The call gets a max or 30k gas per instance, of which there are three. This means gas estimations should + // account for 90k gas overhead due to the interface check. + if (localPoolAddress == address(0) || !localPoolAddress.supportsInterface(Pool.CCIP_POOL_V1)) { + revert NotACompatiblePool(localPoolAddress); + } + + // We determined that the pool address is a valid EVM address, but that does not mean the code at this + // address is a (compatible) pool contract. _callWithExactGasSafeReturnData will check if the location + // contains a contract. If it doesn't it reverts with a known error, which we catch gracefully. + // We call the pool with exact gas to increase resistance against malicious tokens or token pools. + // We protects against return data bombs by capping the return data size at MAX_RET_BYTES. + (bool success, bytes memory returnData,) = CallWithExactGas._callWithExactGasSafeReturnData( + abi.encodeCall( + IPoolV1.releaseOrMint, + Pool.ReleaseOrMintInV1({ + originalSender: originalSender, + receiver: receiver, + amount: sourceTokenAmount.amount, + localToken: localToken, + remoteChainSelector: sourceChainSelector, + sourcePoolAddress: sourceTokenAmount.sourcePoolAddress, + sourcePoolData: sourceTokenAmount.extraData, + offchainTokenData: offchainTokenData + }) + ), + localPoolAddress, + s_dynamicConfig.maxPoolReleaseOrMintGas, + Internal.GAS_FOR_CALL_EXACT_CHECK, + Internal.MAX_RET_BYTES + ); + + // wrap and rethrow the error so we can catch it lower in the stack + if (!success) revert TokenHandlingError(returnData); + + // If the call was successful, the returnData should be the local token address. + if (returnData.length != Pool.CCIP_POOL_V1_RET_BYTES) { + revert InvalidDataLength(Pool.CCIP_POOL_V1_RET_BYTES, returnData.length); + } + uint256 localAmount = abi.decode(returnData, (uint256)); + // Since token pools send the tokens to the msg.sender, which is this offRamp, we need to + // transfer them to the final receiver. We use the _callWithExactGasSafeReturnData function because + // the token contracts are not considered trusted. + (success, returnData,) = CallWithExactGas._callWithExactGasSafeReturnData( + abi.encodeCall(IERC20.transfer, (receiver, localAmount)), + localToken, + s_dynamicConfig.maxTokenTransferGas, + Internal.GAS_FOR_CALL_EXACT_CHECK, + Internal.MAX_RET_BYTES + ); + + if (!success) revert TokenHandlingError(returnData); + + return Client.EVMTokenAmount({token: localToken, amount: localAmount}); + } + + /// @notice Uses pools to release or mint a number of different tokens to a receiver address. + /// @param sourceTokenAmounts List of token amounts with source data of the tokens to be released/minted. + /// @param originalSender The message sender on the source chain. + /// @param receiver The address that will receive the tokens. + /// @param sourceChainSelector The remote source chain selector + /// @param offchainTokenData Array of token data fetched offchain by the DON. + /// @return destTokenAmounts local token addresses with amounts + /// @dev This function wrappes the token pool call in a try catch block to gracefully handle + /// any non-rate limiting errors that may occur. If we encounter a rate limiting related error + /// we bubble it up. If we encounter a non-rate limiting error we wrap it in a TokenHandlingError. + function _releaseOrMintTokens( + Internal.RampTokenAmount[] memory sourceTokenAmounts, + bytes memory originalSender, + address receiver, + uint64 sourceChainSelector, + bytes[] memory offchainTokenData + ) internal returns (Client.EVMTokenAmount[] memory destTokenAmounts) { + destTokenAmounts = new Client.EVMTokenAmount[](sourceTokenAmounts.length); + for (uint256 i = 0; i < sourceTokenAmounts.length; ++i) { + destTokenAmounts[i] = _releaseOrMintSingleToken( + sourceTokenAmounts[i], originalSender, receiver, sourceChainSelector, offchainTokenData[i] + ); + } + + return destTokenAmounts; + } + + // ================================================================ + // │ Access and RMN │ + // ================================================================ + + /// @notice Reverts as this contract should not access CCIP messages + function ccipReceive(Client.Any2EVMMessage calldata) external pure { + // solhint-disable-next-line + revert(); + } + + /// @notice Validates that the source chain -> this chain lane, and reverts if it is cursed + /// @param sourceChainSelector Source chain selector to check for cursing + function _whenNotCursed(uint64 sourceChainSelector) internal view { + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(sourceChainSelector)))) { + revert CursedByRMN(sourceChainSelector); + } + } +} diff --git a/contracts/src/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol b/contracts/src/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol new file mode 100644 index 00000000000..1aec436ef8c --- /dev/null +++ b/contracts/src/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol @@ -0,0 +1,721 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IAny2EVMMessageReceiver} from "../interfaces/IAny2EVMMessageReceiver.sol"; +import {IAny2EVMOffRamp} from "../interfaces/IAny2EVMOffRamp.sol"; +import {ICommitStore} from "../interfaces/ICommitStore.sol"; +import {IPoolV1} from "../interfaces/IPool.sol"; +import {IPriceRegistry} from "../interfaces/IPriceRegistry.sol"; +import {IRMN} from "../interfaces/IRMN.sol"; +import {IRouter} from "../interfaces/IRouter.sol"; +import {ITokenAdminRegistry} from "../interfaces/ITokenAdminRegistry.sol"; + +import {CallWithExactGas} from "../../shared/call/CallWithExactGas.sol"; +import {EnumerableMapAddresses} from "../../shared/enumerable/EnumerableMapAddresses.sol"; +import {AggregateRateLimiter} from "../AggregateRateLimiter.sol"; +import {Client} from "../libraries/Client.sol"; +import {Internal} from "../libraries/Internal.sol"; +import {Pool} from "../libraries/Pool.sol"; +import {RateLimiter} from "../libraries/RateLimiter.sol"; +import {OCR2BaseNoChecks} from "../ocr/OCR2BaseNoChecks.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {ERC165Checker} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol"; + +/// @notice EVM2EVMOffRamp enables OCR networks to execute multiple messages +/// in an OffRamp in a single transaction. +/// @dev The EVM2EVMOnRamp, CommitStore and EVM2EVMOffRamp form an xchain upgradeable unit. Any change to one of them +/// results an onchain upgrade of all 3. +/// @dev OCR2BaseNoChecks is used to save gas, signatures are not required as the offramp can only execute +/// messages which are committed in the commitStore. We still make use of OCR2 as an executor whitelist +/// and turn-taking mechanism. +contract EVM2EVMOffRamp is IAny2EVMOffRamp, AggregateRateLimiter, ITypeAndVersion, OCR2BaseNoChecks { + using ERC165Checker for address; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + error AlreadyAttempted(uint64 sequenceNumber); + error AlreadyExecuted(uint64 sequenceNumber); + error ZeroAddressNotAllowed(); + error CommitStoreAlreadyInUse(); + error ExecutionError(bytes err); + error InvalidSourceChain(uint64 sourceChainSelector); + error MessageTooLarge(uint256 maxSize, uint256 actualSize); + error TokenDataMismatch(uint64 sequenceNumber); + error UnexpectedTokenData(); + error UnsupportedNumberOfTokens(uint64 sequenceNumber); + error ManualExecutionNotYetEnabled(); + error ManualExecutionGasLimitMismatch(); + error InvalidManualExecutionGasLimit(uint256 index, uint256 newLimit); + error RootNotCommitted(); + error CanOnlySelfCall(); + error ReceiverError(bytes err); + error TokenHandlingError(bytes err); + error EmptyReport(); + error CursedByRMN(); + error InvalidMessageId(); + error NotACompatiblePool(address notPool); + error InvalidDataLength(uint256 expected, uint256 got); + error InvalidNewState(uint64 sequenceNumber, Internal.MessageExecutionState newState); + + /// @dev Atlas depends on this event, if changing, please notify Atlas. + event ConfigSet(StaticConfig staticConfig, DynamicConfig dynamicConfig); + event SkippedIncorrectNonce(uint64 indexed nonce, address indexed sender); + event SkippedSenderWithPreviousRampMessageInflight(uint64 indexed nonce, address indexed sender); + /// @dev RMN depends on this event, if changing, please notify the RMN maintainers. + event ExecutionStateChanged( + uint64 indexed sequenceNumber, bytes32 indexed messageId, Internal.MessageExecutionState state, bytes returnData + ); + event TokenAggregateRateLimitAdded(address sourceToken, address destToken); + event TokenAggregateRateLimitRemoved(address sourceToken, address destToken); + event SkippedAlreadyExecutedMessage(uint64 indexed sequenceNumber); + + /// @notice Static offRamp config + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + //solhint-disable gas-struct-packing + struct StaticConfig { + address commitStore; // ────────╮ CommitStore address on the destination chain + uint64 chainSelector; // ───────╯ Destination chainSelector + uint64 sourceChainSelector; // ─╮ Source chainSelector + address onRamp; // ─────────────╯ OnRamp address on the source chain + address prevOffRamp; // Address of previous-version OffRamp + address rmnProxy; // RMN proxy address + address tokenAdminRegistry; // Token admin registry address + } + + /// @notice Dynamic offRamp config + /// @dev since OffRampConfig is part of OffRampConfigChanged event, if changing it, we should update the ABI on Atlas + struct DynamicConfig { + uint32 permissionLessExecutionThresholdSeconds; // ─╮ Waiting time before manual execution is enabled + uint32 maxDataBytes; // │ Maximum payload data size in bytes + uint16 maxNumberOfTokensPerMsg; // │ Maximum number of ERC20 token transfers that can be included per message + address router; // ─────────────────────────────────╯ Router address + address priceRegistry; // ──────────╮ Price registry address + uint32 maxPoolReleaseOrMintGas; // │ Maximum amount of gas passed on to token pool `releaseOrMint` call + uint32 maxTokenTransferGas; // ─────╯ Maximum amount of gas passed on to token `transfer` call + } + + /// @notice RateLimitToken struct containing both the source and destination token addresses + struct RateLimitToken { + address sourceToken; + address destToken; + } + + // STATIC CONFIG + string public constant override typeAndVersion = "EVM2EVMOffRamp 1.5.0-dev"; + + /// @dev Commit store address on the destination chain + address internal immutable i_commitStore; + /// @dev ChainSelector of the source chain + uint64 internal immutable i_sourceChainSelector; + /// @dev ChainSelector of this chain + uint64 internal immutable i_chainSelector; + /// @dev OnRamp address on the source chain + address internal immutable i_onRamp; + /// @dev metadataHash is a lane-specific prefix for a message hash preimage which ensures global uniqueness. + /// Ensures that 2 identical messages sent to 2 different lanes will have a distinct hash. + /// Must match the metadataHash used in computing leaf hashes offchain for the root committed in + /// the commitStore and i_metadataHash in the onRamp. + bytes32 internal immutable i_metadataHash; + /// @dev The address of previous-version OffRamp for this lane. + /// Used to be able to provide sequencing continuity during a zero downtime upgrade. + address internal immutable i_prevOffRamp; + /// @dev The address of the RMN proxy + address internal immutable i_rmnProxy; + /// @dev The address of the token admin registry + address internal immutable i_tokenAdminRegistry; + + // DYNAMIC CONFIG + DynamicConfig internal s_dynamicConfig; + /// @dev Tokens that should be included in Aggregate Rate Limiting + /// An (address => address) map is used for backwards compatability of offchain code + EnumerableMapAddresses.AddressToAddressMap internal s_rateLimitedTokensDestToSource; + + // STATE + /// @dev The expected nonce for a given sender. + /// Corresponds to s_senderNonce in the OnRamp, used to enforce that messages are + /// executed in the same order they are sent (assuming they are DON). Note that re-execution + /// of FAILED messages however, can be out of order. + mapping(address sender => uint64 nonce) internal s_senderNonce; + /// @dev A mapping of sequence numbers to execution state using a bitmap with each execution + /// state only taking up 2 bits of the uint256, packing 128 states into a single slot. + /// Message state is tracked to ensure message can only be executed successfully once. + mapping(uint64 seqNum => uint256 executionStateBitmap) internal s_executionStates; + + constructor( + StaticConfig memory staticConfig, + RateLimiter.Config memory rateLimiterConfig + ) OCR2BaseNoChecks() AggregateRateLimiter(rateLimiterConfig) { + if ( + staticConfig.onRamp == address(0) || staticConfig.commitStore == address(0) + || staticConfig.tokenAdminRegistry == address(0) + ) revert ZeroAddressNotAllowed(); + // Ensures we can never deploy a new offRamp that points to a commitStore that + // already has roots committed. + if (ICommitStore(staticConfig.commitStore).getExpectedNextSequenceNumber() != 1) revert CommitStoreAlreadyInUse(); + + i_commitStore = staticConfig.commitStore; + i_sourceChainSelector = staticConfig.sourceChainSelector; + i_chainSelector = staticConfig.chainSelector; + i_onRamp = staticConfig.onRamp; + i_prevOffRamp = staticConfig.prevOffRamp; + i_rmnProxy = staticConfig.rmnProxy; + i_tokenAdminRegistry = staticConfig.tokenAdminRegistry; + + i_metadataHash = _metadataHash(Internal.EVM_2_EVM_MESSAGE_HASH); + } + + // ================================================================ + // │ Messaging │ + // ================================================================ + + // The size of the execution state in bits + uint256 private constant MESSAGE_EXECUTION_STATE_BIT_WIDTH = 2; + // The mask for the execution state bits + uint256 private constant MESSAGE_EXECUTION_STATE_MASK = (1 << MESSAGE_EXECUTION_STATE_BIT_WIDTH) - 1; + + /// @notice Returns the current execution state of a message based on its sequenceNumber. + /// @param sequenceNumber The sequence number of the message to get the execution state for. + /// @return The current execution state of the message. + /// @dev we use the literal number 128 because using a constant increased gas usage. + function getExecutionState(uint64 sequenceNumber) public view returns (Internal.MessageExecutionState) { + return Internal.MessageExecutionState( + (s_executionStates[sequenceNumber / 128] >> ((sequenceNumber % 128) * MESSAGE_EXECUTION_STATE_BIT_WIDTH)) + & MESSAGE_EXECUTION_STATE_MASK + ); + } + + /// @notice Sets a new execution state for a given sequence number. It will overwrite any existing state. + /// @param sequenceNumber The sequence number for which the state will be saved. + /// @param newState The new value the state will be in after this function is called. + /// @dev we use the literal number 128 because using a constant increased gas usage. + function _setExecutionState(uint64 sequenceNumber, Internal.MessageExecutionState newState) internal { + uint256 offset = (sequenceNumber % 128) * MESSAGE_EXECUTION_STATE_BIT_WIDTH; + uint256 bitmap = s_executionStates[sequenceNumber / 128]; + // to unset any potential existing state we zero the bits of the section the state occupies, + // then we do an AND operation to blank out any existing state for the section. + bitmap &= ~(MESSAGE_EXECUTION_STATE_MASK << offset); + // Set the new state + bitmap |= uint256(newState) << offset; + + s_executionStates[sequenceNumber / 128] = bitmap; + } + + /// @inheritdoc IAny2EVMOffRamp + function getSenderNonce(address sender) external view returns (uint64 nonce) { + uint256 senderNonce = s_senderNonce[sender]; + + if (senderNonce == 0) { + if (i_prevOffRamp != address(0)) { + // If OffRamp was upgraded, check if sender has a nonce from the previous OffRamp. + return IAny2EVMOffRamp(i_prevOffRamp).getSenderNonce(sender); + } + } + return uint64(senderNonce); + } + + /// @notice Manually execute a message. + /// @param report Internal.ExecutionReport. + /// @param gasLimitOverrides New gasLimit for each message in the report. + /// @dev We permit gas limit overrides so that users may manually execute messages which failed due to + /// insufficient gas provided. + function manuallyExecute(Internal.ExecutionReport memory report, uint256[] memory gasLimitOverrides) external { + // We do this here because the other _execute path is already covered OCR2BaseXXX. + _checkChainForked(); + + uint256 numMsgs = report.messages.length; + if (numMsgs != gasLimitOverrides.length) revert ManualExecutionGasLimitMismatch(); + for (uint256 i = 0; i < numMsgs; ++i) { + uint256 newLimit = gasLimitOverrides[i]; + // Checks to ensure message cannot be executed with less gas than specified. + if (newLimit != 0) { + if (newLimit < report.messages[i].gasLimit) { + revert InvalidManualExecutionGasLimit(i, newLimit); + } + } + } + + _execute(report, gasLimitOverrides); + } + + /// @notice Entrypoint for execution, called by the OCR network + /// @dev Expects an encoded ExecutionReport + function _report(bytes calldata report) internal override { + _execute(abi.decode(report, (Internal.ExecutionReport)), new uint256[](0)); + } + + /// @notice Executes a report, executing each message in order. + /// @param report The execution report containing the messages and proofs. + /// @param manualExecGasLimits An array of gas limits to use for manual execution. + /// @dev If called from the DON, this array is always empty. + /// @dev If called from manual execution, this array is always same length as messages. + function _execute(Internal.ExecutionReport memory report, uint256[] memory manualExecGasLimits) internal { + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(i_sourceChainSelector)))) revert CursedByRMN(); + + uint256 numMsgs = report.messages.length; + if (numMsgs == 0) revert EmptyReport(); + if (numMsgs != report.offchainTokenData.length) revert UnexpectedTokenData(); + + bytes32[] memory hashedLeaves = new bytes32[](numMsgs); + + for (uint256 i = 0; i < numMsgs; ++i) { + Internal.EVM2EVMMessage memory message = report.messages[i]; + // We do this hash here instead of in _verifyMessages to avoid two separate loops + // over the same data, which increases gas cost + hashedLeaves[i] = Internal._hash(message, i_metadataHash); + // For EVM2EVM offramps, the messageID is the leaf hash. + // Asserting that this is true ensures we don't accidentally commit and then execute + // a message with an unexpected hash. + if (hashedLeaves[i] != message.messageId) revert InvalidMessageId(); + } + + // SECURITY CRITICAL CHECK + uint256 timestampCommitted = ICommitStore(i_commitStore).verify(hashedLeaves, report.proofs, report.proofFlagBits); + if (timestampCommitted == 0) revert RootNotCommitted(); + + // Execute messages + bool manualExecution = manualExecGasLimits.length != 0; + for (uint256 i = 0; i < numMsgs; ++i) { + Internal.EVM2EVMMessage memory message = report.messages[i]; + Internal.MessageExecutionState originalState = getExecutionState(message.sequenceNumber); + if (originalState == Internal.MessageExecutionState.SUCCESS) { + // If the message has already been executed, we skip it. We want to not revert on race conditions between + // executing parties. This will allow us to open up manual exec while also attempting with the DON, without + // reverting an entire DON batch when a user manually executes while the tx is inflight. + emit SkippedAlreadyExecutedMessage(message.sequenceNumber); + continue; + } + // Two valid cases here, we either have never touched this message before, or we tried to execute + // and failed. This check protects against reentry and re-execution because the other state is + // IN_PROGRESS which should not be allowed to execute. + if ( + !( + originalState == Internal.MessageExecutionState.UNTOUCHED + || originalState == Internal.MessageExecutionState.FAILURE + ) + ) revert AlreadyExecuted(message.sequenceNumber); + + if (manualExecution) { + bool isOldCommitReport = + (block.timestamp - timestampCommitted) > s_dynamicConfig.permissionLessExecutionThresholdSeconds; + // Manually execution is fine if we previously failed or if the commit report is just too old + // Acceptable state transitions: FAILURE->SUCCESS, UNTOUCHED->SUCCESS, FAILURE->FAILURE + if (!(isOldCommitReport || originalState == Internal.MessageExecutionState.FAILURE)) { + revert ManualExecutionNotYetEnabled(); + } + + // Manual execution gas limit can override gas limit specified in the message. Value of 0 indicates no override. + if (manualExecGasLimits[i] != 0) { + message.gasLimit = manualExecGasLimits[i]; + } + } else { + // DON can only execute a message once + // Acceptable state transitions: UNTOUCHED->SUCCESS, UNTOUCHED->FAILURE + if (originalState != Internal.MessageExecutionState.UNTOUCHED) revert AlreadyAttempted(message.sequenceNumber); + } + + if (message.nonce != 0) { + // In the scenario where we upgrade offRamps, we still want to have sequential nonces. + // Referencing the old offRamp to check the expected nonce if none is set for a + // given sender allows us to skip the current message if it would not be the next according + // to the old offRamp. This preserves sequencing between updates. + uint64 prevNonce = s_senderNonce[message.sender]; + if (prevNonce == 0) { + if (i_prevOffRamp != address(0)) { + prevNonce = IAny2EVMOffRamp(i_prevOffRamp).getSenderNonce(message.sender); + if (prevNonce + 1 != message.nonce) { + // the starting v2 onramp nonce, i.e. the 1st message nonce v2 offramp is expected to receive, + // is guaranteed to equal (largest v1 onramp nonce + 1). + // if this message's nonce isn't (v1 offramp nonce + 1), then v1 offramp nonce != largest v1 onramp nonce, + // it tells us there are still messages inflight for v1 offramp + emit SkippedSenderWithPreviousRampMessageInflight(message.nonce, message.sender); + continue; + } + // Otherwise this nonce is indeed the "transitional nonce", that is + // all messages sent to v1 ramp have been executed by the DON and the sequence can resume in V2. + // Note if first time user in V2, then prevNonce will be 0, and message.nonce = 1, so this will be a no-op. + s_senderNonce[message.sender] = prevNonce; + } + } + + // UNTOUCHED messages MUST be executed in order always IF message.nonce > 0. + if (originalState == Internal.MessageExecutionState.UNTOUCHED) { + if (prevNonce + 1 != message.nonce) { + // We skip the message if the nonce is incorrect, since message.nonce > 0. + emit SkippedIncorrectNonce(message.nonce, message.sender); + continue; + } + } + } + + // Although we expect only valid messages will be committed, we check again + // when executing as a defense in depth measure. + bytes[] memory offchainTokenData = report.offchainTokenData[i]; + _isWellFormed( + message.sequenceNumber, + message.sourceChainSelector, + message.tokenAmounts.length, + message.data.length, + offchainTokenData.length + ); + + _setExecutionState(message.sequenceNumber, Internal.MessageExecutionState.IN_PROGRESS); + (Internal.MessageExecutionState newState, bytes memory returnData) = _trialExecute(message, offchainTokenData); + _setExecutionState(message.sequenceNumber, newState); + + // Since it's hard to estimate whether manual execution will succeed, we + // revert the entire transaction if it fails. This will show the user if + // their manual exec will fail before they submit it. + if (manualExecution) { + if (newState == Internal.MessageExecutionState.FAILURE) { + if (originalState != Internal.MessageExecutionState.UNTOUCHED) { + // If manual execution fails, we revert the entire transaction, unless the originalState is UNTOUCHED as we + // would still be making progress by changing the state from UNTOUCHED to FAILURE. + revert ExecutionError(returnData); + } + } + } + + // The only valid prior states are UNTOUCHED and FAILURE (checked above) + // The only valid post states are SUCCESS and FAILURE (checked below) + if (newState != Internal.MessageExecutionState.SUCCESS) { + if (newState != Internal.MessageExecutionState.FAILURE) { + revert InvalidNewState(message.sequenceNumber, newState); + } + } + + // Nonce changes per state transition. + // These only apply for ordered messages. + // UNTOUCHED -> FAILURE nonce bump + // UNTOUCHED -> SUCCESS nonce bump + // FAILURE -> FAILURE no nonce bump + // FAILURE -> SUCCESS no nonce bump + if (message.nonce != 0) { + if (originalState == Internal.MessageExecutionState.UNTOUCHED) { + s_senderNonce[message.sender]++; + } + } + + emit ExecutionStateChanged(message.sequenceNumber, message.messageId, newState, returnData); + } + } + + /// @notice Does basic message validation. Should never fail. + /// @param sequenceNumber Sequence number of the message. + /// @param sourceChainSelector SourceChainSelector of the message. + /// @param numberOfTokens Length of tokenAmounts array in the message. + /// @param dataLength Length of data field in the message. + /// @param offchainTokenDataLength Length of offchainTokenData array. + /// @dev reverts on validation failures. + function _isWellFormed( + uint64 sequenceNumber, + uint64 sourceChainSelector, + uint256 numberOfTokens, + uint256 dataLength, + uint256 offchainTokenDataLength + ) private view { + if (sourceChainSelector != i_sourceChainSelector) revert InvalidSourceChain(sourceChainSelector); + if (numberOfTokens > uint256(s_dynamicConfig.maxNumberOfTokensPerMsg)) { + revert UnsupportedNumberOfTokens(sequenceNumber); + } + if (numberOfTokens != offchainTokenDataLength) revert TokenDataMismatch(sequenceNumber); + if (dataLength > uint256(s_dynamicConfig.maxDataBytes)) { + revert MessageTooLarge(uint256(s_dynamicConfig.maxDataBytes), dataLength); + } + } + + /// @notice Try executing a message. + /// @param message Internal.EVM2EVMMessage memory message. + /// @param offchainTokenData Data provided by the DON for token transfers. + /// @return the new state of the message, being either SUCCESS or FAILURE. + /// @return revert data in bytes if CCIP receiver reverted during execution. + function _trialExecute( + Internal.EVM2EVMMessage memory message, + bytes[] memory offchainTokenData + ) internal returns (Internal.MessageExecutionState, bytes memory) { + try this.executeSingleMessage(message, offchainTokenData) {} + catch (bytes memory err) { + if ( + ReceiverError.selector == bytes4(err) || TokenHandlingError.selector == bytes4(err) + || Internal.InvalidEVMAddress.selector == bytes4(err) || InvalidDataLength.selector == bytes4(err) + || CallWithExactGas.NoContract.selector == bytes4(err) || NotACompatiblePool.selector == bytes4(err) + ) { + // If CCIP receiver execution is not successful, bubble up receiver revert data, + // prepended by the 4 bytes of ReceiverError.selector, TokenHandlingError.selector or InvalidPoolAddress.selector. + // Max length of revert data is Router.MAX_RET_BYTES, max length of err is 4 + Router.MAX_RET_BYTES + return (Internal.MessageExecutionState.FAILURE, err); + } + // If revert is not caused by CCIP receiver, it is unexpected, bubble up the revert. + revert ExecutionError(err); + } + // If message execution succeeded, no CCIP receiver return data is expected, return with empty bytes. + return (Internal.MessageExecutionState.SUCCESS, ""); + } + + /// @notice Execute a single message. + /// @param message The message that will be executed. + /// @param offchainTokenData Token transfer data to be passed to TokenPool. + /// @dev We make this external and callable by the contract itself, in order to try/catch + /// its execution and enforce atomicity among successful message processing and token transfer. + /// @dev We use ERC-165 to check for the ccipReceive interface to permit sending tokens to contracts + /// (for example smart contract wallets) without an associated message. + function executeSingleMessage(Internal.EVM2EVMMessage memory message, bytes[] memory offchainTokenData) external { + if (msg.sender != address(this)) revert CanOnlySelfCall(); + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](0); + if (message.tokenAmounts.length > 0) { + destTokenAmounts = _releaseOrMintTokens( + message.tokenAmounts, abi.encode(message.sender), message.receiver, message.sourceTokenData, offchainTokenData + ); + } + // There are three cases in which we skip calling the receiver: + // 1. If the message data is empty AND the gas limit is 0. + // This indicates a message that only transfers tokens. It is valid to only send tokens to a contract + // that supports the IAny2EVMMessageReceiver interface, but without this first check we would call the + // receiver without any gas, which would revert the transaction. + // 2. If the receiver is not a contract. + // 3. If the receiver is a contract but it does not support the IAny2EVMMessageReceiver interface. + // + // The ordering of these checks is important, as the first check is the cheapest to execute. + if ( + (message.data.length == 0 && message.gasLimit == 0) || message.receiver.code.length == 0 + || !message.receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId) + ) return; + + (bool success, bytes memory returnData,) = IRouter(s_dynamicConfig.router).routeMessage( + Client.Any2EVMMessage({ + messageId: message.messageId, + sourceChainSelector: message.sourceChainSelector, + sender: abi.encode(message.sender), + data: message.data, + destTokenAmounts: destTokenAmounts + }), + Internal.GAS_FOR_CALL_EXACT_CHECK, + message.gasLimit, + message.receiver + ); + // If CCIP receiver execution is not successful, revert the call including token transfers + if (!success) revert ReceiverError(returnData); + } + + /// @notice creates a unique hash to be used in message hashing. + function _metadataHash(bytes32 prefix) internal view returns (bytes32) { + return keccak256(abi.encode(prefix, i_sourceChainSelector, i_chainSelector, i_onRamp)); + } + + // ================================================================ + // │ Config │ + // ================================================================ + + /// @notice Returns the static config. + /// @dev This function will always return the same struct as the contents is static and can never change. + /// RMN depends on this function, if changing, please notify the RMN maintainers. + function getStaticConfig() external view returns (StaticConfig memory) { + return StaticConfig({ + commitStore: i_commitStore, + chainSelector: i_chainSelector, + sourceChainSelector: i_sourceChainSelector, + onRamp: i_onRamp, + prevOffRamp: i_prevOffRamp, + rmnProxy: i_rmnProxy, + tokenAdminRegistry: i_tokenAdminRegistry + }); + } + + /// @notice Returns the current dynamic config. + /// @return The current config. + function getDynamicConfig() external view returns (DynamicConfig memory) { + return s_dynamicConfig; + } + + /// @notice Sets the dynamic config. This function is called during `setOCR2Config` flow + function _beforeSetConfig(bytes memory onchainConfig) internal override { + DynamicConfig memory dynamicConfig = abi.decode(onchainConfig, (DynamicConfig)); + + if (dynamicConfig.router == address(0)) revert ZeroAddressNotAllowed(); + + s_dynamicConfig = dynamicConfig; + + emit ConfigSet( + StaticConfig({ + commitStore: i_commitStore, + chainSelector: i_chainSelector, + sourceChainSelector: i_sourceChainSelector, + onRamp: i_onRamp, + prevOffRamp: i_prevOffRamp, + rmnProxy: i_rmnProxy, + tokenAdminRegistry: i_tokenAdminRegistry + }), + dynamicConfig + ); + } + + /// @notice Get all tokens which are included in Aggregate Rate Limiting. + /// @return sourceTokens The source representation of the tokens that are rate limited. + /// @return destTokens The destination representation of the tokens that are rate limited. + /// @dev the order of IDs in the list is **not guaranteed**, therefore, if ordering matters when + /// making successive calls, one should keep the block height constant to ensure a consistent result. + function getAllRateLimitTokens() external view returns (address[] memory sourceTokens, address[] memory destTokens) { + uint256 numRateLimitedTokens = s_rateLimitedTokensDestToSource.length(); + sourceTokens = new address[](numRateLimitedTokens); + destTokens = new address[](numRateLimitedTokens); + + for (uint256 i = 0; i < numRateLimitedTokens; ++i) { + (address destToken, address sourceToken) = s_rateLimitedTokensDestToSource.at(i); + sourceTokens[i] = sourceToken; + destTokens[i] = destToken; + } + return (sourceTokens, destTokens); + } + + /// @notice Adds or removes tokens from being used in Aggregate Rate Limiting. + /// @param removes - A list of one or more tokens to be removed. + /// @param adds - A list of one or more tokens to be added. + function updateRateLimitTokens(RateLimitToken[] memory removes, RateLimitToken[] memory adds) external onlyOwner { + for (uint256 i = 0; i < removes.length; ++i) { + if (s_rateLimitedTokensDestToSource.remove(removes[i].destToken)) { + emit TokenAggregateRateLimitRemoved(removes[i].sourceToken, removes[i].destToken); + } + } + + for (uint256 i = 0; i < adds.length; ++i) { + if (s_rateLimitedTokensDestToSource.set(adds[i].destToken, adds[i].sourceToken)) { + emit TokenAggregateRateLimitAdded(adds[i].sourceToken, adds[i].destToken); + } + } + } + + // ================================================================ + // │ Tokens and pools │ + // ================================================================ + + /// @notice Uses a pool to release or mint a token to a receiver address in two steps. First, the pool is called + /// to release the tokens to the offRamp, then the offRamp calls the token contract to transfer the tokens to the + /// receiver. This is done to ensure the exact number of tokens, the pool claims to release are actually transferred. + /// @dev The local token address is validated through the TokenAdminRegistry. If, due to some misconfiguration, the + /// token is unknown to the registry, the offRamp will revert. The tx, and the tokens, can be retrieved by + /// registering the token on this chain, and re-trying the msg. + /// @param sourceAmount The amount of tokens to be released/minted. + /// @param originalSender The message sender on the source chain. + /// @param receiver The address that will receive the tokens. + /// @param sourceTokenData A struct containing the local token address, the source pool address and optional data + /// returned from the source pool. + /// @param offchainTokenData Data fetched offchain by the DON. + function _releaseOrMintToken( + uint256 sourceAmount, + bytes memory originalSender, + address receiver, + Internal.SourceTokenData memory sourceTokenData, + bytes memory offchainTokenData + ) internal returns (Client.EVMTokenAmount memory destTokenAmount) { + // We need to safely decode the token address from the sourceTokenData, as it could be wrong, + // in which case it doesn't have to be a valid EVM address. + address localToken = Internal._validateEVMAddress(sourceTokenData.destTokenAddress); + // We check with the token admin registry if the token has a pool on this chain. + address localPoolAddress = ITokenAdminRegistry(i_tokenAdminRegistry).getPool(localToken); + // This will call the supportsInterface through the ERC165Checker, and not directly on the pool address. + // This is done to prevent a pool from reverting the entire transaction if it doesn't support the interface. + // The call gets a max or 30k gas per instance, of which there are three. This means gas estimations should + // account for 90k gas overhead due to the interface check. + if (localPoolAddress == address(0) || !localPoolAddress.supportsInterface(Pool.CCIP_POOL_V1)) { + revert NotACompatiblePool(localPoolAddress); + } + + // We determined that the pool address is a valid EVM address, but that does not mean the code at this + // address is a (compatible) pool contract. _callWithExactGasSafeReturnData will check if the location + // contains a contract. If it doesn't it reverts with a known error, which we catch gracefully. + // We call the pool with exact gas to increase resistance against malicious tokens or token pools. + // We protects against return data bombs by capping the return data size at MAX_RET_BYTES. + (bool success, bytes memory returnData,) = CallWithExactGas._callWithExactGasSafeReturnData( + abi.encodeCall( + IPoolV1.releaseOrMint, + Pool.ReleaseOrMintInV1({ + originalSender: originalSender, + receiver: receiver, + amount: sourceAmount, + localToken: localToken, + remoteChainSelector: i_sourceChainSelector, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData + }) + ), + localPoolAddress, + s_dynamicConfig.maxPoolReleaseOrMintGas, + Internal.GAS_FOR_CALL_EXACT_CHECK, + Internal.MAX_RET_BYTES + ); + + // wrap and rethrow the error so we can catch it lower in the stack + if (!success) revert TokenHandlingError(returnData); + + // If the call was successful, the returnData should contain only the local token amount. + if (returnData.length != Pool.CCIP_POOL_V1_RET_BYTES) { + revert InvalidDataLength(Pool.CCIP_POOL_V1_RET_BYTES, returnData.length); + } + uint256 localAmount = abi.decode(returnData, (uint256)); + // Since token pools send the tokens to the msg.sender, which is this offRamp, we need to + // transfer them to the final receiver. We use the _callWithExactGasSafeReturnData function because + // the token contracts are not considered trusted. + (success, returnData,) = CallWithExactGas._callWithExactGasSafeReturnData( + abi.encodeCall(IERC20.transfer, (receiver, localAmount)), + localToken, + s_dynamicConfig.maxTokenTransferGas, + Internal.GAS_FOR_CALL_EXACT_CHECK, + Internal.MAX_RET_BYTES + ); + + if (!success) revert TokenHandlingError(returnData); + + return Client.EVMTokenAmount({token: localToken, amount: localAmount}); + } + + /// @notice Uses pools to release or mint a number of different tokens to a receiver address. + /// @param sourceTokenAmounts List of tokens and amount values to be released/minted. + /// @param originalSender The message sender. + /// @param receiver The address that will receive the tokens. + /// @param encodedSourceTokenData Array of token data returned by token pools on the source chain. + /// @param offchainTokenData Array of token data fetched offchain by the DON. + /// @dev This function wrappes the token pool call in a try catch block to gracefully handle + /// any non-rate limiting errors that may occur. If we encounter a rate limiting related error + /// we bubble it up. If we encounter a non-rate limiting error we wrap it in a TokenHandlingError. + function _releaseOrMintTokens( + Client.EVMTokenAmount[] memory sourceTokenAmounts, + bytes memory originalSender, + address receiver, + bytes[] memory encodedSourceTokenData, + bytes[] memory offchainTokenData + ) internal returns (Client.EVMTokenAmount[] memory destTokenAmounts) { + // Creating a copy is more gas efficient than initializing a new array. + destTokenAmounts = sourceTokenAmounts; + uint256 value = 0; + for (uint256 i = 0; i < sourceTokenAmounts.length; ++i) { + destTokenAmounts[i] = _releaseOrMintToken( + sourceTokenAmounts[i].amount, + originalSender, + receiver, + // This should never revert as the onRamp encodes the sourceTokenData struct. Only the inner components from + // this struct come from untrusted sources. + abi.decode(encodedSourceTokenData[i], (Internal.SourceTokenData)), + offchainTokenData[i] + ); + + if (s_rateLimitedTokensDestToSource.contains(destTokenAmounts[i].token)) { + value += _getTokenValue(destTokenAmounts[i], IPriceRegistry(s_dynamicConfig.priceRegistry)); + } + } + + if (value > 0) _rateLimitValue(value); + + return destTokenAmounts; + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Reverts as this contract should not access CCIP messages + function ccipReceive(Client.Any2EVMMessage calldata) external pure { + // solhint-disable-next-line + revert(); + } +} diff --git a/contracts/src/v0.8/ccip/onRamp/EVM2EVMMultiOnRamp.sol b/contracts/src/v0.8/ccip/onRamp/EVM2EVMMultiOnRamp.sol new file mode 100644 index 00000000000..fc455cc869e --- /dev/null +++ b/contracts/src/v0.8/ccip/onRamp/EVM2EVMMultiOnRamp.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IEVM2AnyOnRampClient} from "../interfaces/IEVM2AnyOnRampClient.sol"; +import {IMessageInterceptor} from "../interfaces/IMessageInterceptor.sol"; +import {INonceManager} from "../interfaces/INonceManager.sol"; +import {IPoolV1} from "../interfaces/IPool.sol"; +import {IPriceRegistry} from "../interfaces/IPriceRegistry.sol"; +import {IRMN} from "../interfaces/IRMN.sol"; +import {ITokenAdminRegistry} from "../interfaces/ITokenAdminRegistry.sol"; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {Client} from "../libraries/Client.sol"; +import {Internal} from "../libraries/Internal.sol"; +import {Pool} from "../libraries/Pool.sol"; +import {USDPriceWith18Decimals} from "../libraries/USDPriceWith18Decimals.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice The EVM2EVMMultiOnRamp is a contract that handles lane-specific fee logic +/// @dev The EVM2EVMMultiOnRamp, MultiCommitStore and EVM2EVMMultiOffRamp form an xchain upgradeable unit. Any change to one of them +/// results an onchain upgrade of all 3. +contract EVM2EVMMultiOnRamp is IEVM2AnyOnRampClient, ITypeAndVersion, OwnerIsCreator { + using SafeERC20 for IERC20; + using USDPriceWith18Decimals for uint224; + + error CannotSendZeroTokens(); + error InvalidExtraArgsTag(); + error ExtraArgOutOfOrderExecutionMustBeTrue(); + error OnlyCallableByOwnerOrAdmin(); + error MessageGasLimitTooHigh(); + error UnsupportedToken(address token); + error MustBeCalledByRouter(); + error RouterMustSetOriginalSender(); + error InvalidConfig(); + error CursedByRMN(uint64 sourceChainSelector); + error GetSupportedTokensFunctionalityRemovedCheckAdminRegistry(); + + event AdminSet(address newAdmin); + event ConfigSet(StaticConfig staticConfig, DynamicConfig dynamicConfig); + event FeePaid(address indexed feeToken, uint256 feeValueJuels); + event FeeTokenWithdrawn(address indexed feeAggregator, address indexed feeToken, uint256 amount); + /// RMN depends on this event, if changing, please notify the RMN maintainers. + event CCIPSendRequested(uint64 indexed destChainSelector, Internal.EVM2AnyRampMessage message); + + /// @dev Struct that contains the static configuration + /// RMN depends on this struct, if changing, please notify the RMN maintainers. + // solhint-disable-next-line gas-struct-packing + struct StaticConfig { + uint64 chainSelector; // ─────╮ Source chainSelector + address rmnProxy; // ─────────╯ Address of RMN proxy + address nonceManager; // Address of the nonce manager + address tokenAdminRegistry; // Token admin registry address + } + + /// @dev Struct to contains the dynamic configuration + // solhint-disable-next-line gas-struct-packing + struct DynamicConfig { + address router; // Router address + address priceRegistry; // Price registry address + address messageValidator; // Optional message validator to validate outbound messages (zero address = no validator) + address feeAggregator; // Fee aggregator address + } + + // STATIC CONFIG + string public constant override typeAndVersion = "EVM2EVMMultiOnRamp 1.6.0-dev"; + /// @dev The chain ID of the source chain that this contract is deployed to + uint64 internal immutable i_chainSelector; + /// @dev The address of the rmn proxy + address internal immutable i_rmnProxy; + /// @dev The address of the nonce manager + address internal immutable i_nonceManager; + /// @dev The address of the token admin registry + address internal immutable i_tokenAdminRegistry; + /// @dev the maximum number of nops that can be configured at the same time. + /// Used to bound gas for loops over nops. + uint256 private constant MAX_NUMBER_OF_NOPS = 64; + + // DYNAMIC CONFIG + /// @dev The config for the onRamp + DynamicConfig internal s_dynamicConfig; + + /// @dev Last used sequence number per destination chain. + /// This is zero in the case where no messages have been sent yet. + /// 0 is not a valid sequence number for any real transaction. + mapping(uint64 destChainSelector => uint64 sequenceNumber) internal s_destChainSequenceNumbers; + + // STATE + /// @dev The amount of LINK available to pay NOPS + uint96 internal s_nopFeesJuels; + /// @dev The combined weight of all NOPs weights + uint32 internal s_nopWeightsTotal; + + constructor(StaticConfig memory staticConfig, DynamicConfig memory dynamicConfig) { + if ( + staticConfig.chainSelector == 0 || staticConfig.rmnProxy == address(0) || staticConfig.nonceManager == address(0) + || staticConfig.tokenAdminRegistry == address(0) + ) { + revert InvalidConfig(); + } + + i_chainSelector = staticConfig.chainSelector; + i_rmnProxy = staticConfig.rmnProxy; + i_nonceManager = staticConfig.nonceManager; + i_tokenAdminRegistry = staticConfig.tokenAdminRegistry; + + _setDynamicConfig(dynamicConfig); + } + + // ================================================================ + // │ Messaging │ + // ================================================================ + + /// @notice Gets the next sequence number to be used in the onRamp + /// @param destChainSelector The destination chain selector + /// @return the next sequence number to be used + function getExpectedNextSequenceNumber(uint64 destChainSelector) external view returns (uint64) { + return s_destChainSequenceNumbers[destChainSelector] + 1; + } + + /// @inheritdoc IEVM2AnyOnRampClient + function forwardFromRouter( + uint64 destChainSelector, + Client.EVM2AnyMessage calldata message, + uint256 feeTokenAmount, + address originalSender + ) external returns (bytes32) { + // NOTE: assumes the message has already been validated through the getFee call + // Validate message sender is set and allowed. Not validated in `getFee` since it is not user-driven. + if (originalSender == address(0)) revert RouterMustSetOriginalSender(); + // Router address may be zero intentionally to pause. + if (msg.sender != s_dynamicConfig.router) revert MustBeCalledByRouter(); + + address messageValidator = s_dynamicConfig.messageValidator; + if (messageValidator != address(0)) { + IMessageInterceptor(messageValidator).onOutboundMessage(destChainSelector, message); + } + + // Convert message fee to juels and retrieve converted args + (uint256 msgFeeJuels, bool isOutOfOrderExecution, bytes memory convertedExtraArgs) = IPriceRegistry( + s_dynamicConfig.priceRegistry + ).processMessageArgs(destChainSelector, message.feeToken, feeTokenAmount, message.extraArgs); + + emit FeePaid(message.feeToken, msgFeeJuels); + + Internal.EVM2AnyRampMessage memory newMessage = Internal.EVM2AnyRampMessage({ + header: Internal.RampMessageHeader({ + // Should be generated after the message is complete + messageId: "", + sourceChainSelector: i_chainSelector, + destChainSelector: destChainSelector, + // We need the next available sequence number so we increment before we use the value + sequenceNumber: ++s_destChainSequenceNumbers[destChainSelector], + // Only bump nonce for messages that specify allowOutOfOrderExecution == false. Otherwise, we + // may block ordered message nonces, which is not what we want. + nonce: isOutOfOrderExecution + ? 0 + : INonceManager(i_nonceManager).getIncrementedOutboundNonce(destChainSelector, originalSender) + }), + sender: originalSender, + data: message.data, + extraArgs: message.extraArgs, + receiver: message.receiver, + feeToken: message.feeToken, + feeTokenAmount: feeTokenAmount, + // Should be populated via lock / burn pool calls + tokenAmounts: new Internal.RampTokenAmount[](message.tokenAmounts.length) + }); + + // Lock the tokens as last step. TokenPools may not always be trusted. + // There should be no state changes after external call to TokenPools. + for (uint256 i = 0; i < message.tokenAmounts.length; ++i) { + newMessage.tokenAmounts[i] = + _lockOrBurnSingleToken(message.tokenAmounts[i], destChainSelector, message.receiver, originalSender); + } + + // Validate pool return data after it is populated (view function - no state changes) + IPriceRegistry(s_dynamicConfig.priceRegistry).validatePoolReturnData( + destChainSelector, newMessage.tokenAmounts, message.tokenAmounts + ); + + // Override extraArgs with latest version + newMessage.extraArgs = convertedExtraArgs; + + // Hash only after all fields have been set + newMessage.header.messageId = Internal._hash( + newMessage, + // Metadata hash preimage to ensure global uniqueness, ensuring 2 identical messages sent to 2 different + // lanes will have a distinct hash. + keccak256(abi.encode(Internal.EVM_2_ANY_MESSAGE_HASH, i_chainSelector, destChainSelector, address(this))) + ); + + // Emit message request + // This must happen after any pool events as some tokens (e.g. USDC) emit events that we expect to precede this + // event in the offchain code. + emit CCIPSendRequested(destChainSelector, newMessage); + return newMessage.header.messageId; + } + + /// @notice Uses a pool to lock or burn a token + /// @param tokenAndAmount Token address and amount to lock or burn + /// @param destChainSelector Target dest chain selector of the message + /// @param receiver Message receiver + /// @param originalSender Message sender + /// @return rampTokenAndAmount Ramp token and amount data + function _lockOrBurnSingleToken( + Client.EVMTokenAmount memory tokenAndAmount, + uint64 destChainSelector, + bytes memory receiver, + address originalSender + ) internal returns (Internal.RampTokenAmount memory) { + if (tokenAndAmount.amount == 0) revert CannotSendZeroTokens(); + + IPoolV1 sourcePool = getPoolBySourceToken(destChainSelector, IERC20(tokenAndAmount.token)); + // We don't have to check if it supports the pool version in a non-reverting way here because + // if we revert here, there is no effect on CCIP. Therefore we directly call the supportsInterface + // function and not through the ERC165Checker. + if (address(sourcePool) == address(0) || !sourcePool.supportsInterface(Pool.CCIP_POOL_V1)) { + revert UnsupportedToken(tokenAndAmount.token); + } + + Pool.LockOrBurnOutV1 memory poolReturnData = sourcePool.lockOrBurn( + Pool.LockOrBurnInV1({ + receiver: receiver, + remoteChainSelector: destChainSelector, + originalSender: originalSender, + amount: tokenAndAmount.amount, + localToken: tokenAndAmount.token + }) + ); + + // NOTE: pool data validations are outsourced to the PriceRegistry to handle family-specific logic handling + + return Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(sourcePool), + destTokenAddress: poolReturnData.destTokenAddress, + extraData: poolReturnData.destPoolData, + amount: tokenAndAmount.amount + }); + } + + // ================================================================ + // │ Config │ + // ================================================================ + + /// @notice Returns the static onRamp config. + /// @dev RMN depends on this function, if changing, please notify the RMN maintainers. + /// @return the configuration. + function getStaticConfig() external view returns (StaticConfig memory) { + return StaticConfig({ + chainSelector: i_chainSelector, + rmnProxy: i_rmnProxy, + nonceManager: i_nonceManager, + tokenAdminRegistry: i_tokenAdminRegistry + }); + } + + /// @notice Returns the dynamic onRamp config. + /// @return dynamicConfig the configuration. + function getDynamicConfig() external view returns (DynamicConfig memory dynamicConfig) { + return s_dynamicConfig; + } + + /// @notice Sets the dynamic configuration. + /// @param dynamicConfig The configuration. + function setDynamicConfig(DynamicConfig memory dynamicConfig) external onlyOwner { + _setDynamicConfig(dynamicConfig); + } + + /// @notice Internal version of setDynamicConfig to allow for reuse in the constructor. + function _setDynamicConfig(DynamicConfig memory dynamicConfig) internal { + // We permit router to be set to zero as a way to pause the contract. + if (dynamicConfig.priceRegistry == address(0) || dynamicConfig.feeAggregator == address(0)) revert InvalidConfig(); + + s_dynamicConfig = dynamicConfig; + + emit ConfigSet( + StaticConfig({ + chainSelector: i_chainSelector, + rmnProxy: i_rmnProxy, + nonceManager: i_nonceManager, + tokenAdminRegistry: i_tokenAdminRegistry + }), + dynamicConfig + ); + } + + // ================================================================ + // │ Tokens and pools │ + // ================================================================ + + /// @inheritdoc IEVM2AnyOnRampClient + function getPoolBySourceToken(uint64, /*destChainSelector*/ IERC20 sourceToken) public view returns (IPoolV1) { + return IPoolV1(ITokenAdminRegistry(i_tokenAdminRegistry).getPool(address(sourceToken))); + } + + /// @inheritdoc IEVM2AnyOnRampClient + function getSupportedTokens(uint64 /*destChainSelector*/ ) external pure returns (address[] memory) { + revert GetSupportedTokensFunctionalityRemovedCheckAdminRegistry(); + } + + // ================================================================ + // │ Fees │ + // ================================================================ + + /// @inheritdoc IEVM2AnyOnRampClient + /// @dev getFee MUST revert if the feeToken is not listed in the fee token config, as the router assumes it does. + /// @param destChainSelector The destination chain selector. + /// @param message The message to get quote for. + /// @return feeTokenAmount The amount of fee token needed for the fee, in smallest denomination of the fee token. + function getFee( + uint64 destChainSelector, + Client.EVM2AnyMessage calldata message + ) external view returns (uint256 feeTokenAmount) { + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(destChainSelector)))) revert CursedByRMN(destChainSelector); + + return IPriceRegistry(s_dynamicConfig.priceRegistry).getValidatedFee(destChainSelector, message); + } + + /// @notice Withdraws the outstanding fee token balances to the fee aggregator. + /// @dev This function can be permissionless as it only transfers accepted fee tokens to the fee aggregator which is a trusted address. + function withdrawFeeTokens() external { + address[] memory feeTokens = IPriceRegistry(s_dynamicConfig.priceRegistry).getFeeTokens(); + address feeAggregator = s_dynamicConfig.feeAggregator; + + for (uint256 i = 0; i < feeTokens.length; ++i) { + IERC20 feeToken = IERC20(feeTokens[i]); + uint256 feeTokenBalance = feeToken.balanceOf(address(this)); + + if (feeTokenBalance > 0) { + feeToken.safeTransfer(feeAggregator, feeTokenBalance); + + emit FeeTokenWithdrawn(feeAggregator, address(feeToken), feeTokenBalance); + } + } + } +} diff --git a/contracts/src/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol b/contracts/src/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol new file mode 100644 index 00000000000..0e978596e4c --- /dev/null +++ b/contracts/src/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol @@ -0,0 +1,916 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IEVM2AnyOnRamp} from "../interfaces/IEVM2AnyOnRamp.sol"; +import {IEVM2AnyOnRampClient} from "../interfaces/IEVM2AnyOnRampClient.sol"; +import {IPoolV1} from "../interfaces/IPool.sol"; +import {IPriceRegistry} from "../interfaces/IPriceRegistry.sol"; +import {IRMN} from "../interfaces/IRMN.sol"; +import {ITokenAdminRegistry} from "../interfaces/ITokenAdminRegistry.sol"; +import {ILinkAvailable} from "../interfaces/automation/ILinkAvailable.sol"; + +import {AggregateRateLimiter} from "../AggregateRateLimiter.sol"; +import {Client} from "../libraries/Client.sol"; +import {Internal} from "../libraries/Internal.sol"; +import {Pool} from "../libraries/Pool.sol"; +import {RateLimiter} from "../libraries/RateLimiter.sol"; +import {USDPriceWith18Decimals} from "../libraries/USDPriceWith18Decimals.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableMap} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableMap.sol"; + +/// @notice The onRamp is a contract that handles lane-specific fee logic, NOP payments and +/// bridgeable token support. +/// @dev The EVM2EVMOnRamp, CommitStore and EVM2EVMOffRamp form an xchain upgradeable unit. Any change to one of them +/// results an onchain upgrade of all 3. +contract EVM2EVMOnRamp is IEVM2AnyOnRamp, ILinkAvailable, AggregateRateLimiter, ITypeAndVersion { + using SafeERC20 for IERC20; + using EnumerableMap for EnumerableMap.AddressToUintMap; + using USDPriceWith18Decimals for uint224; + + error InvalidExtraArgsTag(); + error ExtraArgOutOfOrderExecutionMustBeTrue(); + error OnlyCallableByOwnerOrAdmin(); + error OnlyCallableByOwnerOrAdminOrNop(); + error InvalidWithdrawParams(); + error NoFeesToPay(); + error NoNopsToPay(); + error InsufficientBalance(); + error TooManyNops(); + error MaxFeeBalanceReached(); + error MessageTooLarge(uint256 maxSize, uint256 actualSize); + error MessageGasLimitTooHigh(); + error UnsupportedNumberOfTokens(); + error UnsupportedToken(address token); + error MustBeCalledByRouter(); + error RouterMustSetOriginalSender(); + error InvalidConfig(); + error CursedByRMN(); + error LinkBalanceNotSettled(); + error InvalidNopAddress(address nop); + error NotAFeeToken(address token); + error CannotSendZeroTokens(); + error SourceTokenDataTooLarge(address token); + error InvalidChainSelector(uint64 chainSelector); + error GetSupportedTokensFunctionalityRemovedCheckAdminRegistry(); + error InvalidDestBytesOverhead(address token, uint32 destBytesOverhead); + + event ConfigSet(StaticConfig staticConfig, DynamicConfig dynamicConfig); + event NopPaid(address indexed nop, uint256 amount); + event FeeConfigSet(FeeTokenConfigArgs[] feeConfig); + event TokenTransferFeeConfigSet(TokenTransferFeeConfigArgs[] transferFeeConfig); + event TokenTransferFeeConfigDeleted(address[] tokens); + /// RMN depends on this event, if changing, please notify the RMN maintainers. + event CCIPSendRequested(Internal.EVM2EVMMessage message); + event NopsSet(uint256 nopWeightsTotal, NopAndWeight[] nopsAndWeights); + + /// @dev Struct that contains the static configuration + /// RMN depends on this struct, if changing, please notify the RMN maintainers. + //solhint-disable gas-struct-packing + struct StaticConfig { + address linkToken; // ────────╮ Link token address + uint64 chainSelector; // ─────╯ Source chainSelector + uint64 destChainSelector; // ─╮ Destination chainSelector + uint64 defaultTxGasLimit; // │ Default gas limit for a tx + uint96 maxNopFeesJuels; // ───╯ Max nop fee balance onramp can have + address prevOnRamp; // Address of previous-version OnRamp + address rmnProxy; // Address of RMN proxy + address tokenAdminRegistry; // Address of the token admin registry + } + + /// @dev Struct to contains the dynamic configuration + struct DynamicConfig { + address router; // ──────────────────────────╮ Router address + uint16 maxNumberOfTokensPerMsg; // │ Maximum number of distinct ERC20 token transferred per message + uint32 destGasOverhead; // │ Gas charged on top of the gasLimit to cover destination chain costs + uint16 destGasPerPayloadByte; // │ Destination chain gas charged for passing each byte of `data` payload to receiver + uint32 destDataAvailabilityOverheadGas; // ──╯ Extra data availability gas charged on top of the message, e.g. for OCR + uint16 destGasPerDataAvailabilityByte; // ───╮ Amount of gas to charge per byte of message data that needs availability + uint16 destDataAvailabilityMultiplierBps; // │ Multiplier for data availability gas, multiples of bps, or 0.0001 + address priceRegistry; // │ Price registry address + uint32 maxDataBytes; // │ Maximum payload data size in bytes + uint32 maxPerMsgGasLimit; // ────────────────╯ Maximum gas limit for messages targeting EVMs + // │ + // The following three properties are defaults, they can be overridden by setting the TokenTransferFeeConfig for a token + uint16 defaultTokenFeeUSDCents; // ──────────╮ Default token fee charged per token transfer + uint32 defaultTokenDestGasOverhead; // │ Default gas charged to execute the token transfer on the destination chain + // │ Default data availability bytes that are returned from the source pool and sent + uint32 defaultTokenDestBytesOverhead; // | to the destination pool. Must be >= Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + bool enforceOutOfOrder; // ──────────────────╯ Whether to enforce the allowOutOfOrderExecution extraArg value to be true. + } + + /// @dev Struct to hold the execution fee configuration for a fee token + struct FeeTokenConfig { + uint32 networkFeeUSDCents; // ─────────╮ Flat network fee to charge for messages, multiples of 0.01 USD + uint64 gasMultiplierWeiPerEth; // │ Multiplier for gas costs, 1e18 based so 11e17 = 10% extra cost. + uint64 premiumMultiplierWeiPerEth; // │ Multiplier for fee-token-specific premiums + bool enabled; // ──────────────────────╯ Whether this fee token is enabled + } + + /// @dev Struct to hold the fee configuration for a fee token, same as the FeeTokenConfig but with + /// token included so that an array of these can be passed in to setFeeTokenConfig to set the mapping + struct FeeTokenConfigArgs { + address token; // ─────────────────────╮ Token address + uint32 networkFeeUSDCents; // │ Flat network fee to charge for messages, multiples of 0.01 USD + uint64 gasMultiplierWeiPerEth; // ─────╯ Multiplier for gas costs, 1e18 based so 11e17 = 10% extra cost + uint64 premiumMultiplierWeiPerEth; // ─╮ Multiplier for fee-token-specific premiums, 1e18 based + bool enabled; // ──────────────────────╯ Whether this fee token is enabled + } + + /// @dev Struct to hold the transfer fee configuration for token transfers + struct TokenTransferFeeConfig { + uint32 minFeeUSDCents; // ──────────╮ Minimum fee to charge per token transfer, multiples of 0.01 USD + uint32 maxFeeUSDCents; // │ Maximum fee to charge per token transfer, multiples of 0.01 USD + uint16 deciBps; // │ Basis points charged on token transfers, multiples of 0.1bps, or 1e-5 + uint32 destGasOverhead; // │ Gas charged to execute the token transfer on the destination chain + // │ Extra data availability bytes that are returned from the source pool and sent + uint32 destBytesOverhead; // │ to the destination pool. Must be >= Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + bool aggregateRateLimitEnabled; // │ Whether this transfer token is to be included in Aggregate Rate Limiting + bool isEnabled; // ─────────────────╯ Whether this token has custom transfer fees + } + + /// @dev Same as TokenTransferFeeConfig + /// token included so that an array of these can be passed in to setTokenTransferFeeConfig + struct TokenTransferFeeConfigArgs { + address token; // ──────────────────╮ Token address + uint32 minFeeUSDCents; // │ Minimum fee to charge per token transfer, multiples of 0.01 USD + uint32 maxFeeUSDCents; // │ Maximum fee to charge per token transfer, multiples of 0.01 USD + uint16 deciBps; // ─────────────────╯ Basis points charged on token transfers, multiples of 0.1bps, or 1e-5 + uint32 destGasOverhead; // ─────────╮ Gas charged to execute the token transfer on the destination chain + // │ Extra data availability bytes that are returned from the source pool and sent + uint32 destBytesOverhead; // │ to the destination pool. Must be >= Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + bool aggregateRateLimitEnabled; // ─╯ Whether this transfer token is to be included in Aggregate Rate Limiting + } + + /// @dev Nop address and weight, used to set the nops and their weights + struct NopAndWeight { + address nop; // ────╮ Address of the node operator + uint16 weight; // ──╯ Weight for nop rewards + } + + // STATIC CONFIG + string public constant override typeAndVersion = "EVM2EVMOnRamp 1.5.0-dev"; + /// @dev metadataHash is a lane-specific prefix for a message hash preimage which ensures global uniqueness + /// Ensures that 2 identical messages sent to 2 different lanes will have a distinct hash. + /// Must match the metadataHash used in computing leaf hashes offchain for the root committed in + /// the commitStore and i_metadataHash in the offRamp. + bytes32 internal immutable i_metadataHash; + /// @dev Default gas limit for a transactions that did not specify + /// a gas limit in the extraArgs. + uint64 internal immutable i_defaultTxGasLimit; + /// @dev Maximum nop fee that can accumulate in this onramp + uint96 internal immutable i_maxNopFeesJuels; + /// @dev The link token address - known to pay nops for their work + address internal immutable i_linkToken; + /// @dev The chain ID of the source chain that this contract is deployed to + uint64 internal immutable i_chainSelector; + /// @dev The chain ID of the destination chain + uint64 internal immutable i_destChainSelector; + /// @dev The address of previous-version OnRamp for this lane + /// Used to be able to provide sequencing continuity during a zero downtime upgrade. + address internal immutable i_prevOnRamp; + /// @dev The address of the RMN proxy + address internal immutable i_rmnProxy; + /// @dev The address of the token admin registry + address internal immutable i_tokenAdminRegistry; + /// @dev the maximum number of nops that can be configured at the same time. + /// Used to bound gas for loops over nops. + uint256 private constant MAX_NUMBER_OF_NOPS = 64; + + // DYNAMIC CONFIG + /// @dev The config for the onRamp + DynamicConfig internal s_dynamicConfig; + /// @dev (address nop => uint256 weight) + EnumerableMap.AddressToUintMap internal s_nops; + + /// @dev The execution fee token config that can be set by the owner or fee admin + mapping(address token => FeeTokenConfig feeTokenConfig) internal s_feeTokenConfig; + /// @dev The token transfer fee config that can be set by the owner or fee admin + mapping(address token => TokenTransferFeeConfig tranferFeeConfig) internal s_tokenTransferFeeConfig; + + // STATE + /// @dev The current nonce per sender. + /// The offramp has a corresponding s_senderNonce mapping to ensure messages + /// are executed in the same order they are sent. + mapping(address sender => uint64 nonce) internal s_senderNonce; + /// @dev The amount of LINK available to pay NOPS + uint96 internal s_nopFeesJuels; + /// @dev The combined weight of all NOPs weights + uint32 internal s_nopWeightsTotal; + /// @dev The last used sequence number. This is zero in the case where no + /// messages has been sent yet. 0 is not a valid sequence number for any + /// real transaction. + uint64 internal s_sequenceNumber; + + constructor( + StaticConfig memory staticConfig, + DynamicConfig memory dynamicConfig, + RateLimiter.Config memory rateLimiterConfig, + FeeTokenConfigArgs[] memory feeTokenConfigs, + TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs, + NopAndWeight[] memory nopsAndWeights + ) AggregateRateLimiter(rateLimiterConfig) { + if ( + staticConfig.linkToken == address(0) || staticConfig.chainSelector == 0 || staticConfig.destChainSelector == 0 + || staticConfig.defaultTxGasLimit == 0 || staticConfig.rmnProxy == address(0) + || staticConfig.tokenAdminRegistry == address(0) + ) revert InvalidConfig(); + + i_metadataHash = keccak256( + abi.encode( + Internal.EVM_2_EVM_MESSAGE_HASH, staticConfig.chainSelector, staticConfig.destChainSelector, address(this) + ) + ); + i_linkToken = staticConfig.linkToken; + i_chainSelector = staticConfig.chainSelector; + i_destChainSelector = staticConfig.destChainSelector; + i_defaultTxGasLimit = staticConfig.defaultTxGasLimit; + i_maxNopFeesJuels = staticConfig.maxNopFeesJuels; + i_prevOnRamp = staticConfig.prevOnRamp; + i_rmnProxy = staticConfig.rmnProxy; + i_tokenAdminRegistry = staticConfig.tokenAdminRegistry; + + _setDynamicConfig(dynamicConfig); + _setFeeTokenConfig(feeTokenConfigs); + _setTokenTransferFeeConfig(tokenTransferFeeConfigArgs, new address[](0)); + _setNops(nopsAndWeights); + } + + // ================================================================ + // │ Messaging │ + // ================================================================ + + /// @inheritdoc IEVM2AnyOnRamp + function getExpectedNextSequenceNumber() external view returns (uint64) { + return s_sequenceNumber + 1; + } + + /// @inheritdoc IEVM2AnyOnRamp + function getSenderNonce(address sender) external view returns (uint64) { + uint256 senderNonce = s_senderNonce[sender]; + + if (i_prevOnRamp != address(0)) { + if (senderNonce == 0) { + // If OnRamp was upgraded, check if sender has a nonce from the previous OnRamp. + return IEVM2AnyOnRamp(i_prevOnRamp).getSenderNonce(sender); + } + } + return uint64(senderNonce); + } + + /// @inheritdoc IEVM2AnyOnRampClient + function forwardFromRouter( + uint64 destChainSelector, + Client.EVM2AnyMessage calldata message, + uint256 feeTokenAmount, + address originalSender + ) external returns (bytes32) { + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(destChainSelector)))) revert CursedByRMN(); + // Validate message sender is set and allowed. Not validated in `getFee` since it is not user-driven. + if (originalSender == address(0)) revert RouterMustSetOriginalSender(); + // Router address may be zero intentionally to pause. + if (msg.sender != s_dynamicConfig.router) revert MustBeCalledByRouter(); + if (destChainSelector != i_destChainSelector) revert InvalidChainSelector(destChainSelector); + + Client.EVMExtraArgsV2 memory extraArgs = _fromBytes(message.extraArgs); + // Validate the message with various checks + uint256 numberOfTokens = message.tokenAmounts.length; + _validateMessage(message.data.length, extraArgs.gasLimit, numberOfTokens, extraArgs.allowOutOfOrderExecution); + + // Only check token value if there are tokens + if (numberOfTokens > 0) { + uint256 value; + for (uint256 i = 0; i < numberOfTokens; ++i) { + if (message.tokenAmounts[i].amount == 0) revert CannotSendZeroTokens(); + if (s_tokenTransferFeeConfig[message.tokenAmounts[i].token].aggregateRateLimitEnabled) { + value += _getTokenValue(message.tokenAmounts[i], IPriceRegistry(s_dynamicConfig.priceRegistry)); + } + } + // Rate limit on aggregated token value + if (value > 0) _rateLimitValue(value); + } + + // Convert feeToken to link if not already in link + if (message.feeToken == i_linkToken) { + // Since there is only 1b link this is safe + s_nopFeesJuels += uint96(feeTokenAmount); + } else { + // the cast from uint256 to uint96 is considered safe, uint96 can store more than max supply of link token + s_nopFeesJuels += uint96( + IPriceRegistry(s_dynamicConfig.priceRegistry).convertTokenAmount(message.feeToken, feeTokenAmount, i_linkToken) + ); + } + if (s_nopFeesJuels > i_maxNopFeesJuels) revert MaxFeeBalanceReached(); + + if (i_prevOnRamp != address(0)) { + if (s_senderNonce[originalSender] == 0) { + // If this is first time send for a sender in new OnRamp, check if they have a nonce + // from the previous OnRamp and start from there instead of zero. + s_senderNonce[originalSender] = IEVM2AnyOnRamp(i_prevOnRamp).getSenderNonce(originalSender); + } + } + + // We need the next available sequence number so we increment before we use the value + Internal.EVM2EVMMessage memory newMessage = Internal.EVM2EVMMessage({ + sourceChainSelector: i_chainSelector, + sender: originalSender, + // EVM destination addresses should be abi encoded and therefore always 32 bytes long + // Not duplicately validated in `getFee`. Invalid address is uncommon, gas cost outweighs UX gain. + receiver: Internal._validateEVMAddress(message.receiver), + sequenceNumber: ++s_sequenceNumber, + gasLimit: extraArgs.gasLimit, + strict: false, + // Only bump nonce for messages that specify allowOutOfOrderExecution == false. Otherwise, we + // may block ordered message nonces, which is not what we want. + nonce: extraArgs.allowOutOfOrderExecution ? 0 : ++s_senderNonce[originalSender], + feeToken: message.feeToken, + feeTokenAmount: feeTokenAmount, + data: message.data, + tokenAmounts: message.tokenAmounts, + sourceTokenData: new bytes[](numberOfTokens), // will be populated below + messageId: "" + }); + + // Lock the tokens as last step. TokenPools may not always be trusted. + // There should be no state changes after external call to TokenPools. + for (uint256 i = 0; i < numberOfTokens; ++i) { + Client.EVMTokenAmount memory tokenAndAmount = message.tokenAmounts[i]; + IPoolV1 sourcePool = getPoolBySourceToken(destChainSelector, IERC20(tokenAndAmount.token)); + // We don't have to check if it supports the pool version in a non-reverting way here because + // if we revert here, there is no effect on CCIP. Therefore we directly call the supportsInterface + // function and not through the ERC165Checker. + if (address(sourcePool) == address(0) || !sourcePool.supportsInterface(Pool.CCIP_POOL_V1)) { + revert UnsupportedToken(tokenAndAmount.token); + } + + Pool.LockOrBurnOutV1 memory poolReturnData = sourcePool.lockOrBurn( + Pool.LockOrBurnInV1({ + receiver: message.receiver, + remoteChainSelector: i_destChainSelector, + originalSender: originalSender, + amount: tokenAndAmount.amount, + localToken: tokenAndAmount.token + }) + ); + + // Since the DON has to pay for the extraData to be included on the destination chain, we cap the length of the + // extraData. This prevents gas bomb attacks on the NOPs. As destBytesOverhead accounts for both + // extraData and offchainData, this caps the worst case abuse to the number of bytes reserved for offchainData. + if (poolReturnData.destPoolData.length > Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) { + if (poolReturnData.destPoolData.length > s_tokenTransferFeeConfig[tokenAndAmount.token].destBytesOverhead) { + revert SourceTokenDataTooLarge(tokenAndAmount.token); + } + } + // We validate the token address to ensure it is a valid EVM address + Internal._validateEVMAddress(poolReturnData.destTokenAddress); + + newMessage.sourceTokenData[i] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(sourcePool), + destTokenAddress: poolReturnData.destTokenAddress, + extraData: poolReturnData.destPoolData + }) + ); + } + + // Hash only after the sourceTokenData has been set + newMessage.messageId = Internal._hash(newMessage, i_metadataHash); + + // Emit message request + // This must happen after any pool events as some tokens (e.g. USDC) emit events that we expect to precede this + // event in the offchain code. + emit CCIPSendRequested(newMessage); + return newMessage.messageId; + } + + /// @dev Convert the extra args bytes into a struct + /// @param extraArgs The extra args bytes + /// @return The extra args struct + function _fromBytes(bytes calldata extraArgs) internal view returns (Client.EVMExtraArgsV2 memory) { + if (extraArgs.length == 0) { + return Client.EVMExtraArgsV2({gasLimit: i_defaultTxGasLimit, allowOutOfOrderExecution: false}); + } + + bytes4 extraArgsTag = bytes4(extraArgs); + if (extraArgsTag == Client.EVM_EXTRA_ARGS_V2_TAG) { + return abi.decode(extraArgs[4:], (Client.EVMExtraArgsV2)); + } else if (extraArgsTag == Client.EVM_EXTRA_ARGS_V1_TAG) { + // EVMExtraArgsV1 originally included a second boolean (strict) field which has been deprecated. + // Clients may still include it but it will be ignored. + return Client.EVMExtraArgsV2({gasLimit: abi.decode(extraArgs[4:], (uint256)), allowOutOfOrderExecution: false}); + } + + revert InvalidExtraArgsTag(); + } + + /// @notice Validate the forwarded message with various checks. + /// @dev This function can be called multiple times during a CCIPSend, + /// only common user-driven mistakes are validated here to minimize duplicate validation cost. + /// @param dataLength The length of the data field of the message. + /// @param gasLimit The gasLimit set in message for destination execution. + /// @param numberOfTokens The number of tokens to be sent. + function _validateMessage( + uint256 dataLength, + uint256 gasLimit, + uint256 numberOfTokens, + bool allowOutOfOrderExecution + ) internal view { + uint256 maxDataBytes = uint256(s_dynamicConfig.maxDataBytes); + if (dataLength > maxDataBytes) revert MessageTooLarge(maxDataBytes, dataLength); + if (gasLimit > uint256(s_dynamicConfig.maxPerMsgGasLimit)) revert MessageGasLimitTooHigh(); + if (numberOfTokens > uint256(s_dynamicConfig.maxNumberOfTokensPerMsg)) revert UnsupportedNumberOfTokens(); + if (!allowOutOfOrderExecution) { + if (s_dynamicConfig.enforceOutOfOrder) { + revert ExtraArgOutOfOrderExecutionMustBeTrue(); + } + } + } + + // ================================================================ + // │ Config │ + // ================================================================ + + /// @notice Returns the static onRamp config. + /// @dev RMN depends on this function, if changing, please notify the RMN maintainers. + /// @return the configuration. + function getStaticConfig() external view returns (StaticConfig memory) { + return StaticConfig({ + linkToken: i_linkToken, + chainSelector: i_chainSelector, + destChainSelector: i_destChainSelector, + defaultTxGasLimit: i_defaultTxGasLimit, + maxNopFeesJuels: i_maxNopFeesJuels, + prevOnRamp: i_prevOnRamp, + rmnProxy: i_rmnProxy, + tokenAdminRegistry: i_tokenAdminRegistry + }); + } + + /// @notice Returns the dynamic onRamp config. + /// @return dynamicConfig the configuration. + function getDynamicConfig() external view returns (DynamicConfig memory dynamicConfig) { + return s_dynamicConfig; + } + + /// @notice Sets the dynamic configuration. + /// @param dynamicConfig The configuration. + function setDynamicConfig(DynamicConfig memory dynamicConfig) external onlyOwner { + _setDynamicConfig(dynamicConfig); + } + + /// @notice Internal version of setDynamicConfig to allow for reuse in the constructor. + function _setDynamicConfig(DynamicConfig memory dynamicConfig) internal { + // We permit router to be set to zero as a way to pause the contract. + if (dynamicConfig.priceRegistry == address(0)) revert InvalidConfig(); + if (dynamicConfig.defaultTokenDestBytesOverhead < Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) { + revert InvalidDestBytesOverhead(address(0), dynamicConfig.defaultTokenDestBytesOverhead); + } + + s_dynamicConfig = dynamicConfig; + + emit ConfigSet( + StaticConfig({ + linkToken: i_linkToken, + chainSelector: i_chainSelector, + destChainSelector: i_destChainSelector, + defaultTxGasLimit: i_defaultTxGasLimit, + maxNopFeesJuels: i_maxNopFeesJuels, + prevOnRamp: i_prevOnRamp, + rmnProxy: i_rmnProxy, + tokenAdminRegistry: i_tokenAdminRegistry + }), + dynamicConfig + ); + } + + // ================================================================ + // │ Tokens and pools │ + // ================================================================ + + /// @inheritdoc IEVM2AnyOnRampClient + function getPoolBySourceToken(uint64, /*destChainSelector*/ IERC20 sourceToken) public view returns (IPoolV1) { + return IPoolV1(ITokenAdminRegistry(i_tokenAdminRegistry).getPool(address(sourceToken))); + } + + /// @inheritdoc IEVM2AnyOnRampClient + function getSupportedTokens(uint64) external pure returns (address[] memory) { + revert GetSupportedTokensFunctionalityRemovedCheckAdminRegistry(); + } + + // ================================================================ + // │ Fees │ + // ================================================================ + + /// @inheritdoc IEVM2AnyOnRampClient + /// @dev getFee MUST revert if the feeToken is not listed in the fee token config, as the router assumes it does. + /// @param destChainSelector The destination chain selector. + /// @param message The message to get quote for. + /// @return feeTokenAmount The amount of fee token needed for the fee, in smallest denomination of the fee token. + function getFee( + uint64 destChainSelector, + Client.EVM2AnyMessage calldata message + ) external view returns (uint256 feeTokenAmount) { + if (destChainSelector != i_destChainSelector) revert InvalidChainSelector(destChainSelector); + + Client.EVMExtraArgsV2 memory extraArgs = _fromBytes(message.extraArgs); + // Validate the message with various checks + _validateMessage( + message.data.length, extraArgs.gasLimit, message.tokenAmounts.length, extraArgs.allowOutOfOrderExecution + ); + + FeeTokenConfig memory feeTokenConfig = s_feeTokenConfig[message.feeToken]; + if (!feeTokenConfig.enabled) revert NotAFeeToken(message.feeToken); + + (uint224 feeTokenPrice, uint224 packedGasPrice) = + IPriceRegistry(s_dynamicConfig.priceRegistry).getTokenAndGasPrices(message.feeToken, destChainSelector); + + // Calculate premiumFee in USD with 18 decimals precision first. + // If message-only and no token transfers, a flat network fee is charged. + // If there are token transfers, premiumFee is calculated from token transfer fee. + // If there are both token transfers and message, premiumFee is only calculated from token transfer fee. + uint256 premiumFee = 0; + uint32 tokenTransferGas = 0; + uint32 tokenTransferBytesOverhead = 0; + if (message.tokenAmounts.length > 0) { + (premiumFee, tokenTransferGas, tokenTransferBytesOverhead) = + _getTokenTransferCost(message.feeToken, feeTokenPrice, message.tokenAmounts); + } else { + // Convert USD cents with 2 decimals to 18 decimals. + premiumFee = uint256(feeTokenConfig.networkFeeUSDCents) * 1e16; + } + + // Calculate data availability cost in USD with 36 decimals. Data availability cost exists on rollups that need to post + // transaction calldata onto another storage layer, e.g. Eth mainnet, incurring additional storage gas costs. + uint256 dataAvailabilityCost = 0; + // Only calculate data availability cost if data availability multiplier is non-zero. + // The multiplier should be set to 0 if destination chain does not charge data availability cost. + if (s_dynamicConfig.destDataAvailabilityMultiplierBps > 0) { + dataAvailabilityCost = _getDataAvailabilityCost( + // Parse the data availability gas price stored in the higher-order 112 bits of the encoded gas price. + uint112(packedGasPrice >> Internal.GAS_PRICE_BITS), + message.data.length, + message.tokenAmounts.length, + tokenTransferBytesOverhead + ); + } + + // Calculate execution gas fee on destination chain in USD with 36 decimals. + // We add the message gas limit, the overhead gas, the gas of passing message data to receiver, and token transfer gas together. + // We then multiply this gas total with the gas multiplier and gas price, converting it into USD with 36 decimals. + // uint112(packedGasPrice) = executionGasPrice + uint256 executionCost = uint112(packedGasPrice) + * ( + extraArgs.gasLimit + s_dynamicConfig.destGasOverhead + + (message.data.length * s_dynamicConfig.destGasPerPayloadByte) + tokenTransferGas + ) * feeTokenConfig.gasMultiplierWeiPerEth; + + // Calculate number of fee tokens to charge. + // Total USD fee is in 36 decimals, feeTokenPrice is in 18 decimals USD for 1e18 smallest token denominations. + // Result of the division is the number of smallest token denominations. + return + ((premiumFee * feeTokenConfig.premiumMultiplierWeiPerEth) + executionCost + dataAvailabilityCost) / feeTokenPrice; + } + + /// @notice Returns the estimated data availability cost of the message. + /// @dev To save on gas, we use a single destGasPerDataAvailabilityByte value for both zero and non-zero bytes. + /// @param dataAvailabilityGasPrice USD per data availability gas in 18 decimals. + /// @param messageDataLength length of the data field in the message. + /// @param numberOfTokens number of distinct token transfers in the message. + /// @param tokenTransferBytesOverhead additional token transfer data passed to destination, e.g. USDC attestation. + /// @return dataAvailabilityCostUSD36Decimal total data availability cost in USD with 36 decimals. + function _getDataAvailabilityCost( + uint112 dataAvailabilityGasPrice, + uint256 messageDataLength, + uint256 numberOfTokens, + uint32 tokenTransferBytesOverhead + ) internal view returns (uint256 dataAvailabilityCostUSD36Decimal) { + // dataAvailabilityLengthBytes sums up byte lengths of fixed message fields and dynamic message fields. + // Fixed message fields do account for the offset and length slot of the dynamic fields. + uint256 dataAvailabilityLengthBytes = Internal.MESSAGE_FIXED_BYTES + messageDataLength + + (numberOfTokens * Internal.MESSAGE_FIXED_BYTES_PER_TOKEN) + tokenTransferBytesOverhead; + + // destDataAvailabilityOverheadGas is a separate config value for flexibility to be updated independently of message cost. + // Its value is determined by CCIP lane implementation, e.g. the overhead data posted for OCR. + uint256 dataAvailabilityGas = (dataAvailabilityLengthBytes * s_dynamicConfig.destGasPerDataAvailabilityByte) + + s_dynamicConfig.destDataAvailabilityOverheadGas; + + // dataAvailabilityGasPrice is in 18 decimals, destDataAvailabilityMultiplierBps is in 4 decimals + // We pad 14 decimals to bring the result to 36 decimals, in line with token bps and execution fee. + return ((dataAvailabilityGas * dataAvailabilityGasPrice) * s_dynamicConfig.destDataAvailabilityMultiplierBps) * 1e14; + } + + /// @notice Returns the token transfer cost parameters. + /// A basis point fee is calculated from the USD value of each token transfer. + /// For each individual transfer, this fee is between [minFeeUSD, maxFeeUSD]. + /// Total transfer fee is the sum of each individual token transfer fee. + /// @dev Assumes that tokenAmounts are validated to be listed tokens elsewhere. + /// @dev Splitting one token transfer into multiple transfers is discouraged, + /// as it will result in a transferFee equal or greater than the same amount aggregated/de-duped. + /// @param feeToken address of the feeToken. + /// @param feeTokenPrice price of feeToken in USD with 18 decimals. + /// @param tokenAmounts token transfers in the message. + /// @return tokenTransferFeeUSDWei total token transfer bps fee in USD with 18 decimals. + /// @return tokenTransferGas total execution gas of the token transfers. + /// @return tokenTransferBytesOverhead additional token transfer data passed to destination, e.g. USDC attestation. + function _getTokenTransferCost( + address feeToken, + uint224 feeTokenPrice, + Client.EVMTokenAmount[] calldata tokenAmounts + ) internal view returns (uint256 tokenTransferFeeUSDWei, uint32 tokenTransferGas, uint32 tokenTransferBytesOverhead) { + uint256 numberOfTokens = tokenAmounts.length; + + for (uint256 i = 0; i < numberOfTokens; ++i) { + Client.EVMTokenAmount memory tokenAmount = tokenAmounts[i]; + + // Validate if the token is supported, do not calculate fee for unsupported tokens. + if (address(getPoolBySourceToken(i_destChainSelector, IERC20(tokenAmount.token))) == address(0)) { + revert UnsupportedToken(tokenAmount.token); + } + + TokenTransferFeeConfig memory transferFeeConfig = s_tokenTransferFeeConfig[tokenAmount.token]; + + // If the token has no specific overrides configured, we use the global defaults. + if (!transferFeeConfig.isEnabled) { + tokenTransferFeeUSDWei += uint256(s_dynamicConfig.defaultTokenFeeUSDCents) * 1e16; + tokenTransferGas += s_dynamicConfig.defaultTokenDestGasOverhead; + tokenTransferBytesOverhead += s_dynamicConfig.defaultTokenDestBytesOverhead; + continue; + } + + uint256 bpsFeeUSDWei = 0; + // Only calculate bps fee if ratio is greater than 0. Ratio of 0 means no bps fee for a token. + // Useful for when the PriceRegistry cannot return a valid price for the token. + if (transferFeeConfig.deciBps > 0) { + uint224 tokenPrice = 0; + if (tokenAmount.token != feeToken) { + tokenPrice = IPriceRegistry(s_dynamicConfig.priceRegistry).getValidatedTokenPrice(tokenAmount.token); + } else { + tokenPrice = feeTokenPrice; + } + + // Calculate token transfer value, then apply fee ratio + // ratio represents multiples of 0.1bps, or 1e-5 + bpsFeeUSDWei = (tokenPrice._calcUSDValueFromTokenAmount(tokenAmount.amount) * transferFeeConfig.deciBps) / 1e5; + } + + tokenTransferGas += transferFeeConfig.destGasOverhead; + tokenTransferBytesOverhead += transferFeeConfig.destBytesOverhead; + + // Bps fees should be kept within range of [minFeeUSD, maxFeeUSD]. + // Convert USD values with 2 decimals to 18 decimals. + uint256 minFeeUSDWei = uint256(transferFeeConfig.minFeeUSDCents) * 1e16; + if (bpsFeeUSDWei < minFeeUSDWei) { + tokenTransferFeeUSDWei += minFeeUSDWei; + continue; + } + + uint256 maxFeeUSDWei = uint256(transferFeeConfig.maxFeeUSDCents) * 1e16; + if (bpsFeeUSDWei > maxFeeUSDWei) { + tokenTransferFeeUSDWei += maxFeeUSDWei; + continue; + } + + tokenTransferFeeUSDWei += bpsFeeUSDWei; + } + + return (tokenTransferFeeUSDWei, tokenTransferGas, tokenTransferBytesOverhead); + } + + /// @notice Gets the fee configuration for a token + /// @param token The token to get the fee configuration for + /// @return feeTokenConfig FeeTokenConfig struct + function getFeeTokenConfig(address token) external view returns (FeeTokenConfig memory feeTokenConfig) { + return s_feeTokenConfig[token]; + } + + /// @notice Sets the fee configuration for a token + /// @param feeTokenConfigArgs Array of FeeTokenConfigArgs structs. + function setFeeTokenConfig(FeeTokenConfigArgs[] memory feeTokenConfigArgs) external { + _onlyOwnerOrAdmin(); + _setFeeTokenConfig(feeTokenConfigArgs); + } + + /// @dev Set the fee config + /// @param feeTokenConfigArgs The fee token configs. + function _setFeeTokenConfig(FeeTokenConfigArgs[] memory feeTokenConfigArgs) internal { + for (uint256 i = 0; i < feeTokenConfigArgs.length; ++i) { + FeeTokenConfigArgs memory configArg = feeTokenConfigArgs[i]; + + s_feeTokenConfig[configArg.token] = FeeTokenConfig({ + networkFeeUSDCents: configArg.networkFeeUSDCents, + gasMultiplierWeiPerEth: configArg.gasMultiplierWeiPerEth, + premiumMultiplierWeiPerEth: configArg.premiumMultiplierWeiPerEth, + enabled: configArg.enabled + }); + } + emit FeeConfigSet(feeTokenConfigArgs); + } + + /// @notice Gets the transfer fee config for a given token. + function getTokenTransferFeeConfig(address token) + external + view + returns (TokenTransferFeeConfig memory tokenTransferFeeConfig) + { + return s_tokenTransferFeeConfig[token]; + } + + /// @notice Sets the transfer fee config. + /// @dev only callable by the owner or admin. + function setTokenTransferFeeConfig( + TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs, + address[] memory tokensToUseDefaultFeeConfigs + ) external { + _onlyOwnerOrAdmin(); + _setTokenTransferFeeConfig(tokenTransferFeeConfigArgs, tokensToUseDefaultFeeConfigs); + } + + /// @notice internal helper to set the token transfer fee config. + function _setTokenTransferFeeConfig( + TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs, + address[] memory tokensToUseDefaultFeeConfigs + ) internal { + for (uint256 i = 0; i < tokenTransferFeeConfigArgs.length; ++i) { + TokenTransferFeeConfigArgs memory configArg = tokenTransferFeeConfigArgs[i]; + + if (configArg.destBytesOverhead < Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) { + revert InvalidDestBytesOverhead(configArg.token, configArg.destBytesOverhead); + } + + s_tokenTransferFeeConfig[configArg.token] = TokenTransferFeeConfig({ + minFeeUSDCents: configArg.minFeeUSDCents, + maxFeeUSDCents: configArg.maxFeeUSDCents, + deciBps: configArg.deciBps, + destGasOverhead: configArg.destGasOverhead, + destBytesOverhead: configArg.destBytesOverhead, + aggregateRateLimitEnabled: configArg.aggregateRateLimitEnabled, + isEnabled: true + }); + } + emit TokenTransferFeeConfigSet(tokenTransferFeeConfigArgs); + + // Remove the custom fee configs for the tokens that are in the tokensToUseDefaultFeeConfigs array + for (uint256 i = 0; i < tokensToUseDefaultFeeConfigs.length; ++i) { + delete s_tokenTransferFeeConfig[tokensToUseDefaultFeeConfigs[i]]; + } + if (tokensToUseDefaultFeeConfigs.length > 0) { + emit TokenTransferFeeConfigDeleted(tokensToUseDefaultFeeConfigs); + } + } + + // ================================================================ + // │ NOP payments │ + // ================================================================ + + /// @notice Get the total amount of fees to be paid to the Nops (in LINK) + /// @return totalNopFees + function getNopFeesJuels() external view returns (uint96) { + return s_nopFeesJuels; + } + + /// @notice Gets the Nops and their weights + /// @return nopsAndWeights Array of NopAndWeight structs + /// @return weightsTotal The sum weight of all Nops + function getNops() external view returns (NopAndWeight[] memory nopsAndWeights, uint256 weightsTotal) { + uint256 length = s_nops.length(); + nopsAndWeights = new NopAndWeight[](length); + for (uint256 i = 0; i < length; ++i) { + (address nopAddress, uint256 nopWeight) = s_nops.at(i); + nopsAndWeights[i] = NopAndWeight({nop: nopAddress, weight: uint16(nopWeight)}); + } + weightsTotal = s_nopWeightsTotal; + return (nopsAndWeights, weightsTotal); + } + + /// @notice Sets the Nops and their weights + /// @param nopsAndWeights Array of NopAndWeight structs + function setNops(NopAndWeight[] calldata nopsAndWeights) external { + _onlyOwnerOrAdmin(); + _setNops(nopsAndWeights); + } + + /// @param nopsAndWeights New set of nops and weights + /// @dev Clears existing nops, sets new nops and weights + /// @dev We permit fees to accrue before nops are configured, in which case + /// they will go to the first set of configured nops. + function _setNops(NopAndWeight[] memory nopsAndWeights) internal { + uint256 numberOfNops = nopsAndWeights.length; + if (numberOfNops > MAX_NUMBER_OF_NOPS) revert TooManyNops(); + + // Make sure all nops have been paid before removing nops + // We only have to pay when there are nops and there is enough + // outstanding NOP balance to trigger a payment. + if (s_nopWeightsTotal > 0) { + if (s_nopFeesJuels >= s_nopWeightsTotal) { + payNops(); + } + } + + // Remove all previous nops, move from end to start to avoid shifting + for (uint256 i = s_nops.length(); i > 0; --i) { + (address nop,) = s_nops.at(i - 1); + s_nops.remove(nop); + } + + // Add new + uint32 nopWeightsTotal = 0; + // nopWeightsTotal is bounded by the MAX_NUMBER_OF_NOPS and the weight of + // a single nop being of type uint16. This ensures nopWeightsTotal will + // always fit into the uint32 type. + for (uint256 i = 0; i < numberOfNops; ++i) { + // Make sure the LINK token is not a nop because the link token doesn't allow + // self transfers. If set as nop, payNops would always revert. Since setNops + // calls payNops, we can never remove the LINK token as a nop. + address nop = nopsAndWeights[i].nop; + uint16 weight = nopsAndWeights[i].weight; + if (nop == i_linkToken || nop == address(0)) revert InvalidNopAddress(nop); + s_nops.set(nop, weight); + nopWeightsTotal += weight; + } + s_nopWeightsTotal = nopWeightsTotal; + emit NopsSet(nopWeightsTotal, nopsAndWeights); + } + + /// @notice Pays the Node Ops their outstanding balances. + /// @dev some balance can remain after payments are done. This is at most the sum + /// of the weight of all nops. Since nop weights are uint16s and we can have at + /// most MAX_NUMBER_OF_NOPS NOPs, the highest possible value is 2**22 or 0.04 gjuels. + function payNops() public { + if (msg.sender != owner()) { + if (msg.sender != s_admin) { + if (!s_nops.contains(msg.sender)) { + revert OnlyCallableByOwnerOrAdminOrNop(); + } + } + } + uint256 weightsTotal = s_nopWeightsTotal; + if (weightsTotal == 0) revert NoNopsToPay(); + + uint96 totalFeesToPay = s_nopFeesJuels; + if (totalFeesToPay < weightsTotal) revert NoFeesToPay(); + if (linkAvailableForPayment() < 0) revert InsufficientBalance(); + + uint96 fundsLeft = totalFeesToPay; + uint256 numberOfNops = s_nops.length(); + for (uint256 i = 0; i < numberOfNops; ++i) { + (address nop, uint256 weight) = s_nops.at(i); + // amount can never be higher than totalFeesToPay so the cast to uint96 is safe + uint96 amount = uint96((totalFeesToPay * weight) / weightsTotal); + fundsLeft -= amount; + IERC20(i_linkToken).safeTransfer(nop, amount); + emit NopPaid(nop, amount); + } + // Some funds can remain, since this is an incredibly small + // amount we consider this OK. + s_nopFeesJuels = fundsLeft; + } + + /// @notice Allows the owner to withdraw any ERC20 token from the contract. + /// The NOP link balance is not withdrawable. + /// @param feeToken The token to withdraw + /// @param to The address to send the tokens to + function withdrawNonLinkFees(address feeToken, address to) external { + _onlyOwnerOrAdmin(); + if (to == address(0)) revert InvalidWithdrawParams(); + + // We require the link balance to be settled before allowing withdrawal of non-link fees. + int256 linkAfterNopFees = linkAvailableForPayment(); + if (linkAfterNopFees < 0) revert LinkBalanceNotSettled(); + + if (feeToken == i_linkToken) { + // Withdraw only the left over link balance + IERC20(feeToken).safeTransfer(to, uint256(linkAfterNopFees)); + } else { + // Withdrawal all non-link tokens in the contract + IERC20(feeToken).safeTransfer(to, IERC20(feeToken).balanceOf(address(this))); + } + } + + // ================================================================ + // │ Link monitoring │ + // ================================================================ + + /// @notice Calculate remaining LINK balance after paying nops + /// @dev Allow keeper to monitor funds available for paying nops + /// @return balance if nops were to be paid + function linkAvailableForPayment() public view returns (int256) { + // Since LINK caps at uint96, casting to int256 is safe + return int256(IERC20(i_linkToken).balanceOf(address(this))) - int256(uint256(s_nopFeesJuels)); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @dev Require that the sender is the owner or the fee admin + /// Not a modifier to save on contract size + function _onlyOwnerOrAdmin() internal view { + if (msg.sender != owner()) { + if (msg.sender != s_admin) { + revert OnlyCallableByOwnerOrAdmin(); + } + } + } +} diff --git a/contracts/src/v0.8/ccip/pools/BurnFromMintTokenPool.sol b/contracts/src/v0.8/ccip/pools/BurnFromMintTokenPool.sol new file mode 100644 index 00000000000..de68b18a302 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/BurnFromMintTokenPool.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {BurnMintTokenPoolAbstract} from "./BurnMintTokenPoolAbstract.sol"; +import {TokenPool} from "./TokenPool.sol"; + +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice This pool mints and burns a 3rd-party token. +/// @dev Pool whitelisting mode is set in the constructor and cannot be modified later. +/// It either accepts any address as originalSender, or only accepts whitelisted originalSender. +/// The only way to change whitelisting mode is to deploy a new pool. +/// If that is expected, please make sure the token's burner/minter roles are adjustable. +/// @dev This contract is a variant of BurnMintTokenPool that uses `burnFrom(from, amount)`. +contract BurnFromMintTokenPool is BurnMintTokenPoolAbstract, ITypeAndVersion { + using SafeERC20 for IBurnMintERC20; + + string public constant override typeAndVersion = "BurnFromMintTokenPool 1.5.0-dev"; + + constructor( + IBurnMintERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) TokenPool(token, allowlist, rmnProxy, router) { + // Some tokens allow burning from the sender without approval, but not all do. + // To be safe, we approve the pool to burn from the pool. + token.safeIncreaseAllowance(address(this), type(uint256).max); + } + + /// @inheritdoc BurnMintTokenPoolAbstract + function _burn(uint256 amount) internal virtual override { + IBurnMintERC20(address(i_token)).burnFrom(address(this), amount); + } +} diff --git a/contracts/src/v0.8/ccip/pools/BurnMintTokenPool.sol b/contracts/src/v0.8/ccip/pools/BurnMintTokenPool.sol new file mode 100644 index 00000000000..a8562ae4d36 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/BurnMintTokenPool.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {BurnMintTokenPoolAbstract} from "./BurnMintTokenPoolAbstract.sol"; +import {TokenPool} from "./TokenPool.sol"; + +/// @notice This pool mints and burns a 3rd-party token. +/// @dev Pool whitelisting mode is set in the constructor and cannot be modified later. +/// It either accepts any address as originalSender, or only accepts whitelisted originalSender. +/// The only way to change whitelisting mode is to deploy a new pool. +/// If that is expected, please make sure the token's burner/minter roles are adjustable. +/// @dev This contract is a variant of BurnMintTokenPool that uses `burn(amount)`. +contract BurnMintTokenPool is BurnMintTokenPoolAbstract, ITypeAndVersion { + string public constant override typeAndVersion = "BurnMintTokenPool 1.5.0-dev"; + + constructor( + IBurnMintERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) TokenPool(token, allowlist, rmnProxy, router) {} + + /// @inheritdoc BurnMintTokenPoolAbstract + function _burn(uint256 amount) internal virtual override { + IBurnMintERC20(address(i_token)).burn(amount); + } +} diff --git a/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol b/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol new file mode 100644 index 00000000000..2085c9427b0 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {Pool} from "../libraries/Pool.sol"; +import {TokenPool} from "./TokenPool.sol"; + +abstract contract BurnMintTokenPoolAbstract is TokenPool { + /// @notice Contains the specific burn call for a pool. + /// @dev overriding this method allows us to create pools with different burn signatures + /// without duplicating the underlying logic. + function _burn(uint256 amount) internal virtual; + + /// @notice Burn the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + virtual + override + returns (Pool.LockOrBurnOutV1 memory) + { + _validateLockOrBurn(lockOrBurnIn); + + _burn(lockOrBurnIn.amount); + + emit Burned(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + /// @notice Mint tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + virtual + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + _validateReleaseOrMint(releaseOrMintIn); + + // Mint to the offRamp, which forwards it to the recipient + IBurnMintERC20(address(i_token)).mint(msg.sender, releaseOrMintIn.amount); + + emit Minted(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } +} diff --git a/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAndProxy.sol b/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAndProxy.sol new file mode 100644 index 00000000000..a3a7e082cc7 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAndProxy.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {Pool} from "../libraries/Pool.sol"; +import {LegacyPoolWrapper} from "./LegacyPoolWrapper.sol"; + +contract BurnMintTokenPoolAndProxy is ITypeAndVersion, LegacyPoolWrapper { + string public constant override typeAndVersion = "BurnMintTokenPoolAndProxy 1.5.0-dev"; + + constructor( + IBurnMintERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) LegacyPoolWrapper(token, allowlist, rmnProxy, router) {} + + /// @notice Burn the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + virtual + override + returns (Pool.LockOrBurnOutV1 memory) + { + _validateLockOrBurn(lockOrBurnIn); + + if (!_hasLegacyPool()) { + IBurnMintERC20(address(i_token)).burn(lockOrBurnIn.amount); + } else { + _lockOrBurnLegacy(lockOrBurnIn); + } + + emit Burned(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + /// @notice Mint tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + virtual + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + _validateReleaseOrMint(releaseOrMintIn); + + if (!_hasLegacyPool()) { + // Mint to the offRamp, which forwards it to the recipient + IBurnMintERC20(address(i_token)).mint(msg.sender, releaseOrMintIn.amount); + } else { + _releaseOrMintLegacy(releaseOrMintIn); + } + + emit Minted(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } +} diff --git a/contracts/src/v0.8/ccip/pools/BurnWithFromMintTokenPool.sol b/contracts/src/v0.8/ccip/pools/BurnWithFromMintTokenPool.sol new file mode 100644 index 00000000000..33f6c43c5b0 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/BurnWithFromMintTokenPool.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {BurnMintTokenPoolAbstract} from "./BurnMintTokenPoolAbstract.sol"; +import {TokenPool} from "./TokenPool.sol"; + +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice This pool mints and burns a 3rd-party token. +/// @dev Pool whitelisting mode is set in the constructor and cannot be modified later. +/// It either accepts any address as originalSender, or only accepts whitelisted originalSender. +/// The only way to change whitelisting mode is to deploy a new pool. +/// If that is expected, please make sure the token's burner/minter roles are adjustable. +/// @dev This contract is a variant of BurnMintTokenPool that uses `burn(from, amount)`. +contract BurnWithFromMintTokenPool is BurnMintTokenPoolAbstract, ITypeAndVersion { + using SafeERC20 for IBurnMintERC20; + + string public constant override typeAndVersion = "BurnWithFromMintTokenPool 1.5.0-dev"; + + constructor( + IBurnMintERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) TokenPool(token, allowlist, rmnProxy, router) { + // Some tokens allow burning from the sender without approval, but not all do. + // To be safe, we approve the pool to burn from the pool. + token.safeIncreaseAllowance(address(this), type(uint256).max); + } + + /// @inheritdoc BurnMintTokenPoolAbstract + function _burn(uint256 amount) internal virtual override { + IBurnMintERC20(address(i_token)).burn(address(this), amount); + } +} diff --git a/contracts/src/v0.8/ccip/pools/LegacyPoolWrapper.sol b/contracts/src/v0.8/ccip/pools/LegacyPoolWrapper.sol new file mode 100644 index 00000000000..125a3a28ee4 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/LegacyPoolWrapper.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IPoolPriorTo1_5} from "../interfaces/IPoolPriorTo1_5.sol"; + +import {Pool} from "../libraries/Pool.sol"; +import {TokenPool} from "./TokenPool.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +abstract contract LegacyPoolWrapper is TokenPool { + using SafeERC20 for IERC20; + + event LegacyPoolChanged(IPoolPriorTo1_5 oldPool, IPoolPriorTo1_5 newPool); + + /// @dev The previous pool, if there is any. This is a property to make the older 1.0-1.4 pools + /// compatible with the current 1.5 pool. To achieve this, we set the previous pool address to the + /// currently deployed legacy pool. Then we configure this new pool as onRamp and offRamp on the legacy pools. + /// In the case of a 1.4 pool, this new pool contract has to be set to the Router as well, as it validates + /// who can call it through the router calls. This contract will always return itself as the only allowed ramp. + /// @dev Can be address(0), this would indicate that this pool is operating as a normal pool as opposed to + /// a proxy pool. + IPoolPriorTo1_5 internal s_previousPool; + + constructor( + IERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) TokenPool(token, allowlist, rmnProxy, router) {} + + // ================================================================ + // │ Legacy Fallbacks │ + // ================================================================ + // Legacy fallbacks for older token pools that do not implement the new interface. + + /// @notice Legacy fallback for the 1.4 token pools. + function getOnRamp(uint64) external view returns (address onRampAddress) { + return address(this); + } + + /// @notice Return true if the given offRamp is a configured offRamp for the given source chain. + function isOffRamp(uint64 sourceChainSelector, address offRamp) external view returns (bool) { + return offRamp == address(this) || s_router.isOffRamp(sourceChainSelector, offRamp); + } + + /// @notice Configures the legacy fallback option. If the previous pool is set, this pool will act as a proxy for + /// the legacy pool. + /// @param prevPool The address of the previous pool. + function setPreviousPool(IPoolPriorTo1_5 prevPool) external onlyOwner { + IPoolPriorTo1_5 oldPrevPool = s_previousPool; + s_previousPool = prevPool; + + emit LegacyPoolChanged(oldPrevPool, prevPool); + } + + function _hasLegacyPool() internal view returns (bool) { + return address(s_previousPool) != address(0); + } + + function _lockOrBurnLegacy(Pool.LockOrBurnInV1 memory lockOrBurnIn) internal { + i_token.safeTransfer(address(s_previousPool), lockOrBurnIn.amount); + s_previousPool.lockOrBurn( + lockOrBurnIn.originalSender, lockOrBurnIn.receiver, lockOrBurnIn.amount, lockOrBurnIn.remoteChainSelector, "" + ); + } + + /// @notice This call converts the arguments from a >=1.5 pool call to those of a <1.5 pool call, and uses these + /// to call the previous pool. + /// @param releaseOrMintIn The 1.5 style release or mint arguments. + /// @dev Overwrites the receiver so the previous pool sends the tokens to the sender of this call, which is the + /// offRamp. This is due to the older pools sending funds directly to the receiver, while the new pools do a hop + /// through the offRamp to ensure the correct tokens are sent. + /// @dev Since extraData has never been used in LockRelease or MintBurn token pools, we can safely ignore it. + function _releaseOrMintLegacy(Pool.ReleaseOrMintInV1 memory releaseOrMintIn) internal { + s_previousPool.releaseOrMint( + releaseOrMintIn.originalSender, msg.sender, releaseOrMintIn.amount, releaseOrMintIn.remoteChainSelector, "" + ); + } +} diff --git a/contracts/src/v0.8/ccip/pools/LockReleaseTokenPool.sol b/contracts/src/v0.8/ccip/pools/LockReleaseTokenPool.sol new file mode 100644 index 00000000000..5716777fb5e --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/LockReleaseTokenPool.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ILiquidityContainer} from "../../liquiditymanager/interfaces/ILiquidityContainer.sol"; +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +import {Pool} from "../libraries/Pool.sol"; +import {RateLimiter} from "../libraries/RateLimiter.sol"; +import {TokenPool} from "./TokenPool.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice Token pool used for tokens on their native chain. This uses a lock and release mechanism. +/// Because of lock/unlock requiring liquidity, this pool contract also has function to add and remove +/// liquidity. This allows for proper bookkeeping for both user and liquidity provider balances. +/// @dev One token per LockReleaseTokenPool. +contract LockReleaseTokenPool is TokenPool, ILiquidityContainer, ITypeAndVersion { + using SafeERC20 for IERC20; + + error InsufficientLiquidity(); + error LiquidityNotAccepted(); + error Unauthorized(address caller); + + string public constant override typeAndVersion = "LockReleaseTokenPool 1.5.0-dev"; + + /// @dev Whether or not the pool accepts liquidity. + /// External liquidity is not required when there is one canonical token deployed to a chain, + /// and CCIP is facilitating mint/burn on all the other chains, in which case the invariant + /// balanceOf(pool) on home chain == sum(totalSupply(mint/burn "wrapped" token) on all remote chains) should always hold + bool internal immutable i_acceptLiquidity; + /// @notice The address of the rebalancer. + address internal s_rebalancer; + /// @notice The address of the rate limiter admin. + /// @dev Can be address(0) if none is configured. + address internal s_rateLimitAdmin; + + constructor( + IERC20 token, + address[] memory allowlist, + address rmnProxy, + bool acceptLiquidity, + address router + ) TokenPool(token, allowlist, rmnProxy, router) { + i_acceptLiquidity = acceptLiquidity; + } + + /// @notice Locks the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + virtual + override + returns (Pool.LockOrBurnOutV1 memory) + { + _validateLockOrBurn(lockOrBurnIn); + + emit Locked(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + /// @notice Release tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + virtual + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + _validateReleaseOrMint(releaseOrMintIn); + + // Release to the offRamp, which forwards it to the recipient + getToken().safeTransfer(msg.sender, releaseOrMintIn.amount); + + emit Released(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } + + // @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(ILiquidityContainer).interfaceId || super.supportsInterface(interfaceId); + } + + /// @notice Gets LiquidityManager, can be address(0) if none is configured. + /// @return The current liquidity manager. + function getRebalancer() external view returns (address) { + return s_rebalancer; + } + + /// @notice Sets the LiquidityManager address. + /// @dev Only callable by the owner. + function setRebalancer(address rebalancer) external onlyOwner { + s_rebalancer = rebalancer; + } + + /// @notice Sets the rate limiter admin address. + /// @dev Only callable by the owner. + /// @param rateLimitAdmin The new rate limiter admin address. + function setRateLimitAdmin(address rateLimitAdmin) external onlyOwner { + s_rateLimitAdmin = rateLimitAdmin; + } + + /// @notice Gets the rate limiter admin address. + function getRateLimitAdmin() external view returns (address) { + return s_rateLimitAdmin; + } + + /// @notice Checks if the pool can accept liquidity. + /// @return true if the pool can accept liquidity, false otherwise. + function canAcceptLiquidity() external view returns (bool) { + return i_acceptLiquidity; + } + + /// @notice Adds liquidity to the pool. The tokens should be approved first. + /// @param amount The amount of liquidity to provide. + function provideLiquidity(uint256 amount) external { + if (!i_acceptLiquidity) revert LiquidityNotAccepted(); + if (s_rebalancer != msg.sender) revert Unauthorized(msg.sender); + + i_token.safeTransferFrom(msg.sender, address(this), amount); + emit LiquidityAdded(msg.sender, amount); + } + + /// @notice Removed liquidity to the pool. The tokens will be sent to msg.sender. + /// @param amount The amount of liquidity to remove. + function withdrawLiquidity(uint256 amount) external { + if (s_rebalancer != msg.sender) revert Unauthorized(msg.sender); + + if (i_token.balanceOf(address(this)) < amount) revert InsufficientLiquidity(); + i_token.safeTransfer(msg.sender, amount); + emit LiquidityRemoved(msg.sender, amount); + } + + /// @notice Sets the rate limiter admin address. + /// @dev Only callable by the owner or the rate limiter admin. NOTE: overwrites the normal + /// onlyAdmin check in the base implementation to also allow the rate limiter admin. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config. + /// @param inboundConfig The new inbound rate limiter config. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external override { + if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } +} diff --git a/contracts/src/v0.8/ccip/pools/LockReleaseTokenPoolAndProxy.sol b/contracts/src/v0.8/ccip/pools/LockReleaseTokenPoolAndProxy.sol new file mode 100644 index 00000000000..91766d5f26a --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/LockReleaseTokenPoolAndProxy.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ILiquidityContainer} from "../../liquiditymanager/interfaces/ILiquidityContainer.sol"; +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +import {Pool} from "../libraries/Pool.sol"; +import {RateLimiter} from "../libraries/RateLimiter.sol"; +import {LegacyPoolWrapper} from "./LegacyPoolWrapper.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice Token pool used for tokens on their native chain. This uses a lock and release mechanism. +/// Because of lock/unlock requiring liquidity, this pool contract also has function to add and remove +/// liquidity. This allows for proper bookkeeping for both user and liquidity provider balances. +/// @dev One token per LockReleaseTokenPool. +contract LockReleaseTokenPoolAndProxy is LegacyPoolWrapper, ILiquidityContainer, ITypeAndVersion { + using SafeERC20 for IERC20; + + error InsufficientLiquidity(); + error LiquidityNotAccepted(); + error Unauthorized(address caller); + + string public constant override typeAndVersion = "LockReleaseTokenPoolAndProxy 1.5.0-dev"; + + /// @dev Whether or not the pool accepts liquidity. + /// External liquidity is not required when there is one canonical token deployed to a chain, + /// and CCIP is facilitating mint/burn on all the other chains, in which case the invariant + /// balanceOf(pool) on home chain == sum(totalSupply(mint/burn "wrapped" token) on all remote chains) should always hold + bool internal immutable i_acceptLiquidity; + /// @notice The address of the rebalancer. + address internal s_rebalancer; + /// @notice The address of the rate limiter admin. + /// @dev Can be address(0) if none is configured. + address internal s_rateLimitAdmin; + + constructor( + IERC20 token, + address[] memory allowlist, + address rmnProxy, + bool acceptLiquidity, + address router + ) LegacyPoolWrapper(token, allowlist, rmnProxy, router) { + i_acceptLiquidity = acceptLiquidity; + } + + /// @notice Locks the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + virtual + override + returns (Pool.LockOrBurnOutV1 memory) + { + _validateLockOrBurn(lockOrBurnIn); + + if (_hasLegacyPool()) { + _lockOrBurnLegacy(lockOrBurnIn); + } + + emit Locked(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + /// @notice Release tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + virtual + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + _validateReleaseOrMint(releaseOrMintIn); + + if (!_hasLegacyPool()) { + // Release to the offRamp, which forwards it to the recipient + getToken().safeTransfer(msg.sender, releaseOrMintIn.amount); + } else { + _releaseOrMintLegacy(releaseOrMintIn); + } + + emit Released(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } + + // @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(ILiquidityContainer).interfaceId || super.supportsInterface(interfaceId); + } + + /// @notice Gets LiquidityManager, can be address(0) if none is configured. + /// @return The current liquidity manager. + function getRebalancer() external view returns (address) { + return s_rebalancer; + } + + /// @notice Sets the LiquidityManager address. + /// @dev Only callable by the owner. + function setRebalancer(address rebalancer) external onlyOwner { + s_rebalancer = rebalancer; + } + + /// @notice Sets the rate limiter admin address. + /// @dev Only callable by the owner. + /// @param rateLimitAdmin The new rate limiter admin address. + function setRateLimitAdmin(address rateLimitAdmin) external onlyOwner { + s_rateLimitAdmin = rateLimitAdmin; + } + + /// @notice Gets the rate limiter admin address. + function getRateLimitAdmin() external view returns (address) { + return s_rateLimitAdmin; + } + + /// @notice Checks if the pool can accept liquidity. + /// @return true if the pool can accept liquidity, false otherwise. + function canAcceptLiquidity() external view returns (bool) { + return i_acceptLiquidity; + } + + /// @notice Adds liquidity to the pool. The tokens should be approved first. + /// @param amount The amount of liquidity to provide. + function provideLiquidity(uint256 amount) external { + if (!i_acceptLiquidity) revert LiquidityNotAccepted(); + if (s_rebalancer != msg.sender) revert Unauthorized(msg.sender); + + i_token.safeTransferFrom(msg.sender, address(this), amount); + emit LiquidityAdded(msg.sender, amount); + } + + /// @notice Removed liquidity to the pool. The tokens will be sent to msg.sender. + /// @param amount The amount of liquidity to remove. + function withdrawLiquidity(uint256 amount) external { + if (s_rebalancer != msg.sender) revert Unauthorized(msg.sender); + + if (i_token.balanceOf(address(this)) < amount) revert InsufficientLiquidity(); + i_token.safeTransfer(msg.sender, amount); + emit LiquidityRemoved(msg.sender, amount); + } + + /// @notice Sets the rate limiter admin address. + /// @dev Only callable by the owner or the rate limiter admin. NOTE: overwrites the normal + /// onlyAdmin check in the base implementation to also allow the rate limiter admin. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config. + /// @param inboundConfig The new inbound rate limiter config. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external override { + if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } +} diff --git a/contracts/src/v0.8/ccip/pools/TokenPool.sol b/contracts/src/v0.8/ccip/pools/TokenPool.sol new file mode 100644 index 00000000000..fb1f8c49e6f --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/TokenPool.sol @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPoolV1} from "../interfaces/IPool.sol"; +import {IRMN} from "../interfaces/IRMN.sol"; +import {IRouter} from "../interfaces/IRouter.sol"; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {Pool} from "../libraries/Pool.sol"; +import {RateLimiter} from "../libraries/RateLimiter.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice Base abstract class with common functions for all token pools. +/// A token pool serves as isolated place for holding tokens and token specific logic +/// that may execute as tokens move across the bridge. +abstract contract TokenPool is IPoolV1, OwnerIsCreator { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; + using RateLimiter for RateLimiter.TokenBucket; + + error CallerIsNotARampOnRouter(address caller); + error ZeroAddressNotAllowed(); + error SenderNotAllowed(address sender); + error AllowListNotEnabled(); + error NonExistentChain(uint64 remoteChainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error CursedByRMN(); + error ChainAlreadyExists(uint64 chainSelector); + error InvalidSourcePoolAddress(bytes sourcePoolAddress); + error InvalidToken(address token); + + event Locked(address indexed sender, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + bytes remoteToken, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event RemotePoolSet(uint64 indexed remoteChainSelector, bytes previousPoolAddress, bytes remotePoolAddress); + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event RouterUpdated(address oldRouter, address newRouter); + + struct ChainUpdate { + uint64 remoteChainSelector; // ──╮ Remote chain selector + bool allowed; // ────────────────╯ Whether the chain should be enabled + bytes remotePoolAddress; // Address of the remote pool, ABI encoded in the case of a remove EVM chain. + bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain. + RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain + RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain + } + + struct RemoteChainConfig { + RateLimiter.TokenBucket outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain + RateLimiter.TokenBucket inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain + bytes remotePoolAddress; // Address of the remote pool, ABI encoded in the case of a remote EVM chain. + bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain. + } + + /// @dev The bridgeable token that is managed by this pool. + IERC20 internal immutable i_token; + /// @dev The address of the RMN proxy + address internal immutable i_rmnProxy; + /// @dev The immutable flag that indicates if the pool is access-controlled. + bool internal immutable i_allowlistEnabled; + /// @dev A set of addresses allowed to trigger lockOrBurn as original senders. + /// Only takes effect if i_allowlistEnabled is true. + /// This can be used to ensure only token-issuer specified addresses can + /// move tokens. + EnumerableSet.AddressSet internal s_allowList; + /// @dev The address of the router + IRouter internal s_router; + /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to + /// be able to quickly determine (without parsing logs) who can access the pool. + /// @dev The chain selectors are in uint256 format because of the EnumerableSet implementation. + EnumerableSet.UintSet internal s_remoteChainSelectors; + mapping(uint64 remoteChainSelector => RemoteChainConfig) internal s_remoteChainConfigs; + + constructor(IERC20 token, address[] memory allowlist, address rmnProxy, address router) { + if (address(token) == address(0) || router == address(0) || rmnProxy == address(0)) revert ZeroAddressNotAllowed(); + i_token = token; + i_rmnProxy = rmnProxy; + s_router = IRouter(router); + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. + i_allowlistEnabled = allowlist.length > 0; + if (i_allowlistEnabled) { + _applyAllowListUpdates(new address[](0), allowlist); + } + } + + /// @notice Get RMN proxy address + /// @return rmnProxy Address of RMN proxy + function getRmnProxy() public view returns (address rmnProxy) { + return i_rmnProxy; + } + + /// @inheritdoc IPoolV1 + function isSupportedToken(address token) public view virtual returns (bool) { + return token == address(i_token); + } + + /// @notice Gets the IERC20 token that this pool can lock or burn. + /// @return token The IERC20 token representation. + function getToken() public view returns (IERC20 token) { + return i_token; + } + + /// @notice Gets the pool's Router + /// @return router The pool's Router + function getRouter() public view returns (address router) { + return address(s_router); + } + + /// @notice Sets the pool's Router + /// @param newRouter The new Router + function setRouter(address newRouter) public onlyOwner { + if (newRouter == address(0)) revert ZeroAddressNotAllowed(); + address oldRouter = address(s_router); + s_router = IRouter(newRouter); + + emit RouterUpdated(oldRouter, newRouter); + } + + /// @notice Signals which version of the pool interface is supported + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == Pool.CCIP_POOL_V1 || interfaceId == type(IPoolV1).interfaceId + || interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ + // │ Validation │ + // ================================================================ + + /// @notice Validates the lock or burn input for correctness on + /// - token to be locked or burned + /// - RMN curse status + /// - allowlist status + /// - if the sender is a valid onRamp + /// - rate limit status + /// @param lockOrBurnIn The input to validate. + /// @dev This function should always be called before executing a lock or burn. Not doing so would allow + /// for various exploits. + function _validateLockOrBurn(Pool.LockOrBurnInV1 memory lockOrBurnIn) internal { + if (!isSupportedToken(lockOrBurnIn.localToken)) revert InvalidToken(lockOrBurnIn.localToken); + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(lockOrBurnIn.remoteChainSelector)))) revert CursedByRMN(); + _checkAllowList(lockOrBurnIn.originalSender); + + _onlyOnRamp(lockOrBurnIn.remoteChainSelector); + _consumeOutboundRateLimit(lockOrBurnIn.remoteChainSelector, lockOrBurnIn.amount); + } + + /// @notice Validates the release or mint input for correctness on + /// - token to be released or minted + /// - RMN curse status + /// - if the sender is a valid offRamp + /// - if the source pool is valid + /// - rate limit status + /// @param releaseOrMintIn The input to validate. + /// @dev This function should always be called before executing a lock or burn. Not doing so would allow + /// for various exploits. + function _validateReleaseOrMint(Pool.ReleaseOrMintInV1 memory releaseOrMintIn) internal { + if (!isSupportedToken(releaseOrMintIn.localToken)) revert InvalidToken(releaseOrMintIn.localToken); + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(releaseOrMintIn.remoteChainSelector)))) revert CursedByRMN(); + _onlyOffRamp(releaseOrMintIn.remoteChainSelector); + + // Validates that the source pool address is configured on this pool. + bytes memory configuredRemotePool = getRemotePool(releaseOrMintIn.remoteChainSelector); + if ( + configuredRemotePool.length == 0 + || keccak256(releaseOrMintIn.sourcePoolAddress) != keccak256(configuredRemotePool) + ) { + revert InvalidSourcePoolAddress(releaseOrMintIn.sourcePoolAddress); + } + _consumeInboundRateLimit(releaseOrMintIn.remoteChainSelector, releaseOrMintIn.amount); + } + + // ================================================================ + // │ Chain permissions │ + // ================================================================ + + /// @notice Gets the pool address on the remote chain. + /// @param remoteChainSelector Remote chain selector. + /// @dev To support non-evm chains, this value is encoded into bytes + function getRemotePool(uint64 remoteChainSelector) public view returns (bytes memory) { + return s_remoteChainConfigs[remoteChainSelector].remotePoolAddress; + } + + /// @notice Gets the token address on the remote chain. + /// @param remoteChainSelector Remote chain selector. + /// @dev To support non-evm chains, this value is encoded into bytes + function getRemoteToken(uint64 remoteChainSelector) public view returns (bytes memory) { + return s_remoteChainConfigs[remoteChainSelector].remoteTokenAddress; + } + + /// @notice Sets the remote pool address for a given chain selector. + /// @param remoteChainSelector The remote chain selector for which the remote pool address is being set. + /// @param remotePoolAddress The address of the remote pool. + function setRemotePool(uint64 remoteChainSelector, bytes calldata remotePoolAddress) external onlyOwner { + if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector); + + bytes memory prevAddress = s_remoteChainConfigs[remoteChainSelector].remotePoolAddress; + s_remoteChainConfigs[remoteChainSelector].remotePoolAddress = remotePoolAddress; + + emit RemotePoolSet(remoteChainSelector, prevAddress, remotePoolAddress); + } + + /// @inheritdoc IPoolV1 + function isSupportedChain(uint64 remoteChainSelector) public view returns (bool) { + return s_remoteChainSelectors.contains(remoteChainSelector); + } + + /// @notice Get list of allowed chains + /// @return list of chains. + function getSupportedChains() public view returns (uint64[] memory) { + uint256[] memory uint256ChainSelectors = s_remoteChainSelectors.values(); + uint64[] memory chainSelectors = new uint64[](uint256ChainSelectors.length); + for (uint256 i = 0; i < uint256ChainSelectors.length; ++i) { + chainSelectors[i] = uint64(uint256ChainSelectors[i]); + } + + return chainSelectors; + } + + /// @notice Sets the permissions for a list of chains selectors. Actual senders for these chains + /// need to be allowed on the Router to interact with this pool. + /// @dev Only callable by the owner + /// @param chains A list of chains and their new permission status & rate limits. Rate limits + /// are only used when the chain is being added through `allowed` being true. + function applyChainUpdates(ChainUpdate[] calldata chains) external virtual onlyOwner { + for (uint256 i = 0; i < chains.length; ++i) { + ChainUpdate memory update = chains[i]; + RateLimiter._validateTokenBucketConfig(update.outboundRateLimiterConfig, !update.allowed); + RateLimiter._validateTokenBucketConfig(update.inboundRateLimiterConfig, !update.allowed); + + if (update.allowed) { + // If the chain already exists, revert + if (!s_remoteChainSelectors.add(update.remoteChainSelector)) { + revert ChainAlreadyExists(update.remoteChainSelector); + } + + if (update.remotePoolAddress.length == 0 || update.remoteTokenAddress.length == 0) { + revert ZeroAddressNotAllowed(); + } + + s_remoteChainConfigs[update.remoteChainSelector] = RemoteChainConfig({ + outboundRateLimiterConfig: RateLimiter.TokenBucket({ + rate: update.outboundRateLimiterConfig.rate, + capacity: update.outboundRateLimiterConfig.capacity, + tokens: update.outboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.outboundRateLimiterConfig.isEnabled + }), + inboundRateLimiterConfig: RateLimiter.TokenBucket({ + rate: update.inboundRateLimiterConfig.rate, + capacity: update.inboundRateLimiterConfig.capacity, + tokens: update.inboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.inboundRateLimiterConfig.isEnabled + }), + remotePoolAddress: update.remotePoolAddress, + remoteTokenAddress: update.remoteTokenAddress + }); + + emit ChainAdded( + update.remoteChainSelector, + update.remoteTokenAddress, + update.outboundRateLimiterConfig, + update.inboundRateLimiterConfig + ); + } else { + // If the chain doesn't exist, revert + if (!s_remoteChainSelectors.remove(update.remoteChainSelector)) { + revert NonExistentChain(update.remoteChainSelector); + } + + delete s_remoteChainConfigs[update.remoteChainSelector]; + + emit ChainRemoved(update.remoteChainSelector); + } + } + } + + // ================================================================ + // │ Rate limiting │ + // ================================================================ + + /// @notice Consumes outbound rate limiting capacity in this pool + function _consumeOutboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._consume(amount, address(i_token)); + } + + /// @notice Consumes inbound rate limiting capacity in this pool + function _consumeInboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._consume(amount, address(i_token)); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentOutboundRateLimiterState(uint64 remoteChainSelector) + external + view + returns (RateLimiter.TokenBucket memory) + { + return s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._currentTokenBucketState(); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentInboundRateLimiterState(uint64 remoteChainSelector) + external + view + returns (RateLimiter.TokenBucket memory) + { + return s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._currentTokenBucketState(); + } + + /// @notice Sets the chain rate limiter config. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config, meaning the onRamp rate limits for the given chain. + /// @param inboundConfig The new inbound rate limiter config, meaning the offRamp rate limits for the given chain. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external virtual onlyOwner { + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } + + function _setRateLimitConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) internal { + if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector); + RateLimiter._validateTokenBucketConfig(outboundConfig, false); + s_remoteChainConfigs[remoteChainSelector].outboundRateLimiterConfig._setTokenBucketConfig(outboundConfig); + RateLimiter._validateTokenBucketConfig(inboundConfig, false); + s_remoteChainConfigs[remoteChainSelector].inboundRateLimiterConfig._setTokenBucketConfig(inboundConfig); + emit ChainConfigured(remoteChainSelector, outboundConfig, inboundConfig); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned onRamp for the given chain on the Router. + function _onlyOnRamp(uint64 remoteChainSelector) internal view { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender); + } + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned offRamp for the given chain on the Router. + function _onlyOffRamp(uint64 remoteChainSelector) internal view { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender); + } + + // ================================================================ + // │ Allowlist │ + // ================================================================ + + function _checkAllowList(address sender) internal view { + if (i_allowlistEnabled) { + if (!s_allowList.contains(sender)) { + revert SenderNotAllowed(sender); + } + } + } + + /// @notice Gets whether the allowList functionality is enabled. + /// @return true is enabled, false if not. + function getAllowListEnabled() external view returns (bool) { + return i_allowlistEnabled; + } + + /// @notice Gets the allowed addresses. + /// @return The allowed addresses. + function getAllowList() external view returns (address[] memory) { + return s_allowList.values(); + } + + /// @notice Apply updates to the allow list. + /// @param removes The addresses to be removed. + /// @param adds The addresses to be added. + function applyAllowListUpdates(address[] calldata removes, address[] calldata adds) external onlyOwner { + _applyAllowListUpdates(removes, adds); + } + + /// @notice Internal version of applyAllowListUpdates to allow for reuse in the constructor. + function _applyAllowListUpdates(address[] memory removes, address[] memory adds) internal { + if (!i_allowlistEnabled) revert AllowListNotEnabled(); + + for (uint256 i = 0; i < removes.length; ++i) { + address toRemove = removes[i]; + if (s_allowList.remove(toRemove)) { + emit AllowListRemove(toRemove); + } + } + for (uint256 i = 0; i < adds.length; ++i) { + address toAdd = adds[i]; + if (toAdd == address(0)) { + continue; + } + if (s_allowList.add(toAdd)) { + emit AllowListAdd(toAdd); + } + } + } +} diff --git a/contracts/src/v0.8/ccip/pools/USDC/IMessageTransmitter.sol b/contracts/src/v0.8/ccip/pools/USDC/IMessageTransmitter.sol new file mode 100644 index 00000000000..1b2a0f90210 --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/USDC/IMessageTransmitter.sol @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity ^0.8.0; + +interface IMessageTransmitter { + /// @notice Unlocks USDC tokens on the destination chain + /// @param message The original message on the source chain + /// * Message format: + /// * Field Bytes Type Index + /// * version 4 uint32 0 + /// * sourceDomain 4 uint32 4 + /// * destinationDomain 4 uint32 8 + /// * nonce 8 uint64 12 + /// * sender 32 bytes32 20 + /// * recipient 32 bytes32 52 + /// * destinationCaller 32 bytes32 84 + /// * messageBody dynamic bytes 116 + /// param attestation A valid attestation is the concatenated 65-byte signature(s) of + /// exactly `thresholdSignature` signatures, in increasing order of attester address. + /// ***If the attester addresses recovered from signatures are not in increasing order, + /// signature verification will fail.*** + /// If incorrect number of signatures or duplicate signatures are supplied, + /// signature verification will fail. + function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success); + + /// Returns domain of chain on which the contract is deployed. + /// @dev immutable + function localDomain() external view returns (uint32); + + /// Returns message format version. + /// @dev immutable + function version() external view returns (uint32); +} diff --git a/contracts/src/v0.8/ccip/pools/USDC/ITokenMessenger.sol b/contracts/src/v0.8/ccip/pools/USDC/ITokenMessenger.sol new file mode 100644 index 00000000000..ce5923cfdcd --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/USDC/ITokenMessenger.sol @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity ^0.8.0; + +interface ITokenMessenger { + /// @notice Emitted when a DepositForBurn message is sent + /// @param nonce Unique nonce reserved by message + /// @param burnToken Address of token burnt on source domain + /// @param amount Deposit amount + /// @param depositor Address where deposit is transferred from + /// @param mintRecipient Address receiving minted tokens on destination domain as bytes32 + /// @param destinationDomain Destination domain + /// @param destinationTokenMessenger Address of TokenMessenger on destination domain as bytes32 + /// @param destinationCaller Authorized caller as bytes32 of receiveMessage() on destination domain, + /// if not equal to bytes32(0). If equal to bytes32(0), any address can call receiveMessage(). + event DepositForBurn( + uint64 indexed nonce, + address indexed burnToken, + uint256 amount, + address indexed depositor, + bytes32 mintRecipient, + uint32 destinationDomain, + bytes32 destinationTokenMessenger, + bytes32 destinationCaller + ); + + /// @notice Burns the tokens on the source side to produce a nonce through + /// Circles Cross Chain Transfer Protocol. + /// @param amount Amount of tokens to deposit and burn. + /// @param destinationDomain Destination domain identifier. + /// @param mintRecipient Address of mint recipient on destination domain. + /// @param burnToken Address of contract to burn deposited tokens, on local domain. + /// @param destinationCaller Caller on the destination domain, as bytes32. + /// @return nonce The unique nonce used in unlocking the funds on the destination chain. + /// @dev emits DepositForBurn + function depositForBurnWithCaller( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller + ) external returns (uint64 nonce); + + /// Returns the version of the message body format. + /// @dev immutable + function messageBodyVersion() external view returns (uint32); + + /// Returns local Message Transmitter responsible for sending and receiving messages + /// to/from remote domainsmessage transmitter for this token messenger. + /// @dev immutable + function localMessageTransmitter() external view returns (address); +} diff --git a/contracts/src/v0.8/ccip/pools/USDC/USDCTokenPool.sol b/contracts/src/v0.8/ccip/pools/USDC/USDCTokenPool.sol new file mode 100644 index 00000000000..339ed09992f --- /dev/null +++ b/contracts/src/v0.8/ccip/pools/USDC/USDCTokenPool.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; +import {IMessageTransmitter} from "./IMessageTransmitter.sol"; +import {ITokenMessenger} from "./ITokenMessenger.sol"; + +import {Pool} from "../../libraries/Pool.sol"; +import {TokenPool} from "../TokenPool.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice This pool mints and burns USDC tokens through the Cross Chain Transfer +/// Protocol (CCTP). +contract USDCTokenPool is TokenPool, ITypeAndVersion { + using SafeERC20 for IERC20; + + event DomainsSet(DomainUpdate[]); + event ConfigSet(address tokenMessenger); + + error UnknownDomain(uint64 domain); + error UnlockingUSDCFailed(); + error InvalidConfig(); + error InvalidDomain(DomainUpdate domain); + error InvalidMessageVersion(uint32 version); + error InvalidTokenMessengerVersion(uint32 version); + error InvalidNonce(uint64 expected, uint64 got); + error InvalidSourceDomain(uint32 expected, uint32 got); + error InvalidDestinationDomain(uint32 expected, uint32 got); + error InvalidReceiver(bytes receiver); + + // This data is supplied from offchain and contains everything needed + // to receive the USDC tokens. + struct MessageAndAttestation { + bytes message; + bytes attestation; + } + + // A domain is a USDC representation of a chain. + struct DomainUpdate { + bytes32 allowedCaller; // Address allowed to mint on the domain + uint32 domainIdentifier; // ──╮ Unique domain ID + uint64 destChainSelector; // │ The destination chain for this domain + bool enabled; // ─────────────╯ Whether the domain is enabled + } + + struct SourceTokenDataPayload { + uint64 nonce; + uint32 sourceDomain; + } + + string public constant override typeAndVersion = "USDCTokenPool 1.4.0"; + + // We restrict to the first version. New pool may be required for subsequent versions. + uint32 public constant SUPPORTED_USDC_VERSION = 0; + + // The local USDC config + ITokenMessenger public immutable i_tokenMessenger; + IMessageTransmitter public immutable i_messageTransmitter; + uint32 public immutable i_localDomainIdentifier; + + /// A domain is a USDC representation of a destination chain. + /// @dev Zero is a valid domain identifier. + /// @dev The address to mint on the destination chain is the corresponding USDC pool. + struct Domain { + bytes32 allowedCaller; // Address allowed to mint on the domain + uint32 domainIdentifier; // ─╮ Unique domain ID + bool enabled; // ────────────╯ Whether the domain is enabled + } + + // A mapping of CCIP chain identifiers to destination domains + mapping(uint64 chainSelector => Domain CCTPDomain) private s_chainToDomain; + + constructor( + ITokenMessenger tokenMessenger, + IERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) TokenPool(token, allowlist, rmnProxy, router) { + if (address(tokenMessenger) == address(0)) revert InvalidConfig(); + IMessageTransmitter transmitter = IMessageTransmitter(tokenMessenger.localMessageTransmitter()); + uint32 transmitterVersion = transmitter.version(); + if (transmitterVersion != SUPPORTED_USDC_VERSION) revert InvalidMessageVersion(transmitterVersion); + uint32 tokenMessengerVersion = tokenMessenger.messageBodyVersion(); + if (tokenMessengerVersion != SUPPORTED_USDC_VERSION) revert InvalidTokenMessengerVersion(tokenMessengerVersion); + + i_tokenMessenger = tokenMessenger; + i_messageTransmitter = transmitter; + i_localDomainIdentifier = transmitter.localDomain(); + i_token.safeIncreaseAllowance(address(i_tokenMessenger), type(uint256).max); + emit ConfigSet(address(tokenMessenger)); + } + + /// @notice Burn the token in the pool + /// @dev Burn is not rate limited at per-pool level. Burn does not contribute to honey pot risk. + /// Benefits of rate limiting here does not justify the extra gas cost. + /// @dev emits ITokenMessenger.DepositForBurn + /// @dev Assumes caller has validated destinationReceiver + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + virtual + override + returns (Pool.LockOrBurnOutV1 memory) + { + _validateLockOrBurn(lockOrBurnIn); + + Domain memory domain = s_chainToDomain[lockOrBurnIn.remoteChainSelector]; + if (!domain.enabled) revert UnknownDomain(lockOrBurnIn.remoteChainSelector); + if (lockOrBurnIn.receiver.length != 32) { + revert InvalidReceiver(lockOrBurnIn.receiver); + } + + // Since this pool is the msg sender of the CCTP transaction, only this contract + // is able to call replaceDepositForBurn. Since this contract does not implement + // replaceDepositForBurn, the tokens cannot be maliciously re-routed to another address. + uint64 nonce = i_tokenMessenger.depositForBurnWithCaller( + // We set the domain.allowedCaller as the receiver of the funds, as this is the token pool. Since 1.5 the + // token pools receiver the funds to hop them through the offRamps. + lockOrBurnIn.amount, + domain.domainIdentifier, + domain.allowedCaller, + address(i_token), + domain.allowedCaller + ); + + emit Burned(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({ + destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), + destPoolData: abi.encode(SourceTokenDataPayload({nonce: nonce, sourceDomain: i_localDomainIdentifier})) + }); + } + + /// @notice Mint tokens from the pool to the recipient + /// * sourceTokenData is part of the verified message and passed directly from + /// the offramp so it is guaranteed to be what the lockOrBurn pool released on the + /// source chain. It contains (nonce, sourceDomain) which is guaranteed by CCTP + /// to be unique. + /// * offchainTokenData is untrusted (can be supplied by manual execution), but we assert + /// that (nonce, sourceDomain) is equal to the message's (nonce, sourceDomain) and + /// receiveMessage will assert that Attestation contains a valid attestation signature + /// for that message, including its (nonce, sourceDomain). This way, the only + /// non-reverting offchainTokenData that can be supplied is a valid attestation for the + /// specific message that was sent on source. + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + _validateReleaseOrMint(releaseOrMintIn); + SourceTokenDataPayload memory sourceTokenDataPayload = + abi.decode(releaseOrMintIn.sourcePoolData, (SourceTokenDataPayload)); + MessageAndAttestation memory msgAndAttestation = + abi.decode(releaseOrMintIn.offchainTokenData, (MessageAndAttestation)); + + _validateMessage(msgAndAttestation.message, sourceTokenDataPayload); + + if (!i_messageTransmitter.receiveMessage(msgAndAttestation.message, msgAndAttestation.attestation)) { + revert UnlockingUSDCFailed(); + } + // Since the tokens are minted to the pool, the pool has to send it to the offRamp + getToken().safeTransfer(msg.sender, releaseOrMintIn.amount); + + emit Minted(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } + + /// @notice Validates the USDC encoded message against the given parameters. + /// @param usdcMessage The USDC encoded message + /// @param sourceTokenData The expected source chain token data to check against + /// @dev Only supports version SUPPORTED_USDC_VERSION of the CCTP message format + /// @dev Message format for USDC: + /// * Field Bytes Type Index + /// * version 4 uint32 0 + /// * sourceDomain 4 uint32 4 + /// * destinationDomain 4 uint32 8 + /// * nonce 8 uint64 12 + /// * sender 32 bytes32 20 + /// * recipient 32 bytes32 52 + /// * destinationCaller 32 bytes32 84 + /// * messageBody dynamic bytes 116 + function _validateMessage(bytes memory usdcMessage, SourceTokenDataPayload memory sourceTokenData) internal view { + uint32 version; + // solhint-disable-next-line no-inline-assembly + assembly { + // We truncate using the datatype of the version variable, meaning + // we will only be left with the first 4 bytes of the message. + version := mload(add(usdcMessage, 4)) // 0 + 4 = 4 + } + // This token pool only supports version 0 of the CCTP message format + // We check the version prior to loading the rest of the message + // to avoid unexpected reverts due to out-of-bounds reads. + if (version != SUPPORTED_USDC_VERSION) revert InvalidMessageVersion(version); + + uint32 sourceDomain; + uint32 destinationDomain; + uint64 nonce; + + // solhint-disable-next-line no-inline-assembly + assembly { + sourceDomain := mload(add(usdcMessage, 8)) // 4 + 4 = 8 + destinationDomain := mload(add(usdcMessage, 12)) // 8 + 4 = 12 + nonce := mload(add(usdcMessage, 20)) // 12 + 8 = 20 + } + + if (sourceDomain != sourceTokenData.sourceDomain) { + revert InvalidSourceDomain(sourceTokenData.sourceDomain, sourceDomain); + } + if (destinationDomain != i_localDomainIdentifier) { + revert InvalidDestinationDomain(i_localDomainIdentifier, destinationDomain); + } + if (nonce != sourceTokenData.nonce) revert InvalidNonce(sourceTokenData.nonce, nonce); + } + + // ================================================================ + // │ Config │ + // ================================================================ + + /// @notice Gets the CCTP domain for a given CCIP chain selector. + function getDomain(uint64 chainSelector) external view returns (Domain memory) { + return s_chainToDomain[chainSelector]; + } + + /// @notice Sets the CCTP domain for a CCIP chain selector. + /// @dev Must verify mapping of selectors -> (domain, caller) offchain. + function setDomains(DomainUpdate[] calldata domains) external onlyOwner { + for (uint256 i = 0; i < domains.length; ++i) { + DomainUpdate memory domain = domains[i]; + if (domain.allowedCaller == bytes32(0) || domain.destChainSelector == 0) revert InvalidDomain(domain); + + s_chainToDomain[domain.destChainSelector] = Domain({ + domainIdentifier: domain.domainIdentifier, + allowedCaller: domain.allowedCaller, + enabled: domain.enabled + }); + } + emit DomainsSet(domains); + } +} diff --git a/contracts/src/v0.8/ccip/test/BaseTest.t.sol b/contracts/src/v0.8/ccip/test/BaseTest.t.sol new file mode 100644 index 00000000000..ee3f3e6fd4c --- /dev/null +++ b/contracts/src/v0.8/ccip/test/BaseTest.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +// Imports to any non-library are not allowed due to the significant cascading +// compile time increase they cause when imported into this base test. +import {Internal} from "../libraries/Internal.sol"; +import {RateLimiter} from "../libraries/RateLimiter.sol"; +import {MockRMN} from "./mocks/MockRMN.sol"; +import {Test} from "forge-std/Test.sol"; + +contract BaseTest is Test { + // Addresses + address internal constant OWNER = 0x00007e64E1fB0C487F25dd6D3601ff6aF8d32e4e; + address internal constant STRANGER = address(999999); + address internal constant DUMMY_CONTRACT_ADDRESS = 0x1111111111111111111111111111111111111112; + address internal constant ON_RAMP_ADDRESS = 0x11118e64e1FB0c487f25dD6D3601FF6aF8d32E4e; + address internal constant ZERO_ADDRESS = address(0); + address internal constant FEE_AGGREGATOR = 0xa33CDB32eAEce34F6affEfF4899cef45744EDea3; + + address internal constant USER_1 = address(1); + address internal constant USER_2 = address(2); + address internal constant USER_3 = address(3); + address internal constant USER_4 = address(4); + + // Message info + uint64 internal constant SOURCE_CHAIN_SELECTOR = 1; + uint64 internal constant DEST_CHAIN_SELECTOR = 2; + uint32 internal constant GAS_LIMIT = 200_000; + + // Timing + uint256 internal constant BLOCK_TIME = 1234567890; + uint32 internal constant TWELVE_HOURS = 60 * 60 * 12; + + // Onramp + uint96 internal constant MAX_NOP_FEES_JUELS = 1e27; + uint96 internal constant MAX_MSG_FEES_JUELS = 1e18; + uint32 internal constant DEST_GAS_OVERHEAD = 350_000; + uint16 internal constant DEST_GAS_PER_PAYLOAD_BYTE = 16; + + uint16 internal constant DEFAULT_TOKEN_FEE_USD_CENTS = 50; + uint32 internal constant DEFAULT_TOKEN_DEST_GAS_OVERHEAD = 34_000; + uint32 internal constant DEFAULT_TOKEN_BYTES_OVERHEAD = 50; + + bool private s_baseTestInitialized; + + // Use 16 gas per data availability byte in our tests. + // This is an overestimation in OP stack, it ignores 4 gas per 0 byte rule. + // Arbitrum on the other hand, does always use 16 gas per data availability byte. + // This value may be substantially decreased after EIP 4844. + uint16 internal constant DEST_GAS_PER_DATA_AVAILABILITY_BYTE = 16; + + // Total L1 data availability overhead estimate is 33_596 gas. + // This value includes complete CommitStore and OffRamp call data. + uint32 internal constant DEST_DATA_AVAILABILITY_OVERHEAD_GAS = 188 // Fixed data availability overhead in OP stack. + + (32 * 31 + 4) * DEST_GAS_PER_DATA_AVAILABILITY_BYTE // CommitStore single-root transmission takes up about 31 slots, plus selector. + + (32 * 34 + 4) * DEST_GAS_PER_DATA_AVAILABILITY_BYTE; // OffRamp transmission excluding EVM2EVMMessage takes up about 34 slots, plus selector. + + // Multiples of bps, or 0.0001, use 6840 to be same as OP mainnet compression factor of 0.684. + uint16 internal constant DEST_GAS_DATA_AVAILABILITY_MULTIPLIER_BPS = 6840; + + // OffRamp + uint32 internal constant MAX_DATA_SIZE = 30_000; + uint16 internal constant MAX_TOKENS_LENGTH = 5; + uint32 internal constant MAX_TOKEN_POOL_RELEASE_OR_MINT_GAS = 200_000; + uint32 internal constant MAX_TOKEN_POOL_TRANSFER_GAS = 50_000; + uint16 internal constant GAS_FOR_CALL_EXACT_CHECK = 5000; + uint32 internal constant PERMISSION_LESS_EXECUTION_THRESHOLD_SECONDS = 500; + uint32 internal constant MAX_GAS_LIMIT = 4_000_000; + + // Rate limiter + address internal constant ADMIN = 0x11118e64e1FB0c487f25dD6D3601FF6aF8d32E4e; + + MockRMN internal s_mockRMN; + + function setUp() public virtual { + // BaseTest.setUp is often called multiple times from tests' setUp due to inheritance. + if (s_baseTestInitialized) return; + s_baseTestInitialized = true; + + // Set the sender to OWNER permanently + vm.startPrank(OWNER); + deal(OWNER, 1e20); + vm.label(OWNER, "Owner"); + vm.label(STRANGER, "Stranger"); + + // Set the block time to a constant known value + vm.warp(BLOCK_TIME); + + s_mockRMN = new MockRMN(); + } + + function getOutboundRateLimiterConfig() internal pure returns (RateLimiter.Config memory) { + return RateLimiter.Config({isEnabled: true, capacity: 100e28, rate: 1e15}); + } + + function getInboundRateLimiterConfig() internal pure returns (RateLimiter.Config memory) { + return RateLimiter.Config({isEnabled: true, capacity: 222e30, rate: 1e18}); + } + + function getSingleTokenPriceUpdateStruct( + address token, + uint224 price + ) internal pure returns (Internal.PriceUpdates memory) { + Internal.TokenPriceUpdate[] memory tokenPriceUpdates = new Internal.TokenPriceUpdate[](1); + tokenPriceUpdates[0] = Internal.TokenPriceUpdate({sourceToken: token, usdPerToken: price}); + + Internal.PriceUpdates memory priceUpdates = + Internal.PriceUpdates({tokenPriceUpdates: tokenPriceUpdates, gasPriceUpdates: new Internal.GasPriceUpdate[](0)}); + + return priceUpdates; + } + + function getSingleGasPriceUpdateStruct( + uint64 chainSelector, + uint224 usdPerUnitGas + ) internal pure returns (Internal.PriceUpdates memory) { + Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](1); + gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: chainSelector, usdPerUnitGas: usdPerUnitGas}); + + Internal.PriceUpdates memory priceUpdates = + Internal.PriceUpdates({tokenPriceUpdates: new Internal.TokenPriceUpdate[](0), gasPriceUpdates: gasPriceUpdates}); + + return priceUpdates; + } +} diff --git a/contracts/src/v0.8/ccip/test/NonceManager.t.sol b/contracts/src/v0.8/ccip/test/NonceManager.t.sol new file mode 100644 index 00000000000..75de4db8c5c --- /dev/null +++ b/contracts/src/v0.8/ccip/test/NonceManager.t.sol @@ -0,0 +1,649 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {NonceManager} from "../NonceManager.sol"; +import {ICommitStore} from "../interfaces/ICommitStore.sol"; +import {Client} from "../libraries/Client.sol"; +import {Internal} from "../libraries/Internal.sol"; +import {Pool} from "../libraries/Pool.sol"; +import {RateLimiter} from "../libraries/RateLimiter.sol"; +import {EVM2EVMMultiOffRamp} from "../offRamp/EVM2EVMMultiOffRamp.sol"; +import {EVM2EVMMultiOnRamp} from "../onRamp/EVM2EVMMultiOnRamp.sol"; +import {EVM2EVMOnRamp} from "../onRamp/EVM2EVMOnRamp.sol"; + +import {BaseTest} from "./BaseTest.t.sol"; +import {EVM2EVMMultiOnRampHelper} from "./helpers/EVM2EVMMultiOnRampHelper.sol"; +import {EVM2EVMOffRampHelper} from "./helpers/EVM2EVMOffRampHelper.sol"; +import {EVM2EVMOnRampHelper} from "./helpers/EVM2EVMOnRampHelper.sol"; +import {MockCommitStore} from "./mocks/MockCommitStore.sol"; +import {EVM2EVMMultiOffRampSetup} from "./offRamp/EVM2EVMMultiOffRampSetup.t.sol"; +import {EVM2EVMMultiOnRampSetup} from "./onRamp/EVM2EVMMultiOnRampSetup.t.sol"; + +contract NonceManager_NonceIncrementation is BaseTest { + NonceManager private s_nonceManager; + + function setUp() public override { + address[] memory authorizedCallers = new address[](1); + authorizedCallers[0] = address(this); + s_nonceManager = new NonceManager(authorizedCallers); + } + + function test_getIncrementedOutboundNonce_Success() public { + address sender = address(this); + + assertEq(s_nonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, sender), 0); + + uint64 outboundNonce = s_nonceManager.getIncrementedOutboundNonce(DEST_CHAIN_SELECTOR, sender); + assertEq(outboundNonce, 1); + } + + function test_incrementInboundNonce_Success() public { + address sender = address(this); + + s_nonceManager.incrementInboundNonce(SOURCE_CHAIN_SELECTOR, 1, abi.encode(sender)); + + assertEq(s_nonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR, abi.encode(sender)), 1); + } + + function test_incrementInboundNonce_Skip() public { + address sender = address(this); + uint64 expectedNonce = 2; + + vm.expectEmit(); + emit NonceManager.SkippedIncorrectNonce(SOURCE_CHAIN_SELECTOR, expectedNonce, abi.encode(sender)); + + s_nonceManager.incrementInboundNonce(SOURCE_CHAIN_SELECTOR, expectedNonce, abi.encode(sender)); + + assertEq(s_nonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR, abi.encode(sender)), 0); + } + + function test_incrementNoncesInboundAndOutbound_Success() public { + address sender = address(this); + + assertEq(s_nonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, sender), 0); + uint64 outboundNonce = s_nonceManager.getIncrementedOutboundNonce(DEST_CHAIN_SELECTOR, sender); + assertEq(outboundNonce, 1); + + // Inbound nonce unchanged + assertEq(s_nonceManager.getInboundNonce(DEST_CHAIN_SELECTOR, abi.encode(sender)), 0); + + s_nonceManager.incrementInboundNonce(DEST_CHAIN_SELECTOR, 1, abi.encode(sender)); + assertEq(s_nonceManager.getInboundNonce(DEST_CHAIN_SELECTOR, abi.encode(sender)), 1); + + // Outbound nonce unchanged + assertEq(s_nonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, sender), 1); + } +} + +contract NonceManager_applyPreviousRampsUpdates is EVM2EVMMultiOnRampSetup { + function test_SingleRampUpdate() public { + address prevOnRamp = makeAddr("prevOnRamp"); + address prevOffRamp = makeAddr("prevOffRamp"); + NonceManager.PreviousRampsArgs[] memory previousRamps = new NonceManager.PreviousRampsArgs[](1); + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(prevOnRamp, prevOffRamp)); + + vm.expectEmit(); + emit NonceManager.PreviousRampsUpdated(DEST_CHAIN_SELECTOR, previousRamps[0].prevRamps); + + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + + _assertPreviousRampsEqual(s_outboundNonceManager.getPreviousRamps(DEST_CHAIN_SELECTOR), previousRamps[0].prevRamps); + } + + function test_MultipleRampsUpdates() public { + address prevOnRamp1 = makeAddr("prevOnRamp1"); + address prevOnRamp2 = makeAddr("prevOnRamp2"); + address prevOffRamp1 = makeAddr("prevOffRamp1"); + address prevOffRamp2 = makeAddr("prevOffRamp2"); + NonceManager.PreviousRampsArgs[] memory previousRamps = new NonceManager.PreviousRampsArgs[](2); + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(prevOnRamp1, prevOffRamp1)); + previousRamps[1] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR + 1, NonceManager.PreviousRamps(prevOnRamp2, prevOffRamp2)); + + vm.expectEmit(); + emit NonceManager.PreviousRampsUpdated(DEST_CHAIN_SELECTOR, previousRamps[0].prevRamps); + vm.expectEmit(); + emit NonceManager.PreviousRampsUpdated(DEST_CHAIN_SELECTOR + 1, previousRamps[1].prevRamps); + + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + + _assertPreviousRampsEqual(s_outboundNonceManager.getPreviousRamps(DEST_CHAIN_SELECTOR), previousRamps[0].prevRamps); + _assertPreviousRampsEqual( + s_outboundNonceManager.getPreviousRamps(DEST_CHAIN_SELECTOR + 1), previousRamps[1].prevRamps + ); + } + + function test_ZeroInput() public { + vm.recordLogs(); + s_outboundNonceManager.applyPreviousRampsUpdates(new NonceManager.PreviousRampsArgs[](0)); + + assertEq(vm.getRecordedLogs().length, 0); + } + + function test_PreviousRampAlreadySetOnRamp_Revert() public { + NonceManager.PreviousRampsArgs[] memory previousRamps = new NonceManager.PreviousRampsArgs[](1); + address prevOnRamp = makeAddr("prevOnRamp"); + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(prevOnRamp, address(0))); + + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(prevOnRamp, address(0))); + + vm.expectRevert(NonceManager.PreviousRampAlreadySet.selector); + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + } + + function test_PreviousRampAlreadySetOffRamp_Revert() public { + NonceManager.PreviousRampsArgs[] memory previousRamps = new NonceManager.PreviousRampsArgs[](1); + address prevOffRamp = makeAddr("prevOffRamp"); + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(address(0), prevOffRamp)); + + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(address(0), prevOffRamp)); + + vm.expectRevert(NonceManager.PreviousRampAlreadySet.selector); + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + } + + function test_PreviousRampAlreadySetOnRampAndOffRamp_Revert() public { + NonceManager.PreviousRampsArgs[] memory previousRamps = new NonceManager.PreviousRampsArgs[](1); + address prevOnRamp = makeAddr("prevOnRamp"); + address prevOffRamp = makeAddr("prevOffRamp"); + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(prevOnRamp, prevOffRamp)); + + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(prevOnRamp, prevOffRamp)); + + vm.expectRevert(NonceManager.PreviousRampAlreadySet.selector); + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + } + + function _assertPreviousRampsEqual( + NonceManager.PreviousRamps memory a, + NonceManager.PreviousRamps memory b + ) internal pure { + assertEq(a.prevOnRamp, b.prevOnRamp); + assertEq(a.prevOffRamp, b.prevOffRamp); + } +} + +contract NonceManager_OnRampUpgrade is EVM2EVMMultiOnRampSetup { + uint256 internal constant FEE_AMOUNT = 1234567890; + EVM2EVMOnRampHelper internal s_prevOnRamp; + + function setUp() public virtual override { + super.setUp(); + + EVM2EVMOnRamp.FeeTokenConfigArgs[] memory feeTokenConfigArgs = new EVM2EVMOnRamp.FeeTokenConfigArgs[](1); + feeTokenConfigArgs[0] = EVM2EVMOnRamp.FeeTokenConfigArgs({ + token: s_sourceFeeToken, + networkFeeUSDCents: 1_00, // 1 USD + gasMultiplierWeiPerEth: 1e18, // 1x + premiumMultiplierWeiPerEth: 5e17, // 0.5x + enabled: true + }); + + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfig = + new EVM2EVMOnRamp.TokenTransferFeeConfigArgs[](1); + + tokenTransferFeeConfig[0] = EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: s_sourceFeeToken, + minFeeUSDCents: 1_00, // 1 USD + maxFeeUSDCents: 1000_00, // 1,000 USD + deciBps: 2_5, // 2.5 bps, or 0.025% + destGasOverhead: 40_000, + destBytesOverhead: uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES), + aggregateRateLimitEnabled: true + }); + + s_prevOnRamp = new EVM2EVMOnRampHelper( + EVM2EVMOnRamp.StaticConfig({ + linkToken: s_sourceTokens[0], + chainSelector: SOURCE_CHAIN_SELECTOR, + destChainSelector: DEST_CHAIN_SELECTOR, + defaultTxGasLimit: GAS_LIMIT, + maxNopFeesJuels: MAX_NOP_FEES_JUELS, + prevOnRamp: address(0), + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + EVM2EVMOnRamp.DynamicConfig({ + router: address(s_sourceRouter), + maxNumberOfTokensPerMsg: MAX_TOKENS_LENGTH, + destGasOverhead: DEST_GAS_OVERHEAD, + destGasPerPayloadByte: DEST_GAS_PER_PAYLOAD_BYTE, + destDataAvailabilityOverheadGas: DEST_DATA_AVAILABILITY_OVERHEAD_GAS, + destGasPerDataAvailabilityByte: DEST_GAS_PER_DATA_AVAILABILITY_BYTE, + destDataAvailabilityMultiplierBps: DEST_GAS_DATA_AVAILABILITY_MULTIPLIER_BPS, + priceRegistry: address(s_priceRegistry), + maxDataBytes: MAX_DATA_SIZE, + maxPerMsgGasLimit: MAX_GAS_LIMIT, + defaultTokenFeeUSDCents: DEFAULT_TOKEN_FEE_USD_CENTS, + defaultTokenDestGasOverhead: DEFAULT_TOKEN_DEST_GAS_OVERHEAD, + defaultTokenDestBytesOverhead: DEFAULT_TOKEN_BYTES_OVERHEAD, + enforceOutOfOrder: false + }), + RateLimiter.Config({isEnabled: true, capacity: 100e28, rate: 1e15}), + feeTokenConfigArgs, + tokenTransferFeeConfig, + new EVM2EVMOnRamp.NopAndWeight[](0) + ); + + NonceManager.PreviousRampsArgs[] memory previousRamps = new NonceManager.PreviousRampsArgs[](1); + previousRamps[0] = + NonceManager.PreviousRampsArgs(DEST_CHAIN_SELECTOR, NonceManager.PreviousRamps(address(s_prevOnRamp), address(0))); + s_outboundNonceManager.applyPreviousRampsUpdates(previousRamps); + + (s_onRamp, s_metadataHash) = _deployOnRamp( + SOURCE_CHAIN_SELECTOR, address(s_sourceRouter), address(s_outboundNonceManager), address(s_tokenAdminRegistry) + ); + + vm.startPrank(address(s_sourceRouter)); + } + + function test_Upgrade_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, FEE_AMOUNT, OWNER)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + } + + function test_UpgradeSenderNoncesReadsPreviousRamp_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + uint64 startNonce = s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER); + + for (uint64 i = 1; i < 4; ++i) { + s_prevOnRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + assertEq(startNonce + i, s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER)); + } + } + + function test_UpgradeNonceStartsAtV1Nonce_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint64 startNonce = s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER); + + // send 1 message from previous onramp + s_prevOnRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + + assertEq(startNonce + 1, s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER)); + + // new onramp nonce should start from 2, while sequence number start from 1 + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested( + DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, startNonce + 2, FEE_AMOUNT, OWNER) + ); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + + assertEq(startNonce + 2, s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER)); + + // after another send, nonce should be 3, and sequence number be 2 + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested( + DEST_CHAIN_SELECTOR, _messageToEvent(message, 2, startNonce + 3, FEE_AMOUNT, OWNER) + ); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + + assertEq(startNonce + 3, s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER)); + } + + function test_UpgradeNonceNewSenderStartsAtZero_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + // send 1 message from previous onramp from OWNER + s_prevOnRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + + address newSender = address(1234567); + // new onramp nonce should start from 1 for new sender + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested( + DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, FEE_AMOUNT, newSender) + ); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, newSender); + } +} + +contract NonceManager_OffRampUpgrade is EVM2EVMMultiOffRampSetup { + EVM2EVMOffRampHelper internal s_prevOffRamp; + EVM2EVMOffRampHelper[] internal s_nestedPrevOffRamps; + + address internal constant SINGLE_LANE_ON_RAMP_ADDRESS_1 = abi.decode(ON_RAMP_ADDRESS_1, (address)); + address internal constant SINGLE_LANE_ON_RAMP_ADDRESS_2 = abi.decode(ON_RAMP_ADDRESS_2, (address)); + address internal constant SINGLE_LANE_ON_RAMP_ADDRESS_3 = abi.decode(ON_RAMP_ADDRESS_3, (address)); + + function setUp() public virtual override { + super.setUp(); + + ICommitStore mockPrevCommitStore = new MockCommitStore(); + s_prevOffRamp = _deploySingleLaneOffRamp( + mockPrevCommitStore, s_destRouter, address(0), SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1 + ); + + s_nestedPrevOffRamps = new EVM2EVMOffRampHelper[](2); + s_nestedPrevOffRamps[0] = _deploySingleLaneOffRamp( + mockPrevCommitStore, s_destRouter, address(0), SOURCE_CHAIN_SELECTOR_2, SINGLE_LANE_ON_RAMP_ADDRESS_2 + ); + s_nestedPrevOffRamps[1] = _deploySingleLaneOffRamp( + mockPrevCommitStore, + s_destRouter, + address(s_nestedPrevOffRamps[0]), + SOURCE_CHAIN_SELECTOR_2, + SINGLE_LANE_ON_RAMP_ADDRESS_2 + ); + + NonceManager.PreviousRampsArgs[] memory previousRamps = new NonceManager.PreviousRampsArgs[](3); + previousRamps[0] = NonceManager.PreviousRampsArgs( + SOURCE_CHAIN_SELECTOR_1, NonceManager.PreviousRamps(address(0), address(s_prevOffRamp)) + ); + previousRamps[1] = NonceManager.PreviousRampsArgs( + SOURCE_CHAIN_SELECTOR_2, NonceManager.PreviousRamps(address(0), address(s_nestedPrevOffRamps[1])) + ); + previousRamps[2] = NonceManager.PreviousRampsArgs( + SOURCE_CHAIN_SELECTOR_3, NonceManager.PreviousRamps(SINGLE_LANE_ON_RAMP_ADDRESS_3, address(0)) + ); + s_inboundNonceManager.applyPreviousRampsUpdates(previousRamps); + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](3); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + isEnabled: true, + onRamp: ON_RAMP_ADDRESS_1 + }); + sourceChainConfigs[1] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_2, + isEnabled: true, + onRamp: ON_RAMP_ADDRESS_2 + }); + sourceChainConfigs[2] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_3, + isEnabled: true, + onRamp: ON_RAMP_ADDRESS_3 + }); + + _setupMultipleOffRampsFromConfigs(sourceChainConfigs); + + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 1); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_3, 1); + } + + function test_Upgraded_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + } + + function test_NoPrevOffRampForChain_Success() public { + Internal.EVM2EVMMessage[] memory messages = + _generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1); + uint64 startNonceChain3 = + s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_3, abi.encode(messages[0].sender)); + s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0)); + + // Nonce unchanged for chain 3 + assertEq( + startNonceChain3, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_3, abi.encode(messages[0].sender)) + ); + + Internal.Any2EVMRampMessage[] memory messagesChain3 = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_3, ON_RAMP_ADDRESS_3); + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_3, + messagesChain3[0].header.sequenceNumber, + messagesChain3[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_3, messagesChain3), new uint256[](0) + ); + assertEq( + startNonceChain3 + 1, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_3, messagesChain3[0].sender) + ); + } + + function test_UpgradedSenderNoncesReadsPreviousRamp_Success() public { + Internal.EVM2EVMMessage[] memory messages = + _generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1); + uint64 startNonce = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messages[0].sender)); + + for (uint64 i = 1; i < 4; ++i) { + s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0)); + + // messages contains a single message - update for the next execution + messages[0].nonce++; + messages[0].sequenceNumber++; + messages[0].messageId = Internal._hash(messages[0], s_prevOffRamp.metadataHash()); + + assertEq( + startNonce + i, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messages[0].sender)) + ); + } + } + + function test_UpgradedSenderNoncesReadsPreviousRampTransitive_Success() public { + Internal.EVM2EVMMessage[] memory messages = + _generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_2, SINGLE_LANE_ON_RAMP_ADDRESS_2); + uint64 startNonce = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_2, abi.encode(messages[0].sender)); + + for (uint64 i = 1; i < 4; ++i) { + s_nestedPrevOffRamps[0].execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0)); + + // messages contains a single message - update for the next execution + messages[0].nonce++; + messages[0].sequenceNumber++; + messages[0].messageId = Internal._hash(messages[0], s_nestedPrevOffRamps[0].metadataHash()); + + // Read through prev sender nonce through prevOffRamp -> prevPrevOffRamp + assertEq( + startNonce + i, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_2, abi.encode(messages[0].sender)) + ); + } + } + + function test_UpgradedNonceStartsAtV1Nonce_Success() public { + Internal.EVM2EVMMessage[] memory messages = + _generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1); + + uint64 startNonce = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messages[0].sender)); + s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0)); + + assertEq( + startNonce + 1, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messages[0].sender)) + ); + + Internal.Any2EVMRampMessage[] memory messagesMultiRamp = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + messagesMultiRamp[0].header.nonce++; + messagesMultiRamp[0].header.messageId = Internal._hash(messagesMultiRamp[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messagesMultiRamp[0].header.sequenceNumber, + messagesMultiRamp[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new uint256[](0) + ); + assertEq( + startNonce + 2, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp[0].sender) + ); + + messagesMultiRamp[0].header.nonce++; + messagesMultiRamp[0].header.sequenceNumber++; + messagesMultiRamp[0].header.messageId = Internal._hash(messagesMultiRamp[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messagesMultiRamp[0].header.sequenceNumber, + messagesMultiRamp[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new uint256[](0) + ); + assertEq( + startNonce + 3, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp[0].sender) + ); + } + + function test_UpgradedNonceNewSenderStartsAtZero_Success() public { + Internal.EVM2EVMMessage[] memory messages = + _generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1); + + s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messages), new uint256[](0)); + + Internal.Any2EVMRampMessage[] memory messagesMultiRamp = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + bytes memory newSender = abi.encode(address(1234567)); + messagesMultiRamp[0].sender = newSender; + messagesMultiRamp[0].header.messageId = Internal._hash(messagesMultiRamp[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messagesMultiRamp[0].header.sequenceNumber, + messagesMultiRamp[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + // new sender nonce in new offramp should go from 0 -> 1 + assertEq(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, newSender), 0); + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesMultiRamp), new uint256[](0) + ); + assertEq(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, newSender), 1); + } + + function test_UpgradedOffRampNonceSkipsIfMsgInFlight_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + address newSender = address(1234567); + messages[0].sender = abi.encode(newSender); + messages[0].header.nonce = 2; + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + uint64 startNonce = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender); + + // new offramp sees msg nonce higher than senderNonce + // it waits for previous offramp to execute + vm.expectEmit(); + emit NonceManager.SkippedIncorrectNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].header.nonce, messages[0].sender); + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + assertEq(startNonce, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender)); + + Internal.EVM2EVMMessage[] memory messagesSingleLane = + _generateSingleLaneSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, SINGLE_LANE_ON_RAMP_ADDRESS_1); + + messagesSingleLane[0].nonce = 1; + messagesSingleLane[0].sender = newSender; + messagesSingleLane[0].messageId = Internal._hash(messagesSingleLane[0], s_prevOffRamp.metadataHash()); + + // previous offramp executes msg and increases nonce + s_prevOffRamp.execute(_generateSingleLaneRampReportFromMessages(messagesSingleLane), new uint256[](0)); + assertEq( + startNonce + 1, + s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(messagesSingleLane[0].sender)) + ); + + messages[0].header.nonce = 2; + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + // new offramp is able to execute + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + assertEq(startNonce + 2, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender)); + } + + function _generateSingleLaneRampReportFromMessages(Internal.EVM2EVMMessage[] memory messages) + internal + pure + returns (Internal.ExecutionReport memory) + { + bytes[][] memory offchainTokenData = new bytes[][](messages.length); + + for (uint256 i = 0; i < messages.length; ++i) { + offchainTokenData[i] = new bytes[](messages[i].tokenAmounts.length); + } + + return Internal.ExecutionReport({ + proofs: new bytes32[](0), + proofFlagBits: 2 ** 256 - 1, + messages: messages, + offchainTokenData: offchainTokenData + }); + } + + function _generateSingleLaneSingleBasicMessage( + uint64 sourceChainSelector, + address onRamp + ) internal view returns (Internal.EVM2EVMMessage[] memory) { + Internal.EVM2EVMMessage[] memory messages = new Internal.EVM2EVMMessage[](1); + + bytes memory data = abi.encode(0); + messages[0] = Internal.EVM2EVMMessage({ + sequenceNumber: 1, + sender: OWNER, + nonce: 1, + gasLimit: GAS_LIMIT, + strict: false, + sourceChainSelector: sourceChainSelector, + receiver: address(s_receiver), + data: data, + tokenAmounts: new Client.EVMTokenAmount[](0), + sourceTokenData: new bytes[](0), + feeToken: s_destFeeToken, + feeTokenAmount: uint256(0), + messageId: "" + }); + + messages[0].messageId = Internal._hash( + messages[0], + keccak256(abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, sourceChainSelector, DEST_CHAIN_SELECTOR, onRamp)) + ); + + return messages; + } +} diff --git a/contracts/src/v0.8/ccip/test/README.md b/contracts/src/v0.8/ccip/test/README.md new file mode 100644 index 00000000000..99223e1a63d --- /dev/null +++ b/contracts/src/v0.8/ccip/test/README.md @@ -0,0 +1,89 @@ +# Foundry Test Guidelines + +We're using Foundry to test our CCIP smart contracts here. This enables us to test in Solidity. If you need to add tests for anything outside the CCIP contracts, please write them in hardhat (for the time being). + +## Directory Structure + +The test directory structure mimics the source contract file structure as closely as possible. Example: + +`./offRamp/SomeOffRamp.sol` should have a test contract `./test/offRamp/SomeOffRamp.t.sol`. + +## Test File Structure + +Break the test file down into multiple contracts, each contract testing a specific function inside the source contract. + +For Example, here's a source contract `SomeOffRamp`: + +``` +contract SomeOffRamp { + + constructor() { + ... set some state + } + + function firstFunction() public { + ... + } + + function theNextFunction() public { + ... + } + + function _anInternalFunction() internal { + ... + } +} +``` + +Our test file `SomeOffRamp.t.sol` should be structured like this: + +``` +contract SomeOffRamp_constructor { + // constructor state setup tests here +} + +contract SomeOffRamp_firstFunction { + // first function tests here +} + +contract SomeOffRamp_theNextFunction { + // tests here too... +} + +contract SomeOffRamp_anInternalFunction { + // This function will require a helper contract to expose it. +} +``` + +## Test Structure + +Inside each test contract, group tests into `Success` and `Reverts` by starting with all the success cases and then adding a `// Reverts` comments to indicate the failure cases below. + +``` +contract SomeOffRamp_firstFunction { + function testZeroValueSuccess() public { + ... + } + + ... + + + // Reverts + + function testOwnerReverts() public { + // test that an ownable function reverts when not called by the owner + ... + } + + ... + +} +``` + +Function naming should follow this structure, where the `_fuzz_` section denotes whether it's a fuzz test. Do not write tests that are named `testSuccess`, always include the description of the test, even if it's just the name of the function that is being called. + +`test{_fuzz_}{description of test}[Success|Reverts]` + +Try to cover all the code paths present in each function being tested. In most cases, this will result in many more failure tests than success tests. + +If a test file requires a complicated setUp, or if it requires many helper functions (like `_generateAMessageWithNoTokensStruct()`), create a separate file to perform this setup in. Using the example above, `SomeOffRampSetup.t.sol`. Inherit this and call the setUp function in the test file. diff --git a/contracts/src/v0.8/ccip/test/TokenSetup.t.sol b/contracts/src/v0.8/ccip/test/TokenSetup.t.sol new file mode 100644 index 00000000000..182d92c5c94 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/TokenSetup.t.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPoolV1} from "../interfaces/IPool.sol"; + +import {BurnMintERC677} from "../../shared/token/ERC677/BurnMintERC677.sol"; +import {Client} from "../libraries/Client.sol"; +import {BurnMintTokenPool} from "../pools/BurnMintTokenPool.sol"; +import {LockReleaseTokenPool} from "../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../pools/TokenPool.sol"; +import {TokenAdminRegistry} from "../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {MaybeRevertingBurnMintTokenPool} from "./helpers/MaybeRevertingBurnMintTokenPool.sol"; +import {RouterSetup} from "./router/RouterSetup.t.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract TokenSetup is RouterSetup { + address[] internal s_sourceTokens; + address[] internal s_destTokens; + + address internal s_sourceFeeToken; + address internal s_destFeeToken; + + TokenAdminRegistry internal s_tokenAdminRegistry; + + mapping(address sourceToken => address sourcePool) internal s_sourcePoolByToken; + mapping(address sourceToken => address destPool) internal s_destPoolBySourceToken; + mapping(address destToken => address destPool) internal s_destPoolByToken; + mapping(address sourceToken => address destToken) internal s_destTokenBySourceToken; + + function _deploySourceToken(string memory tokenName, uint256 dealAmount, uint8 decimals) internal returns (address) { + BurnMintERC677 token = new BurnMintERC677(tokenName, tokenName, decimals, 0); + s_sourceTokens.push(address(token)); + deal(address(token), OWNER, dealAmount); + return address(token); + } + + function _deployDestToken(string memory tokenName, uint256 dealAmount) internal returns (address) { + BurnMintERC677 token = new BurnMintERC677(tokenName, tokenName, 18, 0); + s_destTokens.push(address(token)); + deal(address(token), OWNER, dealAmount); + return address(token); + } + + function _deployLockReleasePool(address token, bool isSourcePool) internal { + address router = address(s_sourceRouter); + if (!isSourcePool) { + router = address(s_destRouter); + } + + LockReleaseTokenPool pool = + new LockReleaseTokenPool(IERC20(token), new address[](0), address(s_mockRMN), true, router); + + if (isSourcePool) { + s_sourcePoolByToken[address(token)] = address(pool); + } else { + s_destPoolByToken[address(token)] = address(pool); + s_destPoolBySourceToken[s_sourceTokens[s_destTokens.length - 1]] = address(pool); + } + } + + function _deployTokenAndBurnMintPool(address token, bool isSourcePool) internal { + address router = address(s_sourceRouter); + if (!isSourcePool) { + router = address(s_destRouter); + } + + BurnMintTokenPool pool = + new MaybeRevertingBurnMintTokenPool(BurnMintERC677(token), new address[](0), address(s_mockRMN), router); + BurnMintERC677(token).grantMintAndBurnRoles(address(pool)); + + if (isSourcePool) { + s_sourcePoolByToken[address(token)] = address(pool); + } else { + s_destPoolByToken[address(token)] = address(pool); + s_destPoolBySourceToken[s_sourceTokens[s_destTokens.length - 1]] = address(pool); + } + } + + function setUp() public virtual override { + RouterSetup.setUp(); + + bool isSetup = s_sourceTokens.length != 0; + if (isSetup) { + return; + } + + // Source tokens & pools + address sourceLink = _deploySourceToken("sLINK", type(uint256).max, 18); + _deployLockReleasePool(sourceLink, true); + s_sourceFeeToken = sourceLink; + + address sourceEth = _deploySourceToken("sETH", 2 ** 128, 18); + _deployTokenAndBurnMintPool(sourceEth, true); + + // Destination tokens & pools + address destLink = _deployDestToken("dLINK", type(uint256).max); + _deployLockReleasePool(destLink, false); + s_destFeeToken = destLink; + + s_destTokenBySourceToken[sourceLink] = destLink; + + address destEth = _deployDestToken("dETH", 2 ** 128); + _deployTokenAndBurnMintPool(destEth, false); + + s_destTokenBySourceToken[sourceEth] = destEth; + + // Float the dest link lock release pool with funds + IERC20(destLink).transfer(s_destPoolByToken[destLink], 1000 ether); + + s_tokenAdminRegistry = new TokenAdminRegistry(); + + // Set pools in the registry + for (uint256 i = 0; i < s_sourceTokens.length; ++i) { + address token = s_sourceTokens[i]; + address pool = s_sourcePoolByToken[token]; + + _setPool( + s_tokenAdminRegistry, token, pool, DEST_CHAIN_SELECTOR, s_destPoolByToken[s_destTokens[i]], s_destTokens[i] + ); + } + + for (uint256 i = 0; i < s_destTokens.length; ++i) { + address token = s_destTokens[i]; + address pool = s_destPoolByToken[token]; + s_tokenAdminRegistry.proposeAdministrator(token, OWNER); + s_tokenAdminRegistry.acceptAdminRole(token); + s_tokenAdminRegistry.setPool(token, pool); + + _setPool( + s_tokenAdminRegistry, + token, + pool, + SOURCE_CHAIN_SELECTOR, + s_sourcePoolByToken[s_sourceTokens[i]], + s_sourceTokens[i] + ); + } + } + + function getCastedSourceEVMTokenAmountsWithZeroAmounts() + internal + view + returns (Client.EVMTokenAmount[] memory tokenAmounts) + { + tokenAmounts = new Client.EVMTokenAmount[](s_sourceTokens.length); + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + tokenAmounts[i].token = s_sourceTokens[i]; + } + } + + function _setPool( + TokenAdminRegistry tokenAdminRegistry, + address token, + address pool, + uint64 remoteChainSelector, + address remotePoolAddress, + address remoteToken + ) internal { + if (!tokenAdminRegistry.isAdministrator(token, OWNER)) { + tokenAdminRegistry.proposeAdministrator(token, OWNER); + tokenAdminRegistry.acceptAdminRole(token); + } + + tokenAdminRegistry.setPool(token, pool); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: remoteChainSelector, + remotePoolAddress: abi.encode(remotePoolAddress), + remoteTokenAddress: abi.encode(remoteToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + + TokenPool(pool).applyChainUpdates(chainUpdates); + } +} diff --git a/contracts/src/v0.8/ccip/test/WETH9.sol b/contracts/src/v0.8/ccip/test/WETH9.sol new file mode 100644 index 00000000000..fbc19ee2c4d --- /dev/null +++ b/contracts/src/v0.8/ccip/test/WETH9.sol @@ -0,0 +1,82 @@ +// Submitted for verification at Etherscan.io on 2017-12-12 + +// Copyright (C) 2015, 2016, 2017 Dapphub + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +pragma solidity 0.8.24; + +// solhint-disable +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + receive() external payable { + _deposit(); + } + + function _deposit() internal { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function deposit() external payable { + _deposit(); + } + + function withdraw(uint256 wad) external { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint128).max) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/contracts/src/v0.8/ccip/test/applications/DefensiveExample.t.sol b/contracts/src/v0.8/ccip/test/applications/DefensiveExample.t.sol new file mode 100644 index 00000000000..18453f9f525 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/applications/DefensiveExample.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {DefensiveExample} from "../../applications/DefensiveExample.sol"; +import {Client} from "../../libraries/Client.sol"; +import {EVM2EVMOnRampSetup} from "../onRamp/EVM2EVMOnRampSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract DefensiveExampleTest is EVM2EVMOnRampSetup { + event MessageFailed(bytes32 indexed messageId, bytes reason); + event MessageSucceeded(bytes32 indexed messageId); + event MessageRecovered(bytes32 indexed messageId); + + DefensiveExample internal s_receiver; + uint64 internal sourceChainSelector = 7331; + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + s_receiver = new DefensiveExample(s_destRouter, IERC20(s_destFeeToken)); + s_receiver.enableChain(sourceChainSelector, abi.encode("")); + } + + function test_Recovery() public { + bytes32 messageId = keccak256("messageId"); + address token = address(s_destFeeToken); + uint256 amount = 111333333777; + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](1); + destTokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount}); + + // Make sure we give the receiver contract enough tokens like CCIP would. + deal(token, address(s_receiver), amount); + + // Make sure the contract call reverts so we can test recovery. + s_receiver.setSimRevert(true); + + // The receiver contract will revert if the router is not the sender. + vm.startPrank(address(s_destRouter)); + + vm.expectEmit(); + emit MessageFailed(messageId, abi.encodeWithSelector(DefensiveExample.ErrorCase.selector)); + + s_receiver.ccipReceive( + Client.Any2EVMMessage({ + messageId: messageId, + sourceChainSelector: sourceChainSelector, + sender: abi.encode(address(0)), // wrong sender, will revert internally + data: "", + destTokenAmounts: destTokenAmounts + }) + ); + + address tokenReceiver = address(0x000001337); + uint256 tokenReceiverBalancePre = IERC20(token).balanceOf(tokenReceiver); + uint256 receiverBalancePre = IERC20(token).balanceOf(address(s_receiver)); + + // Recovery can only be done by the owner. + vm.startPrank(OWNER); + + vm.expectEmit(); + emit MessageRecovered(messageId); + + s_receiver.retryFailedMessage(messageId, tokenReceiver); + + // Assert the tokens have successfully been rescued from the contract. + assertEq(IERC20(token).balanceOf(tokenReceiver), tokenReceiverBalancePre + amount); + assertEq(IERC20(token).balanceOf(address(s_receiver)), receiverBalancePre - amount); + } + + function test_HappyPath_Success() public { + bytes32 messageId = keccak256("messageId"); + address token = address(s_destFeeToken); + uint256 amount = 111333333777; + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](1); + destTokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount}); + + // Make sure we give the receiver contract enough tokens like CCIP would. + deal(token, address(s_receiver), amount); + + // The receiver contract will revert if the router is not the sender. + vm.startPrank(address(s_destRouter)); + + vm.expectEmit(); + emit MessageSucceeded(messageId); + + s_receiver.ccipReceive( + Client.Any2EVMMessage({ + messageId: messageId, + sourceChainSelector: sourceChainSelector, + sender: abi.encode(address(s_receiver)), // correct sender + data: "", + destTokenAmounts: destTokenAmounts + }) + ); + } +} diff --git a/contracts/src/v0.8/ccip/test/applications/EtherSenderReceiver.t.sol b/contracts/src/v0.8/ccip/test/applications/EtherSenderReceiver.t.sol new file mode 100644 index 00000000000..cfd402d9106 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/applications/EtherSenderReceiver.t.sol @@ -0,0 +1,718 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; + +import {CCIPRouter} from "../../applications/EtherSenderReceiver.sol"; + +import {IRouterClient} from "../../interfaces/IRouterClient.sol"; +import {Client} from "../../libraries/Client.sol"; +import {WETH9} from "../WETH9.sol"; +import {EtherSenderReceiverHelper} from "./../helpers/EtherSenderReceiverHelper.sol"; + +import {ERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/ERC20.sol"; + +contract EtherSenderReceiverTest is Test { + EtherSenderReceiverHelper internal s_etherSenderReceiver; + WETH9 internal s_weth; + WETH9 internal s_someOtherWeth; + ERC20 internal s_linkToken; + + address internal constant OWNER = 0x00007e64E1fB0C487F25dd6D3601ff6aF8d32e4e; + address internal constant ROUTER = 0x0F3779ee3a832D10158073ae2F5e61ac7FBBF880; + address internal constant XCHAIN_RECEIVER = 0xBd91b2073218AF872BF73b65e2e5950ea356d147; + + function setUp() public { + vm.startPrank(OWNER); + + s_linkToken = new ERC20("Chainlink Token", "LINK"); + s_someOtherWeth = new WETH9(); + s_weth = new WETH9(); + vm.mockCall(ROUTER, abi.encodeWithSelector(CCIPRouter.getWrappedNative.selector), abi.encode(address(s_weth))); + s_etherSenderReceiver = new EtherSenderReceiverHelper(ROUTER); + + deal(OWNER, 1_000_000 ether); + deal(address(s_linkToken), OWNER, 1_000_000 ether); + + // deposit some eth into the weth contract. + s_weth.deposit{value: 10 ether}(); + uint256 wethSupply = s_weth.totalSupply(); + assertEq(wethSupply, 10 ether, "total weth supply must be 10 ether"); + } +} + +contract EtherSenderReceiverTest_constructor is EtherSenderReceiverTest { + function test_constructor() public view { + assertEq(s_etherSenderReceiver.getRouter(), ROUTER, "router must be set correctly"); + uint256 allowance = s_weth.allowance(address(s_etherSenderReceiver), ROUTER); + assertEq(allowance, type(uint256).max, "allowance must be set infinite"); + } +} + +contract EtherSenderReceiverTest_validateFeeToken is EtherSenderReceiverTest { + uint256 internal constant amount = 100; + + error InsufficientMsgValue(uint256 gotAmount, uint256 msgValue); + error TokenAmountNotEqualToMsgValue(uint256 gotAmount, uint256 msgValue); + + function test_validateFeeToken_valid_native() public { + Client.EVMTokenAmount[] memory tokenAmount = new Client.EVMTokenAmount[](1); + tokenAmount[0] = Client.EVMTokenAmount({token: address(s_weth), amount: amount}); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmount, + feeToken: address(0), + extraArgs: "" + }); + + s_etherSenderReceiver.validateFeeToken{value: amount + 1}(message); + } + + function test_validateFeeToken_valid_feeToken() public { + Client.EVMTokenAmount[] memory tokenAmount = new Client.EVMTokenAmount[](1); + tokenAmount[0] = Client.EVMTokenAmount({token: address(s_weth), amount: amount}); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmount, + feeToken: address(s_weth), + extraArgs: "" + }); + + s_etherSenderReceiver.validateFeeToken{value: amount}(message); + } + + function test_validateFeeToken_reverts_feeToken_tokenAmountNotEqualToMsgValue() public { + Client.EVMTokenAmount[] memory tokenAmount = new Client.EVMTokenAmount[](1); + tokenAmount[0] = Client.EVMTokenAmount({token: address(s_weth), amount: amount}); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmount, + feeToken: address(s_weth), + extraArgs: "" + }); + + vm.expectRevert(abi.encodeWithSelector(TokenAmountNotEqualToMsgValue.selector, amount, amount + 1)); + s_etherSenderReceiver.validateFeeToken{value: amount + 1}(message); + } +} + +contract EtherSenderReceiverTest_validatedMessage is EtherSenderReceiverTest { + error InvalidDestinationReceiver(bytes destReceiver); + error InvalidTokenAmounts(uint256 gotAmounts); + error InvalidWethAddress(address want, address got); + error GasLimitTooLow(uint256 minLimit, uint256 gotLimit); + + uint256 internal constant amount = 100; + + function test_Fuzz_validatedMessage_msgSenderOverwrite(bytes memory data) public view { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: data, + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + assertEq(validatedMessage.receiver, abi.encode(XCHAIN_RECEIVER), "receiver must be XCHAIN_RECEIVER"); + assertEq(validatedMessage.data, abi.encode(OWNER), "data must be msg.sender"); + assertEq(validatedMessage.tokenAmounts[0].token, address(s_weth), "token must be weth"); + assertEq(validatedMessage.tokenAmounts[0].amount, amount, "amount must be correct"); + assertEq(validatedMessage.feeToken, address(0), "feeToken must be 0"); + assertEq(validatedMessage.extraArgs, bytes(""), "extraArgs must be empty"); + } + + function test_Fuzz_validatedMessage_tokenAddressOverwrite(address token) public view { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount}); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + assertEq(validatedMessage.receiver, abi.encode(XCHAIN_RECEIVER), "receiver must be XCHAIN_RECEIVER"); + assertEq(validatedMessage.data, abi.encode(OWNER), "data must be msg.sender"); + assertEq(validatedMessage.tokenAmounts[0].token, address(s_weth), "token must be weth"); + assertEq(validatedMessage.tokenAmounts[0].amount, amount, "amount must be correct"); + assertEq(validatedMessage.feeToken, address(0), "feeToken must be 0"); + assertEq(validatedMessage.extraArgs, bytes(""), "extraArgs must be empty"); + } + + function test_validatedMessage_emptyDataOverwrittenToMsgSender() public view { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + assertEq(validatedMessage.receiver, abi.encode(XCHAIN_RECEIVER), "receiver must be XCHAIN_RECEIVER"); + assertEq(validatedMessage.data, abi.encode(OWNER), "data must be msg.sender"); + assertEq(validatedMessage.tokenAmounts[0].token, address(s_weth), "token must be weth"); + assertEq(validatedMessage.tokenAmounts[0].amount, amount, "amount must be correct"); + assertEq(validatedMessage.feeToken, address(0), "feeToken must be 0"); + assertEq(validatedMessage.extraArgs, bytes(""), "extraArgs must be empty"); + } + + function test_validatedMessage_dataOverwrittenToMsgSender() public view { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: abi.encode(address(42)), + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + assertEq(validatedMessage.receiver, abi.encode(XCHAIN_RECEIVER), "receiver must be XCHAIN_RECEIVER"); + assertEq(validatedMessage.data, abi.encode(OWNER), "data must be msg.sender"); + assertEq(validatedMessage.tokenAmounts[0].token, address(s_weth), "token must be weth"); + assertEq(validatedMessage.tokenAmounts[0].amount, amount, "amount must be correct"); + assertEq(validatedMessage.feeToken, address(0), "feeToken must be 0"); + assertEq(validatedMessage.extraArgs, bytes(""), "extraArgs must be empty"); + } + + function test_validatedMessage_tokenOverwrittenToWeth() public view { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(42), // incorrect token. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + assertEq(validatedMessage.receiver, abi.encode(XCHAIN_RECEIVER), "receiver must be XCHAIN_RECEIVER"); + assertEq(validatedMessage.data, abi.encode(OWNER), "data must be msg.sender"); + assertEq(validatedMessage.tokenAmounts[0].token, address(s_weth), "token must be weth"); + assertEq(validatedMessage.tokenAmounts[0].amount, amount, "amount must be correct"); + assertEq(validatedMessage.feeToken, address(0), "feeToken must be 0"); + assertEq(validatedMessage.extraArgs, bytes(""), "extraArgs must be empty"); + } + + function test_validatedMessage_validMessage_extraArgs() public view { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})) + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + assertEq(validatedMessage.receiver, abi.encode(XCHAIN_RECEIVER), "receiver must be XCHAIN_RECEIVER"); + assertEq(validatedMessage.data, abi.encode(OWNER), "data must be msg.sender"); + assertEq(validatedMessage.tokenAmounts[0].token, address(s_weth), "token must be weth"); + assertEq(validatedMessage.tokenAmounts[0].amount, amount, "amount must be correct"); + assertEq(validatedMessage.feeToken, address(0), "feeToken must be 0"); + assertEq( + validatedMessage.extraArgs, + Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})), + "extraArgs must be correct" + ); + } + + function test_validatedMessage_invalidTokenAmounts() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: address(0), amount: amount}); + tokenAmounts[1] = Client.EVMTokenAmount({token: address(0), amount: amount}); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + vm.expectRevert(abi.encodeWithSelector(InvalidTokenAmounts.selector, uint256(2))); + s_etherSenderReceiver.validatedMessage(message); + } +} + +contract EtherSenderReceiverTest_getFee is EtherSenderReceiverTest { + uint64 internal constant destinationChainSelector = 424242; + uint256 internal constant feeWei = 121212; + uint256 internal constant amount = 100; + + function test_getFee() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: address(0), amount: amount}); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeWei) + ); + + uint256 fee = s_etherSenderReceiver.getFee(destinationChainSelector, message); + assertEq(fee, feeWei, "fee must be feeWei"); + } +} + +contract EtherSenderReceiverTest_ccipReceive is EtherSenderReceiverTest { + uint256 internal constant amount = 100; + uint64 internal constant sourceChainSelector = 424242; + address internal constant XCHAIN_SENDER = 0x9951529C13B01E542f7eE3b6D6665D292e9BA2E0; + + error InvalidTokenAmounts(uint256 gotAmounts); + error InvalidToken(address gotToken, address expectedToken); + + function test_Fuzz_ccipReceive(uint256 tokenAmount) public { + // cap to 10 ether because OWNER only has 10 ether. + if (tokenAmount > 10 ether) { + return; + } + + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](1); + destTokenAmounts[0] = Client.EVMTokenAmount({token: address(s_weth), amount: tokenAmount}); + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: keccak256(abi.encode("ccip send")), + sourceChainSelector: sourceChainSelector, + sender: abi.encode(XCHAIN_SENDER), + data: abi.encode(OWNER), + destTokenAmounts: destTokenAmounts + }); + + // simulate a cross-chain token transfer, just transfer the weth to s_etherSenderReceiver. + s_weth.transfer(address(s_etherSenderReceiver), tokenAmount); + + uint256 balanceBefore = OWNER.balance; + s_etherSenderReceiver.publicCcipReceive(message); + uint256 balanceAfter = OWNER.balance; + assertEq(balanceAfter, balanceBefore + tokenAmount, "balance must be correct"); + } + + function test_ccipReceive_happyPath() public { + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](1); + destTokenAmounts[0] = Client.EVMTokenAmount({token: address(s_weth), amount: amount}); + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: keccak256(abi.encode("ccip send")), + sourceChainSelector: 424242, + sender: abi.encode(XCHAIN_SENDER), + data: abi.encode(OWNER), + destTokenAmounts: destTokenAmounts + }); + + // simulate a cross-chain token transfer, just transfer the weth to s_etherSenderReceiver. + s_weth.transfer(address(s_etherSenderReceiver), amount); + + uint256 balanceBefore = OWNER.balance; + s_etherSenderReceiver.publicCcipReceive(message); + uint256 balanceAfter = OWNER.balance; + assertEq(balanceAfter, balanceBefore + amount, "balance must be correct"); + } + + function test_ccipReceive_fallbackToWethTransfer() public { + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](1); + destTokenAmounts[0] = Client.EVMTokenAmount({token: address(s_weth), amount: amount}); + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: keccak256(abi.encode("ccip send")), + sourceChainSelector: 424242, + sender: abi.encode(XCHAIN_SENDER), + data: abi.encode(address(s_linkToken)), // ERC20 cannot receive() ether. + destTokenAmounts: destTokenAmounts + }); + + // simulate a cross-chain token transfer, just transfer the weth to s_etherSenderReceiver. + s_weth.transfer(address(s_etherSenderReceiver), amount); + + uint256 balanceBefore = address(s_linkToken).balance; + s_etherSenderReceiver.publicCcipReceive(message); + uint256 balanceAfter = address(s_linkToken).balance; + assertEq(balanceAfter, balanceBefore, "balance must be unchanged"); + uint256 wethBalance = s_weth.balanceOf(address(s_linkToken)); + assertEq(wethBalance, amount, "weth balance must be correct"); + } + + function test_ccipReceive_wrongTokenAmount() public { + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](2); + destTokenAmounts[0] = Client.EVMTokenAmount({token: address(s_weth), amount: amount}); + destTokenAmounts[1] = Client.EVMTokenAmount({token: address(s_weth), amount: amount}); + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: keccak256(abi.encode("ccip send")), + sourceChainSelector: 424242, + sender: abi.encode(XCHAIN_SENDER), + data: abi.encode(OWNER), + destTokenAmounts: destTokenAmounts + }); + + vm.expectRevert(abi.encodeWithSelector(InvalidTokenAmounts.selector, uint256(2))); + s_etherSenderReceiver.publicCcipReceive(message); + } + + function test_ccipReceive_wrongToken() public { + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](1); + destTokenAmounts[0] = Client.EVMTokenAmount({token: address(s_someOtherWeth), amount: amount}); + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: keccak256(abi.encode("ccip send")), + sourceChainSelector: 424242, + sender: abi.encode(XCHAIN_SENDER), + data: abi.encode(OWNER), + destTokenAmounts: destTokenAmounts + }); + + vm.expectRevert(abi.encodeWithSelector(InvalidToken.selector, address(s_someOtherWeth), address(s_weth))); + s_etherSenderReceiver.publicCcipReceive(message); + } +} + +contract EtherSenderReceiverTest_ccipSend is EtherSenderReceiverTest { + error InsufficientFee(uint256 gotFee, uint256 fee); + + uint256 internal constant amount = 100; + uint64 internal constant destinationChainSelector = 424242; + uint256 internal constant feeWei = 121212; + uint256 internal constant feeJuels = 232323; + + function test_Fuzz_ccipSend(uint256 feeFromRouter, uint256 feeSupplied) public { + // cap the fuzzer because OWNER only has a million ether. + vm.assume(feeSupplied < 1_000_000 ether - amount); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeFromRouter) + ); + + if (feeSupplied < feeFromRouter) { + vm.expectRevert(); + s_etherSenderReceiver.ccipSend{value: amount + feeSupplied}(destinationChainSelector, message); + } else { + bytes32 expectedMsgId = keccak256(abi.encode("ccip send")); + vm.mockCall( + ROUTER, + feeSupplied, + abi.encodeWithSelector(IRouterClient.ccipSend.selector, destinationChainSelector, validatedMessage), + abi.encode(expectedMsgId) + ); + + bytes32 actualMsgId = + s_etherSenderReceiver.ccipSend{value: amount + feeSupplied}(destinationChainSelector, message); + assertEq(actualMsgId, expectedMsgId, "message id must be correct"); + } + } + + function test_Fuzz_ccipSend_feeToken(uint256 feeFromRouter, uint256 feeSupplied) public { + // cap the fuzzer because OWNER only has a million LINK. + vm.assume(feeSupplied < 1_000_000 ether - amount); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(s_linkToken), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeFromRouter) + ); + + s_linkToken.approve(address(s_etherSenderReceiver), feeSupplied); + + if (feeSupplied < feeFromRouter) { + vm.expectRevert(); + s_etherSenderReceiver.ccipSend{value: amount}(destinationChainSelector, message); + } else { + bytes32 expectedMsgId = keccak256(abi.encode("ccip send")); + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.ccipSend.selector, destinationChainSelector, validatedMessage), + abi.encode(expectedMsgId) + ); + + bytes32 actualMsgId = s_etherSenderReceiver.ccipSend{value: amount}(destinationChainSelector, message); + assertEq(actualMsgId, expectedMsgId, "message id must be correct"); + } + } + + function test_ccipSend_reverts_insufficientFee_weth() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(s_weth), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeWei) + ); + + s_weth.approve(address(s_etherSenderReceiver), feeWei - 1); + + vm.expectRevert("SafeERC20: low-level call failed"); + s_etherSenderReceiver.ccipSend{value: amount}(destinationChainSelector, message); + } + + function test_ccipSend_reverts_insufficientFee_feeToken() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(s_linkToken), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeJuels) + ); + + s_linkToken.approve(address(s_etherSenderReceiver), feeJuels - 1); + + vm.expectRevert("ERC20: insufficient allowance"); + s_etherSenderReceiver.ccipSend{value: amount}(destinationChainSelector, message); + } + + function test_ccipSend_reverts_insufficientFee_native() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeWei) + ); + + vm.expectRevert(); + s_etherSenderReceiver.ccipSend{value: amount + feeWei - 1}(destinationChainSelector, message); + } + + function test_ccipSend_success_nativeExcess() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + bytes32 expectedMsgId = keccak256(abi.encode("ccip send")); + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeWei) + ); + + // we assert that the correct value is sent to the router call, which should be + // the msg.value - feeWei. + vm.mockCall( + ROUTER, + feeWei + 1, + abi.encodeWithSelector(IRouterClient.ccipSend.selector, destinationChainSelector, validatedMessage), + abi.encode(expectedMsgId) + ); + + bytes32 actualMsgId = s_etherSenderReceiver.ccipSend{value: amount + feeWei + 1}(destinationChainSelector, message); + assertEq(actualMsgId, expectedMsgId, "message id must be correct"); + } + + function test_ccipSend_success_native() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(0), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + bytes32 expectedMsgId = keccak256(abi.encode("ccip send")); + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeWei) + ); + vm.mockCall( + ROUTER, + feeWei, + abi.encodeWithSelector(IRouterClient.ccipSend.selector, destinationChainSelector, validatedMessage), + abi.encode(expectedMsgId) + ); + + bytes32 actualMsgId = s_etherSenderReceiver.ccipSend{value: amount + feeWei}(destinationChainSelector, message); + assertEq(actualMsgId, expectedMsgId, "message id must be correct"); + } + + function test_ccipSend_success_feeToken() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(s_linkToken), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + bytes32 expectedMsgId = keccak256(abi.encode("ccip send")); + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeJuels) + ); + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.ccipSend.selector, destinationChainSelector, validatedMessage), + abi.encode(expectedMsgId) + ); + + s_linkToken.approve(address(s_etherSenderReceiver), feeJuels); + + bytes32 actualMsgId = s_etherSenderReceiver.ccipSend{value: amount}(destinationChainSelector, message); + assertEq(actualMsgId, expectedMsgId, "message id must be correct"); + uint256 routerAllowance = s_linkToken.allowance(address(s_etherSenderReceiver), ROUTER); + assertEq(routerAllowance, feeJuels, "router allowance must be feeJuels"); + } + + function test_ccipSend_success_weth() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({ + token: address(0), // callers may not specify this. + amount: amount + }); + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(XCHAIN_RECEIVER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: address(s_weth), + extraArgs: "" + }); + + Client.EVM2AnyMessage memory validatedMessage = s_etherSenderReceiver.validatedMessage(message); + + bytes32 expectedMsgId = keccak256(abi.encode("ccip send")); + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.getFee.selector, destinationChainSelector, validatedMessage), + abi.encode(feeWei) + ); + vm.mockCall( + ROUTER, + abi.encodeWithSelector(IRouterClient.ccipSend.selector, destinationChainSelector, validatedMessage), + abi.encode(expectedMsgId) + ); + + s_weth.approve(address(s_etherSenderReceiver), feeWei); + + bytes32 actualMsgId = s_etherSenderReceiver.ccipSend{value: amount}(destinationChainSelector, message); + assertEq(actualMsgId, expectedMsgId, "message id must be correct"); + uint256 routerAllowance = s_weth.allowance(address(s_etherSenderReceiver), ROUTER); + assertEq(routerAllowance, type(uint256).max, "router allowance must be max for weth"); + } +} diff --git a/contracts/src/v0.8/ccip/test/applications/ImmutableExample.t.sol b/contracts/src/v0.8/ccip/test/applications/ImmutableExample.t.sol new file mode 100644 index 00000000000..eb12e6205a4 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/applications/ImmutableExample.t.sol @@ -0,0 +1,61 @@ +pragma solidity ^0.8.0; + +import {IAny2EVMMessageReceiver} from "../../interfaces/IAny2EVMMessageReceiver.sol"; + +import {CCIPClientExample} from "../../applications/CCIPClientExample.sol"; +import {Client} from "../../libraries/Client.sol"; +import {EVM2EVMOnRampSetup} from "../onRamp/EVM2EVMOnRampSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {ERC165Checker} from + "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol"; + +contract CCIPClientExample_sanity is EVM2EVMOnRampSetup { + function test_ImmutableExamples_Success() public { + CCIPClientExample exampleContract = new CCIPClientExample(s_sourceRouter, IERC20(s_sourceFeeToken)); + deal(address(exampleContract), 100 ether); + deal(s_sourceFeeToken, address(exampleContract), 100 ether); + + // feeToken approval works + assertEq(IERC20(s_sourceFeeToken).allowance(address(exampleContract), address(s_sourceRouter)), 2 ** 256 - 1); + + // Can set chain + Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000}); + bytes memory encodedExtraArgs = Client._argsToBytes(extraArgs); + exampleContract.enableChain(DEST_CHAIN_SELECTOR, encodedExtraArgs); + assertEq(exampleContract.s_chains(DEST_CHAIN_SELECTOR), encodedExtraArgs); + + address toAddress = makeAddr("toAddress"); + + // Can send data pay native + exampleContract.sendDataPayNative(DEST_CHAIN_SELECTOR, abi.encode(toAddress), bytes("hello")); + + // Can send data pay feeToken + exampleContract.sendDataPayFeeToken(DEST_CHAIN_SELECTOR, abi.encode(toAddress), bytes("hello")); + + // Can send data tokens + address sourceToken = s_sourceTokens[1]; + assertEq( + address(s_onRamp.getPoolBySourceToken(DEST_CHAIN_SELECTOR, IERC20(sourceToken))), + address(s_sourcePoolByToken[sourceToken]) + ); + deal(sourceToken, OWNER, 100 ether); + IERC20(sourceToken).approve(address(exampleContract), 1 ether); + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: sourceToken, amount: 1 ether}); + exampleContract.sendDataAndTokens(DEST_CHAIN_SELECTOR, abi.encode(toAddress), bytes("hello"), tokenAmounts); + // Tokens transferred from owner to router then burned in pool. + assertEq(IERC20(sourceToken).balanceOf(OWNER), 99 ether); + assertEq(IERC20(sourceToken).balanceOf(address(s_sourceRouter)), 0); + + // Can send just tokens + IERC20(sourceToken).approve(address(exampleContract), 1 ether); + exampleContract.sendTokens(DEST_CHAIN_SELECTOR, abi.encode(toAddress), tokenAmounts); + + // Can receive + assertTrue(ERC165Checker.supportsInterface(address(exampleContract), type(IAny2EVMMessageReceiver).interfaceId)); + + // Can disable chain + exampleContract.disableChain(DEST_CHAIN_SELECTOR); + } +} diff --git a/contracts/src/v0.8/ccip/test/applications/PingPongDemo.t.sol b/contracts/src/v0.8/ccip/test/applications/PingPongDemo.t.sol new file mode 100644 index 00000000000..3297e1f4fbc --- /dev/null +++ b/contracts/src/v0.8/ccip/test/applications/PingPongDemo.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {PingPongDemo} from "../../applications/PingPongDemo.sol"; +import {Client} from "../../libraries/Client.sol"; +import "../onRamp/EVM2EVMOnRampSetup.t.sol"; + +// setup +contract PingPongDappSetup is EVM2EVMOnRampSetup { + PingPongDemo internal s_pingPong; + IERC20 internal s_feeToken; + + address internal immutable i_pongContract = makeAddr("ping_pong_counterpart"); + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + s_feeToken = IERC20(s_sourceTokens[0]); + s_pingPong = new PingPongDemo(address(s_sourceRouter), s_feeToken); + s_pingPong.setCounterpart(DEST_CHAIN_SELECTOR, i_pongContract); + + uint256 fundingAmount = 1e18; + + // Fund the contract with LINK tokens + s_feeToken.transfer(address(s_pingPong), fundingAmount); + } +} + +contract PingPong_startPingPong is PingPongDappSetup { + function test_StartPingPong_Success() public { + uint256 pingPongNumber = 1; + bytes memory data = abi.encode(pingPongNumber); + + Client.EVM2AnyMessage memory sentMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(i_pongContract), + data: data, + tokenAmounts: new Client.EVMTokenAmount[](0), + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 2e5})) + }); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, sentMessage); + + Internal.EVM2EVMMessage memory message = Internal.EVM2EVMMessage({ + sequenceNumber: 1, + feeTokenAmount: expectedFee, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + sender: address(s_pingPong), + receiver: i_pongContract, + nonce: 1, + data: data, + tokenAmounts: sentMessage.tokenAmounts, + sourceTokenData: new bytes[](sentMessage.tokenAmounts.length), + gasLimit: 2e5, + feeToken: sentMessage.feeToken, + strict: false, + messageId: "" + }); + message.messageId = Internal._hash(message, s_metadataHash); + + vm.expectEmit(); + emit PingPongDemo.Ping(pingPongNumber); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(message); + + s_pingPong.startPingPong(); + } +} + +contract PingPong_ccipReceive is PingPongDappSetup { + function test_CcipReceive_Success() public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](0); + + uint256 pingPongNumber = 5; + + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: bytes32("a"), + sourceChainSelector: DEST_CHAIN_SELECTOR, + sender: abi.encode(i_pongContract), + data: abi.encode(pingPongNumber), + destTokenAmounts: tokenAmounts + }); + + vm.startPrank(address(s_sourceRouter)); + + vm.expectEmit(); + emit PingPongDemo.Pong(pingPongNumber + 1); + + s_pingPong.ccipReceive(message); + } +} + +contract PingPong_plumbing is PingPongDappSetup { + function test_Fuzz_CounterPartChainSelector_Success(uint64 chainSelector) public { + s_pingPong.setCounterpartChainSelector(chainSelector); + + assertEq(s_pingPong.getCounterpartChainSelector(), chainSelector); + } + + function test_Fuzz_CounterPartAddress_Success(address counterpartAddress) public { + s_pingPong.setCounterpartAddress(counterpartAddress); + + assertEq(s_pingPong.getCounterpartAddress(), counterpartAddress); + } + + function test_Fuzz_CounterPartAddress_Success(uint64 chainSelector, address counterpartAddress) public { + s_pingPong.setCounterpart(chainSelector, counterpartAddress); + + assertEq(s_pingPong.getCounterpartAddress(), counterpartAddress); + assertEq(s_pingPong.getCounterpartChainSelector(), chainSelector); + } + + function test_Pausing_Success() public { + assertFalse(s_pingPong.isPaused()); + + s_pingPong.setPaused(true); + + assertTrue(s_pingPong.isPaused()); + } +} diff --git a/contracts/src/v0.8/ccip/test/applications/SelfFundedPingPong.t.sol b/contracts/src/v0.8/ccip/test/applications/SelfFundedPingPong.t.sol new file mode 100644 index 00000000000..d5db9d1f9d0 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/applications/SelfFundedPingPong.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {SelfFundedPingPong} from "../../applications/SelfFundedPingPong.sol"; +import {Client} from "../../libraries/Client.sol"; +import {EVM2EVMOnRamp} from "../../onRamp/EVM2EVMOnRamp.sol"; +import {EVM2EVMOnRampSetup} from "../onRamp/EVM2EVMOnRampSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract SelfFundedPingPongDappSetup is EVM2EVMOnRampSetup { + SelfFundedPingPong internal s_pingPong; + IERC20 internal s_feeToken; + uint8 internal constant s_roundTripsBeforeFunding = 0; + + address internal immutable i_pongContract = makeAddr("ping_pong_counterpart"); + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + s_feeToken = IERC20(s_sourceTokens[0]); + s_pingPong = new SelfFundedPingPong(address(s_sourceRouter), s_feeToken, s_roundTripsBeforeFunding); + s_pingPong.setCounterpart(DEST_CHAIN_SELECTOR, i_pongContract); + + uint256 fundingAmount = 5e18; + + // set ping pong as an onRamp nop to make sure that funding runs + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = new EVM2EVMOnRamp.NopAndWeight[](1); + nopsAndWeights[0] = EVM2EVMOnRamp.NopAndWeight({nop: address(s_pingPong), weight: 1}); + s_onRamp.setNops(nopsAndWeights); + + // Fund the contract with LINK tokens + s_feeToken.transfer(address(s_pingPong), fundingAmount); + } +} + +contract SelfFundedPingPong_ccipReceive is SelfFundedPingPongDappSetup { + function test_Funding_Success() public { + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: keccak256("msg id"), + sourceChainSelector: DEST_CHAIN_SELECTOR, + sender: abi.encode(i_pongContract), + data: "", + destTokenAmounts: new Client.EVMTokenAmount[](0) + }); + + uint8 countIncrBeforeFunding = 5; + + vm.expectEmit(); + emit SelfFundedPingPong.CountIncrBeforeFundingSet(countIncrBeforeFunding); + + s_pingPong.setCountIncrBeforeFunding(countIncrBeforeFunding); + + vm.startPrank(address(s_sourceRouter)); + for (uint256 pingPongNumber = 0; pingPongNumber <= countIncrBeforeFunding; ++pingPongNumber) { + message.data = abi.encode(pingPongNumber); + if (pingPongNumber == countIncrBeforeFunding - 1) { + vm.expectEmit(); + emit SelfFundedPingPong.Funded(); + vm.expectCall(address(s_onRamp), ""); + } + s_pingPong.ccipReceive(message); + } + } + + function test_FundingIfNotANop_Revert() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = new EVM2EVMOnRamp.NopAndWeight[](0); + s_onRamp.setNops(nopsAndWeights); + + uint8 countIncrBeforeFunding = 3; + s_pingPong.setCountIncrBeforeFunding(countIncrBeforeFunding); + + vm.startPrank(address(s_sourceRouter)); + Client.Any2EVMMessage memory message = Client.Any2EVMMessage({ + messageId: bytes32("a"), + sourceChainSelector: DEST_CHAIN_SELECTOR, + sender: abi.encode(i_pongContract), + data: abi.encode(countIncrBeforeFunding), + destTokenAmounts: new Client.EVMTokenAmount[](0) + }); + + // because pingPong is not set as a nop + vm.expectRevert(EVM2EVMOnRamp.OnlyCallableByOwnerOrAdminOrNop.selector); + s_pingPong.ccipReceive(message); + } +} + +contract SelfFundedPingPong_setCountIncrBeforeFunding is SelfFundedPingPongDappSetup { + function test_setCountIncrBeforeFunding() public { + uint8 c = s_pingPong.getCountIncrBeforeFunding(); + + vm.expectEmit(); + emit SelfFundedPingPong.CountIncrBeforeFundingSet(c + 1); + + s_pingPong.setCountIncrBeforeFunding(c + 1); + uint8 c2 = s_pingPong.getCountIncrBeforeFunding(); + assertEq(c2, c + 1); + } +} diff --git a/contracts/src/v0.8/ccip/test/applications/TokenProxy.t.sol b/contracts/src/v0.8/ccip/test/applications/TokenProxy.t.sol new file mode 100644 index 00000000000..9e78f6e369f --- /dev/null +++ b/contracts/src/v0.8/ccip/test/applications/TokenProxy.t.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {TokenProxy} from "../../applications/TokenProxy.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {EVM2EVMOnRamp} from "../../onRamp/EVM2EVMOnRamp.sol"; +import {EVM2EVMOnRampSetup} from "../onRamp/EVM2EVMOnRampSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract TokenProxySetup is EVM2EVMOnRampSetup { + TokenProxy internal s_tokenProxy; + IERC20 internal s_feeToken; + IERC20 internal s_transferToken; + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + s_feeToken = IERC20(s_sourceTokens[0]); + s_transferToken = IERC20(s_sourceTokens[1]); + s_tokenProxy = new TokenProxy(address(s_sourceRouter), address(s_transferToken)); + + s_transferToken.approve(address(s_tokenProxy), type(uint256).max); + s_feeToken.approve(address(s_tokenProxy), type(uint256).max); + } +} + +contract TokenProxy_constructor is TokenProxySetup { + function test_Constructor() public view { + assertEq(address(s_tokenProxy.getRouter()), address(s_sourceRouter)); + assertEq(address(s_tokenProxy.getToken()), address(s_transferToken)); + } +} + +contract TokenProxy_getFee is TokenProxySetup { + function test_GetFee_Success() public view { + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_transferToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(s_tokenProxy), + data: "", + tokenAmounts: tokens, + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})) + }); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + uint256 actualFee = s_tokenProxy.getFee(DEST_CHAIN_SELECTOR, message); + assertEq(expectedFee, actualFee); + } + + // Reverts + + function test_GetFeeInvalidToken_Revert() public { + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(s_tokenProxy), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](0), + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})) + }); + + vm.expectRevert(TokenProxy.InvalidToken.selector); + + s_tokenProxy.getFee(DEST_CHAIN_SELECTOR, message); + } + + function test_GetFeeNoDataAllowed_Revert() public { + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_transferToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(s_tokenProxy), + data: "not empty", + tokenAmounts: tokens, + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})) + }); + + vm.expectRevert(TokenProxy.NoDataAllowed.selector); + + s_tokenProxy.getFee(DEST_CHAIN_SELECTOR, message); + } + + function test_GetFeeGasShouldBeZero_Revert() public { + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_transferToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(s_tokenProxy), + data: "", + tokenAmounts: tokens, + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 10})) + }); + + vm.expectRevert(TokenProxy.GasShouldBeZero.selector); + + s_tokenProxy.getFee(DEST_CHAIN_SELECTOR, message); + } +} + +contract TokenProxy_ccipSend is TokenProxySetup { + function test_CcipSend_Success() public { + vm.pauseGasMetering(); + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_transferToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = tokens; + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + + s_feeToken.approve(address(s_tokenProxy), expectedFee); + + Internal.EVM2EVMMessage memory msgEvent = _messageToEvent(message, 1, 1, expectedFee, OWNER); + msgEvent.sender = address(s_tokenProxy); + msgEvent.messageId = Internal._hash(msgEvent, s_metadataHash); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent); + + vm.resumeGasMetering(); + s_tokenProxy.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_CcipSendNative_Success() public { + vm.pauseGasMetering(); + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_transferToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = tokens; + message.feeToken = address(0); + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + + Internal.EVM2EVMMessage memory msgEvent = _messageToEvent(message, 1, 1, expectedFee, OWNER); + msgEvent.sender = address(s_tokenProxy); + msgEvent.feeToken = s_sourceRouter.getWrappedNative(); + msgEvent.messageId = Internal._hash(msgEvent, s_metadataHash); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent); + + vm.resumeGasMetering(); + s_tokenProxy.ccipSend{value: expectedFee}(DEST_CHAIN_SELECTOR, message); + } + + // Reverts + + function test_CcipSendInsufficientAllowance_Revert() public { + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_transferToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = tokens; + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})); + + // Revoke allowance + s_transferToken.approve(address(s_tokenProxy), 0); + + vm.expectRevert("ERC20: insufficient allowance"); + + s_tokenProxy.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_CcipSendInvalidToken_Revert() public { + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_feeToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = tokens; + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})); + + vm.expectRevert(TokenProxy.InvalidToken.selector); + + s_tokenProxy.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_CcipSendNoDataAllowed_Revert() public { + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_transferToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = tokens; + message.data = "not empty"; + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})); + + vm.expectRevert(TokenProxy.NoDataAllowed.selector); + + s_tokenProxy.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_CcipSendGasShouldBeZero_Revert() public { + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: address(s_transferToken), amount: 1e18}); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = tokens; + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 1})); + + vm.expectRevert(TokenProxy.GasShouldBeZero.selector); + + s_tokenProxy.ccipSend(DEST_CHAIN_SELECTOR, message); + } +} diff --git a/contracts/src/v0.8/ccip/test/arm/ARMProxy.t.sol b/contracts/src/v0.8/ccip/test/arm/ARMProxy.t.sol new file mode 100644 index 00000000000..24b617c82a0 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/arm/ARMProxy.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IRMN} from "../../interfaces/IRMN.sol"; + +import {ARMProxy} from "../../ARMProxy.sol"; +import {RMN} from "../../RMN.sol"; +import {MockRMN} from "../mocks/MockRMN.sol"; +import {RMNSetup, makeSubjects} from "./RMNSetup.t.sol"; + +contract ARMProxyTest is RMNSetup { + MockRMN internal s_mockRMN; + ARMProxy internal s_armProxy; + + function setUp() public virtual override { + RMNSetup.setUp(); + s_mockRMN = new MockRMN(); + s_armProxy = new ARMProxy(address(s_rmn)); + } + + function test_ARMIsCursed_Success() public { + s_armProxy.setARM(address(s_mockRMN)); + assertFalse(IRMN(address(s_armProxy)).isCursed()); + s_mockRMN.setGlobalCursed(true); + assertTrue(IRMN(address(s_armProxy)).isCursed()); + } + + function test_ARMIsBlessed_Success() public { + s_armProxy.setARM(address(s_mockRMN)); + s_mockRMN.setTaggedRootBlessed(IRMN.TaggedRoot({commitStore: address(0), root: bytes32(0)}), true); + assertTrue(IRMN(address(s_armProxy)).isBlessed(IRMN.TaggedRoot({commitStore: address(0), root: bytes32(0)}))); + s_mockRMN.setTaggedRootBlessed(IRMN.TaggedRoot({commitStore: address(0), root: bytes32(0)}), false); + assertFalse(IRMN(address(s_armProxy)).isBlessed(IRMN.TaggedRoot({commitStore: address(0), root: bytes32(0)}))); + } + + function test_ARMCallRevertReasonForwarded() public { + bytes memory err = bytes("revert"); + s_mockRMN.setIsCursedRevert(err); + s_armProxy.setARM(address(s_mockRMN)); + vm.expectRevert(abi.encodeWithSelector(MockRMN.CustomError.selector, err)); + IRMN(address(s_armProxy)).isCursed(); + } +} diff --git a/contracts/src/v0.8/ccip/test/arm/ARMProxy_standalone.t.sol b/contracts/src/v0.8/ccip/test/arm/ARMProxy_standalone.t.sol new file mode 100644 index 00000000000..4f3e96fafa2 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/arm/ARMProxy_standalone.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ARMProxy} from "../../ARMProxy.sol"; +import {Test} from "forge-std/Test.sol"; + +contract ARMProxyStandaloneTest is Test { + address internal constant EMPTY_ADDRESS = address(0x1); + address internal constant OWNER_ADDRESS = 0xC0ffeeEeC0fFeeeEc0ffeEeEc0ffEEEEC0FfEEee; + address internal constant MOCK_RMN_ADDRESS = 0x1337133713371337133713371337133713371337; + + ARMProxy internal s_armProxy; + + function setUp() public virtual { + // needed so that the extcodesize check in ARMProxy.fallback doesn't revert + vm.etch(MOCK_RMN_ADDRESS, bytes("fake bytecode")); + + vm.prank(OWNER_ADDRESS); + s_armProxy = new ARMProxy(MOCK_RMN_ADDRESS); + } + + function test_Constructor() public { + vm.expectEmit(); + emit ARMProxy.ARMSet(MOCK_RMN_ADDRESS); + ARMProxy proxy = new ARMProxy(MOCK_RMN_ADDRESS); + assertEq(proxy.getARM(), MOCK_RMN_ADDRESS); + } + + function test_SetARM() public { + vm.expectEmit(); + emit ARMProxy.ARMSet(MOCK_RMN_ADDRESS); + vm.prank(OWNER_ADDRESS); + s_armProxy.setARM(MOCK_RMN_ADDRESS); + assertEq(s_armProxy.getARM(), MOCK_RMN_ADDRESS); + } + + function test_SetARMzero() public { + vm.expectRevert(abi.encodeWithSelector(ARMProxy.ZeroAddressNotAllowed.selector)); + vm.prank(OWNER_ADDRESS); + s_armProxy.setARM(address(0x0)); + } + + /* + function test_Fuzz_ARMCall(bool expectedSuccess, bytes memory call, bytes memory ret) public { + // filter out calls to functions that will be handled on the ARMProxy instead + // of the underlying ARM contract + vm.assume( + call.length < 4 || + (bytes4(call) != s_armProxy.getARM.selector && + bytes4(call) != s_armProxy.setARM.selector && + bytes4(call) != s_armProxy.owner.selector && + bytes4(call) != s_armProxy.acceptOwnership.selector && + bytes4(call) != s_armProxy.transferOwnership.selector && + bytes4(call) != s_armProxy.typeAndVersion.selector) + ); + + if (expectedSuccess) { + vm.mockCall(MOCK_RMN_ADDRESS, 0, call, ret); + } else { + vm.mockCallRevert(MOCK_RMN_ADDRESS, 0, call, ret); + } + (bool actualSuccess, bytes memory result) = address(s_armProxy).call(call); + vm.clearMockedCalls(); + + assertEq(result, ret); + assertEq(expectedSuccess, actualSuccess); + } + */ + + function test_ARMCallEmptyContractRevert() public { + vm.prank(OWNER_ADDRESS); + s_armProxy.setARM(EMPTY_ADDRESS); // No code at address 1, should revert. + vm.expectRevert(); + bytes memory b = new bytes(0); + (bool success,) = address(s_armProxy).call(b); + success; + } +} diff --git a/contracts/src/v0.8/ccip/test/arm/RMN.t.sol b/contracts/src/v0.8/ccip/test/arm/RMN.t.sol new file mode 100644 index 00000000000..d3237592f29 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/arm/RMN.t.sol @@ -0,0 +1,1068 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IRMN} from "../../interfaces/IRMN.sol"; + +import {GLOBAL_CURSE_SUBJECT, LIFT_CURSE_VOTE_ADDR, OWNER_CURSE_VOTE_ADDR, RMN} from "../../RMN.sol"; +import {RMNSetup, makeCursesHash, makeSubjects} from "./RMNSetup.t.sol"; + +import {Test} from "forge-std/Test.sol"; + +bytes28 constant GARBAGE_CURSES_HASH = bytes28(keccak256("GARBAGE_CURSES_HASH")); + +contract ConfigCompare is Test { + function assertConfigEq(RMN.Config memory actualConfig, RMN.Config memory expectedConfig) public pure { + assertEq(actualConfig.voters.length, expectedConfig.voters.length); + for (uint256 i = 0; i < expectedConfig.voters.length; ++i) { + RMN.Voter memory expectedVoter = expectedConfig.voters[i]; + RMN.Voter memory actualVoter = actualConfig.voters[i]; + assertEq(actualVoter.blessVoteAddr, expectedVoter.blessVoteAddr); + assertEq(actualVoter.curseVoteAddr, expectedVoter.curseVoteAddr); + assertEq(actualVoter.blessWeight, expectedVoter.blessWeight); + assertEq(actualVoter.curseWeight, expectedVoter.curseWeight); + } + assertEq(actualConfig.blessWeightThreshold, expectedConfig.blessWeightThreshold); + assertEq(actualConfig.curseWeightThreshold, expectedConfig.curseWeightThreshold); + } +} + +contract RMN_constructor is ConfigCompare, RMNSetup { + function test_Constructor_Success() public view { + RMN.Config memory expectedConfig = rmnConstructorArgs(); + (uint32 actualVersion,, RMN.Config memory actualConfig) = s_rmn.getConfigDetails(); + assertEq(actualVersion, 1); + assertConfigEq(actualConfig, expectedConfig); + } +} + +contract RMN_voteToBless is RMNSetup { + function _getFirstBlessVoterAndWeight() internal pure returns (address, uint8) { + RMN.Config memory cfg = rmnConstructorArgs(); + return (cfg.voters[0].blessVoteAddr, cfg.voters[0].blessWeight); + } + + // Success + + function test_RootSuccess() public { + uint256 numRoots = 10; + + (address voter, uint8 voterWeight) = _getFirstBlessVoterAndWeight(); + + for (uint256 i = 1; i <= numRoots; ++i) { + vm.expectEmit(); + emit RMN.VotedToBless(1, voter, makeTaggedRoot(i), voterWeight); + } + + vm.prank(voter); + s_rmn.voteToBless(makeTaggedRootsInclusive(1, numRoots)); + + for (uint256 i = 1; i <= numRoots; ++i) { + assertFalse(s_rmn.isBlessed(makeTaggedRoot(i))); + assertEq(voterWeight, getWeightOfVotesToBlessRoot(makeTaggedRoot(i))); + assertTrue(hasVotedToBlessRoot(voter, makeTaggedRoot(1))); + } + } + + // Reverts + + function test_SenderAlreadyVoted_Revert() public { + (address voter,) = _getFirstBlessVoterAndWeight(); + + vm.startPrank(voter); + s_rmn.voteToBless(makeTaggedRootSingleton(1)); + assertTrue(hasVotedToBlessRoot(voter, makeTaggedRoot(1))); + + uint256 votesToBlessBefore = getWeightOfVotesToBlessRoot(makeTaggedRoot(1)); + vm.expectRevert(RMN.VoteToBlessNoop.selector); + s_rmn.voteToBless(makeTaggedRootSingleton(1)); + assertEq(votesToBlessBefore, getWeightOfVotesToBlessRoot(makeTaggedRoot(1))); + } + + function test_IsAlreadyBlessed_Revert() public { + RMN.Config memory cfg = rmnConstructorArgs(); + + // Bless voters 2,3,4 vote to bless + for (uint256 i = 1; i < cfg.voters.length; i++) { + vm.startPrank(cfg.voters[i].blessVoteAddr); + s_rmn.voteToBless(makeTaggedRootSingleton(1)); + } + + uint256 votesToBlessBefore = getWeightOfVotesToBlessRoot(makeTaggedRoot(1)); + vm.startPrank(cfg.voters[0].blessVoteAddr); + vm.expectRevert(RMN.VoteToBlessNoop.selector); + s_rmn.voteToBless(makeTaggedRootSingleton(1)); + assertEq(votesToBlessBefore, getWeightOfVotesToBlessRoot(makeTaggedRoot(1))); + } + + function test_Curse_Revert() public { + RMN.Config memory cfg = rmnConstructorArgs(); + + for (uint256 i = 0; i < cfg.voters.length; i++) { + vm.startPrank(cfg.voters[i].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(i), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } + + vm.startPrank(cfg.voters[0].blessVoteAddr); + vm.expectRevert(RMN.VoteToBlessForbiddenDuringActiveGlobalCurse.selector); + s_rmn.voteToBless(makeTaggedRootSingleton(12903)); + } + + function test_UnauthorizedVoter_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(RMN.UnauthorizedVoter.selector, STRANGER)); + s_rmn.voteToBless(makeTaggedRootSingleton(12321)); + } +} + +contract RMN_ownerUnbless is RMNSetup { + function test_Unbless_Success() public { + RMN.Config memory cfg = rmnConstructorArgs(); + for (uint256 i = 0; i < cfg.voters.length; ++i) { + vm.startPrank(cfg.voters[i].blessVoteAddr); + s_rmn.voteToBless(makeTaggedRootSingleton(1)); + } + assertTrue(s_rmn.isBlessed(makeTaggedRoot(1))); + + vm.startPrank(OWNER); + s_rmn.ownerResetBlessVotes(makeTaggedRootSingleton(1)); + assertFalse(s_rmn.isBlessed(makeTaggedRoot(1))); + } +} + +contract RMN_unvoteToCurse is RMNSetup { + uint256 internal s_curser; + bytes28 internal s_cursesHash; + + function setUp() public override { + RMNSetup.setUp(); + RMN.Config memory cfg = rmnConstructorArgs(); + + s_curser = 0; + vm.startPrank(cfg.voters[s_curser].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + bytes28 expectedCursesHash = makeCursesHash(makeCurseId(1)); + assertFalse(s_rmn.isCursed()); + (address[] memory cursers, bytes28[] memory cursesHashes, uint16 weight, bool cursed) = s_rmn.getCurseProgress(0); + assertEq(1, cursers.length); + assertEq(cfg.voters[s_curser].curseVoteAddr, cursers[0]); + assertEq(cfg.voters[s_curser].curseWeight, weight); + assertEq(1, cursesHashes.length); + assertEq(expectedCursesHash, cursesHashes[0]); + assertFalse(cursed); + + s_cursesHash = expectedCursesHash; + } + + function test_UnauthorizedVoter() public { + RMN.Config memory cfg = rmnConstructorArgs(); + // Someone else cannot unvote to curse on the curser's behalf. + address[] memory unauthorized = new address[](3); + unauthorized[0] = cfg.voters[s_curser].blessVoteAddr; + unauthorized[1] = cfg.voters[s_curser ^ 1].blessVoteAddr; + unauthorized[2] = OWNER; + + for (uint256 i = 0; i < unauthorized.length; ++i) { + bytes memory expectedRevert = abi.encodeWithSelector(RMN.UnauthorizedVoter.selector, unauthorized[i]); + vm.startPrank(unauthorized[i]); + { + // should fail when using the correct curses hash + RMN.UnvoteToCurseRequest[] memory reqs = new RMN.UnvoteToCurseRequest[](1); + reqs[0] = RMN.UnvoteToCurseRequest({subject: 0, cursesHash: s_cursesHash}); + vm.expectRevert(expectedRevert); + s_rmn.unvoteToCurse(reqs); + } + { + // should fail when using garbage curses hash + RMN.UnvoteToCurseRequest[] memory reqs = new RMN.UnvoteToCurseRequest[](1); + reqs[0] = RMN.UnvoteToCurseRequest({subject: 0, cursesHash: GARBAGE_CURSES_HASH}); + vm.expectRevert(expectedRevert); + s_rmn.unvoteToCurse(reqs); + } + } + } + + function test_InvalidCursesHash() public { + RMN.Config memory cfg = rmnConstructorArgs(); + vm.startPrank(cfg.voters[s_curser].curseVoteAddr); + RMN.UnvoteToCurseRequest[] memory reqs = new RMN.UnvoteToCurseRequest[](1); + reqs[0] = RMN.UnvoteToCurseRequest({subject: 0, cursesHash: GARBAGE_CURSES_HASH}); + vm.expectRevert(RMN.UnvoteToCurseNoop.selector); + s_rmn.unvoteToCurse(reqs); + } + + function test_ValidCursesHash() public { + RMN.Config memory cfg = rmnConstructorArgs(); + vm.startPrank(cfg.voters[s_curser].curseVoteAddr); + RMN.UnvoteToCurseRequest[] memory reqs = new RMN.UnvoteToCurseRequest[](1); + reqs[0] = RMN.UnvoteToCurseRequest({subject: 0, cursesHash: s_cursesHash}); + s_rmn.unvoteToCurse(reqs); // succeeds + } + + function test_OwnerSucceeds() public { + RMN.Config memory cfg = rmnConstructorArgs(); + vm.startPrank(OWNER); + RMN.OwnerUnvoteToCurseRequest[] memory reqs = new RMN.OwnerUnvoteToCurseRequest[](1); + reqs[0] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: cfg.voters[s_curser].curseVoteAddr, + unit: RMN.UnvoteToCurseRequest({subject: 0, cursesHash: s_cursesHash}), + forceUnvote: false + }); + s_rmn.ownerUnvoteToCurse(reqs); + } + + function test_OwnerSkips() public { + RMN.Config memory cfg = rmnConstructorArgs(); + vm.startPrank(OWNER); + RMN.OwnerUnvoteToCurseRequest[] memory reqs = new RMN.OwnerUnvoteToCurseRequest[](1); + reqs[0] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: cfg.voters[s_curser].curseVoteAddr, + unit: RMN.UnvoteToCurseRequest({subject: 0, cursesHash: GARBAGE_CURSES_HASH}), + forceUnvote: false + }); + + vm.expectEmit(); + emit RMN.SkippedUnvoteToCurse(cfg.voters[s_curser].curseVoteAddr, 0, s_cursesHash, GARBAGE_CURSES_HASH); + vm.expectRevert(RMN.UnvoteToCurseNoop.selector); + s_rmn.ownerUnvoteToCurse(reqs); + } + + function test_VotersCantLiftCurseButOwnerCan() public { + vm.stopPrank(); + RMN.Config memory cfg = rmnConstructorArgs(); + // s_curser has voted to curse during setUp + { + (address[] memory voters, bytes28[] memory cursesHashes, uint16 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(accWeight, cfg.voters[s_curser].curseWeight); + assertFalse(cursed); + assertEq(voters.length, 1); + assertEq(cursesHashes.length, 1); + assertEq(voters[0], cfg.voters[s_curser].curseVoteAddr); + assertEq(cursesHashes[0], makeCursesHash(makeCurseId(1))); + } + // everyone else votes now, same curse id, same subject + { + for (uint256 i = 0; i < cfg.voters.length; ++i) { + if (i == s_curser) continue; // already voted to curse + vm.prank(cfg.voters[i].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + } + } + // subject must be cursed now + { + assertTrue(s_rmn.isCursed(0)); + } + // curse progress should be as full as it can get + { + (address[] memory voters, bytes28[] memory cursesHashes, uint16 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + uint256 allWeights; + for (uint256 i = 0; i < cfg.voters.length; i++) { + allWeights += cfg.voters[i].curseWeight; + } + assertEq(accWeight, allWeights); + assertTrue(cursed); + assertEq(voters.length, cfg.voters.length); + assertEq(cursesHashes.length, cfg.voters.length); + for (uint256 i = 0; i < cfg.voters.length; ++i) { + assertEq(voters[i], cfg.voters[i].curseVoteAddr); + assertEq(cursesHashes[i], makeCursesHash(makeCurseId(1))); + } + } + // everyone unvotes to curse, successfully + { + for (uint256 i = 0; i < cfg.voters.length; ++i) { + vm.prank(cfg.voters[i].curseVoteAddr); + RMN.UnvoteToCurseRequest[] memory reqs = new RMN.UnvoteToCurseRequest[](1); + reqs[0] = RMN.UnvoteToCurseRequest({subject: 0, cursesHash: makeCursesHash(makeCurseId(1))}); + s_rmn.unvoteToCurse(reqs); + } + } + // curse should still be in place as only the owner can lift it + { + assertTrue(s_rmn.isCursed(0)); + } + // curse progress should be empty, expect for the cursed flag + { + (address[] memory voters, bytes28[] memory cursesHashes, uint16 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(accWeight, 0); + assertTrue(cursed); + assertEq(voters.length, 0); + assertEq(cursesHashes.length, 0); + } + // owner lifts curse + { + RMN.OwnerUnvoteToCurseRequest[] memory ownerReq = new RMN.OwnerUnvoteToCurseRequest[](1); + ownerReq[0] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: LIFT_CURSE_VOTE_ADDR, + unit: RMN.UnvoteToCurseRequest({subject: 0, cursesHash: 0}), + forceUnvote: false + }); + vm.prank(OWNER); + s_rmn.ownerUnvoteToCurse(ownerReq); + } + // curse should now be lifted + { + assertFalse(s_rmn.isCursed(0)); + } + } +} + +contract RMN_voteToCurse_2 is RMNSetup { + function initialConfig() internal pure returns (RMN.Config memory) { + RMN.Config memory cfg = RMN.Config({voters: new RMN.Voter[](3), blessWeightThreshold: 1, curseWeightThreshold: 3}); + cfg.voters[0] = + RMN.Voter({blessVoteAddr: BLESS_VOTER_1, curseVoteAddr: CURSE_VOTER_1, blessWeight: 1, curseWeight: 1}); + cfg.voters[1] = + RMN.Voter({blessVoteAddr: BLESS_VOTER_2, curseVoteAddr: CURSE_VOTER_2, blessWeight: 1, curseWeight: 1}); + cfg.voters[2] = + RMN.Voter({blessVoteAddr: BLESS_VOTER_3, curseVoteAddr: CURSE_VOTER_3, blessWeight: 1, curseWeight: 1}); + return cfg; + } + + function setUp() public override { + vm.prank(OWNER); + s_rmn = new RMN(initialConfig()); + } + + function test_VotesAreDroppedIfSubjectIsNotCursedDuringConfigChange() public { + // vote to curse the subject from an insufficient number of voters, one voter + { + RMN.Config memory cfg = initialConfig(); + vm.prank(cfg.voters[0].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + } + // vote must be in place + { + (address[] memory voters, bytes28[] memory cursesHashes, uint16 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(voters.length, 1); + assertEq(cursesHashes.length, 1); + assertEq(accWeight, 1); + assertFalse(cursed); + } + // change config to include only the first voter, i.e., initialConfig().voters[0] + { + RMN.Config memory cfg = initialConfig(); + RMN.Voter[] memory voters = cfg.voters; + assembly { + mstore(voters, 1) + } + cfg.curseWeightThreshold = 1; + vm.prank(OWNER); + s_rmn.setConfig(cfg); + } + // vote must be dropped + { + (address[] memory voters, bytes28[] memory cursesHashes, uint16 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(voters.length, 0); + assertEq(cursesHashes.length, 0); + assertEq(accWeight, 0); + assertFalse(cursed); + } + // cause an owner curse now + { + vm.prank(OWNER); + s_rmn.ownerCurse(makeCurseId(1), makeSubjects(0)); + } + // only the owner curse must be visible + { + (address[] memory voters, bytes28[] memory cursesHashes, uint16 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(voters.length, 1); + assertEq(voters[0], OWNER_CURSE_VOTE_ADDR); + assertEq(cursesHashes.length, 1); + assertEq(cursesHashes[0], makeCursesHash(makeCurseId(1))); + assertEq(accWeight, 0); + assertTrue(cursed); + } + } + + function test_VotesAreRetainedIfSubjectIsCursedDuringConfigChange() public { + uint256 numVotersInitially = initialConfig().voters.length; + // curse the subject with votes from all voters + { + RMN.Config memory cfg = initialConfig(); + for (uint256 i = 0; i < cfg.voters.length; ++i) { + vm.prank(cfg.voters[i].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + } + } + // subject is now cursed + { + assertTrue(s_rmn.isCursed(0)); + } + // throw in an owner curse + { + vm.prank(OWNER); + s_rmn.ownerCurse(makeCurseId(1), makeSubjects(0)); + } + + uint256 snapshot = vm.snapshot(); + + for (uint256 keepVoters = 1; keepVoters <= numVotersInitially; ++keepVoters) { + vm.revertTo(snapshot); + + // change config to include only the first #keepVoters voters, i.e., initialConfig().voters[0..keepVoters] + { + RMN.Config memory cfg = initialConfig(); + RMN.Voter[] memory voters = cfg.voters; + assembly { + mstore(voters, keepVoters) + } + cfg.curseWeightThreshold = uint16(keepVoters); + vm.prank(OWNER); + s_rmn.setConfig(cfg); + } + // subject is still cursed + { + assertTrue(s_rmn.isCursed(0)); + } + // all votes from the first keepVoters & owner must be present + { + (address[] memory voters, bytes28[] memory cursesHashes, uint16 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(voters.length, keepVoters + 1 /* owner */ ); + assertEq(cursesHashes.length, keepVoters + 1 /* owner */ ); + assertEq(accWeight, keepVoters /* 1 per voter */ ); + assertTrue(cursed); + for (uint256 i = 0; i < keepVoters; ++i) { + assertEq(voters[i], initialConfig().voters[i].curseVoteAddr); + assertEq(cursesHashes[i], makeCursesHash(makeCurseId(1))); + } + assertEq(voters[voters.length - 1], OWNER_CURSE_VOTE_ADDR); + assertEq(cursesHashes[cursesHashes.length - 1], makeCursesHash(makeCurseId(1))); + } + // the owner unvoting for all is not enough to lift the curse, because remember that the owner has an active vote + // also + { + for (uint256 i = 0; i < keepVoters; ++i) { + RMN.OwnerUnvoteToCurseRequest[] memory ownerReq = new RMN.OwnerUnvoteToCurseRequest[](1); + ownerReq[0] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: initialConfig().voters[i].curseVoteAddr, + unit: RMN.UnvoteToCurseRequest({subject: 0, cursesHash: makeCursesHash(makeCurseId(1))}), + forceUnvote: false + }); + vm.prank(OWNER); + s_rmn.ownerUnvoteToCurse(ownerReq); + + assertTrue(s_rmn.isCursed(0)); + } + } + // after owner unvotes for themselves, finally, the curse will be lifted + { + RMN.OwnerUnvoteToCurseRequest[] memory ownerReq = new RMN.OwnerUnvoteToCurseRequest[](1); + ownerReq[0] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: OWNER_CURSE_VOTE_ADDR, + unit: RMN.UnvoteToCurseRequest({subject: 0, cursesHash: makeCursesHash(makeCurseId(1))}), + forceUnvote: false + }); + vm.prank(OWNER); + s_rmn.ownerUnvoteToCurse(ownerReq); + + assertFalse(s_rmn.isCursed(0)); + } + } + } +} + +contract RMN_voteToCurse is RMNSetup { + function _getFirstCurseVoterAndWeight() internal pure returns (address, uint8) { + RMN.Config memory cfg = rmnConstructorArgs(); + return (cfg.voters[0].curseVoteAddr, cfg.voters[0].curseWeight); + } + + // Success + + function test_CurseOnlyWhenThresholdReached_Success() public { + uint256 numSubjects = 3; + uint256 maxNumRevotes = 2; + + RMN.Config memory cfg = rmnConstructorArgs(); + bytes16[] memory subjects = new bytes16[](numSubjects); + for (uint256 i = 0; i < numSubjects; ++i) { + subjects[i] = bytes16(uint128(i)); + } + for (uint256 numRevotes = 1; numRevotes <= maxNumRevotes; ++numRevotes) { + // all voters but the last vote, but can't surpass the curse weight threshold + for (uint256 i = 0; i < cfg.voters.length - 1; ++i) { + vm.prank(cfg.voters[i].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(numRevotes), subjects); + } + // no curse is yet active, last voter also needs to vote for any curse to be active + { + // ensure every subject is not cursed + for (uint256 i = 0; i < numSubjects; ++i) { + assertFalse(s_rmn.isCursed(subjects[i])); + } + // ensure every vote has been recorded + assertEq( + s_rmn.getRecordedCurseRelatedOpsCount(), + 1 /* setConfig */ + (cfg.voters.length - 1) * numRevotes * numSubjects + ); + } + } + + // last voter now votes + vm.prank(cfg.voters[cfg.voters.length - 1].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(0), subjects); + // curses should be now active + { + // ensure every subject is now cursed + for (uint256 i = 0; i < numSubjects; ++i) { + assertTrue(s_rmn.isCursed(subjects[i])); + } + // ensure every vote has been recorded + assertEq( + s_rmn.getRecordedCurseRelatedOpsCount(), + 1 /* setConfig */ + ((cfg.voters.length - 1) * maxNumRevotes + 1) * numSubjects + ); + } + } + + function test_VoteToCurse_NoCurse_Success() public { + (address voter, uint8 weight) = _getFirstCurseVoterAndWeight(); + vm.startPrank(voter); + vm.expectEmit(); + emit RMN.VotedToCurse( + 1, // configVersion + voter, + GLOBAL_CURSE_SUBJECT, + makeCurseId(123), + weight, + 1234567890, // blockTimestamp + makeCursesHash(makeCurseId(123)), // cursesHash + weight + ); + + s_rmn.voteToCurse(makeCurseId(123), makeSubjects(GLOBAL_CURSE_SUBJECT)); + + (address[] memory voters,, uint16 votes, bool cursed) = s_rmn.getCurseProgress(GLOBAL_CURSE_SUBJECT); + assertEq(1, voters.length); + assertEq(voter, voters[0]); + assertEq(weight, votes); + assertFalse(cursed); + } + + function test_VoteToCurse_YesCurse_Success() public { + RMN.Config memory cfg = rmnConstructorArgs(); + for (uint256 i = 0; i < cfg.voters.length - 1; ++i) { + vm.startPrank(cfg.voters[i].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + } + + vm.expectEmit(); + emit RMN.Cursed(1, 0, uint64(block.timestamp)); + + vm.startPrank(cfg.voters[cfg.voters.length - 1].curseVoteAddr); + vm.resumeGasMetering(); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + } + + function test_EvenIfAlreadyCursed_Success() public { + RMN.Config memory cfg = rmnConstructorArgs(); + uint16 weightSum = 0; + for (uint256 i = 0; i < cfg.voters.length; ++i) { + vm.startPrank(cfg.voters[i].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(i), makeSubjects(0)); + weightSum += cfg.voters[i].curseWeight; + } + + // Not part of the assertion of this test but good to have as a sanity + // check. We want a curse to be active in order for the ultimate assertion + // to make sense. + assert(s_rmn.isCursed(0)); + + vm.expectEmit(); + emit RMN.VotedToCurse( + 1, // configVersion + cfg.voters[cfg.voters.length - 1].curseVoteAddr, + 0, // subject + makeCurseId(cfg.voters.length + 1), // this curse id + cfg.voters[cfg.voters.length - 1].curseWeight, + uint64(block.timestamp), // blockTimestamp + makeCursesHash(makeCurseId(cfg.voters.length - 1), makeCurseId(cfg.voters.length + 1)), // cursesHash + weightSum // accumulatedWeight + ); + // Asserts that this call to vote with a new curse id goes through with no + // reverts even when the RMN contract is cursed. + s_rmn.voteToCurse(makeCurseId(cfg.voters.length + 1), makeSubjects(0)); + } + + function test_OwnerCanCurseAndUncurse() public { + vm.startPrank(OWNER); + bytes28 expectedCursesHash = makeCursesHash(makeCurseId(0)); + vm.expectEmit(); + emit RMN.VotedToCurse( + 1, // configVersion + OWNER_CURSE_VOTE_ADDR, // owner + 0, // subject + makeCurseId(0), // curse id + 0, // weight + uint64(block.timestamp), // blockTimestamp + expectedCursesHash, // cursesHash + 0 // accumulatedWeight + ); + vm.expectEmit(); + emit RMN.Cursed( + 1, // configVersion + 0, // subject + uint64(block.timestamp) // blockTimestamp + ); + s_rmn.ownerCurse(makeCurseId(0), makeSubjects(0)); + + { + (address[] memory voters, bytes28[] memory cursesHashes, uint24 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(voters.length, 1); + assertEq(voters[0], OWNER_CURSE_VOTE_ADDR /* owner */ ); + assertEq(cursesHashes.length, 1); + assertEq(cursesHashes[0], expectedCursesHash); + assertEq(accWeight, 0); + assertTrue(cursed); + } + + // ownerCurse again, should cause a vote to appear and a change in curses hash + expectedCursesHash = makeCursesHash(makeCurseId(0), makeCurseId(1)); + vm.expectEmit(); + emit RMN.VotedToCurse( + 1, // configVersion + OWNER_CURSE_VOTE_ADDR, // owner + 0, // subject + makeCurseId(1), // curse id + 0, // weight + uint64(block.timestamp), // blockTimestamp + expectedCursesHash, // cursesHash + 0 // accumulatedWeight + ); + s_rmn.ownerCurse(makeCurseId(1), makeSubjects(0)); + + { + (address[] memory voters, bytes28[] memory cursesHashes, uint24 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(voters.length, 1); + assertEq(voters[0], OWNER_CURSE_VOTE_ADDR /* owner */ ); + assertEq(cursesHashes.length, 1); + assertEq(cursesHashes[0], expectedCursesHash); + assertEq(accWeight, 0); + assertTrue(cursed); + } + + RMN.OwnerUnvoteToCurseRequest[] memory unvoteReqs = new RMN.OwnerUnvoteToCurseRequest[](1); + unvoteReqs[0] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: OWNER_CURSE_VOTE_ADDR, + unit: RMN.UnvoteToCurseRequest({subject: 0, cursesHash: 0}), + forceUnvote: true // TODO: test with forceUnvote false also + }); + vm.expectEmit(); + emit RMN.CurseLifted(0); + s_rmn.ownerUnvoteToCurse(unvoteReqs); + { + (address[] memory voters, bytes28[] memory cursesHashes, uint24 accWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(voters.length, 0); + assertEq(cursesHashes.length, 0); + assertEq(accWeight, 0); + assertFalse(cursed); + } + } + + // Reverts + + function test_UnauthorizedVoter_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert(abi.encodeWithSelector(RMN.UnauthorizedVoter.selector, STRANGER)); + s_rmn.voteToCurse(makeCurseId(12312), makeSubjects(0)); + } + + function test_ReusedCurseId_Revert() public { + (address voter,) = _getFirstCurseVoterAndWeight(); + vm.startPrank(voter); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + + vm.expectRevert(abi.encodeWithSelector(RMN.ReusedCurseId.selector, voter, makeCurseId(1))); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + } + + function test_RepeatedSubject_Revert() public { + (address voter,) = _getFirstCurseVoterAndWeight(); + vm.prank(voter); + + bytes16 subject = bytes16(uint128(1)); + + vm.expectRevert(RMN.SubjectsMustBeStrictlyIncreasing.selector); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(subject, subject)); + } + + function test_EmptySubjects_Revert() public { + (address voter,) = _getFirstCurseVoterAndWeight(); + vm.prank(voter); + + vm.expectRevert(RMN.VoteToCurseNoop.selector); + s_rmn.voteToCurse(makeCurseId(1), new bytes16[](0)); + } +} + +contract RMN_ownerUnvoteToCurse is RMNSetup { + // These cursers are going to curse in setUp curseCount times. + function getCursersAndCurseCounts() internal pure returns (address[] memory cursers, uint32[] memory curseCounts) { + // NOTE: Change this when changing setUp or rmnConstructorArgs. + // This is a bit ugly and error prone but if we read from storage we would + // not get an accurate gas reading for ownerUnvoteToCurse when we need it. + cursers = new address[](4); + cursers[0] = CURSE_VOTER_1; + cursers[1] = CURSE_VOTER_2; + cursers[2] = CURSE_VOTER_3; + cursers[3] = CURSE_VOTER_4; + curseCounts = new uint32[](cursers.length); + for (uint256 i = 0; i < cursers.length; ++i) { + curseCounts[i] = 1; + } + } + + function setUp() public virtual override { + RMNSetup.setUp(); + (address[] memory cursers, uint32[] memory curseCounts) = getCursersAndCurseCounts(); + for (uint256 i = 0; i < cursers.length; ++i) { + vm.startPrank(cursers[i]); + for (uint256 j = 0; j < curseCounts[i]; ++j) { + s_rmn.voteToCurse(makeCurseId(j), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } + } + } + + function ownerUnvoteToCurse() internal { + s_rmn.ownerUnvoteToCurse(makeOwnerUnvoteToCurseRequests()); + } + + function makeOwnerUnvoteToCurseRequests() internal pure returns (RMN.OwnerUnvoteToCurseRequest[] memory) { + (address[] memory cursers,) = getCursersAndCurseCounts(); + RMN.OwnerUnvoteToCurseRequest[] memory reqs = new RMN.OwnerUnvoteToCurseRequest[](cursers.length); + for (uint256 i = 0; i < cursers.length; ++i) { + reqs[i] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: cursers[i], + unit: RMN.UnvoteToCurseRequest({subject: GLOBAL_CURSE_SUBJECT, cursesHash: bytes28(0)}), + forceUnvote: true + }); + } + return reqs; + } + + // Success + + function test_OwnerUnvoteToCurseSuccess_gas() public { + vm.pauseGasMetering(); + vm.startPrank(OWNER); + + vm.expectEmit(); + emit RMN.CurseLifted(GLOBAL_CURSE_SUBJECT); + + vm.resumeGasMetering(); + ownerUnvoteToCurse(); + vm.pauseGasMetering(); + + assertFalse(s_rmn.isCursed()); + (address[] memory voters, bytes28[] memory cursesHashes, uint256 weight, bool cursed) = + s_rmn.getCurseProgress(GLOBAL_CURSE_SUBJECT); + assertEq(voters.length, 0); + assertEq(cursesHashes.length, 0); + assertEq(weight, 0); + assertFalse(cursed); + vm.resumeGasMetering(); + } + + function test_IsIdempotent() public { + vm.startPrank(OWNER); + ownerUnvoteToCurse(); + vm.expectRevert(RMN.UnvoteToCurseNoop.selector); + ownerUnvoteToCurse(); + + assertFalse(s_rmn.isCursed()); + (address[] memory voters, bytes28[] memory cursesHashes, uint256 weight, bool cursed) = + s_rmn.getCurseProgress(GLOBAL_CURSE_SUBJECT); + assertEq(voters.length, 0); + assertEq(cursesHashes.length, 0); + assertEq(weight, 0); + assertFalse(cursed); + } + + function test_CanBlessAndCurseAfterGlobalCurseIsLifted() public { + // Contract is already cursed due to setUp. + + // Owner unvotes to curse. + vm.startPrank(OWNER); + vm.expectEmit(); + emit RMN.CurseLifted(GLOBAL_CURSE_SUBJECT); + ownerUnvoteToCurse(); + + // Contract is now uncursed. + assertFalse(s_rmn.isCursed()); + + // Vote to bless should go through. + vm.startPrank(BLESS_VOTER_1); + s_rmn.voteToBless(makeTaggedRootSingleton(2387489729)); + + // Vote to curse should go through. + vm.startPrank(CURSE_VOTER_1); + s_rmn.voteToCurse(makeCurseId(73894728973), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } + + // Reverts + + function test_NonOwner_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + ownerUnvoteToCurse(); + } + + function test_UnknownVoter_Revert() public { + vm.stopPrank(); + RMN.OwnerUnvoteToCurseRequest[] memory reqs = new RMN.OwnerUnvoteToCurseRequest[](1); + reqs[0] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: STRANGER, + unit: RMN.UnvoteToCurseRequest({subject: GLOBAL_CURSE_SUBJECT, cursesHash: bytes28(0)}), + forceUnvote: true + }); + + vm.prank(OWNER); + vm.expectEmit(); + emit RMN.SkippedUnvoteToCurse(STRANGER, GLOBAL_CURSE_SUBJECT, bytes28(0), bytes28(0)); + vm.expectRevert(RMN.UnvoteToCurseNoop.selector); + s_rmn.ownerUnvoteToCurse(reqs); + + // no effect on cursedness + assertTrue(s_rmn.isCursed(GLOBAL_CURSE_SUBJECT)); + } +} + +contract RMN_setConfig is ConfigCompare, RMNSetup { + /// @notice Test-specific function to use only in setConfig tests + function getDifferentConfigArgs() private pure returns (RMN.Config memory) { + RMN.Voter[] memory voters = new RMN.Voter[](2); + voters[0] = RMN.Voter({ + blessVoteAddr: BLESS_VOTER_1, + curseVoteAddr: CURSE_VOTER_1, + blessWeight: WEIGHT_1, + curseWeight: WEIGHT_1 + }); + voters[1] = RMN.Voter({ + blessVoteAddr: BLESS_VOTER_2, + curseVoteAddr: CURSE_VOTER_2, + blessWeight: WEIGHT_10, + curseWeight: WEIGHT_10 + }); + return RMN.Config({ + voters: voters, + blessWeightThreshold: WEIGHT_1 + WEIGHT_10, + curseWeightThreshold: WEIGHT_1 + WEIGHT_10 + }); + } + + function setUp() public virtual override { + RMNSetup.setUp(); + RMN.Config memory cfg = rmnConstructorArgs(); + + // Setup some partial state + vm.startPrank(cfg.voters[0].blessVoteAddr); + s_rmn.voteToBless(makeTaggedRootSingleton(1)); + vm.startPrank(cfg.voters[1].blessVoteAddr); + s_rmn.voteToBless(makeTaggedRootSingleton(1)); + vm.startPrank(cfg.voters[1].curseVoteAddr); + s_rmn.voteToCurse(makeCurseId(1), makeSubjects(0)); + } + + // Success + + event ConfigSet(uint32 indexed configVersion, RMN.Config config); + + function test_VoteToBlessByEjectedVoter_Revert() public { + // Previous config included BLESS_VOTER_4. Change to new config that doesn't. + RMN.Config memory cfg = getDifferentConfigArgs(); + vm.startPrank(OWNER); + s_rmn.setConfig(cfg); + + // BLESS_VOTER_4 is not part of cfg anymore, vote to bless should revert. + vm.startPrank(BLESS_VOTER_4); + vm.expectRevert(abi.encodeWithSelector(RMN.UnauthorizedVoter.selector, BLESS_VOTER_4)); + s_rmn.voteToBless(makeTaggedRootSingleton(2)); + } + + function test_SetConfigSuccess_gas() public { + vm.pauseGasMetering(); + RMN.Config memory cfg = getDifferentConfigArgs(); + + vm.startPrank(OWNER); + vm.expectEmit(); + emit ConfigSet(2, cfg); + + (uint32 configVersionBefore,,) = s_rmn.getConfigDetails(); + vm.resumeGasMetering(); + s_rmn.setConfig(cfg); + vm.pauseGasMetering(); + // Assert VersionedConfig has changed correctly + (uint32 configVersionAfter,, RMN.Config memory configAfter) = s_rmn.getConfigDetails(); + assertEq(configVersionBefore + 1, configVersionAfter); + assertConfigEq(configAfter, cfg); + + // Assert that curse votes have been cleared + + (address[] memory curseVoters, bytes28[] memory cursesHashes, uint256 curseWeight, bool cursed) = + s_rmn.getCurseProgress(0); + assertEq(0, curseVoters.length); + assertEq(0, cursesHashes.length); + assertEq(0, curseWeight); + assertFalse(cursed); + + // Assert that good votes have been cleared + uint256 votesToBlessRoot = getWeightOfVotesToBlessRoot(makeTaggedRoot(1)); + assertEq(ZERO, votesToBlessRoot); + assertFalse(hasVotedToBlessRoot(cfg.voters[0].blessVoteAddr, makeTaggedRoot(1))); + assertFalse(hasVotedToBlessRoot(cfg.voters[1].blessVoteAddr, makeTaggedRoot(1))); + vm.resumeGasMetering(); + } + + // Reverts + + function test_NonOwner_Revert() public { + RMN.Config memory cfg = getDifferentConfigArgs(); + + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + s_rmn.setConfig(cfg); + } + + function test_VotersLengthIsZero_Revert() public { + vm.startPrank(OWNER); + vm.expectRevert(RMN.InvalidConfig.selector); + s_rmn.setConfig(RMN.Config({voters: new RMN.Voter[](0), blessWeightThreshold: 1, curseWeightThreshold: 1})); + } + + function test_EitherThresholdIsZero_Revert() public { + RMN.Config memory cfg = getDifferentConfigArgs(); + + vm.startPrank(OWNER); + vm.expectRevert(RMN.InvalidConfig.selector); + s_rmn.setConfig( + RMN.Config({voters: cfg.voters, blessWeightThreshold: ZERO, curseWeightThreshold: cfg.curseWeightThreshold}) + ); + vm.expectRevert(RMN.InvalidConfig.selector); + s_rmn.setConfig( + RMN.Config({voters: cfg.voters, blessWeightThreshold: cfg.blessWeightThreshold, curseWeightThreshold: ZERO}) + ); + } + + function test_BlessVoterIsZeroAddress_Revert() public { + RMN.Config memory cfg = getDifferentConfigArgs(); + + vm.startPrank(OWNER); + cfg.voters[0].blessVoteAddr = ZERO_ADDRESS; + vm.expectRevert(RMN.InvalidConfig.selector); + s_rmn.setConfig(cfg); + } + + function test_WeightIsZeroAddress_Revert() public { + RMN.Config memory cfg = getDifferentConfigArgs(); + + vm.startPrank(OWNER); + cfg.voters[0].blessWeight = ZERO; + cfg.voters[0].curseWeight = ZERO; + vm.expectRevert(RMN.InvalidConfig.selector); + s_rmn.setConfig(cfg); + } + + function test_TotalWeightsSmallerThanEachThreshold_Revert() public { + RMN.Config memory cfg = getDifferentConfigArgs(); + + vm.startPrank(OWNER); + vm.expectRevert(RMN.InvalidConfig.selector); + s_rmn.setConfig( + RMN.Config({voters: cfg.voters, blessWeightThreshold: WEIGHT_40, curseWeightThreshold: cfg.curseWeightThreshold}) + ); + vm.expectRevert(RMN.InvalidConfig.selector); + s_rmn.setConfig( + RMN.Config({voters: cfg.voters, blessWeightThreshold: cfg.blessWeightThreshold, curseWeightThreshold: WEIGHT_40}) + ); + } + + function test_RepeatedAddress_Revert() public { + RMN.Config memory cfg = getDifferentConfigArgs(); + + vm.startPrank(OWNER); + cfg.voters[0].blessVoteAddr = cfg.voters[1].curseVoteAddr; + vm.expectRevert(RMN.InvalidConfig.selector); + s_rmn.setConfig(cfg); + } +} + +contract RMN_permaBlessing is RMNSetup { + function addresses() private pure returns (address[] memory) { + return new address[](0); + } + + function addresses(address a) private pure returns (address[] memory) { + address[] memory arr = new address[](1); + arr[0] = a; + return arr; + } + + function addresses(address a, address b) private pure returns (address[] memory) { + address[] memory arr = new address[](2); + arr[0] = a; + arr[1] = b; + return arr; + } + + function test_PermaBlessing() public { + bytes32 SOME_ROOT = bytes32(~uint256(0)); + address COMMIT_STORE_1 = makeAddr("COMMIT_STORE_1"); + address COMMIT_STORE_2 = makeAddr("COMMIT_STORE_2"); + IRMN.TaggedRoot memory taggedRootCommitStore1 = IRMN.TaggedRoot({root: SOME_ROOT, commitStore: COMMIT_STORE_1}); + IRMN.TaggedRoot memory taggedRootCommitStore2 = IRMN.TaggedRoot({root: SOME_ROOT, commitStore: COMMIT_STORE_2}); + + assertFalse(s_rmn.isBlessed(taggedRootCommitStore1)); + assertFalse(s_rmn.isBlessed(taggedRootCommitStore2)); + assertEq(s_rmn.getPermaBlessedCommitStores(), addresses()); + + // only owner can mutate permaBlessedCommitStores + vm.prank(STRANGER); + vm.expectRevert("Only callable by owner"); + s_rmn.ownerRemoveThenAddPermaBlessedCommitStores(addresses(), addresses(COMMIT_STORE_1)); + + vm.prank(OWNER); + s_rmn.ownerRemoveThenAddPermaBlessedCommitStores(addresses(), addresses(COMMIT_STORE_1)); + assertTrue(s_rmn.isBlessed(taggedRootCommitStore1)); + assertFalse(s_rmn.isBlessed(taggedRootCommitStore2)); + assertEq(s_rmn.getPermaBlessedCommitStores(), addresses(COMMIT_STORE_1)); + + vm.prank(OWNER); + s_rmn.ownerRemoveThenAddPermaBlessedCommitStores(addresses(COMMIT_STORE_1), addresses(COMMIT_STORE_2)); + assertFalse(s_rmn.isBlessed(taggedRootCommitStore1)); + assertTrue(s_rmn.isBlessed(taggedRootCommitStore2)); + assertEq(s_rmn.getPermaBlessedCommitStores(), addresses(COMMIT_STORE_2)); + + vm.prank(OWNER); + s_rmn.ownerRemoveThenAddPermaBlessedCommitStores(addresses(), addresses(COMMIT_STORE_1)); + assertTrue(s_rmn.isBlessed(taggedRootCommitStore1)); + assertTrue(s_rmn.isBlessed(taggedRootCommitStore2)); + assertEq(s_rmn.getPermaBlessedCommitStores(), addresses(COMMIT_STORE_2, COMMIT_STORE_1)); + + vm.prank(OWNER); + s_rmn.ownerRemoveThenAddPermaBlessedCommitStores(addresses(COMMIT_STORE_1, COMMIT_STORE_2), addresses()); + assertFalse(s_rmn.isBlessed(taggedRootCommitStore1)); + assertFalse(s_rmn.isBlessed(taggedRootCommitStore2)); + assertEq(s_rmn.getPermaBlessedCommitStores(), addresses()); + } +} + +contract RMN_getRecordedCurseRelatedOps is RMNSetup { + function test_OpsPostDeployment() public { + // The constructor call includes a setConfig, so that's the only thing we should expect to find. + assertEq(s_rmn.getRecordedCurseRelatedOpsCount(), 1); + RMN.RecordedCurseRelatedOp[] memory recordedCurseRelatedOps = s_rmn.getRecordedCurseRelatedOps(0, type(uint256).max); + assertEq(recordedCurseRelatedOps.length, 1); + assertEq(uint8(recordedCurseRelatedOps[0].tag), uint8(RMN.RecordedCurseRelatedOpTag.SetConfig)); + } +} diff --git a/contracts/src/v0.8/ccip/test/arm/RMNSetup.t.sol b/contracts/src/v0.8/ccip/test/arm/RMNSetup.t.sol new file mode 100644 index 00000000000..8feacb95f45 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/arm/RMNSetup.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {RMN} from "../../RMN.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; + +import {Test} from "forge-std/Test.sol"; + +function makeSubjects(bytes16 a) pure returns (bytes16[] memory) { + bytes16[] memory subjects = new bytes16[](1); + subjects[0] = a; + return subjects; +} + +function makeSubjects(bytes16 a, bytes16 b) pure returns (bytes16[] memory) { + bytes16[] memory subjects = new bytes16[](2); + subjects[0] = a; + subjects[1] = b; + return subjects; +} + +// in order from earliest to latest curse ids +function makeCursesHashFromList(bytes32[] memory curseIds) pure returns (bytes28 cursesHash) { + for (uint256 i = 0; i < curseIds.length; ++i) { + cursesHash = bytes28(keccak256(abi.encode(cursesHash, curseIds[i]))); + } +} + +// hides the ugliness from tests +function makeCursesHash(bytes32 a) pure returns (bytes28) { + bytes32[] memory curseIds = new bytes32[](1); + curseIds[0] = a; + return makeCursesHashFromList(curseIds); +} + +function makeCursesHash(bytes32 a, bytes32 b) pure returns (bytes28) { + bytes32[] memory curseIds = new bytes32[](2); + curseIds[0] = a; + curseIds[1] = b; + return makeCursesHashFromList(curseIds); +} + +contract RMNSetup is Test { + // Addresses + address internal constant OWNER = 0x00007e64E1fB0C487F25dd6D3601ff6aF8d32e4e; + address internal constant STRANGER = address(999999); + address internal constant ZERO_ADDRESS = address(0); + address internal constant BLESS_VOTER_1 = address(1); + address internal constant CURSE_VOTER_1 = address(10); + address internal constant BLESS_VOTER_2 = address(2); + address internal constant CURSE_VOTER_2 = address(12); + address internal constant BLESS_VOTER_3 = address(3); + address internal constant CURSE_VOTER_3 = address(13); + address internal constant BLESS_VOTER_4 = address(4); + address internal constant CURSE_VOTER_4 = address(14); + + // Arm + function rmnConstructorArgs() internal pure returns (RMN.Config memory) { + RMN.Voter[] memory voters = new RMN.Voter[](4); + voters[0] = RMN.Voter({ + blessVoteAddr: BLESS_VOTER_1, + curseVoteAddr: CURSE_VOTER_1, + blessWeight: WEIGHT_1, + curseWeight: WEIGHT_1 + }); + voters[1] = RMN.Voter({ + blessVoteAddr: BLESS_VOTER_2, + curseVoteAddr: CURSE_VOTER_2, + blessWeight: WEIGHT_10, + curseWeight: WEIGHT_10 + }); + voters[2] = RMN.Voter({ + blessVoteAddr: BLESS_VOTER_3, + curseVoteAddr: CURSE_VOTER_3, + blessWeight: WEIGHT_20, + curseWeight: WEIGHT_20 + }); + voters[3] = RMN.Voter({ + blessVoteAddr: BLESS_VOTER_4, + curseVoteAddr: CURSE_VOTER_4, + blessWeight: WEIGHT_40, + curseWeight: WEIGHT_40 + }); + return RMN.Config({ + voters: voters, + blessWeightThreshold: WEIGHT_10 + WEIGHT_20 + WEIGHT_40, + curseWeightThreshold: WEIGHT_1 + WEIGHT_10 + WEIGHT_20 + WEIGHT_40 + }); + } + + uint8 internal constant ZERO = 0; + uint8 internal constant WEIGHT_1 = 1; + uint8 internal constant WEIGHT_10 = 10; + uint8 internal constant WEIGHT_20 = 20; + uint8 internal constant WEIGHT_40 = 40; + + function makeTaggedRootsInclusive(uint256 from, uint256 to) internal pure returns (IRMN.TaggedRoot[] memory) { + IRMN.TaggedRoot[] memory votes = new IRMN.TaggedRoot[](to - from + 1); + for (uint256 i = from; i <= to; ++i) { + votes[i - from] = IRMN.TaggedRoot({commitStore: address(1), root: bytes32(uint256(i))}); + } + return votes; + } + + function makeTaggedRootSingleton(uint256 index) internal pure returns (IRMN.TaggedRoot[] memory) { + return makeTaggedRootsInclusive(index, index); + } + + function makeTaggedRoot(uint256 index) internal pure returns (IRMN.TaggedRoot memory) { + return makeTaggedRootSingleton(index)[0]; + } + + function makeTaggedRootHash(uint256 index) internal pure returns (bytes32) { + IRMN.TaggedRoot memory taggedRoot = makeTaggedRootSingleton(index)[0]; + return keccak256(abi.encode(taggedRoot.commitStore, taggedRoot.root)); + } + + function makeCurseId(uint256 index) internal pure returns (bytes16) { + return bytes16(uint128(index)); + } + + RMN internal s_rmn; + + function setUp() public virtual { + vm.startPrank(OWNER); + s_rmn = new RMN(rmnConstructorArgs()); + vm.stopPrank(); + } + + function hasVotedToBlessRoot(address voter, IRMN.TaggedRoot memory taggedRoot_) internal view returns (bool) { + (address[] memory voters,,) = s_rmn.getBlessProgress(taggedRoot_); + for (uint256 i = 0; i < voters.length; ++i) { + if (voters[i] == voter) { + return true; + } + } + return false; + } + + function getWeightOfVotesToBlessRoot(IRMN.TaggedRoot memory taggedRoot_) internal view returns (uint16) { + (, uint16 weight,) = s_rmn.getBlessProgress(taggedRoot_); + return weight; + } +} diff --git a/contracts/src/v0.8/ccip/test/arm/RMN_benchmark.t.sol b/contracts/src/v0.8/ccip/test/arm/RMN_benchmark.t.sol new file mode 100644 index 00000000000..8564614a748 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/arm/RMN_benchmark.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {GLOBAL_CURSE_SUBJECT, OWNER_CURSE_VOTE_ADDR, RMN} from "../../RMN.sol"; +import {RMNSetup, makeCursesHash, makeSubjects} from "./RMNSetup.t.sol"; + +contract RMN_voteToBless_Benchmark is RMNSetup { + function test_RootSuccess_gas(uint256 n) internal { + vm.prank(BLESS_VOTER_1); + s_rmn.voteToBless(makeTaggedRootsInclusive(1, n)); + } + + function test_1RootSuccess_gas() public { + test_RootSuccess_gas(1); + } + + function test_3RootSuccess_gas() public { + test_RootSuccess_gas(3); + } + + function test_5RootSuccess_gas() public { + test_RootSuccess_gas(5); + } +} + +contract RMN_voteToBless_Blessed_Benchmark is RMN_voteToBless_Benchmark { + function setUp() public virtual override { + RMNSetup.setUp(); + vm.prank(BLESS_VOTER_2); + s_rmn.voteToBless(makeTaggedRootsInclusive(1, 1)); + vm.prank(BLESS_VOTER_3); + s_rmn.voteToBless(makeTaggedRootsInclusive(1, 1)); + } + + function test_1RootSuccessBecameBlessed_gas() public { + vm.prank(BLESS_VOTER_4); + s_rmn.voteToBless(makeTaggedRootsInclusive(1, 1)); + } +} + +abstract contract RMN_voteToCurse_Benchmark is RMNSetup { + struct PreVote { + address voter; + bytes16 subject; + } + + PreVote[] internal s_preVotes; + + function setUp() public virtual override { + // Intentionally does not inherit RMNSetup setUp(), because we set up a simpler config here. + // The only way to ensure that storage slots are cold for the actual functions to be benchmarked is to perform the + // setup in setUp(). + + RMN.Config memory cfg = RMN.Config({voters: new RMN.Voter[](3), blessWeightThreshold: 3, curseWeightThreshold: 3}); + cfg.voters[0] = + RMN.Voter({blessVoteAddr: BLESS_VOTER_1, curseVoteAddr: CURSE_VOTER_1, blessWeight: 1, curseWeight: 1}); + cfg.voters[1] = + RMN.Voter({blessVoteAddr: BLESS_VOTER_2, curseVoteAddr: CURSE_VOTER_2, blessWeight: 1, curseWeight: 1}); + cfg.voters[2] = + RMN.Voter({blessVoteAddr: BLESS_VOTER_3, curseVoteAddr: CURSE_VOTER_3, blessWeight: 1, curseWeight: 1}); + vm.prank(OWNER); + s_rmn = new RMN(cfg); + + for (uint256 i = 0; i < s_preVotes.length; ++i) { + vm.prank(s_preVotes[i].voter); + s_rmn.voteToCurse(makeCurseId(i), makeSubjects(s_preVotes[i].subject)); + } + } +} + +contract RMN_voteToCurse_Benchmark_1 is RMN_voteToCurse_Benchmark { + constructor() { + // some irrelevant subject & voter so that we don't pay for the nonzero->zero SSTORE of + // s_recordedVotesToCurse.length in the benchmark below + s_preVotes.push(PreVote({voter: CURSE_VOTER_3, subject: bytes16(~uint128(0))})); + } + + function test_VoteToCurse_NewSubject_NewVoter_NoCurse_gas() public { + vm.prank(CURSE_VOTER_1); + s_rmn.voteToCurse(makeCurseId(0xffff), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } + + function test_VoteToCurse_NewSubject_NewVoter_YesCurse_gas() public { + vm.prank(OWNER); + s_rmn.ownerCurse(makeCurseId(0xffff), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } +} + +contract RMN_voteToCurse_Benchmark_2 is RMN_voteToCurse_Benchmark { + constructor() { + s_preVotes.push(PreVote({voter: CURSE_VOTER_1, subject: GLOBAL_CURSE_SUBJECT})); + } + + function test_VoteToCurse_OldSubject_OldVoter_NoCurse_gas() public { + vm.prank(CURSE_VOTER_1); + s_rmn.voteToCurse(makeCurseId(0xffff), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } + + function test_VoteToCurse_OldSubject_NewVoter_NoCurse_gas() public { + vm.prank(CURSE_VOTER_2); + s_rmn.voteToCurse(makeCurseId(0xffff), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } +} + +contract RMN_voteToCurse_Benchmark_3 is RMN_voteToCurse_Benchmark { + constructor() { + s_preVotes.push(PreVote({voter: CURSE_VOTER_1, subject: GLOBAL_CURSE_SUBJECT})); + s_preVotes.push(PreVote({voter: CURSE_VOTER_2, subject: GLOBAL_CURSE_SUBJECT})); + } + + function test_VoteToCurse_OldSubject_NewVoter_YesCurse_gas() public { + vm.prank(CURSE_VOTER_3); + s_rmn.voteToCurse(makeCurseId(0xffff), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } +} + +contract RMN_lazyVoteToCurseUpdate_Benchmark is RMN_voteToCurse_Benchmark { + constructor() { + s_preVotes.push(PreVote({voter: CURSE_VOTER_1, subject: GLOBAL_CURSE_SUBJECT})); + s_preVotes.push(PreVote({voter: CURSE_VOTER_2, subject: GLOBAL_CURSE_SUBJECT})); + s_preVotes.push(PreVote({voter: CURSE_VOTER_3, subject: GLOBAL_CURSE_SUBJECT})); + } + + function setUp() public override { + RMN_voteToCurse_Benchmark.setUp(); // sends the prevotes + // initial config includes voters CURSE_VOTER_1, CURSE_VOTER_2, CURSE_VOTER_3 + // include a new voter in the config + { + (,, RMN.Config memory cfg) = s_rmn.getConfigDetails(); + RMN.Voter[] memory newVoters = new RMN.Voter[](cfg.voters.length + 1); + for (uint256 i = 0; i < cfg.voters.length; ++i) { + newVoters[i] = cfg.voters[i]; + } + newVoters[newVoters.length - 1] = + RMN.Voter({blessVoteAddr: BLESS_VOTER_4, curseVoteAddr: CURSE_VOTER_4, blessWeight: 1, curseWeight: 1}); + cfg.voters = newVoters; + + vm.prank(OWNER); + s_rmn.setConfig(cfg); + } + } + + function test_VoteToCurseLazilyRetain3VotersUponConfigChange_gas() public { + // send a vote as the new voter, should cause a lazy update and votes from CURSE_VOTER_1, CURSE_VOTER_2, + // CURSE_VOTER_3 to be retained, which is the worst case for the prior config + vm.prank(CURSE_VOTER_4); + s_rmn.voteToCurse(makeCurseId(0xffff), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } +} + +contract RMN_setConfig_Benchmark is RMNSetup { + uint256 s_numVoters; + + function configWithVoters(uint256 numVoters) internal pure returns (RMN.Config memory) { + RMN.Config memory cfg = + RMN.Config({voters: new RMN.Voter[](numVoters), blessWeightThreshold: 1, curseWeightThreshold: 1}); + for (uint256 i = 1; i <= numVoters; ++i) { + cfg.voters[i - 1] = RMN.Voter({ + blessVoteAddr: address(uint160(2 * i)), + curseVoteAddr: address(uint160(2 * i + 1)), + blessWeight: 1, + curseWeight: 1 + }); + } + return cfg; + } + + function setUp() public virtual override { + vm.prank(OWNER); + s_rmn = new RMN(configWithVoters(s_numVoters)); + } +} + +contract RMN_setConfig_Benchmark_1 is RMN_setConfig_Benchmark { + constructor() { + s_numVoters = 1; + } + + function test_SetConfig_7Voters_gas() public { + vm.prank(OWNER); + s_rmn.setConfig(configWithVoters(7)); + } +} + +contract RMN_setConfig_Benchmark_2 is RMN_setConfig_Benchmark { + constructor() { + s_numVoters = 7; + } + + function test_ResetConfig_7Voters_gas() public { + vm.prank(OWNER); + s_rmn.setConfig(configWithVoters(7)); + } +} + +contract RMN_ownerUnvoteToCurse_Benchmark is RMN_setConfig_Benchmark { + constructor() { + s_numVoters = 7; + } + + function setUp() public override { + RMN_setConfig_Benchmark.setUp(); + vm.prank(OWNER); + s_rmn.ownerCurse(makeCurseId(0xffff), makeSubjects(GLOBAL_CURSE_SUBJECT)); + } + + function test_OwnerUnvoteToCurse_1Voter_LiftsCurse_gas() public { + RMN.OwnerUnvoteToCurseRequest[] memory reqs = new RMN.OwnerUnvoteToCurseRequest[](1); + reqs[0] = RMN.OwnerUnvoteToCurseRequest({ + curseVoteAddr: OWNER_CURSE_VOTE_ADDR, + unit: RMN.UnvoteToCurseRequest({cursesHash: makeCursesHash(makeCurseId(0xffff)), subject: GLOBAL_CURSE_SUBJECT}), + forceUnvote: false + }); + vm.prank(OWNER); + s_rmn.ownerUnvoteToCurse(reqs); + } +} diff --git a/contracts/src/v0.8/ccip/test/attacks/onRamp/FacadeClient.sol b/contracts/src/v0.8/ccip/test/attacks/onRamp/FacadeClient.sol new file mode 100644 index 00000000000..ad549e6ccc2 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/attacks/onRamp/FacadeClient.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IRouterClient} from "../../../interfaces/IRouterClient.sol"; + +import {Client} from "../../../libraries/Client.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +/// @title FacadeClient - A simple proxy for calling Router +contract FacadeClient { + address private immutable i_router; + uint64 private immutable i_destChainSelector; + IERC20 private immutable i_sourceToken; + IERC20 private immutable i_feeToken; + address private immutable i_receiver; + + uint256 private s_msg_sequence = 1; + + constructor(address router, uint64 destChainSelector, IERC20 sourceToken, IERC20 feeToken, address receiver) { + i_router = router; + i_destChainSelector = destChainSelector; + i_sourceToken = sourceToken; + i_feeToken = feeToken; + i_receiver = receiver; + + sourceToken.approve(address(router), 2 ** 256 - 1); + feeToken.approve(address(router), 2 ** 256 - 1); + } + + /// @dev Calls Router to initiate CCIP send. + /// The expectation is that s_msg_sequence will always match the sequence in emitted CCIP messages. + function send(uint256 amount) public { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0].token = address(i_sourceToken); + tokenAmounts[0].amount = amount; + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(i_receiver), + data: abi.encodePacked(s_msg_sequence), + tokenAmounts: tokenAmounts, + extraArgs: "", + feeToken: address(i_feeToken) + }); + + s_msg_sequence++; + + IRouterClient(i_router).ccipSend(i_destChainSelector, message); + } + + function getSequence() public view returns (uint256) { + return s_msg_sequence; + } +} diff --git a/contracts/src/v0.8/ccip/test/attacks/onRamp/MultiOnRampTokenPoolReentrancy.t.sol b/contracts/src/v0.8/ccip/test/attacks/onRamp/MultiOnRampTokenPoolReentrancy.t.sol new file mode 100644 index 00000000000..5deeda64063 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/attacks/onRamp/MultiOnRampTokenPoolReentrancy.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Client} from "../../../libraries/Client.sol"; +import {Internal} from "../../../libraries/Internal.sol"; +import {EVM2EVMMultiOnRamp} from "../../../onRamp/EVM2EVMMultiOnRamp.sol"; +import {TokenPool} from "../../../pools/TokenPool.sol"; +import {EVM2EVMMultiOnRampSetup} from "../../onRamp/EVM2EVMMultiOnRampSetup.t.sol"; +import {FacadeClient} from "./FacadeClient.sol"; +import {ReentrantMaliciousTokenPool} from "./ReentrantMaliciousTokenPool.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +import {console} from "forge-std/console.sol"; + +/// @title MultiOnRampTokenPoolReentrancy +/// Attempts to perform a reentrancy exploit on Onramp with a malicious TokenPool +contract MultiOnRampTokenPoolReentrancy is EVM2EVMMultiOnRampSetup { + FacadeClient internal s_facadeClient; + ReentrantMaliciousTokenPool internal s_maliciousTokenPool; + IERC20 internal s_sourceToken; + IERC20 internal s_feeToken; + address internal immutable i_receiver = makeAddr("receiver"); + + function setUp() public virtual override { + EVM2EVMMultiOnRampSetup.setUp(); + + s_sourceToken = IERC20(s_sourceTokens[0]); + s_feeToken = IERC20(s_sourceTokens[0]); + + s_facadeClient = + new FacadeClient(address(s_sourceRouter), DEST_CHAIN_SELECTOR, s_sourceToken, s_feeToken, i_receiver); + + s_maliciousTokenPool = new ReentrantMaliciousTokenPool( + address(s_facadeClient), s_sourceToken, address(s_mockRMN), address(s_sourceRouter) + ); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_destPoolBySourceToken[s_sourceTokens[0]]), + remoteTokenAddress: abi.encode(s_destTokens[0]), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_maliciousTokenPool.applyChainUpdates(chainUpdates); + s_sourcePoolByToken[address(s_sourceToken)] = address(s_maliciousTokenPool); + + Internal.PoolUpdate[] memory removes = new Internal.PoolUpdate[](1); + removes[0].token = address(s_sourceToken); + removes[0].pool = address(s_sourcePoolByToken[address(s_sourceToken)]); + Internal.PoolUpdate[] memory adds = new Internal.PoolUpdate[](1); + adds[0].token = address(s_sourceToken); + adds[0].pool = address(s_maliciousTokenPool); + + s_tokenAdminRegistry.setPool(address(s_sourceToken), address(s_maliciousTokenPool)); + + s_sourceToken.transfer(address(s_facadeClient), 1e18); + s_feeToken.transfer(address(s_facadeClient), 1e18); + } + + /// @dev This test was used to showcase a reentrancy exploit on OnRamp with malicious TokenPool. + /// How it worked: OnRamp used to construct EVM2Any messages after calling TokenPool's lockOrBurn. + /// This allowed the malicious TokenPool to break message sequencing expectations as follows: + /// Any user -> Facade -> 1st call to ccipSend -> pool’s lockOrBurn —> + /// (reenter)-> Facade -> 2nd call to ccipSend + /// In this case, Facade's second call would produce an EVM2Any msg with a lower sequence number. + /// The issue was fixed by moving state updates and event construction to before TokenPool calls. + /// This test is kept to verify message sequence expectations are not broken. + function test_OnRampTokenPoolReentrancy_Success() public { + uint256 amount = 1; + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0].token = address(s_sourceToken); + tokenAmounts[0].amount = amount; + + Client.EVM2AnyMessage memory message1 = Client.EVM2AnyMessage({ + receiver: abi.encode(i_receiver), + data: abi.encodePacked(uint256(1)), // message 1 contains data 1 + tokenAmounts: tokenAmounts, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})), + feeToken: address(s_feeToken) + }); + + Client.EVM2AnyMessage memory message2 = Client.EVM2AnyMessage({ + receiver: abi.encode(i_receiver), + data: abi.encodePacked(uint256(2)), // message 2 contains data 2 + tokenAmounts: tokenAmounts, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})), + feeToken: address(s_feeToken) + }); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message1); + assertGt(expectedFee, 0); + + // Outcome of a successful exploit: + // Message 1 event from OnRamp contains sequence/nonce 2, message 2 contains sequence/nonce 1 + // Internal.EVM2EVMMessage memory msgEvent1 = _messageToEvent(message1, 2, 2, expectedFee, address(s_facadeClient)); + // Internal.EVM2EVMMessage memory msgEvent2 = _messageToEvent(message2, 1, 1, expectedFee, address(s_facadeClient)); + + // vm.expectEmit(); + // emit CCIPSendRequested(msgEvent2); + // vm.expectEmit(); + // emit CCIPSendRequested(msgEvent1); + + // After issue is fixed, sequence now increments as expected + Internal.EVM2AnyRampMessage memory msgEvent1 = _messageToEvent(message1, 1, 1, expectedFee, address(s_facadeClient)); + Internal.EVM2AnyRampMessage memory msgEvent2 = _messageToEvent(message2, 2, 2, expectedFee, address(s_facadeClient)); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, msgEvent2); + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, msgEvent1); + + s_facadeClient.send(amount); + } +} diff --git a/contracts/src/v0.8/ccip/test/attacks/onRamp/OnRampTokenPoolReentrancy.t.sol b/contracts/src/v0.8/ccip/test/attacks/onRamp/OnRampTokenPoolReentrancy.t.sol new file mode 100644 index 00000000000..8fc71be8573 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/attacks/onRamp/OnRampTokenPoolReentrancy.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Client} from "../../../libraries/Client.sol"; +import {Internal} from "../../../libraries/Internal.sol"; +import {EVM2EVMOnRamp} from "../../../onRamp/EVM2EVMOnRamp.sol"; +import {TokenPool} from "../../../pools/TokenPool.sol"; +import {EVM2EVMOnRampSetup} from "../../onRamp/EVM2EVMOnRampSetup.t.sol"; +import {FacadeClient} from "./FacadeClient.sol"; +import {ReentrantMaliciousTokenPool} from "./ReentrantMaliciousTokenPool.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +/// @title OnRampTokenPoolReentrancy +/// Attempts to perform a reentrancy exploit on Onramp with a malicious TokenPool +contract OnRampTokenPoolReentrancy is EVM2EVMOnRampSetup { + FacadeClient internal s_facadeClient; + ReentrantMaliciousTokenPool internal s_maliciousTokenPool; + IERC20 internal s_sourceToken; + IERC20 internal s_feeToken; + address internal immutable i_receiver = makeAddr("receiver"); + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + s_sourceToken = IERC20(s_sourceTokens[0]); + s_feeToken = IERC20(s_sourceTokens[0]); + + s_facadeClient = + new FacadeClient(address(s_sourceRouter), DEST_CHAIN_SELECTOR, s_sourceToken, s_feeToken, i_receiver); + + s_maliciousTokenPool = new ReentrantMaliciousTokenPool( + address(s_facadeClient), s_sourceToken, address(s_mockRMN), address(s_sourceRouter) + ); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_destPoolBySourceToken[s_sourceTokens[0]]), + remoteTokenAddress: abi.encode(s_destTokens[0]), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_maliciousTokenPool.applyChainUpdates(chainUpdates); + s_sourcePoolByToken[address(s_sourceToken)] = address(s_maliciousTokenPool); + + Internal.PoolUpdate[] memory removes = new Internal.PoolUpdate[](1); + removes[0].token = address(s_sourceToken); + removes[0].pool = address(s_sourcePoolByToken[address(s_sourceToken)]); + Internal.PoolUpdate[] memory adds = new Internal.PoolUpdate[](1); + adds[0].token = address(s_sourceToken); + adds[0].pool = address(s_maliciousTokenPool); + + s_tokenAdminRegistry.setPool(address(s_sourceToken), address(s_maliciousTokenPool)); + + s_sourceToken.transfer(address(s_facadeClient), 1e18); + s_feeToken.transfer(address(s_facadeClient), 1e18); + } + + /// @dev This test was used to showcase a reentrancy exploit on OnRamp with malicious TokenPool. + /// How it worked: OnRamp used to construct EVM2EVM messages after calling TokenPool's lockOrBurn. + /// This allowed the malicious TokenPool to break message sequencing expectations as follows: + /// Any user -> Facade -> 1st call to ccipSend -> pool’s lockOrBurn —> + /// (reenter)-> Facade -> 2nd call to ccipSend + /// In this case, Facade's second call would produce an EVM2EVM msg with a lower sequence number. + /// The issue was fixed by moving state updates and event construction to before TokenPool calls. + /// This test is kept to verify message sequence expectations are not broken. + function test_OnRampTokenPoolReentrancy_Success() public { + uint256 amount = 1; + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0].token = address(s_sourceToken); + tokenAmounts[0].amount = amount; + + Client.EVM2AnyMessage memory message1 = Client.EVM2AnyMessage({ + receiver: abi.encode(i_receiver), + data: abi.encodePacked(uint256(1)), // message 1 contains data 1 + tokenAmounts: tokenAmounts, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})), + feeToken: address(s_feeToken) + }); + + Client.EVM2AnyMessage memory message2 = Client.EVM2AnyMessage({ + receiver: abi.encode(i_receiver), + data: abi.encodePacked(uint256(2)), // message 2 contains data 2 + tokenAmounts: tokenAmounts, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})), + feeToken: address(s_feeToken) + }); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message1); + assertGt(expectedFee, 0); + + // Outcome of a successful exploit: + // Message 1 event from OnRamp contains sequence/nonce 2, message 2 contains sequence/nonce 1 + // Internal.EVM2EVMMessage memory msgEvent1 = _messageToEvent(message1, 2, 2, expectedFee, address(s_facadeClient)); + // Internal.EVM2EVMMessage memory msgEvent2 = _messageToEvent(message2, 1, 1, expectedFee, address(s_facadeClient)); + + // vm.expectEmit(); + // emit CCIPSendRequested(msgEvent2); + // vm.expectEmit(); + // emit CCIPSendRequested(msgEvent1); + + // After issue is fixed, sequence now increments as expected + Internal.EVM2EVMMessage memory msgEvent1 = _messageToEvent(message1, 1, 1, expectedFee, address(s_facadeClient)); + Internal.EVM2EVMMessage memory msgEvent2 = _messageToEvent(message2, 2, 2, expectedFee, address(s_facadeClient)); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent2); + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent1); + + s_facadeClient.send(amount); + } +} diff --git a/contracts/src/v0.8/ccip/test/attacks/onRamp/ReentrantMaliciousTokenPool.sol b/contracts/src/v0.8/ccip/test/attacks/onRamp/ReentrantMaliciousTokenPool.sol new file mode 100644 index 00000000000..17c13a8148e --- /dev/null +++ b/contracts/src/v0.8/ccip/test/attacks/onRamp/ReentrantMaliciousTokenPool.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Pool} from "../../../libraries/Pool.sol"; +import {TokenPool} from "../../../pools/TokenPool.sol"; +import {FacadeClient} from "./FacadeClient.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract ReentrantMaliciousTokenPool is TokenPool { + address private i_facade; + + bool private s_attacked; + + constructor( + address facade, + IERC20 token, + address rmnProxy, + address router + ) TokenPool(token, new address[](0), rmnProxy, router) { + i_facade = facade; + } + + /// @dev Calls into Facade to reenter Router exactly 1 time + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + override + returns (Pool.LockOrBurnOutV1 memory) + { + if (s_attacked) { + return + Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + s_attacked = true; + + FacadeClient(i_facade).send(lockOrBurnIn.amount); + emit Burned(msg.sender, lockOrBurnIn.amount); + return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + pure + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } +} diff --git a/contracts/src/v0.8/ccip/test/capability/CCIPConfig.t.sol b/contracts/src/v0.8/ccip/test/capability/CCIPConfig.t.sol new file mode 100644 index 00000000000..0c3108d279f --- /dev/null +++ b/contracts/src/v0.8/ccip/test/capability/CCIPConfig.t.sol @@ -0,0 +1,1681 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; + +import {SortedSetValidationUtil} from "../../../shared/util/SortedSetValidationUtil.sol"; +import {CCIPConfig} from "../../capability/CCIPConfig.sol"; +import {ICapabilitiesRegistry} from "../../capability/interfaces/ICapabilitiesRegistry.sol"; +import {CCIPConfigTypes} from "../../capability/libraries/CCIPConfigTypes.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {CCIPConfigHelper} from "../helpers/CCIPConfigHelper.sol"; + +contract CCIPConfigSetup is Test { + address public constant OWNER = 0x82ae2B4F57CA5C1CBF8f744ADbD3697aD1a35AFe; + address public constant CAPABILITIES_REGISTRY = 0x272aF4BF7FBFc4944Ed59F914Cd864DfD912D55e; + + CCIPConfigHelper public s_ccipCC; + + function setUp() public { + changePrank(OWNER); + s_ccipCC = new CCIPConfigHelper(CAPABILITIES_REGISTRY); + } + + function _makeBytes32Array(uint256 length, uint256 seed) internal pure returns (bytes32[] memory arr) { + arr = new bytes32[](length); + for (uint256 i = 0; i < length; i++) { + arr[i] = keccak256(abi.encode(i, 1, seed)); + } + return arr; + } + + function _makeBytesArray(uint256 length, uint256 seed) internal pure returns (bytes[] memory arr) { + arr = new bytes[](length); + for (uint256 i = 0; i < length; i++) { + arr[i] = abi.encodePacked(keccak256(abi.encode(i, 1, seed))); + } + return arr; + } + + function _subset(bytes32[] memory arr, uint256 start, uint256 end) internal pure returns (bytes32[] memory) { + bytes32[] memory subset = new bytes32[](end - start); + for (uint256 i = start; i < end; i++) { + subset[i - start] = arr[i]; + } + return subset; + } + + //TODO: Use OZ's Arrays.sort when we upgrade to OZ v5 + function _sort(bytes32[] memory arr, int256 left, int256 right) private pure { + int256 i = left; + int256 j = right; + if (i == j) return; + bytes32 pivot = arr[uint256(left + (right - left) / 2)]; + while (i <= j) { + while (arr[uint256(i)] < pivot) i++; + while (pivot < arr[uint256(j)]) j--; + if (i <= j) { + (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]); + i++; + j--; + } + } + if (left < j) _sort(arr, left, j); + if (i < right) _sort(arr, i, right); + } + + function _addChainConfig(uint256 numNodes) + internal + returns (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) + { + p2pIds = _makeBytes32Array(numNodes, 0); + _sort(p2pIds, 0, int256(numNodes - 1)); + signers = _makeBytesArray(numNodes, 10); + transmitters = _makeBytesArray(numNodes, 20); + for (uint256 i = 0; i < numNodes; i++) { + vm.mockCall( + CAPABILITIES_REGISTRY, + abi.encodeWithSelector(ICapabilitiesRegistry.getNode.selector, p2pIds[i]), + abi.encode( + ICapabilitiesRegistry.NodeInfo({ + nodeOperatorId: 1, + signer: bytes32(signers[i]), + p2pId: p2pIds[i], + hashedCapabilityIds: new bytes32[](0), + configCount: uint32(1), + workflowDONId: uint32(1), + capabilitiesDONIds: new uint256[](0) + }) + ) + ); + } + // Add chain selector for chain 1. + CCIPConfigTypes.ChainConfigInfo[] memory adds = new CCIPConfigTypes.ChainConfigInfo[](1); + adds[0] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: 1, + chainConfig: CCIPConfigTypes.ChainConfig({readers: p2pIds, fChain: 1, config: bytes("config1")}) + }); + + vm.expectEmit(); + emit CCIPConfig.ChainConfigSet(1, adds[0].chainConfig); + s_ccipCC.applyChainConfigUpdates(new uint64[](0), adds); + + return (p2pIds, signers, transmitters); + } + + function test_getCapabilityConfiguration_Success() public { + bytes memory capConfig = s_ccipCC.getCapabilityConfiguration(42 /* doesn't matter, not used */ ); + assertEq(capConfig.length, 0, "capability config length must be 0"); + } +} + +contract CCIPConfig_chainConfig is CCIPConfigSetup { + // Successes. + + function test_applyChainConfigUpdates_addChainConfigs_Success() public { + bytes32[] memory chainReaders = new bytes32[](1); + chainReaders[0] = keccak256(abi.encode(1)); + CCIPConfigTypes.ChainConfigInfo[] memory adds = new CCIPConfigTypes.ChainConfigInfo[](2); + adds[0] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: 1, + chainConfig: CCIPConfigTypes.ChainConfig({readers: chainReaders, fChain: 1, config: bytes("config1")}) + }); + adds[1] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: 2, + chainConfig: CCIPConfigTypes.ChainConfig({readers: chainReaders, fChain: 1, config: bytes("config2")}) + }); + + vm.mockCall( + CAPABILITIES_REGISTRY, + abi.encodeWithSelector(ICapabilitiesRegistry.getNode.selector, chainReaders[0]), + abi.encode( + ICapabilitiesRegistry.NodeInfo({ + nodeOperatorId: 1, + signer: bytes32(uint256(1)), + p2pId: chainReaders[0], + hashedCapabilityIds: new bytes32[](0), + configCount: uint32(1), + workflowDONId: uint32(1), + capabilitiesDONIds: new uint256[](0) + }) + ) + ); + + vm.expectEmit(); + emit CCIPConfig.ChainConfigSet(1, adds[0].chainConfig); + vm.expectEmit(); + emit CCIPConfig.ChainConfigSet(2, adds[1].chainConfig); + s_ccipCC.applyChainConfigUpdates(new uint64[](0), adds); + + CCIPConfigTypes.ChainConfigInfo[] memory configs = s_ccipCC.getAllChainConfigs(); + assertEq(configs.length, 2, "chain configs length must be 2"); + assertEq(configs[0].chainSelector, 1, "chain selector must match"); + assertEq(configs[1].chainSelector, 2, "chain selector must match"); + } + + function test_applyChainConfigUpdates_removeChainConfigs_Success() public { + bytes32[] memory chainReaders = new bytes32[](1); + chainReaders[0] = keccak256(abi.encode(1)); + CCIPConfigTypes.ChainConfigInfo[] memory adds = new CCIPConfigTypes.ChainConfigInfo[](2); + adds[0] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: 1, + chainConfig: CCIPConfigTypes.ChainConfig({readers: chainReaders, fChain: 1, config: bytes("config1")}) + }); + adds[1] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: 2, + chainConfig: CCIPConfigTypes.ChainConfig({readers: chainReaders, fChain: 1, config: bytes("config2")}) + }); + + vm.mockCall( + CAPABILITIES_REGISTRY, + abi.encodeWithSelector(ICapabilitiesRegistry.getNode.selector, chainReaders[0]), + abi.encode( + ICapabilitiesRegistry.NodeInfo({ + nodeOperatorId: 1, + signer: bytes32(uint256(1)), + p2pId: chainReaders[0], + hashedCapabilityIds: new bytes32[](0), + configCount: uint32(1), + workflowDONId: uint32(1), + capabilitiesDONIds: new uint256[](0) + }) + ) + ); + + vm.expectEmit(); + emit CCIPConfig.ChainConfigSet(1, adds[0].chainConfig); + vm.expectEmit(); + emit CCIPConfig.ChainConfigSet(2, adds[1].chainConfig); + s_ccipCC.applyChainConfigUpdates(new uint64[](0), adds); + + uint64[] memory removes = new uint64[](1); + removes[0] = uint64(1); + + vm.expectEmit(); + emit CCIPConfig.ChainConfigRemoved(1); + s_ccipCC.applyChainConfigUpdates(removes, new CCIPConfigTypes.ChainConfigInfo[](0)); + } + + // Reverts. + + function test_applyChainConfigUpdates_selectorNotFound_Reverts() public { + uint64[] memory removes = new uint64[](1); + removes[0] = uint64(1); + + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.ChainSelectorNotFound.selector, 1)); + s_ccipCC.applyChainConfigUpdates(removes, new CCIPConfigTypes.ChainConfigInfo[](0)); + } + + function test_applyChainConfigUpdates_nodeNotInRegistry_Reverts() public { + bytes32[] memory chainReaders = new bytes32[](1); + chainReaders[0] = keccak256(abi.encode(1)); + CCIPConfigTypes.ChainConfigInfo[] memory adds = new CCIPConfigTypes.ChainConfigInfo[](1); + adds[0] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: 1, + chainConfig: CCIPConfigTypes.ChainConfig({readers: chainReaders, fChain: 1, config: abi.encode(1, 2, 3)}) + }); + + vm.mockCall( + CAPABILITIES_REGISTRY, + abi.encodeWithSelector(ICapabilitiesRegistry.getNode.selector, chainReaders[0]), + abi.encode( + ICapabilitiesRegistry.NodeInfo({ + nodeOperatorId: 0, + signer: bytes32(0), + p2pId: bytes32(uint256(0)), + hashedCapabilityIds: new bytes32[](0), + configCount: uint32(1), + workflowDONId: uint32(1), + capabilitiesDONIds: new uint256[](0) + }) + ) + ); + + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.NodeNotInRegistry.selector, chainReaders[0])); + s_ccipCC.applyChainConfigUpdates(new uint64[](0), adds); + } + + function test__applyChainConfigUpdates_FChainNotPositive_Reverts() public { + bytes32[] memory chainReaders = new bytes32[](1); + chainReaders[0] = keccak256(abi.encode(1)); + CCIPConfigTypes.ChainConfigInfo[] memory adds = new CCIPConfigTypes.ChainConfigInfo[](2); + adds[0] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: 1, + chainConfig: CCIPConfigTypes.ChainConfig({readers: chainReaders, fChain: 1, config: bytes("config1")}) + }); + adds[1] = CCIPConfigTypes.ChainConfigInfo({ + chainSelector: 2, + chainConfig: CCIPConfigTypes.ChainConfig({readers: chainReaders, fChain: 0, config: bytes("config2")}) // bad fChain + }); + + vm.mockCall( + CAPABILITIES_REGISTRY, + abi.encodeWithSelector(ICapabilitiesRegistry.getNode.selector, chainReaders[0]), + abi.encode( + ICapabilitiesRegistry.NodeInfo({ + nodeOperatorId: 1, + signer: bytes32(uint256(1)), + p2pId: chainReaders[0], + hashedCapabilityIds: new bytes32[](0), + configCount: uint32(1), + workflowDONId: uint32(1), + capabilitiesDONIds: new uint256[](0) + }) + ) + ); + + vm.expectRevert(CCIPConfig.FChainMustBePositive.selector); + s_ccipCC.applyChainConfigUpdates(new uint64[](0), adds); + } +} + +contract CCIPConfig_validateConfig is CCIPConfigSetup { + // Successes. + + function test__validateConfig_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + + // Config is for 4 nodes, so f == 1. + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + s_ccipCC.validateConfig(config); + } + + // Reverts. + + function test__validateConfig_ChainSelectorNotSet_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + + // Config is for 4 nodes, so f == 1. + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 0, // invalid + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(CCIPConfig.ChainSelectorNotSet.selector); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_OfframpAddressCannotBeZero_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + + // Config is for 4 nodes, so f == 1. + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: bytes(""), // invalid + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(CCIPConfig.OfframpAddressCannotBeZero.selector); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_ChainSelectorNotFound_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + + // Config is for 4 nodes, so f == 1. + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 2, // not set + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.ChainSelectorNotFound.selector, 2)); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_TooManySigners_Reverts() public { + // 32 > 31 (max num oracles) + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(32); + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(CCIPConfig.TooManySigners.selector); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_TooManyTransmitters_Reverts() public { + // 32 > 31 (max num oracles) + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(32); + + // truncate signers but keep transmitters > 31 + assembly { + mstore(signers, 30) + } + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(CCIPConfig.TooManyTransmitters.selector); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_NotEnoughTransmitters_Reverts() public { + // 32 > 31 (max num oracles) + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(31); + + // truncate transmitters to < 3 * fChain + 1 + // since fChain is 1 in this case, we need to truncate to 3 transmitters. + assembly { + mstore(transmitters, 3) + } + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.NotEnoughTransmitters.selector, 3, 4)); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_FMustBePositive_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + + // Config is for 4 nodes, so f == 1. + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 0, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(CCIPConfig.FMustBePositive.selector); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_FTooHigh_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 2, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(CCIPConfig.FTooHigh.selector); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_P2PIdsLengthNotMatching_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + // truncate the p2pIds length + assembly { + mstore(p2pIds, 3) + } + + // Config is for 4 nodes, so f == 1. + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert( + abi.encodeWithSelector(CCIPConfig.P2PIdsLengthNotMatching.selector, uint256(3), uint256(4), uint256(4)) + ); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_TooManyBootstrapP2PIds_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + + // Config is for 4 nodes, so f == 1. + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _makeBytes32Array(5, 0), // too many bootstrap p2pIds, 5 > 4 + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(CCIPConfig.TooManyBootstrapP2PIds.selector); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_NodeNotInRegistry_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + bytes32 nonExistentP2PId = keccak256("notInRegistry"); + p2pIds[0] = nonExistentP2PId; + + vm.mockCall( + CAPABILITIES_REGISTRY, + abi.encodeWithSelector(ICapabilitiesRegistry.getNode.selector, nonExistentP2PId), + abi.encode( + ICapabilitiesRegistry.NodeInfo({ + nodeOperatorId: 0, + signer: bytes32(0), + p2pId: bytes32(uint256(0)), + hashedCapabilityIds: new bytes32[](0), + configCount: uint32(1), + workflowDONId: uint32(1), + capabilitiesDONIds: new uint256[](0) + }) + ) + ); + + // Config is for 4 nodes, so f == 1. + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.NodeNotInRegistry.selector, nonExistentP2PId)); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_P2PIdsNotSorted_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + // Config is for 4 nodes, so f == 1. + + //swapping two adjacent p2pIds to make it unsorted + (p2pIds[2], p2pIds[3]) = (p2pIds[3], p2pIds[2]); + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(abi.encodeWithSelector(SortedSetValidationUtil.NotASortedSet.selector, p2pIds)); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_BootstrapP2PIdsNotSorted_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + // Config is for 4 nodes, so f == 1. + + bytes32[] memory bootstrapP2PIds = _subset(p2pIds, 0, 2); + + //swapping bootstrapP2PIds to make it unsorted + (bootstrapP2PIds[0], bootstrapP2PIds[1]) = (bootstrapP2PIds[1], bootstrapP2PIds[0]); + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: bootstrapP2PIds, + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(abi.encodeWithSelector(SortedSetValidationUtil.NotASortedSet.selector, bootstrapP2PIds)); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_P2PIdsHasDuplicates_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + // Config is for 4 nodes, so f == 1. + + //forcing duplicate p2pIds + p2pIds[1] = p2pIds[2]; + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 2), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(abi.encodeWithSelector(SortedSetValidationUtil.NotASortedSet.selector, p2pIds)); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_BootstrapP2PIdsHasDuplicates_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + // Config is for 4 nodes, so f == 1. + + bytes32[] memory bootstrapP2PIds = _subset(p2pIds, 0, 2); + //forcing duplicate bootstrapP2PIds + bootstrapP2PIds[1] = bootstrapP2PIds[0]; + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: bootstrapP2PIds, + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(abi.encodeWithSelector(SortedSetValidationUtil.NotASortedSet.selector, bootstrapP2PIds)); + s_ccipCC.validateConfig(config); + } + + function test__validateConfig_BootstrapP2PIdsNotASubsetOfP2PIds_Reverts() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + // Config is for 4 nodes, so f == 1. + + //forcing invalid bootstrapP2PIds where the bootstrapP2PIds is sorted, but one of the element is not in the p2pIdsSet + bytes32[] memory bootstrapP2PIds = _subset(p2pIds, 0, 2); + p2pIds[1] = bytes32(uint256(p2pIds[0]) + 100); + + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: bootstrapP2PIds, + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + + vm.expectRevert(abi.encodeWithSelector(SortedSetValidationUtil.NotASubset.selector, bootstrapP2PIds, p2pIds)); + s_ccipCC.validateConfig(config); + } +} + +contract CCIPConfig_ConfigStateMachine is CCIPConfigSetup { + // Successful cases. + + function test__stateFromConfigLength_Success() public { + uint256 configLen = 0; + CCIPConfigTypes.ConfigState state = s_ccipCC.stateFromConfigLength(configLen); + assertEq(uint256(state), uint256(CCIPConfigTypes.ConfigState.Init)); + + configLen = 1; + state = s_ccipCC.stateFromConfigLength(configLen); + assertEq(uint256(state), uint256(CCIPConfigTypes.ConfigState.Running)); + + configLen = 2; + state = s_ccipCC.stateFromConfigLength(configLen); + assertEq(uint256(state), uint256(CCIPConfigTypes.ConfigState.Staging)); + } + + function test__validateConfigStateTransition_Success() public { + s_ccipCC.validateConfigStateTransition(CCIPConfigTypes.ConfigState.Init, CCIPConfigTypes.ConfigState.Running); + + s_ccipCC.validateConfigStateTransition(CCIPConfigTypes.ConfigState.Running, CCIPConfigTypes.ConfigState.Staging); + + s_ccipCC.validateConfigStateTransition(CCIPConfigTypes.ConfigState.Staging, CCIPConfigTypes.ConfigState.Running); + } + + function test__computeConfigDigest_Success() public { + // config digest must change upon: + // - ocr config change (e.g plugin type, chain selector, etc.) + // - don id change + // - config count change + bytes32[] memory p2pIds = _makeBytes32Array(4, 0); + bytes[] memory signers = _makeBytesArray(2, 10); + bytes[] memory transmitters = _makeBytesArray(2, 20); + CCIPConfigTypes.OCR3Config memory config = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("offchainConfig") + }); + uint32 donId = 1; + uint32 configCount = 1; + + bytes32 configDigest1 = s_ccipCC.computeConfigDigest(donId, configCount, config); + + donId = 2; + bytes32 configDigest2 = s_ccipCC.computeConfigDigest(donId, configCount, config); + + donId = 1; + configCount = 2; + bytes32 configDigest3 = s_ccipCC.computeConfigDigest(donId, configCount, config); + + configCount = 1; + config.pluginType = Internal.OCRPluginType.Execution; + bytes32 configDigest4 = s_ccipCC.computeConfigDigest(donId, configCount, config); + + assertNotEq(configDigest1, configDigest2, "config digests 1 and 2 must not match"); + assertNotEq(configDigest1, configDigest3, "config digests 1 and 3 must not match"); + assertNotEq(configDigest1, configDigest4, "config digests 1 and 4 must not match"); + + assertNotEq(configDigest2, configDigest3, "config digests 2 and 3 must not match"); + assertNotEq(configDigest2, configDigest4, "config digests 2 and 4 must not match"); + } + + function test_Fuzz__groupByPluginType_Success(uint256 numCommitCfgs, uint256 numExecCfgs) public { + numCommitCfgs = bound(numCommitCfgs, 0, 2); + numExecCfgs = bound(numExecCfgs, 0, 2); + + bytes32[] memory p2pIds = _makeBytes32Array(4, 0); + bytes[] memory signers = _makeBytesArray(4, 10); + bytes[] memory transmitters = _makeBytesArray(4, 20); + CCIPConfigTypes.OCR3Config[] memory cfgs = new CCIPConfigTypes.OCR3Config[](numCommitCfgs + numExecCfgs); + for (uint256 i = 0; i < numCommitCfgs; i++) { + cfgs[i] = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: abi.encode("commit", i) + }); + } + for (uint256 i = 0; i < numExecCfgs; i++) { + cfgs[numCommitCfgs + i] = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Execution, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: abi.encode("exec", numCommitCfgs + i) + }); + } + (CCIPConfigTypes.OCR3Config[] memory commitCfgs, CCIPConfigTypes.OCR3Config[] memory execCfgs) = + s_ccipCC.groupByPluginType(cfgs); + + assertEq(commitCfgs.length, numCommitCfgs, "commitCfgs length must match"); + assertEq(execCfgs.length, numExecCfgs, "execCfgs length must match"); + for (uint256 i = 0; i < commitCfgs.length; i++) { + assertEq(uint8(commitCfgs[i].pluginType), uint8(Internal.OCRPluginType.Commit), "plugin type must be commit"); + assertEq(commitCfgs[i].offchainConfig, abi.encode("commit", i), "offchain config must match"); + } + for (uint256 i = 0; i < execCfgs.length; i++) { + assertEq(uint8(execCfgs[i].pluginType), uint8(Internal.OCRPluginType.Execution), "plugin type must be execution"); + assertEq(execCfgs[i].offchainConfig, abi.encode("exec", numCommitCfgs + i), "offchain config must match"); + } + } + + function test__computeNewConfigWithMeta_InitToRunning_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + uint32 donId = 1; + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](0); + CCIPConfigTypes.OCR3Config[] memory newConfig = new CCIPConfigTypes.OCR3Config[](1); + newConfig[0] = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.ConfigState currentState = CCIPConfigTypes.ConfigState.Init; + CCIPConfigTypes.ConfigState newState = CCIPConfigTypes.ConfigState.Running; + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfigWithMeta = + s_ccipCC.computeNewConfigWithMeta(donId, currentConfig, newConfig, currentState, newState); + assertEq(newConfigWithMeta.length, 1, "new config with meta length must be 1"); + assertEq(newConfigWithMeta[0].configCount, uint64(1), "config count must be 1"); + assertEq(uint8(newConfigWithMeta[0].config.pluginType), uint8(newConfig[0].pluginType), "plugin type must match"); + assertEq(newConfigWithMeta[0].config.offchainConfig, newConfig[0].offchainConfig, "offchain config must match"); + assertEq( + newConfigWithMeta[0].configDigest, + s_ccipCC.computeConfigDigest(donId, 1, newConfig[0]), + "config digest must match" + ); + + // This ensures that the test case is using correct inputs. + s_ccipCC.validateConfigTransition(currentConfig, newConfigWithMeta); + } + + function test__computeNewConfigWithMeta_RunningToStaging_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + currentConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + + CCIPConfigTypes.OCR3Config[] memory newConfig = new CCIPConfigTypes.OCR3Config[](2); + // existing blue config first. + newConfig[0] = blueConfig; + // green config next. + newConfig[1] = greenConfig; + + CCIPConfigTypes.ConfigState currentState = CCIPConfigTypes.ConfigState.Running; + CCIPConfigTypes.ConfigState newState = CCIPConfigTypes.ConfigState.Staging; + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfigWithMeta = + s_ccipCC.computeNewConfigWithMeta(donId, currentConfig, newConfig, currentState, newState); + assertEq(newConfigWithMeta.length, 2, "new config with meta length must be 2"); + + assertEq(newConfigWithMeta[0].configCount, uint64(1), "config count of blue must be 1"); + assertEq( + uint8(newConfigWithMeta[0].config.pluginType), uint8(blueConfig.pluginType), "plugin type of blue must match" + ); + assertEq( + newConfigWithMeta[0].config.offchainConfig, blueConfig.offchainConfig, "offchain config of blue must match" + ); + assertEq( + newConfigWithMeta[0].configDigest, + s_ccipCC.computeConfigDigest(donId, 1, blueConfig), + "config digest of blue must match" + ); + + assertEq(newConfigWithMeta[1].configCount, uint64(2), "config count of green must be 2"); + assertEq( + uint8(newConfigWithMeta[1].config.pluginType), uint8(greenConfig.pluginType), "plugin type of green must match" + ); + assertEq( + newConfigWithMeta[1].config.offchainConfig, greenConfig.offchainConfig, "offchain config of green must match" + ); + assertEq( + newConfigWithMeta[1].configDigest, + s_ccipCC.computeConfigDigest(donId, 2, greenConfig), + "config digest of green must match" + ); + + // This ensures that the test case is using correct inputs. + s_ccipCC.validateConfigTransition(currentConfig, newConfigWithMeta); + } + + function test__computeNewConfigWithMeta_StagingToRunning_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](2); + currentConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + currentConfig[1] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 2, + config: greenConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 2, greenConfig) + }); + CCIPConfigTypes.OCR3Config[] memory newConfig = new CCIPConfigTypes.OCR3Config[](1); + newConfig[0] = greenConfig; + + CCIPConfigTypes.ConfigState currentState = CCIPConfigTypes.ConfigState.Staging; + CCIPConfigTypes.ConfigState newState = CCIPConfigTypes.ConfigState.Running; + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfigWithMeta = + s_ccipCC.computeNewConfigWithMeta(donId, currentConfig, newConfig, currentState, newState); + + assertEq(newConfigWithMeta.length, 1, "new config with meta length must be 1"); + assertEq(newConfigWithMeta[0].configCount, uint64(2), "config count must be 2"); + assertEq(uint8(newConfigWithMeta[0].config.pluginType), uint8(greenConfig.pluginType), "plugin type must match"); + assertEq(newConfigWithMeta[0].config.offchainConfig, greenConfig.offchainConfig, "offchain config must match"); + assertEq( + newConfigWithMeta[0].configDigest, s_ccipCC.computeConfigDigest(donId, 2, greenConfig), "config digest must match" + ); + + // This ensures that the test case is using correct inputs. + s_ccipCC.validateConfigTransition(currentConfig, newConfigWithMeta); + } + + function test__validateConfigTransition_InitToRunning_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + newConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](0); + + s_ccipCC.validateConfigTransition(currentConfig, newConfig); + } + + function test__validateConfigTransition_RunningToStaging_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](2); + newConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + newConfig[1] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 2, + config: greenConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 2, greenConfig) + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + currentConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + + s_ccipCC.validateConfigTransition(currentConfig, newConfig); + } + + function test__validateConfigTransition_StagingToRunning_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](2); + currentConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + currentConfig[1] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 2, + config: greenConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 2, greenConfig) + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + newConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 2, + config: greenConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 2, greenConfig) + }); + + s_ccipCC.validateConfigTransition(currentConfig, newConfig); + } + + // Reverts. + + function test_Fuzz__stateFromConfigLength_Reverts(uint256 configLen) public { + vm.assume(configLen > 2); + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.InvalidConfigLength.selector, configLen)); + s_ccipCC.stateFromConfigLength(configLen); + } + + function test__groupByPluginType_threeCommitConfigs_Reverts() public { + bytes32[] memory p2pIds = _makeBytes32Array(4, 0); + bytes[] memory signers = _makeBytesArray(4, 10); + bytes[] memory transmitters = _makeBytesArray(4, 20); + CCIPConfigTypes.OCR3Config[] memory cfgs = new CCIPConfigTypes.OCR3Config[](3); + for (uint256 i = 0; i < 3; i++) { + cfgs[i] = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: abi.encode("commit", i) + }); + } + vm.expectRevert(); + s_ccipCC.groupByPluginType(cfgs); + } + + function test__groupByPluginType_threeExecutionConfigs_Reverts() public { + bytes32[] memory p2pIds = _makeBytes32Array(4, 0); + bytes[] memory signers = _makeBytesArray(4, 10); + bytes[] memory transmitters = _makeBytesArray(4, 20); + CCIPConfigTypes.OCR3Config[] memory cfgs = new CCIPConfigTypes.OCR3Config[](3); + for (uint256 i = 0; i < 3; i++) { + cfgs[i] = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Execution, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: abi.encode("exec", i) + }); + } + vm.expectRevert(); + s_ccipCC.groupByPluginType(cfgs); + } + + function test__groupByPluginType_TooManyOCR3Configs_Reverts() public { + CCIPConfigTypes.OCR3Config[] memory cfgs = new CCIPConfigTypes.OCR3Config[](5); + vm.expectRevert(CCIPConfig.TooManyOCR3Configs.selector); + s_ccipCC.groupByPluginType(cfgs); + } + + function test__validateConfigTransition_InitToRunning_WrongConfigCount_Reverts() public { + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(_makeBytes32Array(4, 0), 0, 1), + p2pIds: _makeBytes32Array(4, 0), + signers: _makeBytesArray(4, 10), + transmitters: _makeBytesArray(4, 20), + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + newConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 0, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](0); + + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.WrongConfigCount.selector, 0, 1)); + s_ccipCC.validateConfigTransition(currentConfig, newConfig); + } + + function test__validateConfigTransition_RunningToStaging_WrongConfigDigestBlueGreen_Reverts() public { + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(_makeBytes32Array(4, 0), 0, 1), + p2pIds: _makeBytes32Array(4, 0), + signers: _makeBytesArray(4, 10), + transmitters: _makeBytesArray(4, 20), + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(_makeBytes32Array(4, 0), 0, 1), + p2pIds: _makeBytes32Array(4, 0), + signers: _makeBytesArray(4, 10), + transmitters: _makeBytesArray(4, 20), + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + currentConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](2); + newConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 3, blueConfig) // wrong config digest (due to diff config count) + }); + newConfig[1] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 2, + config: greenConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 2, greenConfig) + }); + + vm.expectRevert( + abi.encodeWithSelector( + CCIPConfig.WrongConfigDigestBlueGreen.selector, + s_ccipCC.computeConfigDigest(donId, 3, blueConfig), + s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + ) + ); + s_ccipCC.validateConfigTransition(currentConfig, newConfig); + } + + function test__validateConfigTransition_RunningToStaging_WrongConfigCount_Reverts() public { + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(_makeBytes32Array(4, 0), 0, 1), + p2pIds: _makeBytes32Array(4, 0), + signers: _makeBytesArray(4, 10), + transmitters: _makeBytesArray(4, 20), + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(_makeBytes32Array(4, 0), 0, 1), + p2pIds: _makeBytes32Array(4, 0), + signers: _makeBytesArray(4, 10), + transmitters: _makeBytesArray(4, 20), + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + currentConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](2); + newConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + newConfig[1] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 3, // wrong config count + config: greenConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 3, greenConfig) + }); + + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.WrongConfigCount.selector, 3, 2)); + s_ccipCC.validateConfigTransition(currentConfig, newConfig); + } + + function test__validateConfigTransition_StagingToRunning_WrongConfigDigest_Reverts() public { + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(_makeBytes32Array(4, 0), 0, 1), + p2pIds: _makeBytes32Array(4, 0), + signers: _makeBytesArray(4, 10), + transmitters: _makeBytesArray(4, 20), + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(_makeBytes32Array(4, 0), 0, 1), + p2pIds: _makeBytes32Array(4, 0), + signers: _makeBytesArray(4, 10), + transmitters: _makeBytesArray(4, 20), + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](2); + currentConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 1, + config: blueConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 1, blueConfig) + }); + currentConfig[1] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 2, + config: greenConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 2, greenConfig) + }); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + newConfig[0] = CCIPConfigTypes.OCR3ConfigWithMeta({ + configCount: 2, + config: greenConfig, + configDigest: s_ccipCC.computeConfigDigest(donId, 3, greenConfig) // wrong config digest + }); + + vm.expectRevert( + abi.encodeWithSelector( + CCIPConfig.WrongConfigDigest.selector, + s_ccipCC.computeConfigDigest(donId, 3, greenConfig), + s_ccipCC.computeConfigDigest(donId, 2, greenConfig) + ) + ); + s_ccipCC.validateConfigTransition(currentConfig, newConfig); + } + + function test__validateConfigTransition_NonExistentConfigTransition_Reverts() public { + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](3); + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfig = new CCIPConfigTypes.OCR3ConfigWithMeta[](1); + vm.expectRevert(CCIPConfig.NonExistentConfigTransition.selector); + s_ccipCC.validateConfigTransition(currentConfig, newConfig); + } +} + +contract CCIPConfig__updatePluginConfig is CCIPConfigSetup { + // Successes. + + function test__updatePluginConfig_InitToRunning_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config[] memory configs = new CCIPConfigTypes.OCR3Config[](1); + configs[0] = blueConfig; + + s_ccipCC.updatePluginConfig(donId, Internal.OCRPluginType.Commit, configs); + + // should see the updated config in the contract state. + CCIPConfigTypes.OCR3ConfigWithMeta[] memory storedConfig = + s_ccipCC.getOCRConfig(donId, Internal.OCRPluginType.Commit); + assertEq(storedConfig.length, 1, "don config length must be 1"); + assertEq(storedConfig[0].configCount, uint64(1), "config count must be 1"); + assertEq(uint256(storedConfig[0].config.pluginType), uint256(blueConfig.pluginType), "plugin type must match"); + } + + function test__updatePluginConfig_RunningToStaging_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + // add blue config. + uint32 donId = 1; + Internal.OCRPluginType pluginType = Internal.OCRPluginType.Commit; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config[] memory startConfigs = new CCIPConfigTypes.OCR3Config[](1); + startConfigs[0] = blueConfig; + + // add blue AND green config to indicate an update. + s_ccipCC.updatePluginConfig(donId, Internal.OCRPluginType.Commit, startConfigs); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + CCIPConfigTypes.OCR3Config[] memory blueAndGreen = new CCIPConfigTypes.OCR3Config[](2); + blueAndGreen[0] = blueConfig; + blueAndGreen[1] = greenConfig; + + s_ccipCC.updatePluginConfig(donId, Internal.OCRPluginType.Commit, blueAndGreen); + + // should see the updated config in the contract state. + CCIPConfigTypes.OCR3ConfigWithMeta[] memory storedConfig = + s_ccipCC.getOCRConfig(donId, Internal.OCRPluginType.Commit); + assertEq(storedConfig.length, 2, "don config length must be 2"); + // 0 index is blue config, 1 index is green config. + assertEq(storedConfig[1].configCount, uint64(2), "config count must be 2"); + assertEq( + uint256(storedConfig[0].config.pluginType), uint256(Internal.OCRPluginType.Commit), "plugin type must match" + ); + assertEq( + uint256(storedConfig[1].config.pluginType), uint256(Internal.OCRPluginType.Commit), "plugin type must match" + ); + assertEq(storedConfig[0].config.offchainConfig, bytes("commit"), "blue offchain config must match"); + assertEq(storedConfig[1].config.offchainConfig, bytes("commit-new"), "green offchain config must match"); + } + + function test__updatePluginConfig_StagingToRunning_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + // add blue config. + uint32 donId = 1; + Internal.OCRPluginType pluginType = Internal.OCRPluginType.Commit; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config[] memory startConfigs = new CCIPConfigTypes.OCR3Config[](1); + startConfigs[0] = blueConfig; + + // add blue AND green config to indicate an update. + s_ccipCC.updatePluginConfig(donId, Internal.OCRPluginType.Commit, startConfigs); + CCIPConfigTypes.OCR3Config memory greenConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit-new") + }); + CCIPConfigTypes.OCR3Config[] memory blueAndGreen = new CCIPConfigTypes.OCR3Config[](2); + blueAndGreen[0] = blueConfig; + blueAndGreen[1] = greenConfig; + + s_ccipCC.updatePluginConfig(donId, Internal.OCRPluginType.Commit, blueAndGreen); + + // should see the updated config in the contract state. + CCIPConfigTypes.OCR3ConfigWithMeta[] memory storedConfig = + s_ccipCC.getOCRConfig(donId, Internal.OCRPluginType.Commit); + assertEq(storedConfig.length, 2, "don config length must be 2"); + // 0 index is blue config, 1 index is green config. + assertEq(storedConfig[1].configCount, uint64(2), "config count must be 2"); + assertEq( + uint256(storedConfig[0].config.pluginType), uint256(Internal.OCRPluginType.Commit), "plugin type must match" + ); + assertEq( + uint256(storedConfig[1].config.pluginType), uint256(Internal.OCRPluginType.Commit), "plugin type must match" + ); + assertEq(storedConfig[0].config.offchainConfig, bytes("commit"), "blue offchain config must match"); + assertEq(storedConfig[1].config.offchainConfig, bytes("commit-new"), "green offchain config must match"); + + // promote green to blue. + CCIPConfigTypes.OCR3Config[] memory promote = new CCIPConfigTypes.OCR3Config[](1); + promote[0] = greenConfig; + + s_ccipCC.updatePluginConfig(donId, Internal.OCRPluginType.Commit, promote); + + // should see the updated config in the contract state. + storedConfig = s_ccipCC.getOCRConfig(donId, Internal.OCRPluginType.Commit); + assertEq(storedConfig.length, 1, "don config length must be 1"); + assertEq(storedConfig[0].configCount, uint64(2), "config count must be 2"); + assertEq( + uint256(storedConfig[0].config.pluginType), uint256(Internal.OCRPluginType.Commit), "plugin type must match" + ); + assertEq(storedConfig[0].config.offchainConfig, bytes("commit-new"), "green offchain config must match"); + } + + // Reverts. + function test__updatePluginConfig_InvalidConfigLength_Reverts() public { + uint32 donId = 1; + CCIPConfigTypes.OCR3Config[] memory newConfig = new CCIPConfigTypes.OCR3Config[](3); + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.InvalidConfigLength.selector, uint256(3))); + s_ccipCC.updatePluginConfig(donId, Internal.OCRPluginType.Commit, newConfig); + } + + function test__updatePluginConfig_InvalidConfigStateTransition_Reverts() public { + uint32 donId = 1; + CCIPConfigTypes.OCR3Config[] memory newConfig = new CCIPConfigTypes.OCR3Config[](2); + // 0 -> 2 is an invalid state transition. + vm.expectRevert(abi.encodeWithSelector(CCIPConfig.InvalidConfigStateTransition.selector, 0, 2)); + s_ccipCC.updatePluginConfig(donId, Internal.OCRPluginType.Commit, newConfig); + } +} + +contract CCIPConfig_beforeCapabilityConfigSet is CCIPConfigSetup { + // Successes. + function test_beforeCapabilityConfigSet_ZeroLengthConfig_Success() public { + changePrank(CAPABILITIES_REGISTRY); + + CCIPConfigTypes.OCR3Config[] memory configs = new CCIPConfigTypes.OCR3Config[](0); + bytes memory encodedConfigs = abi.encode(configs); + s_ccipCC.beforeCapabilityConfigSet(new bytes32[](0), encodedConfigs, 1, 1); + } + + function test_beforeCapabilityConfigSet_CommitConfigOnly_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + changePrank(CAPABILITIES_REGISTRY); + + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config[] memory configs = new CCIPConfigTypes.OCR3Config[](1); + configs[0] = blueConfig; + + bytes memory encoded = abi.encode(configs); + s_ccipCC.beforeCapabilityConfigSet(new bytes32[](0), encoded, 1, donId); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory storedConfigs = + s_ccipCC.getOCRConfig(donId, Internal.OCRPluginType.Commit); + assertEq(storedConfigs.length, 1, "config length must be 1"); + assertEq(storedConfigs[0].configCount, uint64(1), "config count must be 1"); + assertEq( + uint256(storedConfigs[0].config.pluginType), uint256(Internal.OCRPluginType.Commit), "plugin type must be commit" + ); + } + + function test_beforeCapabilityConfigSet_ExecConfigOnly_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + changePrank(CAPABILITIES_REGISTRY); + + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Execution, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("exec") + }); + CCIPConfigTypes.OCR3Config[] memory configs = new CCIPConfigTypes.OCR3Config[](1); + configs[0] = blueConfig; + + bytes memory encoded = abi.encode(configs); + s_ccipCC.beforeCapabilityConfigSet(new bytes32[](0), encoded, 1, donId); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory storedConfigs = + s_ccipCC.getOCRConfig(donId, Internal.OCRPluginType.Execution); + assertEq(storedConfigs.length, 1, "config length must be 1"); + assertEq(storedConfigs[0].configCount, uint64(1), "config count must be 1"); + assertEq( + uint256(storedConfigs[0].config.pluginType), + uint256(Internal.OCRPluginType.Execution), + "plugin type must be execution" + ); + } + + function test_beforeCapabilityConfigSet_CommitAndExecConfig_Success() public { + (bytes32[] memory p2pIds, bytes[] memory signers, bytes[] memory transmitters) = _addChainConfig(4); + changePrank(CAPABILITIES_REGISTRY); + + uint32 donId = 1; + CCIPConfigTypes.OCR3Config memory blueCommitConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Commit, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("commit") + }); + CCIPConfigTypes.OCR3Config memory blueExecConfig = CCIPConfigTypes.OCR3Config({ + pluginType: Internal.OCRPluginType.Execution, + offrampAddress: abi.encodePacked(keccak256(abi.encode("offramp"))), + chainSelector: 1, + bootstrapP2PIds: _subset(p2pIds, 0, 1), + p2pIds: p2pIds, + signers: signers, + transmitters: transmitters, + F: 1, + offchainConfigVersion: 30, + offchainConfig: bytes("exec") + }); + CCIPConfigTypes.OCR3Config[] memory configs = new CCIPConfigTypes.OCR3Config[](2); + configs[0] = blueExecConfig; + configs[1] = blueCommitConfig; + + bytes memory encoded = abi.encode(configs); + s_ccipCC.beforeCapabilityConfigSet(new bytes32[](0), encoded, 1, donId); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory storedExecConfigs = + s_ccipCC.getOCRConfig(donId, Internal.OCRPluginType.Execution); + assertEq(storedExecConfigs.length, 1, "config length must be 1"); + assertEq(storedExecConfigs[0].configCount, uint64(1), "config count must be 1"); + assertEq( + uint256(storedExecConfigs[0].config.pluginType), + uint256(Internal.OCRPluginType.Execution), + "plugin type must be execution" + ); + + CCIPConfigTypes.OCR3ConfigWithMeta[] memory storedCommitConfigs = + s_ccipCC.getOCRConfig(donId, Internal.OCRPluginType.Commit); + assertEq(storedCommitConfigs.length, 1, "config length must be 1"); + assertEq(storedCommitConfigs[0].configCount, uint64(1), "config count must be 1"); + assertEq( + uint256(storedCommitConfigs[0].config.pluginType), + uint256(Internal.OCRPluginType.Commit), + "plugin type must be commit" + ); + } + + // Reverts. + + function test_beforeCapabilityConfigSet_OnlyCapabilitiesRegistryCanCall_Reverts() public { + bytes32[] memory nodes = new bytes32[](0); + bytes memory config = bytes(""); + uint64 configCount = 1; + uint32 donId = 1; + vm.expectRevert(CCIPConfig.OnlyCapabilitiesRegistryCanCall.selector); + s_ccipCC.beforeCapabilityConfigSet(nodes, config, configCount, donId); + } +} diff --git a/contracts/src/v0.8/ccip/test/commitStore/CommitStore.t.sol b/contracts/src/v0.8/ccip/test/commitStore/CommitStore.t.sol new file mode 100644 index 00000000000..7598f9ccb69 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/commitStore/CommitStore.t.sol @@ -0,0 +1,618 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPriceRegistry} from "../../interfaces/IPriceRegistry.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; + +import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol"; +import {CommitStore} from "../../CommitStore.sol"; +import {PriceRegistry} from "../../PriceRegistry.sol"; +import {RMN} from "../../RMN.sol"; +import {MerkleMultiProof} from "../../libraries/MerkleMultiProof.sol"; +import {OCR2Abstract} from "../../ocr/OCR2Abstract.sol"; +import {CommitStoreHelper} from "../helpers/CommitStoreHelper.sol"; +import {OCR2BaseSetup} from "../ocr/OCR2Base.t.sol"; +import {PriceRegistrySetup} from "../priceRegistry/PriceRegistry.t.sol"; + +contract CommitStoreSetup is PriceRegistrySetup, OCR2BaseSetup { + CommitStoreHelper internal s_commitStore; + + function setUp() public virtual override(PriceRegistrySetup, OCR2BaseSetup) { + PriceRegistrySetup.setUp(); + OCR2BaseSetup.setUp(); + + s_commitStore = new CommitStoreHelper( + CommitStore.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + onRamp: ON_RAMP_ADDRESS, + rmnProxy: address(s_mockRMN) + }) + ); + CommitStore.DynamicConfig memory dynamicConfig = + CommitStore.DynamicConfig({priceRegistry: address(s_priceRegistry)}); + s_commitStore.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + + address[] memory priceUpdaters = new address[](1); + priceUpdaters[0] = address(s_commitStore); + s_priceRegistry.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: priceUpdaters, removedCallers: new address[](0)}) + ); + } +} + +contract CommitStoreRealRMNSetup is PriceRegistrySetup, OCR2BaseSetup { + CommitStoreHelper internal s_commitStore; + + RMN internal s_rmn; + + address internal constant BLESS_VOTE_ADDR = address(8888); + + function setUp() public virtual override(PriceRegistrySetup, OCR2BaseSetup) { + PriceRegistrySetup.setUp(); + OCR2BaseSetup.setUp(); + + RMN.Voter[] memory voters = new RMN.Voter[](1); + voters[0] = + RMN.Voter({blessVoteAddr: BLESS_VOTE_ADDR, curseVoteAddr: address(9999), blessWeight: 1, curseWeight: 1}); + // Overwrite base mock rmn with real. + s_rmn = new RMN(RMN.Config({voters: voters, blessWeightThreshold: 1, curseWeightThreshold: 1})); + s_commitStore = new CommitStoreHelper( + CommitStore.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + onRamp: ON_RAMP_ADDRESS, + rmnProxy: address(s_rmn) + }) + ); + CommitStore.DynamicConfig memory dynamicConfig = + CommitStore.DynamicConfig({priceRegistry: address(s_priceRegistry)}); + s_commitStore.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + } +} + +contract CommitStore_constructor is PriceRegistrySetup, OCR2BaseSetup { + function setUp() public virtual override(PriceRegistrySetup, OCR2BaseSetup) { + PriceRegistrySetup.setUp(); + OCR2BaseSetup.setUp(); + } + + function test_Constructor_Success() public { + CommitStore.StaticConfig memory staticConfig = CommitStore.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + onRamp: 0x2C44CDDdB6a900Fa2B585dd299E03D12Fa4293Bc, + rmnProxy: address(s_mockRMN) + }); + CommitStore.DynamicConfig memory dynamicConfig = + CommitStore.DynamicConfig({priceRegistry: address(s_priceRegistry)}); + + vm.expectEmit(); + emit CommitStore.ConfigSet(staticConfig, dynamicConfig); + + CommitStore commitStore = new CommitStore(staticConfig); + commitStore.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + + CommitStore.StaticConfig memory gotStaticConfig = commitStore.getStaticConfig(); + + assertEq(staticConfig.chainSelector, gotStaticConfig.chainSelector); + assertEq(staticConfig.sourceChainSelector, gotStaticConfig.sourceChainSelector); + assertEq(staticConfig.onRamp, gotStaticConfig.onRamp); + assertEq(staticConfig.rmnProxy, gotStaticConfig.rmnProxy); + + CommitStore.DynamicConfig memory gotDynamicConfig = commitStore.getDynamicConfig(); + + assertEq(dynamicConfig.priceRegistry, gotDynamicConfig.priceRegistry); + + // CommitStore initial values + assertEq(0, commitStore.getLatestPriceEpochAndRound()); + assertEq(1, commitStore.getExpectedNextSequenceNumber()); + assertEq(commitStore.typeAndVersion(), "CommitStore 1.5.0-dev"); + assertEq(OWNER, commitStore.owner()); + assertTrue(commitStore.isUnpausedAndNotCursed()); + } +} + +contract CommitStore_setMinSeqNr is CommitStoreSetup { + function test_Fuzz_SetMinSeqNr_Success(uint64 minSeqNr) public { + vm.expectEmit(); + emit CommitStore.SequenceNumberSet(s_commitStore.getExpectedNextSequenceNumber(), minSeqNr); + + s_commitStore.setMinSeqNr(minSeqNr); + + assertEq(s_commitStore.getExpectedNextSequenceNumber(), minSeqNr); + } + + // Reverts + function test_OnlyOwner_Revert() public { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + s_commitStore.setMinSeqNr(6723); + } +} + +contract CommitStore_setDynamicConfig is CommitStoreSetup { + function test_Fuzz_SetDynamicConfig_Success(address priceRegistry) public { + vm.assume(priceRegistry != address(0)); + CommitStore.StaticConfig memory staticConfig = s_commitStore.getStaticConfig(); + CommitStore.DynamicConfig memory dynamicConfig = CommitStore.DynamicConfig({priceRegistry: priceRegistry}); + bytes memory onchainConfig = abi.encode(dynamicConfig); + + vm.expectEmit(); + emit CommitStore.ConfigSet(staticConfig, dynamicConfig); + + uint32 configCount = 1; + + vm.expectEmit(); + emit OCR2Abstract.ConfigSet( + uint32(block.number), + getBasicConfigDigest(address(s_commitStore), s_f, configCount, onchainConfig), + configCount + 1, + s_valid_signers, + s_valid_transmitters, + s_f, + onchainConfig, + s_offchainConfigVersion, + abi.encode("") + ); + + s_commitStore.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, onchainConfig, s_offchainConfigVersion, abi.encode("") + ); + + CommitStore.DynamicConfig memory gotDynamicConfig = s_commitStore.getDynamicConfig(); + assertEq(gotDynamicConfig.priceRegistry, dynamicConfig.priceRegistry); + } + + function test_PriceEpochCleared_Success() public { + // Set latest price epoch and round to non-zero. + uint40 latestEpochAndRound = 1782155; + s_commitStore.setLatestPriceEpochAndRound(latestEpochAndRound); + assertEq(latestEpochAndRound, s_commitStore.getLatestPriceEpochAndRound()); + + CommitStore.DynamicConfig memory dynamicConfig = CommitStore.DynamicConfig({priceRegistry: address(1)}); + // New config should clear it. + s_commitStore.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + // Assert cleared. + assertEq(0, s_commitStore.getLatestPriceEpochAndRound()); + } + + // Reverts + function test_OnlyOwner_Revert() public { + CommitStore.DynamicConfig memory dynamicConfig = CommitStore.DynamicConfig({priceRegistry: address(23784264)}); + + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + s_commitStore.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + } + + function test_InvalidCommitStoreConfig_Revert() public { + CommitStore.DynamicConfig memory dynamicConfig = CommitStore.DynamicConfig({priceRegistry: address(0)}); + + vm.expectRevert(CommitStore.InvalidCommitStoreConfig.selector); + s_commitStore.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + } +} + +contract CommitStore_resetUnblessedRoots is CommitStoreRealRMNSetup { + function test_ResetUnblessedRoots_Success() public { + bytes32[] memory rootsToReset = new bytes32[](3); + rootsToReset[0] = "1"; + rootsToReset[1] = "2"; + rootsToReset[2] = "3"; + + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(1, 2), + merkleRoot: rootsToReset[0] + }); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + + report = CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(3, 4), + merkleRoot: rootsToReset[1] + }); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + + report = CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(5, 5), + merkleRoot: rootsToReset[2] + }); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + + IRMN.TaggedRoot[] memory blessedTaggedRoots = new IRMN.TaggedRoot[](1); + blessedTaggedRoots[0] = IRMN.TaggedRoot({commitStore: address(s_commitStore), root: rootsToReset[1]}); + + vm.startPrank(BLESS_VOTE_ADDR); + s_rmn.voteToBless(blessedTaggedRoots); + + vm.expectEmit(false, false, false, true); + emit CommitStore.RootRemoved(rootsToReset[0]); + + vm.expectEmit(false, false, false, true); + emit CommitStore.RootRemoved(rootsToReset[2]); + + vm.startPrank(OWNER); + s_commitStore.resetUnblessedRoots(rootsToReset); + + assertEq(0, s_commitStore.getMerkleRoot(rootsToReset[0])); + assertEq(BLOCK_TIME, s_commitStore.getMerkleRoot(rootsToReset[1])); + assertEq(0, s_commitStore.getMerkleRoot(rootsToReset[2])); + } + + // Reverts + + function test_OnlyOwner_Revert() public { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + bytes32[] memory rootToReset; + s_commitStore.resetUnblessedRoots(rootToReset); + } +} + +contract CommitStore_report is CommitStoreSetup { + function test_ReportOnlyRootSuccess_gas() public { + vm.pauseGasMetering(); + uint64 max1 = 931; + bytes32 root = "Only a single root"; + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(1, max1), + merkleRoot: root + }); + + vm.expectEmit(); + emit CommitStore.ReportAccepted(report); + + bytes memory encodedReport = abi.encode(report); + + vm.resumeGasMetering(); + s_commitStore.report(encodedReport, ++s_latestEpochAndRound); + vm.pauseGasMetering(); + + assertEq(max1 + 1, s_commitStore.getExpectedNextSequenceNumber()); + assertEq(block.timestamp, s_commitStore.getMerkleRoot(root)); + vm.resumeGasMetering(); + } + + function test_ReportAndPriceUpdate_Success() public { + uint64 max1 = 12; + + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + interval: CommitStore.Interval(1, max1), + merkleRoot: "test #2" + }); + + vm.expectEmit(); + emit CommitStore.ReportAccepted(report); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + + assertEq(max1 + 1, s_commitStore.getExpectedNextSequenceNumber()); + assertEq(s_latestEpochAndRound, s_commitStore.getLatestPriceEpochAndRound()); + } + + function test_StaleReportWithRoot_Success() public { + uint64 maxSeq = 12; + uint224 tokenStartPrice = + IPriceRegistry(s_commitStore.getDynamicConfig().priceRegistry).getTokenPrice(s_sourceFeeToken).value; + + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + interval: CommitStore.Interval(1, maxSeq), + merkleRoot: "stale report 1" + }); + + vm.expectEmit(); + emit CommitStore.ReportAccepted(report); + + s_commitStore.report(abi.encode(report), s_latestEpochAndRound); + assertEq(maxSeq + 1, s_commitStore.getExpectedNextSequenceNumber()); + assertEq(s_latestEpochAndRound, s_commitStore.getLatestPriceEpochAndRound()); + + report = CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(maxSeq + 1, maxSeq * 2), + merkleRoot: "stale report 2" + }); + + vm.expectEmit(); + emit CommitStore.ReportAccepted(report); + + s_commitStore.report(abi.encode(report), s_latestEpochAndRound); + assertEq(maxSeq * 2 + 1, s_commitStore.getExpectedNextSequenceNumber()); + assertEq(s_latestEpochAndRound, s_commitStore.getLatestPriceEpochAndRound()); + assertEq( + tokenStartPrice, + IPriceRegistry(s_commitStore.getDynamicConfig().priceRegistry).getTokenPrice(s_sourceFeeToken).value + ); + } + + function test_OnlyTokenPriceUpdates_Success() public { + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + interval: CommitStore.Interval(0, 0), + merkleRoot: "" + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, 4e18, block.timestamp); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + assertEq(s_latestEpochAndRound, s_commitStore.getLatestPriceEpochAndRound()); + } + + function test_OnlyGasPriceUpdates_Success() public { + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + interval: CommitStore.Interval(0, 0), + merkleRoot: "" + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, 4e18, block.timestamp); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + assertEq(s_latestEpochAndRound, s_commitStore.getLatestPriceEpochAndRound()); + } + + function test_ValidPriceUpdateThenStaleReportWithRoot_Success() public { + uint64 maxSeq = 12; + uint224 tokenPrice1 = 4e18; + uint224 tokenPrice2 = 5e18; + + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, tokenPrice1), + interval: CommitStore.Interval(0, 0), + merkleRoot: "" + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, tokenPrice1, block.timestamp); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + assertEq(s_latestEpochAndRound, s_commitStore.getLatestPriceEpochAndRound()); + + report = CommitStore.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, tokenPrice2), + interval: CommitStore.Interval(1, maxSeq), + merkleRoot: "stale report" + }); + + vm.expectEmit(); + emit CommitStore.ReportAccepted(report); + + s_commitStore.report(abi.encode(report), s_latestEpochAndRound); + + assertEq(maxSeq + 1, s_commitStore.getExpectedNextSequenceNumber()); + assertEq( + tokenPrice1, IPriceRegistry(s_commitStore.getDynamicConfig().priceRegistry).getTokenPrice(s_sourceFeeToken).value + ); + assertEq(s_latestEpochAndRound, s_commitStore.getLatestPriceEpochAndRound()); + } + + // Reverts + + function test_Paused_Revert() public { + s_commitStore.pause(); + bytes memory report; + vm.expectRevert(CommitStore.PausedError.selector); + s_commitStore.report(report, ++s_latestEpochAndRound); + } + + function test_Unhealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + vm.expectRevert(CommitStore.CursedByRMN.selector); + bytes memory report; + s_commitStore.report(report, ++s_latestEpochAndRound); + } + + function test_InvalidRootRevert() public { + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(1, 4), + merkleRoot: bytes32(0) + }); + + vm.expectRevert(CommitStore.InvalidRoot.selector); + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + } + + function test_InvalidInterval_Revert() public { + CommitStore.Interval memory interval = CommitStore.Interval(2, 2); + CommitStore.CommitReport memory report = + CommitStore.CommitReport({priceUpdates: getEmptyPriceUpdates(), interval: interval, merkleRoot: bytes32(0)}); + + vm.expectRevert(abi.encodeWithSelector(CommitStore.InvalidInterval.selector, interval)); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + } + + function test_InvalidIntervalMinLargerThanMax_Revert() public { + CommitStore.Interval memory interval = CommitStore.Interval(1, 0); + CommitStore.CommitReport memory report = + CommitStore.CommitReport({priceUpdates: getEmptyPriceUpdates(), interval: interval, merkleRoot: bytes32(0)}); + + vm.expectRevert(abi.encodeWithSelector(CommitStore.InvalidInterval.selector, interval)); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + } + + function test_ZeroEpochAndRound_Revert() public { + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + interval: CommitStore.Interval(0, 0), + merkleRoot: bytes32(0) + }); + + vm.expectRevert(CommitStore.StaleReport.selector); + + s_commitStore.report(abi.encode(report), 0); + } + + function test_OnlyPriceUpdateStaleReport_Revert() public { + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + interval: CommitStore.Interval(0, 0), + merkleRoot: bytes32(0) + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, 4e18, block.timestamp); + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + + vm.expectRevert(CommitStore.StaleReport.selector); + s_commitStore.report(abi.encode(report), s_latestEpochAndRound); + } + + function test_RootAlreadyCommitted_Revert() public { + CommitStore.CommitReport memory report = CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(1, 2), + merkleRoot: "Only a single root" + }); + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + + report = CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(3, 3), + merkleRoot: "Only a single root" + }); + + vm.expectRevert(CommitStore.RootAlreadyCommitted.selector); + + s_commitStore.report(abi.encode(report), ++s_latestEpochAndRound); + } +} + +contract CommitStore_verify is CommitStoreRealRMNSetup { + function test_NotBlessed_Success() public { + bytes32[] memory leaves = new bytes32[](1); + leaves[0] = "root"; + s_commitStore.report( + abi.encode( + CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(1, 2), + merkleRoot: leaves[0] + }) + ), + ++s_latestEpochAndRound + ); + bytes32[] memory proofs = new bytes32[](0); + // We have not blessed this root, should return 0. + uint256 timestamp = s_commitStore.verify(leaves, proofs, 0); + assertEq(uint256(0), timestamp); + } + + function test_Blessed_Success() public { + bytes32[] memory leaves = new bytes32[](1); + leaves[0] = "root"; + s_commitStore.report( + abi.encode( + CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(1, 2), + merkleRoot: leaves[0] + }) + ), + ++s_latestEpochAndRound + ); + // Bless that root. + IRMN.TaggedRoot[] memory taggedRoots = new IRMN.TaggedRoot[](1); + taggedRoots[0] = IRMN.TaggedRoot({commitStore: address(s_commitStore), root: leaves[0]}); + vm.startPrank(BLESS_VOTE_ADDR); + s_rmn.voteToBless(taggedRoots); + bytes32[] memory proofs = new bytes32[](0); + uint256 timestamp = s_commitStore.verify(leaves, proofs, 0); + assertEq(BLOCK_TIME, timestamp); + } + + // Reverts + + function test_Paused_Revert() public { + s_commitStore.pause(); + + bytes32[] memory hashedLeaves = new bytes32[](0); + bytes32[] memory proofs = new bytes32[](0); + uint256 proofFlagBits = 0; + + vm.expectRevert(CommitStore.PausedError.selector); + s_commitStore.verify(hashedLeaves, proofs, proofFlagBits); + } + + function test_TooManyLeaves_Revert() public { + bytes32[] memory leaves = new bytes32[](258); + bytes32[] memory proofs = new bytes32[](0); + + vm.expectRevert(MerkleMultiProof.InvalidProof.selector); + + s_commitStore.verify(leaves, proofs, 0); + } +} + +contract CommitStore_isUnpausedAndRMNHealthy is CommitStoreSetup { + function test_RMN_Success() public { + // Test pausing + assertFalse(s_commitStore.paused()); + assertTrue(s_commitStore.isUnpausedAndNotCursed()); + s_commitStore.pause(); + assertTrue(s_commitStore.paused()); + assertFalse(s_commitStore.isUnpausedAndNotCursed()); + s_commitStore.unpause(); + assertFalse(s_commitStore.paused()); + assertTrue(s_commitStore.isUnpausedAndNotCursed()); + + // Test rmn + s_mockRMN.setGlobalCursed(true); + assertFalse(s_commitStore.isUnpausedAndNotCursed()); + s_mockRMN.setGlobalCursed(false); + // TODO: also test with s_mockRMN.setChainCursed(sourceChainSelector), + // also for other similar tests (e.g., OffRamp, OnRamp) + assertTrue(s_commitStore.isUnpausedAndNotCursed()); + + s_mockRMN.setGlobalCursed(true); + s_commitStore.pause(); + assertFalse(s_commitStore.isUnpausedAndNotCursed()); + } +} + +contract CommitStore_setLatestPriceEpochAndRound is CommitStoreSetup { + function test_SetLatestPriceEpochAndRound_Success() public { + uint40 latestRoundAndEpoch = 1782155; + + vm.expectEmit(); + emit CommitStore.LatestPriceEpochAndRoundSet( + uint40(s_commitStore.getLatestPriceEpochAndRound()), latestRoundAndEpoch + ); + + s_commitStore.setLatestPriceEpochAndRound(latestRoundAndEpoch); + + assertEq(uint40(s_commitStore.getLatestPriceEpochAndRound()), latestRoundAndEpoch); + } + + // Reverts + function test_OnlyOwner_Revert() public { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + s_commitStore.setLatestPriceEpochAndRound(6723); + } +} diff --git a/contracts/src/v0.8/ccip/test/e2e/End2End.t.sol b/contracts/src/v0.8/ccip/test/e2e/End2End.t.sol new file mode 100644 index 00000000000..816862cbdfc --- /dev/null +++ b/contracts/src/v0.8/ccip/test/e2e/End2End.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import "../commitStore/CommitStore.t.sol"; +import "../helpers/MerkleHelper.sol"; +import "../offRamp/EVM2EVMOffRampSetup.t.sol"; +import "../onRamp/EVM2EVMOnRampSetup.t.sol"; + +contract E2E is EVM2EVMOnRampSetup, CommitStoreSetup, EVM2EVMOffRampSetup { + using Internal for Internal.EVM2EVMMessage; + + function setUp() public virtual override(EVM2EVMOnRampSetup, CommitStoreSetup, EVM2EVMOffRampSetup) { + EVM2EVMOnRampSetup.setUp(); + CommitStoreSetup.setUp(); + EVM2EVMOffRampSetup.setUp(); + + deployOffRamp(s_commitStore, s_destRouter, address(0)); + } + + function test_E2E_3MessagesSuccess_gas() public { + vm.pauseGasMetering(); + IERC20 token0 = IERC20(s_sourceTokens[0]); + IERC20 token1 = IERC20(s_sourceTokens[1]); + uint256 balance0Pre = token0.balanceOf(OWNER); + uint256 balance1Pre = token1.balanceOf(OWNER); + + Internal.EVM2EVMMessage[] memory messages = new Internal.EVM2EVMMessage[](3); + messages[0] = sendRequest(1); + messages[1] = sendRequest(2); + messages[2] = sendRequest(3); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, _generateTokenMessage()); + // Asserts that the tokens have been sent and the fee has been paid. + assertEq(balance0Pre - messages.length * (i_tokenAmount0 + expectedFee), token0.balanceOf(OWNER)); + assertEq(balance1Pre - messages.length * i_tokenAmount1, token1.balanceOf(OWNER)); + + bytes32 metaDataHash = s_offRamp.metadataHash(); + + bytes32[] memory hashedMessages = new bytes32[](3); + hashedMessages[0] = messages[0]._hash(metaDataHash); + messages[0].messageId = hashedMessages[0]; + hashedMessages[1] = messages[1]._hash(metaDataHash); + messages[1].messageId = hashedMessages[1]; + hashedMessages[2] = messages[2]._hash(metaDataHash); + messages[2].messageId = hashedMessages[2]; + + bytes32[] memory merkleRoots = new bytes32[](1); + merkleRoots[0] = MerkleHelper.getMerkleRoot(hashedMessages); + + address[] memory onRamps = new address[](1); + onRamps[0] = ON_RAMP_ADDRESS; + + bytes memory commitReport = abi.encode( + CommitStore.CommitReport({ + priceUpdates: getEmptyPriceUpdates(), + interval: CommitStore.Interval(messages[0].sequenceNumber, messages[2].sequenceNumber), + merkleRoot: merkleRoots[0] + }) + ); + + vm.resumeGasMetering(); + s_commitStore.report(commitReport, ++s_latestEpochAndRound); + vm.pauseGasMetering(); + + s_mockRMN.setTaggedRootBlessed(IRMN.TaggedRoot({commitStore: address(s_commitStore), root: merkleRoots[0]}), true); + + bytes32[] memory proofs = new bytes32[](0); + uint256 timestamp = s_commitStore.verify(merkleRoots, proofs, 2 ** 2 - 1); + assertEq(BLOCK_TIME, timestamp); + + // We change the block time so when execute would e.g. use the current + // block time instead of the committed block time the value would be + // incorrect in the checks below. + vm.warp(BLOCK_TIME + 2000); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[1].sequenceNumber, messages[1].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[2].sequenceNumber, messages[2].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + Internal.ExecutionReport memory execReport = _generateReportFromMessages(messages); + vm.resumeGasMetering(); + s_offRamp.execute(execReport, new uint256[](0)); + } + + function sendRequest(uint64 expectedSeqNum) public returns (Internal.EVM2EVMMessage memory) { + Client.EVM2AnyMessage memory message = _generateTokenMessage(); + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + + IERC20(s_sourceTokens[0]).approve(address(s_sourceRouter), i_tokenAmount0 + expectedFee); + IERC20(s_sourceTokens[1]).approve(address(s_sourceRouter), i_tokenAmount1); + + message.receiver = abi.encode(address(s_receiver)); + Internal.EVM2EVMMessage memory msgEvent = + _messageToEvent(message, expectedSeqNum, expectedSeqNum, expectedFee, OWNER); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent); + + vm.resumeGasMetering(); + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + vm.pauseGasMetering(); + + return msgEvent; + } +} diff --git a/contracts/src/v0.8/ccip/test/e2e/MultiRampsEnd2End.sol b/contracts/src/v0.8/ccip/test/e2e/MultiRampsEnd2End.sol new file mode 100644 index 00000000000..cbe8a35dce5 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/e2e/MultiRampsEnd2End.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol"; +import {NonceManager} from "../../NonceManager.sol"; +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import "../helpers/MerkleHelper.sol"; +import "../offRamp/EVM2EVMMultiOffRampSetup.t.sol"; +import "../onRamp/EVM2EVMMultiOnRampSetup.t.sol"; + +/// @notice This E2E test implements the following scenario: +/// 1. Send multiple messages from multiple source chains to a single destination chain (2 messages from source chain 1 and 1 from +/// source chain 2). +/// 2. Commit multiple merkle roots (1 for each source chain). +/// 3. Batch execute all the committed messages. +contract MultiRampsE2E is EVM2EVMMultiOnRampSetup, EVM2EVMMultiOffRampSetup { + using Internal for Internal.Any2EVMRampMessage; + + Router internal s_sourceRouter2; + EVM2EVMMultiOnRampHelper internal s_onRamp2; + TokenAdminRegistry internal s_tokenAdminRegistry2; + NonceManager internal s_nonceManager2; + + bytes32 internal s_metadataHash2; + + mapping(address destPool => address sourcePool) internal s_sourcePoolByDestPool; + + function setUp() public virtual override(EVM2EVMMultiOnRampSetup, EVM2EVMMultiOffRampSetup) { + EVM2EVMMultiOnRampSetup.setUp(); + EVM2EVMMultiOffRampSetup.setUp(); + + // Deploy new source router for the new source chain + s_sourceRouter2 = new Router(s_sourceRouter.getWrappedNative(), address(s_mockRMN)); + + // Deploy new TokenAdminRegistry for the new source chain + s_tokenAdminRegistry2 = new TokenAdminRegistry(); + + // Deploy new token pools and set them on the new TokenAdminRegistry + for (uint256 i = 0; i < s_sourceTokens.length; ++i) { + address token = s_sourceTokens[i]; + address pool = address( + new LockReleaseTokenPool(IERC20(token), new address[](0), address(s_mockRMN), true, address(s_sourceRouter2)) + ); + + s_sourcePoolByDestPool[s_destPoolBySourceToken[token]] = pool; + + _setPool( + s_tokenAdminRegistry2, token, pool, DEST_CHAIN_SELECTOR, s_destPoolByToken[s_destTokens[i]], s_destTokens[i] + ); + } + + for (uint256 i = 0; i < s_destTokens.length; ++i) { + address token = s_destTokens[i]; + address pool = s_destPoolByToken[token]; + + _setPool( + s_tokenAdminRegistry2, token, pool, SOURCE_CHAIN_SELECTOR + 1, s_sourcePoolByDestPool[pool], s_sourceTokens[i] + ); + } + + s_nonceManager2 = new NonceManager(new address[](0)); + + ( + // Deploy the new source chain onramp + // Outsource to shared helper function with EVM2EVMMultiOnRampSetup + s_onRamp2, + s_metadataHash2 + ) = _deployOnRamp( + SOURCE_CHAIN_SELECTOR + 1, address(s_sourceRouter2), address(s_nonceManager2), address(s_tokenAdminRegistry2) + ); + + address[] memory authorizedCallers = new address[](1); + authorizedCallers[0] = address(s_onRamp2); + s_nonceManager2.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: authorizedCallers, removedCallers: new address[](0)}) + ); + + // Enable destination chain on new source chain router + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: address(s_onRamp2)}); + s_sourceRouter2.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), new Router.OffRamp[](0)); + + // Deploy offramp + _deployOffRamp(s_destRouter, s_mockRMN, s_inboundNonceManager); + + // Enable source chains on offramp + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](2); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + isEnabled: true, + // Must match OnRamp address + onRamp: abi.encode(address(s_onRamp)) + }); + sourceChainConfigs[1] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR + 1, + isEnabled: true, + onRamp: abi.encode(address(s_onRamp2)) + }); + + _setupMultipleOffRampsFromConfigs(sourceChainConfigs); + } + + function test_E2E_3MessagesSuccess_gas() public { + vm.pauseGasMetering(); + IERC20 token0 = IERC20(s_sourceTokens[0]); + IERC20 token1 = IERC20(s_sourceTokens[1]); + uint256 balance0Pre = token0.balanceOf(OWNER); + uint256 balance1Pre = token1.balanceOf(OWNER); + + // Send messages + Internal.Any2EVMRampMessage[] memory messages1 = new Internal.Any2EVMRampMessage[](2); + messages1[0] = _sendRequest(1, SOURCE_CHAIN_SELECTOR, 1, s_metadataHash, s_sourceRouter, s_tokenAdminRegistry); + messages1[1] = _sendRequest(2, SOURCE_CHAIN_SELECTOR, 2, s_metadataHash, s_sourceRouter, s_tokenAdminRegistry); + Internal.Any2EVMRampMessage[] memory messages2 = new Internal.Any2EVMRampMessage[](1); + messages2[0] = + _sendRequest(1, SOURCE_CHAIN_SELECTOR + 1, 1, s_metadataHash2, s_sourceRouter2, s_tokenAdminRegistry2); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, _generateTokenMessage()); + // Asserts that the tokens have been sent and the fee has been paid. + assertEq( + balance0Pre - (messages1.length + messages2.length) * (i_tokenAmount0 + expectedFee), token0.balanceOf(OWNER) + ); + assertEq(balance1Pre - (messages1.length + messages2.length) * i_tokenAmount1, token1.balanceOf(OWNER)); + + // Commit + bytes32[] memory hashedMessages1 = new bytes32[](2); + hashedMessages1[0] = messages1[0]._hash(abi.encode(address(s_onRamp))); + hashedMessages1[1] = messages1[1]._hash(abi.encode(address(s_onRamp))); + bytes32[] memory hashedMessages2 = new bytes32[](1); + hashedMessages2[0] = messages2[0]._hash(abi.encode(address(s_onRamp2))); + + bytes32[] memory merkleRoots = new bytes32[](2); + merkleRoots[0] = MerkleHelper.getMerkleRoot(hashedMessages1); + merkleRoots[1] = MerkleHelper.getMerkleRoot(hashedMessages2); + + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](2); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + interval: EVM2EVMMultiOffRamp.Interval(messages1[0].header.sequenceNumber, messages1[1].header.sequenceNumber), + merkleRoot: merkleRoots[0] + }); + roots[1] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR + 1, + interval: EVM2EVMMultiOffRamp.Interval(messages2[0].header.sequenceNumber, messages2[0].header.sequenceNumber), + merkleRoot: merkleRoots[1] + }); + + EVM2EVMMultiOffRamp.CommitReport memory report = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + vm.resumeGasMetering(); + _commit(report, ++s_latestSequenceNumber); + vm.pauseGasMetering(); + + s_mockRMN.setTaggedRootBlessed(IRMN.TaggedRoot({commitStore: address(s_offRamp), root: merkleRoots[0]}), true); + s_mockRMN.setTaggedRootBlessed(IRMN.TaggedRoot({commitStore: address(s_offRamp), root: merkleRoots[1]}), true); + + bytes32[] memory proofs = new bytes32[](0); + bytes32[] memory hashedLeaves = new bytes32[](1); + hashedLeaves[0] = merkleRoots[0]; + uint256 timestamp = s_offRamp.verify(SOURCE_CHAIN_SELECTOR, hashedLeaves, proofs, 2 ** 2 - 1); + assertEq(BLOCK_TIME, timestamp); + hashedLeaves[0] = merkleRoots[1]; + timestamp = s_offRamp.verify(SOURCE_CHAIN_SELECTOR + 1, hashedLeaves, proofs, 2 ** 2 - 1); + assertEq(BLOCK_TIME, timestamp); + + // We change the block time so when execute would e.g. use the current + // block time instead of the committed block time the value would be + // incorrect in the checks below. + vm.warp(BLOCK_TIME + 2000); + + // Execute + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR, + messages1[0].header.sequenceNumber, + messages1[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR, + messages1[1].header.sequenceNumber, + messages1[1].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR + 1, + messages2[0].header.sequenceNumber, + messages2[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR, messages1); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR + 1, messages2); + + vm.resumeGasMetering(); + _execute(reports); + } + + function _sendRequest( + uint64 expectedSeqNum, + uint64 sourceChainSelector, + uint64 nonce, + bytes32 metadataHash, + Router router, + TokenAdminRegistry tokenAdminRegistry + ) public returns (Internal.Any2EVMRampMessage memory) { + Client.EVM2AnyMessage memory message = _generateTokenMessage(); + uint256 expectedFee = router.getFee(DEST_CHAIN_SELECTOR, message); + + IERC20(s_sourceTokens[0]).approve(address(router), i_tokenAmount0 + expectedFee); + IERC20(s_sourceTokens[1]).approve(address(router), i_tokenAmount1); + + message.receiver = abi.encode(address(s_receiver)); + Internal.EVM2AnyRampMessage memory msgEvent = _messageToEvent( + message, + sourceChainSelector, + DEST_CHAIN_SELECTOR, + expectedSeqNum, + nonce, + expectedFee, + OWNER, + metadataHash, + tokenAdminRegistry + ); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, msgEvent); + + vm.resumeGasMetering(); + router.ccipSend(DEST_CHAIN_SELECTOR, message); + vm.pauseGasMetering(); + + uint256 gasLimit = s_priceRegistry.parseEVMExtraArgsFromBytes(msgEvent.extraArgs, DEST_CHAIN_SELECTOR).gasLimit; + + return Internal.Any2EVMRampMessage({ + header: Internal.RampMessageHeader({ + messageId: msgEvent.header.messageId, + sourceChainSelector: sourceChainSelector, + destChainSelector: DEST_CHAIN_SELECTOR, + sequenceNumber: msgEvent.header.sequenceNumber, + nonce: msgEvent.header.nonce + }), + sender: abi.encode(msgEvent.sender), + data: msgEvent.data, + receiver: abi.decode(msgEvent.receiver, (address)), + gasLimit: gasLimit, + tokenAmounts: msgEvent.tokenAmounts + }); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/AggregateRateLimiterHelper.sol b/contracts/src/v0.8/ccip/test/helpers/AggregateRateLimiterHelper.sol new file mode 100644 index 00000000000..ced605a7524 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/AggregateRateLimiterHelper.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import "../../AggregateRateLimiter.sol"; + +contract AggregateRateLimiterHelper is AggregateRateLimiter { + constructor(RateLimiter.Config memory config) AggregateRateLimiter(config) {} + + function rateLimitValue(uint256 value) public { + _rateLimitValue(value); + } + + function getTokenValue( + Client.EVMTokenAmount memory tokenAmount, + IPriceRegistry priceRegistry + ) public view returns (uint256) { + return _getTokenValue(tokenAmount, priceRegistry); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/BurnMintERC677Helper.sol b/contracts/src/v0.8/ccip/test/helpers/BurnMintERC677Helper.sol new file mode 100644 index 00000000000..9d2346996ae --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/BurnMintERC677Helper.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {IGetCCIPAdmin} from "../../interfaces/IGetCCIPAdmin.sol"; + +contract BurnMintERC677Helper is BurnMintERC677, IGetCCIPAdmin { + constructor(string memory name, string memory symbol) BurnMintERC677(name, symbol, 18, 0) {} + + // Gives one full token to any given address. + function drip(address to) external { + _mint(to, 1e18); + } + + function getCCIPAdmin() external view override returns (address) { + return owner(); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/BurnMintMultiTokenPool.sol b/contracts/src/v0.8/ccip/test/helpers/BurnMintMultiTokenPool.sol new file mode 100644 index 00000000000..a21fcde8357 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/BurnMintMultiTokenPool.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {Pool} from "../../libraries/Pool.sol"; +import {MultiTokenPool} from "./MultiTokenPool.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract BurnMintMultiTokenPool is MultiTokenPool { + constructor( + IERC20[] memory tokens, + address[] memory allowlist, + address rmnProxy, + address router + ) MultiTokenPool(tokens, allowlist, rmnProxy, router) {} + + /// @notice Burn the token in the pool + /// @dev The _validateLockOrBurn check is an essential security check + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + virtual + override + returns (Pool.LockOrBurnOutV1 memory) + { + _validateLockOrBurn(lockOrBurnIn); + + IBurnMintERC20(lockOrBurnIn.localToken).burn(lockOrBurnIn.amount); + + emit Burned(msg.sender, lockOrBurnIn.amount); + + return Pool.LockOrBurnOutV1({ + destTokenAddress: getRemoteToken(lockOrBurnIn.localToken, lockOrBurnIn.remoteChainSelector), + destPoolData: "" + }); + } + + /// @notice Mint tokens from the pool to the recipient + /// @dev The _validateReleaseOrMint check is an essential security check + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + virtual + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + _validateReleaseOrMint(releaseOrMintIn); + + // Mint to the offRamp, which forwards it to the recipient + IBurnMintERC20(releaseOrMintIn.localToken).mint(msg.sender, releaseOrMintIn.amount); + + emit Minted(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/CCIPConfigHelper.sol b/contracts/src/v0.8/ccip/test/helpers/CCIPConfigHelper.sol new file mode 100644 index 00000000000..74f03890d3b --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/CCIPConfigHelper.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import {CCIPConfig} from "../../capability/CCIPConfig.sol"; +import {CCIPConfigTypes} from "../../capability/libraries/CCIPConfigTypes.sol"; +import {Internal} from "../../libraries/Internal.sol"; + +contract CCIPConfigHelper is CCIPConfig { + constructor(address capabilitiesRegistry) CCIPConfig(capabilitiesRegistry) {} + + function stateFromConfigLength(uint256 configLength) public pure returns (CCIPConfigTypes.ConfigState) { + return _stateFromConfigLength(configLength); + } + + function validateConfigStateTransition( + CCIPConfigTypes.ConfigState currentState, + CCIPConfigTypes.ConfigState newState + ) public pure { + _validateConfigStateTransition(currentState, newState); + } + + function validateConfigTransition( + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig, + CCIPConfigTypes.OCR3ConfigWithMeta[] memory newConfigWithMeta + ) public pure { + _validateConfigTransition(currentConfig, newConfigWithMeta); + } + + function computeNewConfigWithMeta( + uint32 donId, + CCIPConfigTypes.OCR3ConfigWithMeta[] memory currentConfig, + CCIPConfigTypes.OCR3Config[] memory newConfig, + CCIPConfigTypes.ConfigState currentState, + CCIPConfigTypes.ConfigState newState + ) public view returns (CCIPConfigTypes.OCR3ConfigWithMeta[] memory) { + return _computeNewConfigWithMeta(donId, currentConfig, newConfig, currentState, newState); + } + + function groupByPluginType(CCIPConfigTypes.OCR3Config[] memory ocr3Configs) + public + pure + returns (CCIPConfigTypes.OCR3Config[] memory commitConfigs, CCIPConfigTypes.OCR3Config[] memory execConfigs) + { + return _groupByPluginType(ocr3Configs); + } + + function computeConfigDigest( + uint32 donId, + uint64 configCount, + CCIPConfigTypes.OCR3Config memory ocr3Config + ) public pure returns (bytes32) { + return _computeConfigDigest(donId, configCount, ocr3Config); + } + + function validateConfig(CCIPConfigTypes.OCR3Config memory cfg) public view { + _validateConfig(cfg); + } + + function updatePluginConfig( + uint32 donId, + Internal.OCRPluginType pluginType, + CCIPConfigTypes.OCR3Config[] memory newConfig + ) public { + _updatePluginConfig(donId, pluginType, newConfig); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/CommitStoreHelper.sol b/contracts/src/v0.8/ccip/test/helpers/CommitStoreHelper.sol new file mode 100644 index 00000000000..c8d66b8d72f --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/CommitStoreHelper.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import "../../CommitStore.sol"; + +contract CommitStoreHelper is CommitStore { + constructor(StaticConfig memory staticConfig) CommitStore(staticConfig) {} + + /// @dev Expose _report for tests + function report(bytes calldata commitReport, uint40 epochAndRound) external { + _report(commitReport, epochAndRound); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOffRampHelper.sol b/contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOffRampHelper.sol new file mode 100644 index 00000000000..581d9bd7051 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOffRampHelper.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {EVM2EVMMultiOffRamp} from "../../offRamp/EVM2EVMMultiOffRamp.sol"; +import {IgnoreContractSize} from "./IgnoreContractSize.sol"; + +contract EVM2EVMMultiOffRampHelper is EVM2EVMMultiOffRamp, IgnoreContractSize { + mapping(uint64 sourceChainSelector => uint256 overrideTimestamp) private s_sourceChainVerificationOverride; + + constructor( + StaticConfig memory staticConfig, + DynamicConfig memory dynamicConfig, + SourceChainConfigArgs[] memory sourceChainConfigs + ) EVM2EVMMultiOffRamp(staticConfig, dynamicConfig, sourceChainConfigs) {} + + function setExecutionStateHelper( + uint64 sourceChainSelector, + uint64 sequenceNumber, + Internal.MessageExecutionState state + ) public { + _setExecutionState(sourceChainSelector, sequenceNumber, state); + } + + function getExecutionStateBitMap(uint64 sourceChainSelector, uint64 bitmapIndex) public view returns (uint256) { + return s_executionStates[sourceChainSelector][bitmapIndex]; + } + + function releaseOrMintSingleToken( + Internal.RampTokenAmount memory sourceTokenAmount, + bytes calldata originalSender, + address receiver, + uint64 sourceChainSelector, + bytes calldata offchainTokenData + ) external returns (Client.EVMTokenAmount memory) { + return + _releaseOrMintSingleToken(sourceTokenAmount, originalSender, receiver, sourceChainSelector, offchainTokenData); + } + + function releaseOrMintTokens( + Internal.RampTokenAmount[] memory sourceTokenAmounts, + bytes memory originalSender, + address receiver, + uint64 sourceChainSelector, + bytes[] calldata offchainTokenData + ) external returns (Client.EVMTokenAmount[] memory) { + return _releaseOrMintTokens(sourceTokenAmounts, originalSender, receiver, sourceChainSelector, offchainTokenData); + } + + function trialExecute( + Internal.Any2EVMRampMessage memory message, + bytes[] memory offchainTokenData + ) external returns (Internal.MessageExecutionState, bytes memory) { + return _trialExecute(message, offchainTokenData); + } + + function executeSingleReport( + Internal.ExecutionReportSingleChain memory rep, + uint256[] memory manualExecGasLimits + ) external { + _executeSingleReport(rep, manualExecGasLimits); + } + + function batchExecute( + Internal.ExecutionReportSingleChain[] memory reports, + uint256[][] memory manualExecGasLimits + ) external { + _batchExecute(reports, manualExecGasLimits); + } + + function verify( + uint64 sourceChainSelector, + bytes32[] memory hashedLeaves, + bytes32[] memory proofs, + uint256 proofFlagBits + ) external view returns (uint256 timestamp) { + return super._verify(sourceChainSelector, hashedLeaves, proofs, proofFlagBits); + } + + function _verify( + uint64 sourceChainSelector, + bytes32[] memory hashedLeaves, + bytes32[] memory proofs, + uint256 proofFlagBits + ) internal view override returns (uint256 timestamp) { + uint256 overrideTimestamp = s_sourceChainVerificationOverride[sourceChainSelector]; + + return overrideTimestamp == 0 + ? super._verify(sourceChainSelector, hashedLeaves, proofs, proofFlagBits) + : overrideTimestamp; + } + + /// @dev Test helper to override _verify result for easier exec testing + function setVerifyOverrideResult(uint64 sourceChainSelector, uint256 overrideTimestamp) external { + s_sourceChainVerificationOverride[sourceChainSelector] = overrideTimestamp; + } + + /// @dev Test helper to directly set a root's timestamp + function setRootTimestamp(uint64 sourceChainSelector, bytes32 root, uint256 timestamp) external { + s_roots[sourceChainSelector][root] = timestamp; + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOnRampHelper.sol b/contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOnRampHelper.sol new file mode 100644 index 00000000000..0532697d649 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/EVM2EVMMultiOnRampHelper.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import "../../onRamp/EVM2EVMMultiOnRamp.sol"; +import {IgnoreContractSize} from "./IgnoreContractSize.sol"; + +contract EVM2EVMMultiOnRampHelper is EVM2EVMMultiOnRamp, IgnoreContractSize { + constructor( + StaticConfig memory staticConfig, + DynamicConfig memory dynamicConfig + ) EVM2EVMMultiOnRamp(staticConfig, dynamicConfig) {} +} diff --git a/contracts/src/v0.8/ccip/test/helpers/EVM2EVMOffRampHelper.sol b/contracts/src/v0.8/ccip/test/helpers/EVM2EVMOffRampHelper.sol new file mode 100644 index 00000000000..e328f0ade29 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/EVM2EVMOffRampHelper.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import "../../offRamp/EVM2EVMOffRamp.sol"; +import {IgnoreContractSize} from "./IgnoreContractSize.sol"; + +contract EVM2EVMOffRampHelper is EVM2EVMOffRamp, IgnoreContractSize { + constructor( + StaticConfig memory staticConfig, + RateLimiter.Config memory rateLimiterConfig + ) EVM2EVMOffRamp(staticConfig, rateLimiterConfig) {} + + function setExecutionStateHelper(uint64 sequenceNumber, Internal.MessageExecutionState state) public { + _setExecutionState(sequenceNumber, state); + } + + function getExecutionStateBitMap(uint64 bitmapIndex) public view returns (uint256) { + return s_executionStates[bitmapIndex]; + } + + function releaseOrMintToken( + uint256 sourceTokenAmount, + bytes calldata originalSender, + address receiver, + Internal.SourceTokenData calldata sourceTokenData, + bytes calldata offchainTokenData + ) external returns (Client.EVMTokenAmount memory) { + return _releaseOrMintToken(sourceTokenAmount, originalSender, receiver, sourceTokenData, offchainTokenData); + } + + function releaseOrMintTokens( + Client.EVMTokenAmount[] memory sourceTokenAmounts, + bytes calldata originalSender, + address receiver, + bytes[] calldata sourceTokenData, + bytes[] calldata offchainTokenData + ) external returns (Client.EVMTokenAmount[] memory) { + return _releaseOrMintTokens(sourceTokenAmounts, originalSender, receiver, sourceTokenData, offchainTokenData); + } + + function trialExecute( + Internal.EVM2EVMMessage memory message, + bytes[] memory offchainTokenData + ) external returns (Internal.MessageExecutionState, bytes memory) { + return _trialExecute(message, offchainTokenData); + } + + function report(bytes calldata executableMessages) external { + _report(executableMessages); + } + + function execute(Internal.ExecutionReport memory rep, uint256[] memory manualExecGasLimits) external { + _execute(rep, manualExecGasLimits); + } + + function metadataHash() external view returns (bytes32) { + return _metadataHash(Internal.EVM_2_EVM_MESSAGE_HASH); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/EVM2EVMOnRampHelper.sol b/contracts/src/v0.8/ccip/test/helpers/EVM2EVMOnRampHelper.sol new file mode 100644 index 00000000000..5cce6aaa445 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/EVM2EVMOnRampHelper.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import "../../onRamp/EVM2EVMOnRamp.sol"; +import {IgnoreContractSize} from "./IgnoreContractSize.sol"; + +contract EVM2EVMOnRampHelper is EVM2EVMOnRamp, IgnoreContractSize { + constructor( + StaticConfig memory staticConfig, + DynamicConfig memory dynamicConfig, + RateLimiter.Config memory rateLimiterConfig, + FeeTokenConfigArgs[] memory feeTokenConfigs, + TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs, + NopAndWeight[] memory nopsAndWeights + ) + EVM2EVMOnRamp( + staticConfig, + dynamicConfig, + rateLimiterConfig, + feeTokenConfigs, + tokenTransferFeeConfigArgs, + nopsAndWeights + ) + {} + + function getDataAvailabilityCost( + uint112 dataAvailabilityGasPrice, + uint256 messageDataLength, + uint256 numberOfTokens, + uint32 tokenTransferBytesOverhead + ) external view returns (uint256) { + return + _getDataAvailabilityCost(dataAvailabilityGasPrice, messageDataLength, numberOfTokens, tokenTransferBytesOverhead); + } + + function getTokenTransferCost( + address feeToken, + uint224 feeTokenPrice, + Client.EVMTokenAmount[] calldata tokenAmounts + ) external view returns (uint256, uint32, uint32) { + return _getTokenTransferCost(feeToken, feeTokenPrice, tokenAmounts); + } + + function getSequenceNumber() external view returns (uint64) { + return s_sequenceNumber; + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/EtherSenderReceiverHelper.sol b/contracts/src/v0.8/ccip/test/helpers/EtherSenderReceiverHelper.sol new file mode 100644 index 00000000000..71a5cdc7ab6 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/EtherSenderReceiverHelper.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {EtherSenderReceiver} from "../../applications/EtherSenderReceiver.sol"; +import {Client} from "../../libraries/Client.sol"; + +contract EtherSenderReceiverHelper is EtherSenderReceiver { + constructor(address router) EtherSenderReceiver(router) {} + + function validatedMessage(Client.EVM2AnyMessage calldata message) public view returns (Client.EVM2AnyMessage memory) { + return _validatedMessage(message); + } + + function validateFeeToken(Client.EVM2AnyMessage calldata message) public payable { + _validateFeeToken(message); + } + + function publicCcipReceive(Client.Any2EVMMessage memory message) public { + _ccipReceive(message); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/IgnoreContractSize.sol b/contracts/src/v0.8/ccip/test/helpers/IgnoreContractSize.sol new file mode 100644 index 00000000000..b30124069f2 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/IgnoreContractSize.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +contract IgnoreContractSize { + // test contracts are excluded from forge build --sizes by default + // --sizes exits with code 1 if any contract is over limit, which fails CI + // for helper contracts that are not explicit test contracts + // use this flag to exclude from --sizes + bool public IS_SCRIPT = true; +} diff --git a/contracts/src/v0.8/ccip/test/helpers/MaybeRevertingBurnMintTokenPool.sol b/contracts/src/v0.8/ccip/test/helpers/MaybeRevertingBurnMintTokenPool.sol new file mode 100644 index 00000000000..e572f798ad9 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/MaybeRevertingBurnMintTokenPool.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {Pool} from "../../libraries/Pool.sol"; +import {BurnMintTokenPool} from "../../pools/BurnMintTokenPool.sol"; + +contract MaybeRevertingBurnMintTokenPool is BurnMintTokenPool { + bytes public s_revertReason = ""; + bytes public s_sourceTokenData = ""; + + constructor( + IBurnMintERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) BurnMintTokenPool(token, allowlist, rmnProxy, router) {} + + function setShouldRevert(bytes calldata revertReason) external { + s_revertReason = revertReason; + } + + function setSourceTokenData(bytes calldata sourceTokenData) external { + s_sourceTokenData = sourceTokenData; + } + + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + virtual + override + returns (Pool.LockOrBurnOutV1 memory) + { + _validateLockOrBurn(lockOrBurnIn); + + bytes memory revertReason = s_revertReason; + if (revertReason.length != 0) { + assembly { + revert(add(32, revertReason), mload(revertReason)) + } + } + + IBurnMintERC20(address(i_token)).burn(lockOrBurnIn.amount); + emit Burned(msg.sender, lockOrBurnIn.amount); + return Pool.LockOrBurnOutV1({ + destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), + destPoolData: s_sourceTokenData + }); + } + + /// @notice Reverts depending on the value of `s_revertReason` + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + virtual + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + _validateReleaseOrMint(releaseOrMintIn); + + bytes memory revertReason = s_revertReason; + if (revertReason.length != 0) { + assembly { + revert(add(32, revertReason), mload(revertReason)) + } + } + IBurnMintERC20(address(i_token)).mint(msg.sender, releaseOrMintIn.amount); + emit Minted(msg.sender, releaseOrMintIn.receiver, releaseOrMintIn.amount); + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/MerkleHelper.sol b/contracts/src/v0.8/ccip/test/helpers/MerkleHelper.sol new file mode 100644 index 00000000000..ccb05681f1c --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/MerkleHelper.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {MerkleMultiProof} from "../../libraries/MerkleMultiProof.sol"; + +library MerkleHelper { + /// @notice Generate a Merkle Root from a full set of leaves. When a tree is unbalanced + /// the value is brought up in the tree. For example consider (a,b,c) as leaves. This would + /// result in the following tree with d being computed from hash(a,c) and the root r from + /// hash(d,c). Notice c is not being rehashed when it is brought up in the tree, so the + /// root is NOT hash(d,hash(c)) but instead hash(d,c) == hash(hash(a,b),c). + /// r + /// / \ + /// d c + /// / \ + /// a b + function getMerkleRoot(bytes32[] memory hashedLeaves) public pure returns (bytes32) { + require(hashedLeaves.length <= 256); + while (hashedLeaves.length > 1) { + hashedLeaves = computeNextLayer(hashedLeaves); + } + return hashedLeaves[0]; + } + + /// @notice Computes a single layer of a merkle proof by hashing each pair (i, i+1) for + /// each i, i+2, i+4.. n. When an uneven number of leaves is supplied the last item + /// is simply included as the last element in the result set and not hashed. + function computeNextLayer(bytes32[] memory layer) public pure returns (bytes32[] memory) { + uint256 leavesLen = layer.length; + if (leavesLen == 1) return layer; + + unchecked { + bytes32[] memory nextLayer = new bytes32[]((leavesLen + 1) / 2); + for (uint256 i = 0; i < leavesLen; i += 2) { + if (i == leavesLen - 1) { + nextLayer[i / 2] = layer[i]; + } else { + nextLayer[i / 2] = hashPair(layer[i], layer[i + 1]); + } + } + return nextLayer; + } + } + + function hashPair(bytes32 a, bytes32 b) public pure returns (bytes32) { + return a < b ? hashInternalNode(a, b) : hashInternalNode(b, a); + } + + function hashInternalNode(bytes32 left, bytes32 right) public pure returns (bytes32 hash) { + return keccak256(abi.encode(MerkleMultiProof.INTERNAL_DOMAIN_SEPARATOR, left, right)); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/MessageHasher.sol b/contracts/src/v0.8/ccip/test/helpers/MessageHasher.sol new file mode 100644 index 00000000000..19f35df7969 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/MessageHasher.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; + +/// @notice MessageHasher is a contract that utility functions to hash an Any2EVMRampMessage +/// and encode various preimages for the final hash of the message. +/// @dev This is only deployed in tests and is not part of the production contracts. +contract MessageHasher { + function hash(Internal.Any2EVMRampMessage memory message, bytes memory onRamp) public pure returns (bytes32) { + return Internal._hash(message, onRamp); + } + + function encodeTokenAmountsHashPreimage(Internal.RampTokenAmount[] memory rampTokenAmounts) + public + pure + returns (bytes memory) + { + return abi.encode(rampTokenAmounts); + } + + function encodeMetadataHashPreimage( + bytes32 any2EVMMessageHash, + uint64 sourceChainSelector, + uint64 destChainSelector, + bytes memory onRamp + ) public pure returns (bytes memory) { + return abi.encode(any2EVMMessageHash, sourceChainSelector, destChainSelector, onRamp); + } + + function encodeFixedSizeFieldsHashPreimage( + bytes32 messageId, + bytes memory sender, + address receiver, + uint64 sequenceNumber, + uint256 gasLimit, + uint64 nonce + ) public pure returns (bytes memory) { + return abi.encode(messageId, sender, receiver, sequenceNumber, gasLimit, nonce); + } + + function encodeFinalHashPreimage( + bytes32 leafDomainSeparator, + bytes32 implicitMetadataHash, + bytes32 fixedSizeFieldsHash, + bytes32 dataHash, + bytes32 tokenAmountsHash + ) public pure returns (bytes memory) { + return abi.encode(leafDomainSeparator, implicitMetadataHash, fixedSizeFieldsHash, dataHash, tokenAmountsHash); + } + + function encodeEVMExtraArgsV1(Client.EVMExtraArgsV1 memory extraArgs) public pure returns (bytes memory) { + return Client._argsToBytes(extraArgs); + } + + function encodeEVMExtraArgsV2(Client.EVMExtraArgsV2 memory extraArgs) public pure returns (bytes memory) { + return Client._argsToBytes(extraArgs); + } + + function decodeEVMExtraArgsV1(uint256 gasLimit) public pure returns (Client.EVMExtraArgsV1 memory) { + return Client.EVMExtraArgsV1(gasLimit); + } + + function decodeEVMExtraArgsV2( + uint256 gasLimit, + bool allowOutOfOrderExecution + ) public pure returns (Client.EVMExtraArgsV2 memory) { + return Client.EVMExtraArgsV2(gasLimit, allowOutOfOrderExecution); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/MessageInterceptorHelper.sol b/contracts/src/v0.8/ccip/test/helpers/MessageInterceptorHelper.sol new file mode 100644 index 00000000000..a54145da84e --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/MessageInterceptorHelper.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IMessageInterceptor} from "../../interfaces/IMessageInterceptor.sol"; +import {Client} from "../../libraries/Client.sol"; + +contract MessageInterceptorHelper is IMessageInterceptor { + mapping(bytes32 messageId => bool isInvalid) internal s_invalidMessageIds; + + constructor() {} + + function setMessageIdValidationState(bytes32 messageId, bool isInvalid) external { + s_invalidMessageIds[messageId] = isInvalid; + } + + /// @inheritdoc IMessageInterceptor + function onInboundMessage(Client.Any2EVMMessage memory message) external view { + if (s_invalidMessageIds[message.messageId]) { + revert MessageValidationError(bytes("Invalid message")); + } + } + + /// @inheritdoc IMessageInterceptor + function onOutboundMessage(uint64, Client.EVM2AnyMessage calldata message) external view { + if (s_invalidMessageIds[keccak256(abi.encode(message))]) { + revert MessageValidationError(bytes("Invalid message")); + } + return; + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/MultiAggregateRateLimiterHelper.sol b/contracts/src/v0.8/ccip/test/helpers/MultiAggregateRateLimiterHelper.sol new file mode 100644 index 00000000000..d9386ca7db0 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/MultiAggregateRateLimiterHelper.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {MultiAggregateRateLimiter} from "../../MultiAggregateRateLimiter.sol"; +import {IPriceRegistry} from "../../interfaces/IPriceRegistry.sol"; +import {Client} from "../../libraries/Client.sol"; + +contract MultiAggregateRateLimiterHelper is MultiAggregateRateLimiter { + constructor( + address priceRegistry, + address[] memory authorizedCallers + ) MultiAggregateRateLimiter(priceRegistry, authorizedCallers) {} + + function getTokenValue(Client.EVMTokenAmount memory tokenAmount) public view returns (uint256) { + return _getTokenValue(tokenAmount); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/MultiOCR3Helper.sol b/contracts/src/v0.8/ccip/test/helpers/MultiOCR3Helper.sol new file mode 100644 index 00000000000..003a5326b89 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/MultiOCR3Helper.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {MultiOCR3Base} from "../../ocr/MultiOCR3Base.sol"; + +contract MultiOCR3Helper is MultiOCR3Base { + event AfterConfigSet(uint8 ocrPluginType); + + /// @dev OCR plugin type used for transmit. + /// Defined in storage since it cannot be passed as calldata due to strict transmit checks + uint8 internal s_transmitOcrPluginType; + + function setTransmitOcrPluginType(uint8 ocrPluginType) external { + s_transmitOcrPluginType = ocrPluginType; + } + + /// @dev transmit function with signatures + function transmitWithSignatures( + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs + ) external { + _transmit(s_transmitOcrPluginType, reportContext, report, rs, ss, rawVs); + } + + /// @dev transmit function with no signatures + function transmitWithoutSignatures(bytes32[3] calldata reportContext, bytes calldata report) external { + bytes32[] memory emptySigs = new bytes32[](0); + _transmit(s_transmitOcrPluginType, reportContext, report, emptySigs, emptySigs, bytes32("")); + } + + function getOracle(uint8 ocrPluginType, address oracleAddress) external view returns (Oracle memory) { + return s_oracles[ocrPluginType][oracleAddress]; + } + + function typeAndVersion() public pure override returns (string memory) { + return "MultiOCR3BaseHelper 1.0.0"; + } + + function _afterOCR3ConfigSet(uint8 ocrPluginType) internal virtual override { + emit AfterConfigSet(ocrPluginType); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/MultiTokenPool.sol b/contracts/src/v0.8/ccip/test/helpers/MultiTokenPool.sol new file mode 100644 index 00000000000..0f7c312f713 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/MultiTokenPool.sol @@ -0,0 +1,420 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPoolV1} from "../../interfaces/IPool.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; +import {IRouter} from "../../interfaces/IRouter.sol"; + +import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice This contract is a proof of concept and should NOT be used in production. +abstract contract MultiTokenPool is IPoolV1, OwnerIsCreator { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; + using RateLimiter for RateLimiter.TokenBucket; + + error CallerIsNotARampOnRouter(address caller); + error ZeroAddressNotAllowed(); + error SenderNotAllowed(address sender); + error AllowListNotEnabled(); + error NonExistentChain(uint64 remoteChainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error CursedByRMN(); + error ChainAlreadyExists(uint64 chainSelector); + error InvalidSourcePoolAddress(bytes sourcePoolAddress); + error InvalidToken(address token); + + event Locked(address indexed sender, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + bytes remoteToken, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event RemotePoolSet(uint64 indexed remoteChainSelector, bytes previousPoolAddress, bytes remotePoolAddress); + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event RouterUpdated(address oldRouter, address newRouter); + + struct ChainUpdate { + uint64 remoteChainSelector; // ──╮ Remote chain selector + bool allowed; // ────────────────╯ Whether the chain is allowed + bytes remotePoolAddress; // Address of the remote pool, ABI encoded in the case of a remove EVM chain. + bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain. + RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain + RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain + } + + struct RemoteChainConfig { + RateLimiter.TokenBucket outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain + RateLimiter.TokenBucket inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain + bytes remotePoolAddress; // Address of the remote pool, ABI encoded in the case of a remote EVM chain. + bytes remoteTokenAddress; // Address of the remote token, ABI encoded in the case of a remote EVM chain. + } + + /// @dev The IERC20 token that this pool supports + EnumerableSet.AddressSet internal s_tokens; + /// @dev The address of the RMN proxy + address internal immutable i_rmnProxy; + /// @dev The immutable flag that indicates if the pool is access-controlled. + bool internal immutable i_allowlistEnabled; + /// @dev A set of addresses allowed to trigger lockOrBurn as original senders. + /// Only takes effect if i_allowlistEnabled is true. + /// This can be used to ensure only token-issuer specified addresses can + /// move tokens. + EnumerableSet.AddressSet internal s_allowList; + /// @dev The address of the router + IRouter internal s_router; + /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to + /// be able to quickly determine (without parsing logs) who can access the pool. + /// @dev The chain selectors are in uin256 format because of the EnumerableSet implementation. + EnumerableSet.UintSet internal s_remoteChainSelectors; + mapping(address token => mapping(uint64 remoteChainSelector => RemoteChainConfig)) internal s_remoteChainConfigs; + + constructor(IERC20[] memory token, address[] memory allowlist, address rmnProxy, address router) { + if (router == address(0) || rmnProxy == address(0)) revert ZeroAddressNotAllowed(); + for (uint256 i = 0; i < token.length; ++i) { + s_tokens.add(address(token[i])); + } + i_rmnProxy = rmnProxy; + s_router = IRouter(router); + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. + i_allowlistEnabled = allowlist.length > 0; + if (i_allowlistEnabled) { + _applyAllowListUpdates(new address[](0), allowlist); + } + } + + /// @notice Get RMN proxy address + /// @return rmnProxy Address of RMN proxy + function getRmnProxy() public view returns (address rmnProxy) { + return i_rmnProxy; + } + + /// @inheritdoc IPoolV1 + function isSupportedToken(address token) public view virtual returns (bool) { + return s_tokens.contains(token); + } + + /// @notice Gets the IERC20 token that this pool can lock or burn. + /// @return tokens The IERC20 token representation. + function getTokens() public view returns (IERC20[] memory tokens) { + tokens = new IERC20[](s_tokens.length()); + for (uint256 i = 0; i < s_tokens.length(); ++i) { + tokens[i] = IERC20(s_tokens.at(i)); + } + return tokens; + } + + /// @notice Gets the pool's Router + /// @return router The pool's Router + function getRouter() public view returns (address router) { + return address(s_router); + } + + /// @notice Sets the pool's Router + /// @param newRouter The new Router + function setRouter(address newRouter) public onlyOwner { + if (newRouter == address(0)) revert ZeroAddressNotAllowed(); + address oldRouter = address(s_router); + s_router = IRouter(newRouter); + + emit RouterUpdated(oldRouter, newRouter); + } + + /// @notice Signals which version of the pool interface is supported + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == Pool.CCIP_POOL_V1 || interfaceId == type(IPoolV1).interfaceId + || interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ + // │ Validation │ + // ================================================================ + + /// @notice Validates the lock or burn input for correctness on + /// - token to be locked or burned + /// - RMN curse status + /// - allowlist status + /// - if the sender is a valid onRamp + /// - rate limit status + /// @param lockOrBurnIn The input to validate. + /// @dev This function should always be called before executing a lock or burn. Not doing so would allow + /// for various exploits. + function _validateLockOrBurn(Pool.LockOrBurnInV1 memory lockOrBurnIn) internal { + if (!isSupportedToken(lockOrBurnIn.localToken)) revert InvalidToken(lockOrBurnIn.localToken); + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(lockOrBurnIn.remoteChainSelector)))) revert CursedByRMN(); + _checkAllowList(lockOrBurnIn.originalSender); + + _onlyOnRamp(lockOrBurnIn.remoteChainSelector); + _consumeOutboundRateLimit(lockOrBurnIn.localToken, lockOrBurnIn.remoteChainSelector, lockOrBurnIn.amount); + } + + /// @notice Validates the release or mint input for correctness on + /// - token to be released or minted + /// - RMN curse status + /// - if the sender is a valid offRamp + /// - if the source pool is valid + /// - rate limit status + /// @param releaseOrMintIn The input to validate. + /// @dev This function should always be called before executing a lock or burn. Not doing so would allow + /// for various exploits. + function _validateReleaseOrMint(Pool.ReleaseOrMintInV1 memory releaseOrMintIn) internal { + if (!isSupportedToken(releaseOrMintIn.localToken)) revert InvalidToken(releaseOrMintIn.localToken); + if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(releaseOrMintIn.remoteChainSelector)))) revert CursedByRMN(); + _onlyOffRamp(releaseOrMintIn.remoteChainSelector); + + // Validates that the source pool address is configured on this pool. + bytes memory configuredRemotePool = getRemotePool(releaseOrMintIn.localToken, releaseOrMintIn.remoteChainSelector); + if ( + configuredRemotePool.length == 0 + || keccak256(releaseOrMintIn.sourcePoolAddress) != keccak256(configuredRemotePool) + ) { + revert InvalidSourcePoolAddress(releaseOrMintIn.sourcePoolAddress); + } + _consumeInboundRateLimit(releaseOrMintIn.localToken, releaseOrMintIn.remoteChainSelector, releaseOrMintIn.amount); + } + + // ================================================================ + // │ Chain permissions │ + // ================================================================ + + /// @notice Gets the pool address on the remote chain. + /// @param remoteChainSelector Remote chain selector. + /// @dev To support non-evm chains, this value is encoded into bytes + function getRemotePool(address token, uint64 remoteChainSelector) public view returns (bytes memory) { + return s_remoteChainConfigs[token][remoteChainSelector].remotePoolAddress; + } + + /// @notice Gets the token address on the remote chain. + /// @param remoteChainSelector Remote chain selector. + /// @dev To support non-evm chains, this value is encoded into bytes + function getRemoteToken(address token, uint64 remoteChainSelector) public view returns (bytes memory) { + return s_remoteChainConfigs[token][remoteChainSelector].remoteTokenAddress; + } + + /// @notice Sets the remote pool address for a given chain selector. + /// @param remoteChainSelector The remote chain selector for which the remote pool address is being set. + /// @param remotePoolAddress The address of the remote pool. + function setRemotePool( + address token, + uint64 remoteChainSelector, + bytes calldata remotePoolAddress + ) external onlyOwner { + if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector); + + bytes memory prevAddress = s_remoteChainConfigs[token][remoteChainSelector].remotePoolAddress; + s_remoteChainConfigs[token][remoteChainSelector].remotePoolAddress = remotePoolAddress; + + emit RemotePoolSet(remoteChainSelector, prevAddress, remotePoolAddress); + } + + /// @inheritdoc IPoolV1 + function isSupportedChain(uint64 remoteChainSelector) public view returns (bool) { + return s_remoteChainSelectors.contains(remoteChainSelector); + } + + /// @notice Get list of allowed chains + /// @return list of chains. + function getSupportedChains() public view returns (uint64[] memory) { + uint256[] memory uint256ChainSelectors = s_remoteChainSelectors.values(); + uint64[] memory chainSelectors = new uint64[](uint256ChainSelectors.length); + for (uint256 i = 0; i < uint256ChainSelectors.length; ++i) { + chainSelectors[i] = uint64(uint256ChainSelectors[i]); + } + + return chainSelectors; + } + + /// @notice Sets the permissions for a list of chains selectors. Actual senders for these chains + /// need to be allowed on the Router to interact with this pool. + /// @dev Only callable by the owner + /// @param chains A list of chains and their new permission status & rate limits. Rate limits + /// are only used when the chain is being added through `allowed` being true. + function applyChainUpdates(address token, ChainUpdate[] calldata chains) external virtual onlyOwner { + for (uint256 i = 0; i < chains.length; ++i) { + ChainUpdate memory update = chains[i]; + RateLimiter._validateTokenBucketConfig(update.outboundRateLimiterConfig, !update.allowed); + RateLimiter._validateTokenBucketConfig(update.inboundRateLimiterConfig, !update.allowed); + + if (update.allowed) { + // If the chain already exists, revert + if (!s_remoteChainSelectors.add(update.remoteChainSelector)) { + revert ChainAlreadyExists(update.remoteChainSelector); + } + + if (update.remotePoolAddress.length == 0 || update.remoteTokenAddress.length == 0) { + revert ZeroAddressNotAllowed(); + } + + s_remoteChainConfigs[token][update.remoteChainSelector] = RemoteChainConfig({ + outboundRateLimiterConfig: RateLimiter.TokenBucket({ + rate: update.outboundRateLimiterConfig.rate, + capacity: update.outboundRateLimiterConfig.capacity, + tokens: update.outboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.outboundRateLimiterConfig.isEnabled + }), + inboundRateLimiterConfig: RateLimiter.TokenBucket({ + rate: update.inboundRateLimiterConfig.rate, + capacity: update.inboundRateLimiterConfig.capacity, + tokens: update.inboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.inboundRateLimiterConfig.isEnabled + }), + remotePoolAddress: update.remotePoolAddress, + remoteTokenAddress: update.remoteTokenAddress + }); + + emit ChainAdded( + update.remoteChainSelector, + update.remoteTokenAddress, + update.outboundRateLimiterConfig, + update.inboundRateLimiterConfig + ); + } else { + // If the chain doesn't exist, revert + if (!s_remoteChainSelectors.remove(update.remoteChainSelector)) { + revert NonExistentChain(update.remoteChainSelector); + } + + delete s_remoteChainConfigs[token][update.remoteChainSelector]; + + emit ChainRemoved(update.remoteChainSelector); + } + } + } + + // ================================================================ + // │ Rate limiting │ + // ================================================================ + + /// @notice Consumes outbound rate limiting capacity in this pool + function _consumeOutboundRateLimit(address token, uint64 remoteChainSelector, uint256 amount) internal { + s_remoteChainConfigs[token][remoteChainSelector].outboundRateLimiterConfig._consume(amount, token); + } + + /// @notice Consumes inbound rate limiting capacity in this pool + function _consumeInboundRateLimit(address token, uint64 remoteChainSelector, uint256 amount) internal { + s_remoteChainConfigs[token][remoteChainSelector].inboundRateLimiterConfig._consume(amount, token); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentOutboundRateLimiterState( + address token, + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory) { + return s_remoteChainConfigs[token][remoteChainSelector].outboundRateLimiterConfig._currentTokenBucketState(); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentInboundRateLimiterState( + address token, + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory) { + return s_remoteChainConfigs[token][remoteChainSelector].inboundRateLimiterConfig._currentTokenBucketState(); + } + + /// @notice Sets the chain rate limiter config. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config, meaning the onRamp rate limits for the given chain. + /// @param inboundConfig The new inbound rate limiter config, meaning the offRamp rate limits for the given chain. + function setChainRateLimiterConfig( + address token, + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) internal { + if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector); + RateLimiter._validateTokenBucketConfig(outboundConfig, false); + s_remoteChainConfigs[token][remoteChainSelector].outboundRateLimiterConfig._setTokenBucketConfig(outboundConfig); + RateLimiter._validateTokenBucketConfig(inboundConfig, false); + s_remoteChainConfigs[token][remoteChainSelector].inboundRateLimiterConfig._setTokenBucketConfig(inboundConfig); + emit ChainConfigured(remoteChainSelector, outboundConfig, inboundConfig); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned onRamp for the given chain on the Router. + function _onlyOnRamp(uint64 remoteChainSelector) internal view { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender); + } + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned offRamp for the given chain on the Router. + function _onlyOffRamp(uint64 remoteChainSelector) internal view { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender); + } + + // ================================================================ + // │ Allowlist │ + // ================================================================ + + function _checkAllowList(address sender) internal view { + if (i_allowlistEnabled && !s_allowList.contains(sender)) revert SenderNotAllowed(sender); + } + + /// @notice Gets whether the allowList functionality is enabled. + /// @return true is enabled, false if not. + function getAllowListEnabled() external view returns (bool) { + return i_allowlistEnabled; + } + + /// @notice Gets the allowed addresses. + /// @return The allowed addresses. + function getAllowList() external view returns (address[] memory) { + return s_allowList.values(); + } + + /// @notice Apply updates to the allow list. + /// @param removes The addresses to be removed. + /// @param adds The addresses to be added. + /// @dev allowListing will be removed before public launch + function applyAllowListUpdates(address[] calldata removes, address[] calldata adds) external onlyOwner { + _applyAllowListUpdates(removes, adds); + } + + /// @notice Internal version of applyAllowListUpdates to allow for reuse in the constructor. + function _applyAllowListUpdates(address[] memory removes, address[] memory adds) internal { + if (!i_allowlistEnabled) revert AllowListNotEnabled(); + + for (uint256 i = 0; i < removes.length; ++i) { + address toRemove = removes[i]; + if (s_allowList.remove(toRemove)) { + emit AllowListRemove(toRemove); + } + } + for (uint256 i = 0; i < adds.length; ++i) { + address toAdd = adds[i]; + if (toAdd == address(0)) { + continue; + } + if (s_allowList.add(toAdd)) { + emit AllowListAdd(toAdd); + } + } + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/OCR2Helper.sol b/contracts/src/v0.8/ccip/test/helpers/OCR2Helper.sol new file mode 100644 index 00000000000..cb66352ff65 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/OCR2Helper.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {OCR2Base} from "../../ocr/OCR2Base.sol"; + +contract OCR2Helper is OCR2Base(false) { + function configDigestFromConfigData( + uint256 chainSelector, + address contractAddress, + uint64 configCount, + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) public pure returns (bytes32) { + return _configDigestFromConfigData( + chainSelector, + contractAddress, + configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + } + + function _report(bytes calldata report, uint40 epochAndRound) internal override {} + + function typeAndVersion() public pure override returns (string memory) { + return "OCR2BaseHelper 1.0.0"; + } + + function _beforeSetConfig(bytes memory _onchainConfig) internal override {} +} diff --git a/contracts/src/v0.8/ccip/test/helpers/OCR2NoChecksHelper.sol b/contracts/src/v0.8/ccip/test/helpers/OCR2NoChecksHelper.sol new file mode 100644 index 00000000000..a1ececa326f --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/OCR2NoChecksHelper.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {OCR2BaseNoChecks} from "../../ocr/OCR2BaseNoChecks.sol"; + +contract OCR2NoChecksHelper is OCR2BaseNoChecks { + function configDigestFromConfigData( + uint256 chainSelector, + address contractAddress, + uint64 configCount, + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) public pure returns (bytes32) { + return _configDigestFromConfigData( + chainSelector, + contractAddress, + configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + } + + function _report(bytes calldata report) internal override {} + + function typeAndVersion() public pure override returns (string memory) { + return "OCR2BaseHelper 1.0.0"; + } + + function _beforeSetConfig(bytes memory _onchainConfig) internal override {} +} diff --git a/contracts/src/v0.8/ccip/test/helpers/PriceRegistryHelper.sol b/contracts/src/v0.8/ccip/test/helpers/PriceRegistryHelper.sol new file mode 100644 index 00000000000..8524df12ccf --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/PriceRegistryHelper.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {PriceRegistry} from "../../PriceRegistry.sol"; +import {Client} from "../../libraries/Client.sol"; + +contract PriceRegistryHelper is PriceRegistry { + constructor( + StaticConfig memory staticConfig, + address[] memory priceUpdaters, + address[] memory feeTokens, + TokenPriceFeedUpdate[] memory tokenPriceFeeds, + TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs, + PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs, + DestChainConfigArgs[] memory destChainConfigArgs + ) + PriceRegistry( + staticConfig, + priceUpdaters, + feeTokens, + tokenPriceFeeds, + tokenTransferFeeConfigArgs, + premiumMultiplierWeiPerEthArgs, + destChainConfigArgs + ) + {} + + function getDataAvailabilityCost( + uint64 destChainSelector, + uint112 dataAvailabilityGasPrice, + uint256 messageDataLength, + uint256 numberOfTokens, + uint32 tokenTransferBytesOverhead + ) external view returns (uint256) { + return _getDataAvailabilityCost( + s_destChainConfigs[destChainSelector], + dataAvailabilityGasPrice, + messageDataLength, + numberOfTokens, + tokenTransferBytesOverhead + ); + } + + function getTokenTransferCost( + uint64 destChainSelector, + address feeToken, + uint224 feeTokenPrice, + Client.EVMTokenAmount[] calldata tokenAmounts + ) external view returns (uint256, uint32, uint32) { + return _getTokenTransferCost( + s_destChainConfigs[destChainSelector], destChainSelector, feeToken, feeTokenPrice, tokenAmounts + ); + } + + function parseEVMExtraArgsFromBytes( + bytes calldata extraArgs, + uint64 destChainSelector + ) external view returns (Client.EVMExtraArgsV2 memory) { + return _parseEVMExtraArgsFromBytes(extraArgs, s_destChainConfigs[destChainSelector]); + } + + function parseEVMExtraArgsFromBytes( + bytes calldata extraArgs, + DestChainConfig memory destChainConfig + ) external pure returns (Client.EVMExtraArgsV2 memory) { + return _parseEVMExtraArgsFromBytes(extraArgs, destChainConfig); + } + + function validateDestFamilyAddress(bytes4 chainFamilySelector, bytes memory destAddress) external pure { + _validateDestFamilyAddress(chainFamilySelector, destAddress); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/RateLimiterHelper.sol b/contracts/src/v0.8/ccip/test/helpers/RateLimiterHelper.sol new file mode 100644 index 00000000000..8fb96a0c1c3 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/RateLimiterHelper.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {RateLimiter} from "../../libraries/RateLimiter.sol"; + +contract RateLimiterHelper { + using RateLimiter for RateLimiter.TokenBucket; + + RateLimiter.TokenBucket internal s_rateLimiter; + + constructor(RateLimiter.Config memory config) { + s_rateLimiter = RateLimiter.TokenBucket({ + rate: config.rate, + capacity: config.capacity, + tokens: config.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: config.isEnabled + }); + } + + function consume(uint256 requestTokens, address tokenAddress) external { + s_rateLimiter._consume(requestTokens, tokenAddress); + } + + function currentTokenBucketState() external view returns (RateLimiter.TokenBucket memory) { + return s_rateLimiter._currentTokenBucketState(); + } + + function setTokenBucketConfig(RateLimiter.Config memory config) external { + s_rateLimiter._setTokenBucketConfig(config); + } + + function getRateLimiter() external view returns (RateLimiter.TokenBucket memory) { + return s_rateLimiter; + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/ReportCodec.sol b/contracts/src/v0.8/ccip/test/helpers/ReportCodec.sol new file mode 100644 index 00000000000..ca53d512c0d --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/ReportCodec.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Internal} from "../../libraries/Internal.sol"; +import {EVM2EVMMultiOffRamp} from "../../offRamp/EVM2EVMMultiOffRamp.sol"; + +contract ReportCodec { + event ExecuteReportDecoded(Internal.ExecutionReportSingleChain[] report); + event CommitReportDecoded(EVM2EVMMultiOffRamp.CommitReport report); + + function decodeExecuteReport(bytes memory report) public pure returns (Internal.ExecutionReportSingleChain[] memory) { + return abi.decode(report, (Internal.ExecutionReportSingleChain[])); + } + + function decodeCommitReport(bytes memory report) public pure returns (EVM2EVMMultiOffRamp.CommitReport memory) { + return abi.decode(report, (EVM2EVMMultiOffRamp.CommitReport)); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/TokenPoolHelper.sol b/contracts/src/v0.8/ccip/test/helpers/TokenPoolHelper.sol new file mode 100644 index 00000000000..c57bfa33119 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/TokenPoolHelper.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Pool} from "../../libraries/Pool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract TokenPoolHelper is TokenPool { + constructor( + IERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) TokenPool(token, allowlist, rmnProxy, router) {} + + function lockOrBurn(Pool.LockOrBurnInV1 calldata lockOrBurnIn) + external + view + override + returns (Pool.LockOrBurnOutV1 memory) + { + return Pool.LockOrBurnOutV1({destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), destPoolData: ""}); + } + + function releaseOrMint(Pool.ReleaseOrMintInV1 calldata releaseOrMintIn) + external + pure + override + returns (Pool.ReleaseOrMintOutV1 memory) + { + return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.amount}); + } + + function onlyOnRampModifier(uint64 remoteChainSelector) external view { + _onlyOnRamp(remoteChainSelector); + } + + function onlyOffRampModifier(uint64 remoteChainSelector) external view { + _onlyOffRamp(remoteChainSelector); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/USDCTokenPoolHelper.sol b/contracts/src/v0.8/ccip/test/helpers/USDCTokenPoolHelper.sol new file mode 100644 index 00000000000..7a3400588a8 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/USDCTokenPoolHelper.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; + +import {ITokenMessenger} from "../../pools/USDC/ITokenMessenger.sol"; +import {USDCTokenPool} from "../../pools/USDC/USDCTokenPool.sol"; + +contract USDCTokenPoolHelper is USDCTokenPool { + constructor( + ITokenMessenger tokenMessenger, + IBurnMintERC20 token, + address[] memory allowlist, + address rmnProxy, + address router + ) USDCTokenPool(tokenMessenger, token, allowlist, rmnProxy, router) {} + + function validateMessage(bytes memory usdcMessage, SourceTokenDataPayload memory sourceTokenData) external view { + return _validateMessage(usdcMessage, sourceTokenData); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/receivers/ConformingReceiver.sol b/contracts/src/v0.8/ccip/test/helpers/receivers/ConformingReceiver.sol new file mode 100644 index 00000000000..159cd7a8514 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/receivers/ConformingReceiver.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {CCIPReceiver} from "../../../applications/CCIPReceiver.sol"; +import {Client} from "../../../libraries/Client.sol"; + +contract ConformingReceiver is CCIPReceiver { + event MessageReceived(); + + constructor(address router, address feeToken) CCIPReceiver(router) {} + + function _ccipReceive(Client.Any2EVMMessage memory) internal virtual override { + emit MessageReceived(); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/receivers/MaybeRevertMessageReceiver.sol b/contracts/src/v0.8/ccip/test/helpers/receivers/MaybeRevertMessageReceiver.sol new file mode 100644 index 00000000000..dd65f202dfe --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/receivers/MaybeRevertMessageReceiver.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IAny2EVMMessageReceiver} from "../../../interfaces/IAny2EVMMessageReceiver.sol"; +import {Client} from "../../../libraries/Client.sol"; + +import {IERC165} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; + +contract MaybeRevertMessageReceiver is IAny2EVMMessageReceiver, IERC165 { + error ReceiveRevert(); + error CustomError(bytes err); + + event ValueReceived(uint256 amount); + event MessageReceived(); + + address private s_manager; + bool public s_toRevert; + bytes private s_err; + + constructor(bool toRevert) { + s_manager = msg.sender; + s_toRevert = toRevert; + } + + function setRevert(bool toRevert) external { + s_toRevert = toRevert; + } + + function setErr(bytes memory err) external { + s_err = err; + } + + /// @notice IERC165 supports an interfaceId + /// @param interfaceId The interfaceId to check + /// @return true if the interfaceId is supported + function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { + return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; + } + + function ccipReceive(Client.Any2EVMMessage calldata) external override { + if (s_toRevert) { + revert CustomError(s_err); + } + emit MessageReceived(); + } + + receive() external payable { + if (s_toRevert) { + revert ReceiveRevert(); + } + + emit ValueReceived(msg.value); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/receivers/MaybeRevertMessageReceiverNo165.sol b/contracts/src/v0.8/ccip/test/helpers/receivers/MaybeRevertMessageReceiverNo165.sol new file mode 100644 index 00000000000..4f56394c4e5 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/receivers/MaybeRevertMessageReceiverNo165.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import "../../../interfaces/IAny2EVMMessageReceiver.sol"; + +contract MaybeRevertMessageReceiverNo165 is IAny2EVMMessageReceiver { + address private s_manager; + bool public s_toRevert; + + event MessageReceived(); + + constructor(bool toRevert) { + s_manager = msg.sender; + s_toRevert = toRevert; + } + + function setRevert(bool toRevert) external { + s_toRevert = toRevert; + } + + function ccipReceive(Client.Any2EVMMessage calldata) external override { + if (s_toRevert) { + revert(); + } + emit MessageReceived(); + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/receivers/ReentrancyAbuser.sol b/contracts/src/v0.8/ccip/test/helpers/receivers/ReentrancyAbuser.sol new file mode 100644 index 00000000000..ae8759099cd --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/receivers/ReentrancyAbuser.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {CCIPReceiver} from "../../../applications/CCIPReceiver.sol"; +import {Client} from "../../../libraries/Client.sol"; +import {Internal} from "../../../libraries/Internal.sol"; +import {EVM2EVMOffRamp} from "../../../offRamp/EVM2EVMOffRamp.sol"; + +contract ReentrancyAbuser is CCIPReceiver { + event ReentrancySucceeded(); + + bool internal s_ReentrancyDone = false; + Internal.ExecutionReport internal s_payload; + EVM2EVMOffRamp internal s_offRamp; + + constructor(address router, EVM2EVMOffRamp offRamp) CCIPReceiver(router) { + s_offRamp = offRamp; + } + + function setPayload(Internal.ExecutionReport calldata payload) public { + s_payload = payload; + } + + function _ccipReceive(Client.Any2EVMMessage memory) internal override { + // Use original message gas limits in manual execution + uint256 numMsgs = s_payload.messages.length; + uint256[] memory gasOverrides = new uint256[](numMsgs); + for (uint256 i = 0; i < numMsgs; ++i) { + gasOverrides[i] = 0; + } + + if (!s_ReentrancyDone) { + // Could do more rounds but a PoC one is enough + s_ReentrancyDone = true; + s_offRamp.manuallyExecute(s_payload, gasOverrides); + } else { + emit ReentrancySucceeded(); + } + } +} diff --git a/contracts/src/v0.8/ccip/test/helpers/receivers/ReentrancyAbuserMultiRamp.sol b/contracts/src/v0.8/ccip/test/helpers/receivers/ReentrancyAbuserMultiRamp.sol new file mode 100644 index 00000000000..c9e7d7e8ad6 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/helpers/receivers/ReentrancyAbuserMultiRamp.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.19; + +import {CCIPReceiver} from "../../../applications/CCIPReceiver.sol"; +import {Client} from "../../../libraries/Client.sol"; +import {Internal} from "../../../libraries/Internal.sol"; +import {EVM2EVMMultiOffRamp} from "../../../offRamp/EVM2EVMMultiOffRamp.sol"; + +contract ReentrancyAbuserMultiRamp is CCIPReceiver { + event ReentrancySucceeded(); + + bool internal s_ReentrancyDone = false; + Internal.ExecutionReportSingleChain internal s_payload; + EVM2EVMMultiOffRamp internal s_offRamp; + + constructor(address router, EVM2EVMMultiOffRamp offRamp) CCIPReceiver(router) { + s_offRamp = offRamp; + } + + function setPayload(Internal.ExecutionReportSingleChain calldata payload) public { + s_payload = payload; + } + + function _ccipReceive(Client.Any2EVMMessage memory) internal override { + // Use original message gas limits in manual execution + uint256 numMsgs = s_payload.messages.length; + uint256[][] memory gasOverrides = new uint256[][](1); + gasOverrides[0] = new uint256[](numMsgs); + for (uint256 i = 0; i < numMsgs; ++i) { + gasOverrides[0][i] = 0; + } + + Internal.ExecutionReportSingleChain[] memory batchPayload = new Internal.ExecutionReportSingleChain[](1); + batchPayload[0] = s_payload; + + if (!s_ReentrancyDone) { + // Could do more rounds but a PoC one is enough + s_ReentrancyDone = true; + s_offRamp.manuallyExecute(batchPayload, gasOverrides); + } else { + emit ReentrancySucceeded(); + } + } +} diff --git a/contracts/src/v0.8/ccip/test/legacy/BurnMintTokenPool1_2.sol b/contracts/src/v0.8/ccip/test/legacy/BurnMintTokenPool1_2.sol new file mode 100644 index 00000000000..2e7878730ea --- /dev/null +++ b/contracts/src/v0.8/ccip/test/legacy/BurnMintTokenPool1_2.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; +import {IPoolPriorTo1_5} from "../../interfaces/IPoolPriorTo1_5.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; + +import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice Base abstract class with common functions for all token pools. +/// A token pool serves as isolated place for holding tokens and token specific logic +/// that may execute as tokens move across the bridge. +abstract contract TokenPool1_2 is IPoolPriorTo1_5, OwnerIsCreator, IERC165 { + using EnumerableSet for EnumerableSet.AddressSet; + using RateLimiter for RateLimiter.TokenBucket; + + error PermissionsError(); + error ZeroAddressNotAllowed(); + error SenderNotAllowed(address sender); + error AllowListNotEnabled(); + error NonExistentRamp(address ramp); + error BadARMSignal(); + error RampAlreadyExists(address ramp); + + event Locked(address indexed sender, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event OnRampAdded(address onRamp, RateLimiter.Config rateLimiterConfig); + event OnRampConfigured(address onRamp, RateLimiter.Config rateLimiterConfig); + event OnRampRemoved(address onRamp); + event OffRampAdded(address offRamp, RateLimiter.Config rateLimiterConfig); + event OffRampConfigured(address offRamp, RateLimiter.Config rateLimiterConfig); + event OffRampRemoved(address offRamp); + event AllowListAdd(address sender); + event AllowListRemove(address sender); + + struct RampUpdate { + address ramp; + bool allowed; + RateLimiter.Config rateLimiterConfig; + } + + /// @dev The bridgeable token that is managed by this pool. + IERC20 internal immutable i_token; + /// @dev The address of the arm proxy + address internal immutable i_armProxy; + /// @dev The immutable flag that indicates if the pool is access-controlled. + bool internal immutable i_allowlistEnabled; + /// @dev A set of addresses allowed to trigger lockOrBurn as original senders. + /// Only takes effect if i_allowlistEnabled is true. + /// This can be used to ensure only token-issuer specified addresses can + /// move tokens. + EnumerableSet.AddressSet internal s_allowList; + + /// @dev A set of allowed onRamps. We want the whitelist to be enumerable to + /// be able to quickly determine (without parsing logs) who can access the pool. + EnumerableSet.AddressSet internal s_onRamps; + /// @dev Inbound rate limits. This allows per destination chain + /// token issuer specified rate limiting (e.g. issuers may trust chains to varying + /// degrees and prefer different limits) + mapping(address => RateLimiter.TokenBucket) internal s_onRampRateLimits; + /// @dev A set of allowed offRamps. + EnumerableSet.AddressSet internal s_offRamps; + /// @dev Outbound rate limits. Corresponds to the inbound rate limit for the pool + /// on the remote chain. + mapping(address => RateLimiter.TokenBucket) internal s_offRampRateLimits; + + constructor(IERC20 token, address[] memory allowlist, address armProxy) { + if (address(token) == address(0)) revert ZeroAddressNotAllowed(); + i_token = token; + i_armProxy = armProxy; + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. + i_allowlistEnabled = allowlist.length > 0; + if (i_allowlistEnabled) { + _applyAllowListUpdates(new address[](0), allowlist); + } + } + + /// @notice Get ARM proxy address + /// @return armProxy Address of arm proxy + function getArmProxy() public view returns (address armProxy) { + return i_armProxy; + } + + /// @inheritdoc IPoolPriorTo1_5 + function getToken() public view override returns (IERC20 token) { + return i_token; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(IPoolPriorTo1_5).interfaceId || interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ + // │ Ramp permissions │ + // ================================================================ + + /// @notice Checks whether something is a permissioned onRamp on this contract. + /// @return true if the given address is a permissioned onRamp. + function isOnRamp(address onRamp) public view returns (bool) { + return s_onRamps.contains(onRamp); + } + + /// @notice Checks whether something is a permissioned offRamp on this contract. + /// @return true if the given address is a permissioned offRamp. + function isOffRamp(address offRamp) public view returns (bool) { + return s_offRamps.contains(offRamp); + } + + /// @notice Get onRamp whitelist + /// @return list of onRamps. + function getOnRamps() public view returns (address[] memory) { + return s_onRamps.values(); + } + + /// @notice Get offRamp whitelist + /// @return list of offramps + function getOffRamps() public view returns (address[] memory) { + return s_offRamps.values(); + } + + /// @notice Sets permissions for all on and offRamps. + /// @dev Only callable by the owner + /// @param onRamps A list of onRamps and their new permission status/rate limits + /// @param offRamps A list of offRamps and their new permission status/rate limits + function applyRampUpdates(RampUpdate[] calldata onRamps, RampUpdate[] calldata offRamps) external virtual onlyOwner { + _applyRampUpdates(onRamps, offRamps); + } + + function _applyRampUpdates(RampUpdate[] calldata onRamps, RampUpdate[] calldata offRamps) internal onlyOwner { + for (uint256 i = 0; i < onRamps.length; ++i) { + RampUpdate memory update = onRamps[i]; + if (update.allowed) { + if (s_onRamps.add(update.ramp)) { + s_onRampRateLimits[update.ramp] = RateLimiter.TokenBucket({ + rate: update.rateLimiterConfig.rate, + capacity: update.rateLimiterConfig.capacity, + tokens: update.rateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.rateLimiterConfig.isEnabled + }); + emit OnRampAdded(update.ramp, update.rateLimiterConfig); + } else { + revert RampAlreadyExists(update.ramp); + } + } else { + if (s_onRamps.remove(update.ramp)) { + delete s_onRampRateLimits[update.ramp]; + emit OnRampRemoved(update.ramp); + } else { + // Cannot remove a non-existent onRamp. + revert NonExistentRamp(update.ramp); + } + } + } + + for (uint256 i = 0; i < offRamps.length; ++i) { + RampUpdate memory update = offRamps[i]; + if (update.allowed) { + if (s_offRamps.add(update.ramp)) { + s_offRampRateLimits[update.ramp] = RateLimiter.TokenBucket({ + rate: update.rateLimiterConfig.rate, + capacity: update.rateLimiterConfig.capacity, + tokens: update.rateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.rateLimiterConfig.isEnabled + }); + emit OffRampAdded(update.ramp, update.rateLimiterConfig); + } else { + revert RampAlreadyExists(update.ramp); + } + } else { + if (s_offRamps.remove(update.ramp)) { + delete s_offRampRateLimits[update.ramp]; + emit OffRampRemoved(update.ramp); + } else { + // Cannot remove a non-existent offRamp. + revert NonExistentRamp(update.ramp); + } + } + } + } + + // ================================================================ + // │ Rate limiting │ + // ================================================================ + + /// @notice Consumes outbound rate limiting capacity in this pool + function _consumeOnRampRateLimit(uint256 amount) internal { + s_onRampRateLimits[msg.sender]._consume(amount, address(i_token)); + } + + /// @notice Consumes inbound rate limiting capacity in this pool + function _consumeOffRampRateLimit(uint256 amount) internal { + s_offRampRateLimits[msg.sender]._consume(amount, address(i_token)); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function currentOnRampRateLimiterState(address onRamp) external view returns (RateLimiter.TokenBucket memory) { + return s_onRampRateLimits[onRamp]._currentTokenBucketState(); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function currentOffRampRateLimiterState(address offRamp) external view returns (RateLimiter.TokenBucket memory) { + return s_offRampRateLimits[offRamp]._currentTokenBucketState(); + } + + /// @notice Sets the onramp rate limited config. + /// @param config The new rate limiter config. + function setOnRampRateLimiterConfig(address onRamp, RateLimiter.Config memory config) external onlyOwner { + if (!isOnRamp(onRamp)) revert NonExistentRamp(onRamp); + s_onRampRateLimits[onRamp]._setTokenBucketConfig(config); + emit OnRampConfigured(onRamp, config); + } + + /// @notice Sets the offramp rate limited config. + /// @param config The new rate limiter config. + function setOffRampRateLimiterConfig(address offRamp, RateLimiter.Config memory config) external onlyOwner { + if (!isOffRamp(offRamp)) revert NonExistentRamp(offRamp); + s_offRampRateLimits[offRamp]._setTokenBucketConfig(config); + emit OffRampConfigured(offRamp, config); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Checks whether the msg.sender is a permissioned onRamp on this contract + /// @dev Reverts with a PermissionsError if check fails + modifier onlyOnRamp() { + if (!isOnRamp(msg.sender)) revert PermissionsError(); + _; + } + + /// @notice Checks whether the msg.sender is a permissioned offRamp on this contract + /// @dev Reverts with a PermissionsError if check fails + modifier onlyOffRamp() { + if (!isOffRamp(msg.sender)) revert PermissionsError(); + _; + } + + // ================================================================ + // │ Allowlist │ + // ================================================================ + + modifier checkAllowList(address sender) { + if (i_allowlistEnabled && !s_allowList.contains(sender)) revert SenderNotAllowed(sender); + _; + } + + /// @notice Gets whether the allowList functionality is enabled. + /// @return true is enabled, false if not. + function getAllowListEnabled() external view returns (bool) { + return i_allowlistEnabled; + } + + /// @notice Gets the allowed addresses. + /// @return The allowed addresses. + function getAllowList() external view returns (address[] memory) { + return s_allowList.values(); + } + + /// @notice Apply updates to the allow list. + /// @param removes The addresses to be removed. + /// @param adds The addresses to be added. + /// @dev allowListing will be removed before public launch + function applyAllowListUpdates(address[] calldata removes, address[] calldata adds) external onlyOwner { + _applyAllowListUpdates(removes, adds); + } + + /// @notice Internal version of applyAllowListUpdates to allow for reuse in the constructor. + function _applyAllowListUpdates(address[] memory removes, address[] memory adds) internal { + if (!i_allowlistEnabled) revert AllowListNotEnabled(); + + for (uint256 i = 0; i < removes.length; ++i) { + address toRemove = removes[i]; + if (s_allowList.remove(toRemove)) { + emit AllowListRemove(toRemove); + } + } + for (uint256 i = 0; i < adds.length; ++i) { + address toAdd = adds[i]; + if (toAdd == address(0)) { + continue; + } + if (s_allowList.add(toAdd)) { + emit AllowListAdd(toAdd); + } + } + } + + /// @notice Ensure that there is no active curse. + modifier whenHealthy() { + if (IRMN(i_armProxy).isCursed()) revert BadARMSignal(); + _; + } +} + +contract BurnMintTokenPool1_2 is ITypeAndVersion, TokenPool1_2 { + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables + string public constant override typeAndVersion = "BurnMintTokenPool 1.2.0"; + + constructor( + IBurnMintERC20 token, + address[] memory allowlist, + address armProxy + ) TokenPool1_2(token, allowlist, armProxy) {} + + /// @notice Burn the token in the pool + /// @param amount Amount to burn + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function lockOrBurn( + address originalSender, + bytes calldata, + uint256 amount, + uint64, + bytes calldata + ) external virtual override onlyOnRamp checkAllowList(originalSender) whenHealthy returns (bytes memory) { + _consumeOnRampRateLimit(amount); + IBurnMintERC20(address(i_token)).burn(amount); + emit Burned(msg.sender, amount); + return ""; + } + + /// @notice Mint tokens from the pool to the recipient + /// @param receiver Recipient address + /// @param amount Amount to mint + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64, + bytes memory + ) external virtual override whenHealthy onlyOffRamp { + _consumeOffRampRateLimit(amount); + IBurnMintERC20(address(i_token)).mint(receiver, amount); + emit Minted(msg.sender, receiver, amount); + } +} diff --git a/contracts/src/v0.8/ccip/test/legacy/BurnMintTokenPool1_4.sol b/contracts/src/v0.8/ccip/test/legacy/BurnMintTokenPool1_4.sol new file mode 100644 index 00000000000..9ac5d66b1cf --- /dev/null +++ b/contracts/src/v0.8/ccip/test/legacy/BurnMintTokenPool1_4.sol @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; +import {IPoolPriorTo1_5} from "../../interfaces/IPoolPriorTo1_5.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; +import {IRouter} from "../../interfaces/IRouter.sol"; + +import {OwnerIsCreator} from "../../../shared/access/OwnerIsCreator.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice Base abstract class with common functions for all token pools. +/// A token pool serves as isolated place for holding tokens and token specific logic +/// that may execute as tokens move across the bridge. +abstract contract TokenPool1_4 is IPoolPriorTo1_5, OwnerIsCreator, IERC165 { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.UintSet; + using RateLimiter for RateLimiter.TokenBucket; + + error CallerIsNotARampOnRouter(address caller); + error ZeroAddressNotAllowed(); + error SenderNotAllowed(address sender); + error AllowListNotEnabled(); + error NonExistentChain(uint64 remoteChainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error BadARMSignal(); + error ChainAlreadyExists(uint64 chainSelector); + + event Locked(address indexed sender, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event RouterUpdated(address oldRouter, address newRouter); + + struct ChainUpdate { + uint64 remoteChainSelector; // ──╮ Remote chain selector + bool allowed; // ────────────────╯ Whether the chain is allowed + RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config, meaning the rate limits for all of the onRamps for the given chain + RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config, meaning the rate limits for all of the offRamps for the given chain + } + + /// @dev The bridgeable token that is managed by this pool. + IERC20 internal immutable i_token; + /// @dev The address of the arm proxy + address internal immutable i_armProxy; + /// @dev The immutable flag that indicates if the pool is access-controlled. + bool internal immutable i_allowlistEnabled; + /// @dev A set of addresses allowed to trigger lockOrBurn as original senders. + /// Only takes effect if i_allowlistEnabled is true. + /// This can be used to ensure only token-issuer specified addresses can + /// move tokens. + EnumerableSet.AddressSet internal s_allowList; + /// @dev The address of the router + IRouter internal s_router; + /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to + /// be able to quickly determine (without parsing logs) who can access the pool. + /// @dev The chain selectors are in uin256 format because of the EnumerableSet implementation. + EnumerableSet.UintSet internal s_remoteChainSelectors; + /// @dev Outbound rate limits. Corresponds to the inbound rate limit for the pool + /// on the remote chain. + mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket) internal s_outboundRateLimits; + /// @dev Inbound rate limits. This allows per destination chain + /// token issuer specified rate limiting (e.g. issuers may trust chains to varying + /// degrees and prefer different limits) + mapping(uint64 remoteChainSelector => RateLimiter.TokenBucket) internal s_inboundRateLimits; + + constructor(IERC20 token, address[] memory allowlist, address armProxy, address router) { + if (address(token) == address(0) || router == address(0)) revert ZeroAddressNotAllowed(); + i_token = token; + i_armProxy = armProxy; + s_router = IRouter(router); + + // Pool can be set as permissioned or permissionless at deployment time only to save hot-path gas. + i_allowlistEnabled = allowlist.length > 0; + if (i_allowlistEnabled) { + _applyAllowListUpdates(new address[](0), allowlist); + } + } + + /// @notice Get ARM proxy address + /// @return armProxy Address of arm proxy + function getArmProxy() public view returns (address armProxy) { + return i_armProxy; + } + + /// @inheritdoc IPoolPriorTo1_5 + function getToken() public view override returns (IERC20 token) { + return i_token; + } + + /// @notice Gets the pool's Router + /// @return router The pool's Router + function getRouter() public view returns (address router) { + return address(s_router); + } + + /// @notice Sets the pool's Router + /// @param newRouter The new Router + function setRouter(address newRouter) public onlyOwner { + if (newRouter == address(0)) revert ZeroAddressNotAllowed(); + address oldRouter = address(s_router); + s_router = IRouter(newRouter); + + emit RouterUpdated(oldRouter, newRouter); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(IPoolPriorTo1_5).interfaceId || interfaceId == type(IERC165).interfaceId; + } + + // ================================================================ + // │ Chain permissions │ + // ================================================================ + + /// @notice Checks whether a chain selector is permissioned on this contract. + /// @return true if the given chain selector is a permissioned remote chain. + function isSupportedChain(uint64 remoteChainSelector) public view returns (bool) { + return s_remoteChainSelectors.contains(remoteChainSelector); + } + + /// @notice Get list of allowed chains + /// @return list of chains. + function getSupportedChains() public view returns (uint64[] memory) { + uint256[] memory uint256ChainSelectors = s_remoteChainSelectors.values(); + uint64[] memory chainSelectors = new uint64[](uint256ChainSelectors.length); + for (uint256 i = 0; i < uint256ChainSelectors.length; ++i) { + chainSelectors[i] = uint64(uint256ChainSelectors[i]); + } + + return chainSelectors; + } + + /// @notice Sets the permissions for a list of chains selectors. Actual senders for these chains + /// need to be allowed on the Router to interact with this pool. + /// @dev Only callable by the owner + /// @param chains A list of chains and their new permission status & rate limits. Rate limits + /// are only used when the chain is being added through `allowed` being true. + function applyChainUpdates(ChainUpdate[] calldata chains) external virtual onlyOwner { + for (uint256 i = 0; i < chains.length; ++i) { + ChainUpdate memory update = chains[i]; + RateLimiter._validateTokenBucketConfig(update.outboundRateLimiterConfig, !update.allowed); + RateLimiter._validateTokenBucketConfig(update.inboundRateLimiterConfig, !update.allowed); + + if (update.allowed) { + // If the chain already exists, revert + if (!s_remoteChainSelectors.add(update.remoteChainSelector)) { + revert ChainAlreadyExists(update.remoteChainSelector); + } + + s_outboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ + rate: update.outboundRateLimiterConfig.rate, + capacity: update.outboundRateLimiterConfig.capacity, + tokens: update.outboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.outboundRateLimiterConfig.isEnabled + }); + + s_inboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ + rate: update.inboundRateLimiterConfig.rate, + capacity: update.inboundRateLimiterConfig.capacity, + tokens: update.inboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.inboundRateLimiterConfig.isEnabled + }); + emit ChainAdded(update.remoteChainSelector, update.outboundRateLimiterConfig, update.inboundRateLimiterConfig); + } else { + // If the chain doesn't exist, revert + if (!s_remoteChainSelectors.remove(update.remoteChainSelector)) { + revert NonExistentChain(update.remoteChainSelector); + } + + delete s_inboundRateLimits[update.remoteChainSelector]; + delete s_outboundRateLimits[update.remoteChainSelector]; + emit ChainRemoved(update.remoteChainSelector); + } + } + } + + // ================================================================ + // │ Rate limiting │ + // ================================================================ + + /// @notice Consumes outbound rate limiting capacity in this pool + function _consumeOutboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_outboundRateLimits[remoteChainSelector]._consume(amount, address(i_token)); + } + + /// @notice Consumes inbound rate limiting capacity in this pool + function _consumeInboundRateLimit(uint64 remoteChainSelector, uint256 amount) internal { + s_inboundRateLimits[remoteChainSelector]._consume(amount, address(i_token)); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentOutboundRateLimiterState(uint64 remoteChainSelector) + external + view + returns (RateLimiter.TokenBucket memory) + { + return s_outboundRateLimits[remoteChainSelector]._currentTokenBucketState(); + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function getCurrentInboundRateLimiterState(uint64 remoteChainSelector) + external + view + returns (RateLimiter.TokenBucket memory) + { + return s_inboundRateLimits[remoteChainSelector]._currentTokenBucketState(); + } + + /// @notice Sets the chain rate limiter config. + /// @param remoteChainSelector The remote chain selector for which the rate limits apply. + /// @param outboundConfig The new outbound rate limiter config, meaning the onRamp rate limits for the given chain. + /// @param inboundConfig The new inbound rate limiter config, meaning the offRamp rate limits for the given chain. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external virtual onlyOwner { + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } + + function _setRateLimitConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) internal { + if (!isSupportedChain(remoteChainSelector)) revert NonExistentChain(remoteChainSelector); + RateLimiter._validateTokenBucketConfig(outboundConfig, false); + s_outboundRateLimits[remoteChainSelector]._setTokenBucketConfig(outboundConfig); + RateLimiter._validateTokenBucketConfig(inboundConfig, false); + s_inboundRateLimits[remoteChainSelector]._setTokenBucketConfig(inboundConfig); + emit ChainConfigured(remoteChainSelector, outboundConfig, inboundConfig); + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned onRamp for the given chain on the Router. + modifier onlyOnRamp(uint64 remoteChainSelector) { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!(msg.sender == s_router.getOnRamp(remoteChainSelector))) revert CallerIsNotARampOnRouter(msg.sender); + _; + } + + /// @notice Checks whether remote chain selector is configured on this contract, and if the msg.sender + /// is a permissioned offRamp for the given chain on the Router. + modifier onlyOffRamp(uint64 remoteChainSelector) { + if (!isSupportedChain(remoteChainSelector)) revert ChainNotAllowed(remoteChainSelector); + if (!s_router.isOffRamp(remoteChainSelector, msg.sender)) revert CallerIsNotARampOnRouter(msg.sender); + _; + } + + // ================================================================ + // │ Allowlist │ + // ================================================================ + + modifier checkAllowList(address sender) { + if (i_allowlistEnabled && !s_allowList.contains(sender)) revert SenderNotAllowed(sender); + _; + } + + /// @notice Gets whether the allowList functionality is enabled. + /// @return true is enabled, false if not. + function getAllowListEnabled() external view returns (bool) { + return i_allowlistEnabled; + } + + /// @notice Gets the allowed addresses. + /// @return The allowed addresses. + function getAllowList() external view returns (address[] memory) { + return s_allowList.values(); + } + + /// @notice Apply updates to the allow list. + /// @param removes The addresses to be removed. + /// @param adds The addresses to be added. + /// @dev allowListing will be removed before public launch + function applyAllowListUpdates(address[] calldata removes, address[] calldata adds) external onlyOwner { + _applyAllowListUpdates(removes, adds); + } + + /// @notice Internal version of applyAllowListUpdates to allow for reuse in the constructor. + function _applyAllowListUpdates(address[] memory removes, address[] memory adds) internal { + if (!i_allowlistEnabled) revert AllowListNotEnabled(); + + for (uint256 i = 0; i < removes.length; ++i) { + address toRemove = removes[i]; + if (s_allowList.remove(toRemove)) { + emit AllowListRemove(toRemove); + } + } + for (uint256 i = 0; i < adds.length; ++i) { + address toAdd = adds[i]; + if (toAdd == address(0)) { + continue; + } + if (s_allowList.add(toAdd)) { + emit AllowListAdd(toAdd); + } + } + } + + /// @notice Ensure that there is no active curse. + modifier whenHealthy() { + if (IRMN(i_armProxy).isCursed()) revert BadARMSignal(); + _; + } +} + +abstract contract BurnMintTokenPoolAbstract is TokenPool1_4 { + /// @notice Contains the specific burn call for a pool. + /// @dev overriding this method allows us to create pools with different burn signatures + /// without duplicating the underlying logic. + function _burn(uint256 amount) internal virtual; + + /// @notice Burn the token in the pool + /// @param amount Amount to burn + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function lockOrBurn( + address originalSender, + bytes calldata, + uint256 amount, + uint64 remoteChainSelector, + bytes calldata + ) + external + virtual + override + onlyOnRamp(remoteChainSelector) + checkAllowList(originalSender) + whenHealthy + returns (bytes memory) + { + _consumeOutboundRateLimit(remoteChainSelector, amount); + _burn(amount); + emit Burned(msg.sender, amount); + return ""; + } + + /// @notice Mint tokens from the pool to the recipient + /// @param receiver Recipient address + /// @param amount Amount to mint + /// @dev The whenHealthy check is important to ensure that even if a ramp is compromised + /// we're able to stop token movement via ARM. + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external virtual override whenHealthy onlyOffRamp(remoteChainSelector) { + _consumeInboundRateLimit(remoteChainSelector, amount); + IBurnMintERC20(address(i_token)).mint(receiver, amount); + emit Minted(msg.sender, receiver, amount); + } +} + +/// @notice This pool mints and burns a 3rd-party token. +/// @dev Pool whitelisting mode is set in the constructor and cannot be modified later. +/// It either accepts any address as originalSender, or only accepts whitelisted originalSender. +/// The only way to change whitelisting mode is to deploy a new pool. +/// If that is expected, please make sure the token's burner/minter roles are adjustable. +contract BurnMintTokenPool1_4 is BurnMintTokenPoolAbstract, ITypeAndVersion { + string public constant override typeAndVersion = "BurnMintTokenPool 1.4.0"; + + constructor( + IBurnMintERC20 token, + address[] memory allowlist, + address armProxy, + address router + ) TokenPool1_4(token, allowlist, armProxy, router) {} + + /// @inheritdoc BurnMintTokenPoolAbstract + function _burn(uint256 amount) internal virtual override { + IBurnMintERC20(address(i_token)).burn(amount); + } +} diff --git a/contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol b/contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol new file mode 100644 index 00000000000..292ac9a3bfd --- /dev/null +++ b/contracts/src/v0.8/ccip/test/legacy/TokenPoolAndProxy.t.sol @@ -0,0 +1,771 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IPoolV1} from "../../interfaces/IPool.sol"; +import {IPoolPriorTo1_5} from "../../interfaces/IPoolPriorTo1_5.sol"; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {PriceRegistry} from "../../PriceRegistry.sol"; +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {BurnMintTokenPoolAndProxy} from "../../pools/BurnMintTokenPoolAndProxy.sol"; +import {LockReleaseTokenPoolAndProxy} from "../../pools/LockReleaseTokenPoolAndProxy.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {TokenSetup} from "../TokenSetup.t.sol"; +import {EVM2EVMOnRampHelper} from "../helpers/EVM2EVMOnRampHelper.sol"; +import {EVM2EVMOnRampSetup} from "../onRamp/EVM2EVMOnRampSetup.t.sol"; +import {RouterSetup} from "../router/RouterSetup.t.sol"; +import {BurnMintTokenPool1_2, TokenPool1_2} from "./BurnMintTokenPool1_2.sol"; +import {BurnMintTokenPool1_4, TokenPool1_4} from "./BurnMintTokenPool1_4.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; + +contract TokenPoolAndProxyMigration is EVM2EVMOnRampSetup { + BurnMintTokenPoolAndProxy internal s_newPool; + IPoolPriorTo1_5 internal s_legacyPool; + BurnMintERC677 internal s_token; + + address internal s_offRamp; + address internal s_sourcePool = makeAddr("source_pool"); + address internal s_sourceToken = makeAddr("source_token"); + uint256 internal constant AMOUNT = 1; + + function setUp() public virtual override { + super.setUp(); + // Create a system with a token and a legacy pool + s_token = new BurnMintERC677("Test", "TEST", 18, type(uint256).max); + // dealing doesn't update the total supply, meaning the first time we burn a token we underflow, which isn't + // guarded against. Then, when we mint a token, we overflow, which is guarded against and will revert. + s_token.grantMintAndBurnRoles(OWNER); + s_token.mint(OWNER, 1e18); + + s_offRamp = s_offRamps[0]; + // Approve enough for a few calls + s_token.approve(address(s_sourceRouter), AMOUNT * 100); + + // Approve infinite fee tokens + IERC20(s_sourceFeeToken).approve(address(s_sourceRouter), type(uint256).max); + } + + /// @notice This test covers the entire migration plan for 1.0-1.2 pools to 1.5 pools. For simplicity + /// we will refer to the 1.0/1.2 pools as 1.2 pools, as they are functionally the same. + function test_tokenPoolMigration_Success_1_2() public { + // ================================================================ + // | 1 1.2 prior to upgrade | + // ================================================================ + _deployPool1_2(); + + // Ensure everything works on the 1.2 pool + _ccipSend_OLD(); + _fakeReleaseOrMintFromOffRamp_OLD(); + + // ================================================================ + // | 2 Deploy self serve | + // ================================================================ + _deploySelfServe(); + + // This doesn't impact the 1.2 pool, so it should still be functional + _ccipSend_OLD(); + _fakeReleaseOrMintFromOffRamp_OLD(); + + // ================================================================ + // | 3 Configure new pool on old pool | + // ================================================================ + // In the 1.2 case, everything keeps working on both the 1.2 and 1.5 pools. This config can be + // done in advance of the actual swap to 1.5 lanes. + vm.startPrank(OWNER); + TokenPool1_2.RampUpdate[] memory rampUpdates = new TokenPool1_2.RampUpdate[](1); + rampUpdates[0] = TokenPool1_2.RampUpdate({ + ramp: address(s_newPool), + allowed: true, + // The rate limits should be turned off for this fake ramp, as the 1.5 pool will handle all the + // rate limiting for us. + rateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + // Since this call doesn't impact the usability of the old pool, we can do it whenever we want + BurnMintTokenPool1_2(address(s_legacyPool)).applyRampUpdates(rampUpdates, rampUpdates); + + // Assert the 1.2 lanes still work + _ccipSend_OLD(); + _fakeReleaseOrMintFromOffRamp_OLD(); + + // ================================================================ + // | 4 Update the router with to 1.5 | + // ================================================================ + + // This will stop any new messages entering the old lanes, and will direct all traffic to the + // new 1.5 lanes, and therefore to the 1.5 pools. Note that the old pools will still receive + // inflight messages, and will need to continue functioning until all of those are processed. + _fakeReleaseOrMintFromOffRamp_OLD(); + + // Everything is configured, we can now send a ccip tx to the new pool + _ccipSend1_5(); + _fakeReleaseOrMintFromOffRamp1_5(); + + // ================================================================ + // | 5 Migrate to using 1.5 the pool | + // ================================================================ + // Turn off the legacy pool, this enabled the 1.5 pool logic. This should be done AFTER the new pool + // has gotten permissions to mint/burn. We see the case where that isn't done below. + vm.startPrank(OWNER); + s_newPool.setPreviousPool(IPoolPriorTo1_5(address(0))); + + // The new pool is now active, but is has not been given permissions to burn/mint yet + vm.expectRevert(abi.encodeWithSelector(BurnMintERC677.SenderNotBurner.selector, address(s_newPool))); + _ccipSend1_5(); + vm.expectRevert(abi.encodeWithSelector(BurnMintERC677.SenderNotMinter.selector, address(s_newPool))); + _fakeReleaseOrMintFromOffRamp1_5(); + + // When we do give burn/mint, the new pool is fully active + vm.startPrank(OWNER); + s_token.grantMintAndBurnRoles(address(s_newPool)); + _ccipSend1_5(); + _fakeReleaseOrMintFromOffRamp1_5(); + + // Even after the pool has taken over as primary, the old pool can still process messages from the old lane + _fakeReleaseOrMintFromOffRamp_OLD(); + } + + function test_tokenPoolMigration_Success_1_4() public { + // ================================================================ + // | 1 1.4 prior to upgrade | + // ================================================================ + _deployPool1_4(); + + // Ensure everything works on the 1.4 pool + _ccipSend_OLD(); + _fakeReleaseOrMintFromOffRamp_OLD(); + + // ================================================================ + // | 2 Deploy self serve | + // ================================================================ + _deploySelfServe(); + + // This doesn't impact the 1.4 pool, so it should still be functional + _ccipSend_OLD(); + _fakeReleaseOrMintFromOffRamp_OLD(); + + // ================================================================ + // | 3 Configure new pool on old pool | + // | AND | + // | Update the router with to 1.5 | + // ================================================================ + // NOTE: when this call is made, the SENDING SIDE of old lanes stop working. + vm.startPrank(OWNER); + BurnMintTokenPool1_4(address(s_legacyPool)).setRouter(address(s_newPool)); + + // This will stop any new messages entering the old lanes, and will direct all traffic to the + // new 1.5 lanes, and therefore to the 1.5 pools. Note that the old pools will still receive + // inflight messages, and will need to continue functioning until all of those are processed. + _fakeReleaseOrMintFromOffRamp_OLD(); + + // Sending to the old 1.4 pool no longer works + _ccipSend_OLD_Reverts(); + + // Everything is configured, we can now send a ccip tx + _ccipSend1_5(); + _fakeReleaseOrMintFromOffRamp1_5(); + + // ================================================================ + // | 4 Migrate to using 1.5 the pool | + // ================================================================ + // Turn off the legacy pool, this enabled the 1.5 pool logic. This should be done AFTER the new pool + // has gotten permissions to mint/burn. We see the case where that isn't done below. + vm.startPrank(OWNER); + s_newPool.setPreviousPool(IPoolPriorTo1_5(address(0))); + + // The new pool is now active, but is has not been given permissions to burn/mint yet + vm.expectRevert(abi.encodeWithSelector(BurnMintERC677.SenderNotBurner.selector, address(s_newPool))); + _ccipSend1_5(); + vm.expectRevert(abi.encodeWithSelector(BurnMintERC677.SenderNotMinter.selector, address(s_newPool))); + _fakeReleaseOrMintFromOffRamp1_5(); + + // When we do give burn/mint, the new pool is fully active + vm.startPrank(OWNER); + s_token.grantMintAndBurnRoles(address(s_newPool)); + _ccipSend1_5(); + _fakeReleaseOrMintFromOffRamp1_5(); + + // Even after the pool has taken over as primary, the old pool can still process messages from the old lane + _fakeReleaseOrMintFromOffRamp_OLD(); + } + + function _ccipSend_OLD() internal { + // We send the funds to the pool manually, as the ramp normally does that + deal(address(s_token), address(s_legacyPool), AMOUNT); + vm.startPrank(address(s_onRamp)); + s_legacyPool.lockOrBurn(OWNER, abi.encode(OWNER), AMOUNT, DEST_CHAIN_SELECTOR, ""); + } + + function _ccipSend_OLD_Reverts() internal { + // We send the funds to the pool manually, as the ramp normally does that + deal(address(s_token), address(s_legacyPool), AMOUNT); + vm.startPrank(address(s_onRamp)); + + vm.expectRevert(abi.encodeWithSelector(TokenPool1_4.CallerIsNotARampOnRouter.selector, address(s_onRamp))); + + s_legacyPool.lockOrBurn(OWNER, abi.encode(OWNER), AMOUNT, DEST_CHAIN_SELECTOR, ""); + } + + function _ccipSend1_5() internal { + vm.startPrank(address(OWNER)); + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: address(s_token), amount: AMOUNT}); + + s_sourceRouter.ccipSend( + DEST_CHAIN_SELECTOR, + Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: s_sourceFeeToken, + extraArgs: "" + }) + ); + } + + function _fakeReleaseOrMintFromOffRamp1_5() internal { + // This is a fake call to simulate the release or mint from the "offRamp" + vm.startPrank(s_offRamp); + s_newPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + receiver: OWNER, + amount: AMOUNT, + localToken: address(s_token), + sourcePoolAddress: abi.encode(s_sourcePool), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + } + + function _fakeReleaseOrMintFromOffRamp_OLD() internal { + // This is a fake call to simulate the release or mint from the "offRamp" + vm.startPrank(s_offRamp); + s_legacyPool.releaseOrMint(abi.encode(OWNER), OWNER, AMOUNT, SOURCE_CHAIN_SELECTOR, ""); + } + + function _deployPool1_2() internal { + vm.startPrank(OWNER); + s_legacyPool = new BurnMintTokenPool1_2(s_token, new address[](0), address(s_mockRMN)); + s_token.grantMintAndBurnRoles(address(s_legacyPool)); + + TokenPool1_2.RampUpdate[] memory onRampUpdates = new TokenPool1_2.RampUpdate[](1); + onRampUpdates[0] = TokenPool1_2.RampUpdate({ + ramp: address(s_onRamp), + allowed: true, + rateLimiterConfig: getInboundRateLimiterConfig() + }); + TokenPool1_2.RampUpdate[] memory offRampUpdates = new TokenPool1_2.RampUpdate[](1); + offRampUpdates[0] = TokenPool1_2.RampUpdate({ + ramp: address(s_offRamp), + allowed: true, + rateLimiterConfig: getInboundRateLimiterConfig() + }); + BurnMintTokenPool1_2(address(s_legacyPool)).applyRampUpdates(onRampUpdates, offRampUpdates); + } + + function _deployPool1_4() internal { + vm.startPrank(OWNER); + s_legacyPool = new BurnMintTokenPool1_4(s_token, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + s_token.grantMintAndBurnRoles(address(s_legacyPool)); + + TokenPool1_4.ChainUpdate[] memory legacyChainUpdates = new TokenPool1_4.ChainUpdate[](2); + legacyChainUpdates[0] = TokenPool1_4.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + legacyChainUpdates[1] = TokenPool1_4.ChainUpdate({ + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + BurnMintTokenPool1_4(address(s_legacyPool)).applyChainUpdates(legacyChainUpdates); + } + + function _deploySelfServe() internal { + vm.startPrank(OWNER); + // Deploy the new pool + s_newPool = new BurnMintTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + // Set the previous pool on the new pool + s_newPool.setPreviousPool(s_legacyPool); + + // Configure the lanes just like the legacy pool + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](2); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_destTokenPool), + remoteTokenAddress: abi.encode(s_destToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + chainUpdates[1] = TokenPool.ChainUpdate({ + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_sourcePool), + remoteTokenAddress: abi.encode(s_sourceToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_newPool.applyChainUpdates(chainUpdates); + + // Register the token on the token admin registry + s_tokenAdminRegistry.proposeAdministrator(address(s_token), OWNER); + // Accept ownership of the token + s_tokenAdminRegistry.acceptAdminRole(address(s_token)); + // Set the pool on the admin registry + s_tokenAdminRegistry.setPool(address(s_token), address(s_newPool)); + } +} + +contract TokenPoolAndProxy is EVM2EVMOnRampSetup { + event Burned(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + + IPoolV1 internal s_pool; + BurnMintERC677 internal s_token; + IPoolPriorTo1_5 internal s_legacyPool; + address internal s_fakeOffRamp = makeAddr("off_ramp"); + + address internal s_destPool = makeAddr("dest_pool"); + + function setUp() public virtual override { + super.setUp(); + s_token = BurnMintERC677(s_sourceFeeToken); + + Router.OffRamp[] memory fakeOffRamps = new Router.OffRamp[](1); + fakeOffRamps[0] = Router.OffRamp({sourceChainSelector: DEST_CHAIN_SELECTOR, offRamp: s_fakeOffRamp}); + s_sourceRouter.applyRampUpdates(new Router.OnRamp[](0), new Router.OffRamp[](0), fakeOffRamps); + + s_token.grantMintAndBurnRoles(OWNER); + s_token.mint(OWNER, 1e18); + } + + function test_lockOrBurn_burnMint_Success() public { + s_pool = new BurnMintTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + _configurePool(); + _deployOldPool(); + _assertLockOrBurnCorrect(); + + vm.startPrank(OWNER); + BurnMintTokenPoolAndProxy(address(s_pool)).setPreviousPool(IPoolPriorTo1_5(address(0))); + + _assertReleaseOrMintCorrect(); + } + + function test_lockOrBurn_lockRelease_Success() public { + s_pool = + new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), false, address(s_sourceRouter)); + _configurePool(); + _deployOldPool(); + _assertLockOrBurnCorrect(); + + vm.startPrank(OWNER); + BurnMintTokenPoolAndProxy(address(s_pool)).setPreviousPool(IPoolPriorTo1_5(address(0))); + + _assertReleaseOrMintCorrect(); + } + + function _deployOldPool() internal { + s_legacyPool = new BurnMintTokenPool1_2(s_token, new address[](0), address(s_mockRMN)); + s_token.grantMintAndBurnRoles(address(s_legacyPool)); + + TokenPool1_2.RampUpdate[] memory onRampUpdates = new TokenPool1_2.RampUpdate[](1); + onRampUpdates[0] = + TokenPool1_2.RampUpdate({ramp: address(s_pool), allowed: true, rateLimiterConfig: getInboundRateLimiterConfig()}); + TokenPool1_2.RampUpdate[] memory offRampUpdates = new TokenPool1_2.RampUpdate[](1); + offRampUpdates[0] = + TokenPool1_2.RampUpdate({ramp: address(s_pool), allowed: true, rateLimiterConfig: getInboundRateLimiterConfig()}); + BurnMintTokenPool1_2(address(s_legacyPool)).applyRampUpdates(onRampUpdates, offRampUpdates); + } + + function _configurePool() internal { + TokenPool.ChainUpdate[] memory chains = new TokenPool.ChainUpdate[](1); + chains[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_destPool), + remoteTokenAddress: abi.encode(s_destToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + + BurnMintTokenPoolAndProxy(address(s_pool)).applyChainUpdates(chains); + + // CCIP Token Admin has already been registered from TokenSetup + s_tokenAdminRegistry.setPool(address(s_token), address(s_pool)); + + s_token.grantMintAndBurnRoles(address(s_pool)); + } + + function _assertLockOrBurnCorrect() internal { + uint256 amount = 1234; + vm.startPrank(address(s_onRamp)); + + // lockOrBurn, assert normal path is taken + deal(address(s_token), address(s_pool), amount); + + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + receiver: abi.encode(OWNER), + remoteChainSelector: DEST_CHAIN_SELECTOR, + originalSender: OWNER, + amount: amount, + localToken: address(s_token) + }) + ); + + // set legacy pool + + vm.startPrank(OWNER); + BurnMintTokenPoolAndProxy(address(s_pool)).setPreviousPool(s_legacyPool); + + // lockOrBurn, assert legacy pool is called + + vm.startPrank(address(s_onRamp)); + deal(address(s_token), address(s_pool), amount); + + vm.expectEmit(address(s_legacyPool)); + emit Burned(address(s_pool), amount); + + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + receiver: abi.encode(OWNER), + remoteChainSelector: DEST_CHAIN_SELECTOR, + originalSender: OWNER, + amount: amount, + localToken: address(s_token) + }) + ); + } + + function _assertReleaseOrMintCorrect() internal { + uint256 amount = 1234; + vm.startPrank(s_fakeOffRamp); + + // releaseOrMint, assert normal path is taken + deal(address(s_token), address(s_pool), amount); + + s_pool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + receiver: OWNER, + remoteChainSelector: DEST_CHAIN_SELECTOR, + originalSender: abi.encode(OWNER), + amount: amount, + localToken: address(s_token), + sourcePoolAddress: abi.encode(s_destPool), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + + // set legacy pool + + vm.startPrank(OWNER); + BurnMintTokenPoolAndProxy(address(s_pool)).setPreviousPool(s_legacyPool); + + // releaseOrMint, assert legacy pool is called + + vm.startPrank(address(s_fakeOffRamp)); + + vm.expectEmit(address(s_legacyPool)); + emit Minted(address(s_pool), s_fakeOffRamp, amount); + + s_pool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + receiver: OWNER, + remoteChainSelector: DEST_CHAIN_SELECTOR, + originalSender: abi.encode(OWNER), + amount: amount, + localToken: address(s_token), + sourcePoolAddress: abi.encode(s_destPool), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + } +} + +//// +/// Duplicated tests from LockReleaseTokenPool.t.sol +/// + +contract LockReleaseTokenPoolAndProxySetup is RouterSetup { + IERC20 internal s_token; + LockReleaseTokenPoolAndProxy internal s_lockReleaseTokenPoolAndProxy; + LockReleaseTokenPoolAndProxy internal s_lockReleaseTokenPoolAndProxyWithAllowList; + address[] internal s_allowedList; + + address internal s_allowedOnRamp = address(123); + address internal s_allowedOffRamp = address(234); + + address internal s_destPoolAddress = address(2736782345); + address internal s_sourcePoolAddress = address(53852352095); + + function setUp() public virtual override { + RouterSetup.setUp(); + s_token = new BurnMintERC677("LINK", "LNK", 18, 0); + deal(address(s_token), OWNER, type(uint256).max); + s_lockReleaseTokenPoolAndProxy = + new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), true, address(s_sourceRouter)); + + s_allowedList.push(USER_1); + s_allowedList.push(DUMMY_CONTRACT_ADDRESS); + s_lockReleaseTokenPoolAndProxyWithAllowList = + new LockReleaseTokenPoolAndProxy(s_token, s_allowedList, address(s_mockRMN), true, address(s_sourceRouter)); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_destPoolAddress), + remoteTokenAddress: abi.encode(address(s_token)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + + s_lockReleaseTokenPoolAndProxy.applyChainUpdates(chainUpdate); + s_lockReleaseTokenPoolAndProxyWithAllowList.applyChainUpdates(chainUpdate); + s_lockReleaseTokenPoolAndProxy.setRebalancer(OWNER); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: s_allowedOnRamp}); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: s_allowedOffRamp}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } +} + +contract LockReleaseTokenPoolAndProxy_setRebalancer is LockReleaseTokenPoolAndProxySetup { + function test_SetRebalancer_Success() public { + assertEq(address(s_lockReleaseTokenPoolAndProxy.getRebalancer()), OWNER); + s_lockReleaseTokenPoolAndProxy.setRebalancer(STRANGER); + assertEq(address(s_lockReleaseTokenPoolAndProxy.getRebalancer()), STRANGER); + } + + function test_SetRebalancer_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert("Only callable by owner"); + s_lockReleaseTokenPoolAndProxy.setRebalancer(STRANGER); + } +} + +contract LockReleaseTokenPoolPoolAndProxy_canAcceptLiquidity is LockReleaseTokenPoolAndProxySetup { + function test_CanAcceptLiquidity_Success() public { + assertEq(true, s_lockReleaseTokenPoolAndProxy.canAcceptLiquidity()); + + s_lockReleaseTokenPoolAndProxy = + new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), false, address(s_sourceRouter)); + assertEq(false, s_lockReleaseTokenPoolAndProxy.canAcceptLiquidity()); + } +} + +contract LockReleaseTokenPoolPoolAndProxy_provideLiquidity is LockReleaseTokenPoolAndProxySetup { + function test_Fuzz_ProvideLiquidity_Success(uint256 amount) public { + uint256 balancePre = s_token.balanceOf(OWNER); + s_token.approve(address(s_lockReleaseTokenPoolAndProxy), amount); + + s_lockReleaseTokenPoolAndProxy.provideLiquidity(amount); + + assertEq(s_token.balanceOf(OWNER), balancePre - amount); + assertEq(s_token.balanceOf(address(s_lockReleaseTokenPoolAndProxy)), amount); + } + + // Reverts + + function test_Unauthorized_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(LockReleaseTokenPoolAndProxy.Unauthorized.selector, STRANGER)); + + s_lockReleaseTokenPoolAndProxy.provideLiquidity(1); + } + + function test_Fuzz_ExceedsAllowance(uint256 amount) public { + vm.assume(amount > 0); + vm.expectRevert("ERC20: insufficient allowance"); + s_lockReleaseTokenPoolAndProxy.provideLiquidity(amount); + } + + function test_LiquidityNotAccepted_Revert() public { + s_lockReleaseTokenPoolAndProxy = + new LockReleaseTokenPoolAndProxy(s_token, new address[](0), address(s_mockRMN), false, address(s_sourceRouter)); + + vm.expectRevert(LockReleaseTokenPoolAndProxy.LiquidityNotAccepted.selector); + s_lockReleaseTokenPoolAndProxy.provideLiquidity(1); + } +} + +contract LockReleaseTokenPoolPoolAndProxy_withdrawalLiquidity is LockReleaseTokenPoolAndProxySetup { + function test_Fuzz_WithdrawalLiquidity_Success(uint256 amount) public { + uint256 balancePre = s_token.balanceOf(OWNER); + s_token.approve(address(s_lockReleaseTokenPoolAndProxy), amount); + s_lockReleaseTokenPoolAndProxy.provideLiquidity(amount); + + s_lockReleaseTokenPoolAndProxy.withdrawLiquidity(amount); + + assertEq(s_token.balanceOf(OWNER), balancePre); + } + + // Reverts + + function test_Unauthorized_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(LockReleaseTokenPoolAndProxy.Unauthorized.selector, STRANGER)); + + s_lockReleaseTokenPoolAndProxy.withdrawLiquidity(1); + } + + function test_InsufficientLiquidity_Revert() public { + uint256 maxUint256 = 2 ** 256 - 1; + s_token.approve(address(s_lockReleaseTokenPoolAndProxy), maxUint256); + s_lockReleaseTokenPoolAndProxy.provideLiquidity(maxUint256); + + vm.startPrank(address(s_lockReleaseTokenPoolAndProxy)); + s_token.transfer(OWNER, maxUint256); + vm.startPrank(OWNER); + + vm.expectRevert(LockReleaseTokenPoolAndProxy.InsufficientLiquidity.selector); + s_lockReleaseTokenPoolAndProxy.withdrawLiquidity(1); + } +} + +contract LockReleaseTokenPoolPoolAndProxy_supportsInterface is LockReleaseTokenPoolAndProxySetup { + function test_SupportsInterface_Success() public view { + assertTrue(s_lockReleaseTokenPoolAndProxy.supportsInterface(type(IPoolV1).interfaceId)); + assertTrue(s_lockReleaseTokenPoolAndProxy.supportsInterface(type(IERC165).interfaceId)); + } +} + +contract LockReleaseTokenPoolPoolAndProxy_setChainRateLimiterConfig is LockReleaseTokenPoolAndProxySetup { + event ConfigChanged(RateLimiter.Config); + event ChainConfigured( + uint64 chainSelector, RateLimiter.Config outboundRateLimiterConfig, RateLimiter.Config inboundRateLimiterConfig + ); + + uint64 internal s_remoteChainSelector; + + function setUp() public virtual override { + LockReleaseTokenPoolAndProxySetup.setUp(); + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + s_remoteChainSelector = 123124; + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: s_remoteChainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_lockReleaseTokenPoolAndProxy.applyChainUpdates(chainUpdates); + } + + function test_Fuzz_SetChainRateLimiterConfig_Success(uint128 capacity, uint128 rate, uint32 newTime) public { + // Cap the lower bound to 4 so 4/2 is still >= 2 + vm.assume(capacity >= 4); + // Cap the lower bound to 2 so 2/2 is still >= 1 + rate = uint128(bound(rate, 2, capacity - 2)); + // Bucket updates only work on increasing time + newTime = uint32(bound(newTime, block.timestamp + 1, type(uint32).max)); + vm.warp(newTime); + + uint256 oldOutboundTokens = + s_lockReleaseTokenPoolAndProxy.getCurrentOutboundRateLimiterState(s_remoteChainSelector).tokens; + uint256 oldInboundTokens = + s_lockReleaseTokenPoolAndProxy.getCurrentInboundRateLimiterState(s_remoteChainSelector).tokens; + + RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({isEnabled: true, capacity: capacity, rate: rate}); + RateLimiter.Config memory newInboundConfig = + RateLimiter.Config({isEnabled: true, capacity: capacity / 2, rate: rate / 2}); + + vm.expectEmit(); + emit ConfigChanged(newOutboundConfig); + vm.expectEmit(); + emit ConfigChanged(newInboundConfig); + vm.expectEmit(); + emit ChainConfigured(s_remoteChainSelector, newOutboundConfig, newInboundConfig); + + s_lockReleaseTokenPoolAndProxy.setChainRateLimiterConfig(s_remoteChainSelector, newOutboundConfig, newInboundConfig); + + uint256 expectedTokens = RateLimiter._min(newOutboundConfig.capacity, oldOutboundTokens); + + RateLimiter.TokenBucket memory bucket = + s_lockReleaseTokenPoolAndProxy.getCurrentOutboundRateLimiterState(s_remoteChainSelector); + assertEq(bucket.capacity, newOutboundConfig.capacity); + assertEq(bucket.rate, newOutboundConfig.rate); + assertEq(bucket.tokens, expectedTokens); + assertEq(bucket.lastUpdated, newTime); + + expectedTokens = RateLimiter._min(newInboundConfig.capacity, oldInboundTokens); + + bucket = s_lockReleaseTokenPoolAndProxy.getCurrentInboundRateLimiterState(s_remoteChainSelector); + assertEq(bucket.capacity, newInboundConfig.capacity); + assertEq(bucket.rate, newInboundConfig.rate); + assertEq(bucket.tokens, expectedTokens); + assertEq(bucket.lastUpdated, newTime); + } + + function test_OnlyOwnerOrRateLimitAdmin_Revert() public { + address rateLimiterAdmin = address(28973509103597907); + + s_lockReleaseTokenPoolAndProxy.setRateLimitAdmin(rateLimiterAdmin); + + vm.startPrank(rateLimiterAdmin); + + s_lockReleaseTokenPoolAndProxy.setChainRateLimiterConfig( + s_remoteChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + + vm.startPrank(OWNER); + + s_lockReleaseTokenPoolAndProxy.setChainRateLimiterConfig( + s_remoteChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + } + + // Reverts + + function test_OnlyOwner_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert(abi.encodeWithSelector(LockReleaseTokenPoolAndProxy.Unauthorized.selector, STRANGER)); + s_lockReleaseTokenPoolAndProxy.setChainRateLimiterConfig( + s_remoteChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + } + + function test_NonExistentChain_Revert() public { + uint64 wrongChainSelector = 9084102894; + + vm.expectRevert(abi.encodeWithSelector(TokenPool.NonExistentChain.selector, wrongChainSelector)); + s_lockReleaseTokenPoolAndProxy.setChainRateLimiterConfig( + wrongChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + } +} + +contract LockReleaseTokenPoolAndProxy_setRateLimitAdmin is LockReleaseTokenPoolAndProxySetup { + function test_SetRateLimitAdmin_Success() public { + assertEq(address(0), s_lockReleaseTokenPoolAndProxy.getRateLimitAdmin()); + s_lockReleaseTokenPoolAndProxy.setRateLimitAdmin(OWNER); + assertEq(OWNER, s_lockReleaseTokenPoolAndProxy.getRateLimitAdmin()); + } + + // Reverts + + function test_SetRateLimitAdmin_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert("Only callable by owner"); + s_lockReleaseTokenPoolAndProxy.setRateLimitAdmin(STRANGER); + } +} diff --git a/contracts/src/v0.8/ccip/test/libraries/MerkleMultiProof.t.sol b/contracts/src/v0.8/ccip/test/libraries/MerkleMultiProof.t.sol new file mode 100644 index 00000000000..e2fc9814d07 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/libraries/MerkleMultiProof.t.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {MerkleMultiProof} from "../../libraries/MerkleMultiProof.sol"; +import {MerkleHelper} from "../helpers/MerkleHelper.sol"; +import {Test} from "forge-std/Test.sol"; + +contract MerkleMultiProofTest is Test { + // This must match the spec + function test_SpecSync_gas() public pure { + bytes32 expectedRoot = 0xd4f0f3c40a4d583d98c17d89e550b1143fe4d3d759f25ccc63131c90b183928e; + + bytes32[] memory leaves = new bytes32[](10); + leaves[0] = 0xa20c0244af79697a4ef4e2378c9d5d14cbd49ddab3427b12594c7cfa67a7f240; + leaves[1] = 0x3de96afb24ce2ac45a5595aa13d1a5163ae0b3c94cef6b2dc306b5966f32dfa5; + leaves[2] = 0xacadf7b4d13cd57c5d25f1d27be39b656347fe8f8e0de8db9c76d979dff57736; + leaves[3] = 0xc21c26a709802fe1ae52a9cd8ad94d15bf142ded26314339cd87a13e5b468165; + leaves[4] = 0x55f6df03562738c9a6437cd9ad221c52b76906a175ae96188cff60e0a2a59933; + leaves[5] = 0x2dbbe66452e43fec839dc65d5945aad6433d410c65863eaf1d876e1e0b06343c; + leaves[6] = 0x8beab00297b94bf079fcd5893b0a33ebf6b0ce862cd06be07c87d3c63e1c4acf; + leaves[7] = 0xcabdd3ad25daeb1e0541042f2ea4cd177f54e67aa4a2c697acd4bb682e94de59; + leaves[8] = 0x7e01d497203685e99e34df33d55465c66b2253fa1630ee2fe5c4997968e4a6fa; + leaves[9] = 0x1a03d013f1e2fa9cc04f89c7528ac3216e3e096a1185d7247304e97c59f9661f; + + bytes32[] memory proofs = new bytes32[](33); + proofs[0] = 0xde96f24fcf9ddd20c803dc9c5fba7c478a5598a08a0faa5f032c65823b8e26a3; + proofs[1] = 0xe1303cffc3958a6b93e2dc04caf21f200ff5aa5be090c5013f37804b91488bc2; + proofs[2] = 0x90d80c76bccb44a91f4e16604976163aaa39e9a1588b0b24b33a61f1d4ba7bb5; + proofs[3] = 0x012a299b25539d513c8677ecf37968774e9e4b045e79737f48defd350224cdfd; + proofs[4] = 0x420a36c5a73f87d8fb98e70c48d0d6f9dd83f50b7b91416a6f5f91fac4db800f; + proofs[5] = 0x5857d8d1b56abcd7f863cedd3c3f8677256f54d675be61f05efa45d6495fc30a; + proofs[6] = 0xbf176d20166fdeb72593ff97efec1ce6244af41ca46cf0bc902d19d50c446f7b; + proofs[7] = 0xa9221608e4380250a1815fb308632bce99f611a673d2e17fc617123fdc6afcd2; + proofs[8] = 0xbd14f3366c73186314f182027217d0f70eba55817561de9e9a1f2c78bf5cbead; + proofs[9] = 0x2f9aa48c0c9f82aaac65d7a9374a52d9dc138ed100a5809ede57e70697f48b56; + proofs[10] = 0x2ae60afa54271cb421c12e4441c2dac0a25f25c9433a6d07cb32419e993fe344; + proofs[11] = 0xc765c091680f0434b74c44507b932e5c80f6e995a975a275e5b130af1de1064c; + proofs[12] = 0x59d2d6e0c4a5d07b169dbcdfa39dad7aea7b7783a814399f4f44c4a36b6336d3; + proofs[13] = 0xdd14d1387d10740187d71ad9500475399559c0922dbe2576882e61f1edd84692; + proofs[14] = 0x5412b8395509935406811ab3da43ab80be7acd8ffb5f398ab70f056ff3740f46; + proofs[15] = 0xeadab258ae7d779ce5f10fbb1bb0273116b8eccbf738ed878db570de78bed1e4; + proofs[16] = 0x6133aa40e6db75373b7cfc79e6f8b8ce80e441e6c1f98b85a593464dda3cf9c0; + proofs[17] = 0x5418948467112660639b932af9b1b212e40d71b24326b4606679d168a765af4f; + proofs[18] = 0x44f618505355c7e4e7c0f81d6bb15d2ec9cf9b366f9e1dc37db52745486e6b0f; + proofs[19] = 0xa410ee174a66a4d64f3c000b93efe15b5b1f3e39e962af2580fcd30bce07d039; + proofs[20] = 0x09c3eb05ac9552022a45c00d01a47cd56f95f94afdd4402299dba1291a17f976; + proofs[21] = 0x0e780f6acd081b07320a55208fa3e1d884e2e95cb13d1c98c74b7e853372c813; + proofs[22] = 0x2b60e8c21f78ef22fa4297f28f1d8c747181edfc465121b39c16be97d4fb8a04; + proofs[23] = 0xf24da95060a8598c06e9dfb3926e1a8c8bd8ec2c65be10e69323442840724888; + proofs[24] = 0x7e220fc095bcd2b0f5ef134d9620d89f6d7a1e8719ce8893bb9aff15e847578f; + proofs[25] = 0xcfe9e475c4bd32f1e36b2cc65a959c403c59979ff914fb629a64385b0c680a71; + proofs[26] = 0x25237fb8d1bfdc01ca5363ec3166a2b40789e38d5adcc8627801da683d2e1d76; + proofs[27] = 0x42647949fed0250139c01212d739d8c83d2852589ebc892d3490ae52e411432c; + proofs[28] = 0x34397a30930e6dd4fb5af48084afc5cfbe02c18dd9544b3faff4e2e90bf00cb9; + proofs[29] = 0xa028f33226adc3d1cb72b19eb6808dab9190b25066a45cacb5dfe5d640e57cf2; + proofs[30] = 0x7cff66ba47a05f932d06d168c294266dcb0d3943a4f2a4a75c860b9fd6e53092; + proofs[31] = 0x5ca1b32f1dbfadd83205882be5eb76f34c49e834726f5239905a0e70d0a5e0eb; + proofs[32] = 0x1b4b087a89e4eca6cdd237210932559dc8fd167d5f4f2d9acb13264e1e305479; + + uint256 flagsUint256 = 0x2f3c0000000; + + bytes32 root = MerkleMultiProof.merkleRoot(leaves, proofs, flagsUint256); + + assertEq(expectedRoot, root); + } + + function test_Fuzz_MerkleRoot2(bytes32 left, bytes32 right) public pure { + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = left; + leaves[1] = right; + bytes32[] memory proofs = new bytes32[](0); + + bytes32 expectedRoot = MerkleHelper.hashPair(left, right); + + bytes32 root = MerkleMultiProof.merkleRoot(leaves, proofs, 2 ** 2 - 1); + + assertEq(root, expectedRoot); + } + + function test_MerkleRoot256() public pure { + bytes32[] memory leaves = new bytes32[](256); + for (uint256 i = 0; i < leaves.length; ++i) { + leaves[i] = keccak256("a"); + } + bytes32[] memory proofs = new bytes32[](0); + + bytes32 expectedRoot = MerkleHelper.getMerkleRoot(leaves); + + bytes32 root = MerkleMultiProof.merkleRoot(leaves, proofs, 2 ** 256 - 1); + + assertEq(root, expectedRoot); + } + + function test_Fuzz_MerkleMulti1of4(bytes32 leaf1, bytes32 proof1, bytes32 proof2) public pure { + bytes32[] memory leaves = new bytes32[](1); + leaves[0] = leaf1; + bytes32[] memory proofs = new bytes32[](2); + proofs[0] = proof1; + proofs[1] = proof2; + + // Proof flag = false + bytes32 result = MerkleHelper.hashPair(leaves[0], proofs[0]); + // Proof flag = false + result = MerkleHelper.hashPair(result, proofs[1]); + + assertEq(MerkleMultiProof.merkleRoot(leaves, proofs, 0), result); + } + + function test_Fuzz_MerkleMulti2of4(bytes32 leaf1, bytes32 leaf2, bytes32 proof1, bytes32 proof2) public pure { + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = leaf1; + leaves[1] = leaf2; + bytes32[] memory proofs = new bytes32[](2); + proofs[0] = proof1; + proofs[1] = proof2; + + // Proof flag = false + bytes32 result1 = MerkleHelper.hashPair(leaves[0], proofs[0]); + // Proof flag = false + bytes32 result2 = MerkleHelper.hashPair(leaves[1], proofs[1]); + // Proof flag = true + bytes32 finalResult = MerkleHelper.hashPair(result1, result2); + + assertEq(MerkleMultiProof.merkleRoot(leaves, proofs, 4), finalResult); + } + + function test_Fuzz_MerkleMulti3of4(bytes32 leaf1, bytes32 leaf2, bytes32 leaf3, bytes32 proof) public pure { + bytes32[] memory leaves = new bytes32[](3); + leaves[0] = leaf1; + leaves[1] = leaf2; + leaves[2] = leaf3; + bytes32[] memory proofs = new bytes32[](1); + proofs[0] = proof; + + // Proof flag = true + bytes32 result1 = MerkleHelper.hashPair(leaves[0], leaves[1]); + // Proof flag = false + bytes32 result2 = MerkleHelper.hashPair(leaves[2], proofs[0]); + // Proof flag = true + bytes32 finalResult = MerkleHelper.hashPair(result1, result2); + + assertEq(MerkleMultiProof.merkleRoot(leaves, proofs, 5), finalResult); + } + + function test_Fuzz_MerkleMulti4of4(bytes32 leaf1, bytes32 leaf2, bytes32 leaf3, bytes32 leaf4) public pure { + bytes32[] memory leaves = new bytes32[](4); + leaves[0] = leaf1; + leaves[1] = leaf2; + leaves[2] = leaf3; + leaves[3] = leaf4; + bytes32[] memory proofs = new bytes32[](0); + + // Proof flag = true + bytes32 result1 = MerkleHelper.hashPair(leaves[0], leaves[1]); + // Proof flag = true + bytes32 result2 = MerkleHelper.hashPair(leaves[2], leaves[3]); + // Proof flag = true + bytes32 finalResult = MerkleHelper.hashPair(result1, result2); + + assertEq(MerkleMultiProof.merkleRoot(leaves, proofs, 7), finalResult); + } + + function test_MerkleRootSingleLeaf_Success() public pure { + bytes32[] memory leaves = new bytes32[](1); + leaves[0] = "root"; + bytes32[] memory proofs = new bytes32[](0); + assertEq(MerkleMultiProof.merkleRoot(leaves, proofs, 0), leaves[0]); + } + + function test_EmptyLeaf_Revert() public { + bytes32[] memory leaves = new bytes32[](0); + bytes32[] memory proofs = new bytes32[](0); + + vm.expectRevert(abi.encodeWithSelector(MerkleMultiProof.LeavesCannotBeEmpty.selector)); + MerkleMultiProof.merkleRoot(leaves, proofs, 0); + } + + function test_CVE_2023_34459() public { + bytes32[] memory leaves = new bytes32[](2); + // leaves[0] stays uninitialized, i.e., 0x000...0 + leaves[1] = "leaf"; + + bytes32[] memory proof = new bytes32[](2); + proof[0] = leaves[1]; + proof[1] = "will never be used"; + + bytes32[] memory malicious = new bytes32[](2); + malicious[0] = "malicious leaf"; + malicious[1] = "another malicious leaf"; + + vm.expectRevert(abi.encodeWithSelector(MerkleMultiProof.InvalidProof.selector)); + MerkleMultiProof.merkleRoot(malicious, proof, 3); + // Note, that without the revert the above computed root + // would equal MerkleHelper.hashPair(leaves[0], leaves[1]). + } +} diff --git a/contracts/src/v0.8/ccip/test/libraries/RateLimiter.t.sol b/contracts/src/v0.8/ccip/test/libraries/RateLimiter.t.sol new file mode 100644 index 00000000000..da6a6f9ada7 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/libraries/RateLimiter.t.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {RateLimiterHelper} from "../helpers/RateLimiterHelper.sol"; +import {Test} from "forge-std/Test.sol"; + +contract RateLimiterSetup is Test { + RateLimiterHelper internal s_helper; + RateLimiter.Config internal s_config; + + uint256 internal constant BLOCK_TIME = 1234567890; + + function setUp() public virtual { + s_config = RateLimiter.Config({isEnabled: true, rate: 5, capacity: 100}); + s_helper = new RateLimiterHelper(s_config); + } +} + +contract RateLimiter_constructor is RateLimiterSetup { + function test_Constructor_Success() public view { + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.rate, rateLimiter.rate); + assertEq(s_config.capacity, rateLimiter.capacity); + assertEq(s_config.capacity, rateLimiter.tokens); + assertEq(s_config.isEnabled, rateLimiter.isEnabled); + assertEq(BLOCK_TIME, rateLimiter.lastUpdated); + } +} + +contract RateLimiter_setTokenBucketConfig is RateLimiterSetup { + function test_SetRateLimiterConfig_Success() public { + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.rate, rateLimiter.rate); + assertEq(s_config.capacity, rateLimiter.capacity); + + s_config = + RateLimiter.Config({isEnabled: true, rate: uint128(rateLimiter.rate * 2), capacity: rateLimiter.capacity * 8}); + + vm.expectEmit(); + emit RateLimiter.ConfigChanged(s_config); + + s_helper.setTokenBucketConfig(s_config); + + rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.rate, rateLimiter.rate); + assertEq(s_config.capacity, rateLimiter.capacity); + assertEq(s_config.capacity / 8, rateLimiter.tokens); + assertEq(s_config.isEnabled, rateLimiter.isEnabled); + assertEq(BLOCK_TIME, rateLimiter.lastUpdated); + } +} + +contract RateLimiter_currentTokenBucketState is RateLimiterSetup { + function test_CurrentTokenBucketState_Success() public { + RateLimiter.TokenBucket memory bucket = s_helper.currentTokenBucketState(); + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.capacity, bucket.tokens); + assertEq(s_config.isEnabled, bucket.isEnabled); + assertEq(BLOCK_TIME, bucket.lastUpdated); + + s_config = RateLimiter.Config({isEnabled: true, rate: uint128(bucket.rate * 2), capacity: bucket.capacity * 8}); + + s_helper.setTokenBucketConfig(s_config); + + bucket = s_helper.currentTokenBucketState(); + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.capacity / 8, bucket.tokens); + assertEq(s_config.isEnabled, bucket.isEnabled); + assertEq(BLOCK_TIME, bucket.lastUpdated); + } + + function test_Refill_Success() public { + RateLimiter.TokenBucket memory bucket = s_helper.currentTokenBucketState(); + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.capacity, bucket.tokens); + assertEq(s_config.isEnabled, bucket.isEnabled); + assertEq(BLOCK_TIME, bucket.lastUpdated); + + s_config = RateLimiter.Config({isEnabled: true, rate: uint128(bucket.rate * 2), capacity: bucket.capacity * 8}); + + s_helper.setTokenBucketConfig(s_config); + + bucket = s_helper.currentTokenBucketState(); + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.capacity / 8, bucket.tokens); + assertEq(s_config.isEnabled, bucket.isEnabled); + assertEq(BLOCK_TIME, bucket.lastUpdated); + + uint256 warpTime = 4; + vm.warp(BLOCK_TIME + warpTime); + + bucket = s_helper.currentTokenBucketState(); + + assertEq(s_config.capacity / 8 + warpTime * s_config.rate, bucket.tokens); + + vm.warp(BLOCK_TIME + warpTime * 100); + + // Bucket overflow + bucket = s_helper.currentTokenBucketState(); + assertEq(s_config.capacity, bucket.tokens); + } +} + +contract RateLimiter_consume is RateLimiterSetup { + address internal s_token = address(100); + + function test_ConsumeAggregateValue_Success() public { + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.rate, rateLimiter.rate); + assertEq(s_config.capacity, rateLimiter.capacity); + assertEq(s_config.capacity, rateLimiter.tokens); + assertEq(s_config.isEnabled, rateLimiter.isEnabled); + assertEq(BLOCK_TIME, rateLimiter.lastUpdated); + + uint256 requestTokens = 50; + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(requestTokens); + + s_helper.consume(requestTokens, address(0)); + + rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.rate, rateLimiter.rate); + assertEq(s_config.capacity, rateLimiter.capacity); + assertEq(s_config.capacity - requestTokens, rateLimiter.tokens); + assertEq(s_config.isEnabled, rateLimiter.isEnabled); + assertEq(BLOCK_TIME, rateLimiter.lastUpdated); + } + + function test_ConsumeTokens_Success() public { + uint256 requestTokens = 50; + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(requestTokens); + + s_helper.consume(requestTokens, s_token); + } + + function test_Refill_Success() public { + uint256 requestTokens = 50; + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(requestTokens); + + s_helper.consume(requestTokens, address(0)); + + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.rate, rateLimiter.rate); + assertEq(s_config.capacity, rateLimiter.capacity); + assertEq(s_config.capacity - requestTokens, rateLimiter.tokens); + assertEq(s_config.isEnabled, rateLimiter.isEnabled); + assertEq(BLOCK_TIME, rateLimiter.lastUpdated); + + uint256 warpTime = 4; + vm.warp(BLOCK_TIME + warpTime); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(requestTokens); + + s_helper.consume(requestTokens, address(0)); + + rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.rate, rateLimiter.rate); + assertEq(s_config.capacity, rateLimiter.capacity); + assertEq(s_config.capacity - requestTokens * 2 + warpTime * s_config.rate, rateLimiter.tokens); + assertEq(s_config.isEnabled, rateLimiter.isEnabled); + assertEq(BLOCK_TIME + warpTime, rateLimiter.lastUpdated); + } + + function test_ConsumeUnlimited_Success() public { + s_helper.consume(0, address(0)); + + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.capacity, rateLimiter.tokens); + assertEq(s_config.isEnabled, rateLimiter.isEnabled); + + RateLimiter.Config memory disableConfig = RateLimiter.Config({isEnabled: false, rate: 0, capacity: 0}); + + s_helper.setTokenBucketConfig(disableConfig); + + uint256 requestTokens = 50; + s_helper.consume(requestTokens, address(0)); + + rateLimiter = s_helper.getRateLimiter(); + assertEq(disableConfig.capacity, rateLimiter.tokens); + assertEq(disableConfig.isEnabled, rateLimiter.isEnabled); + + s_helper.setTokenBucketConfig(s_config); + + vm.expectRevert(abi.encodeWithSelector(RateLimiter.AggregateValueRateLimitReached.selector, 10, 0)); + s_helper.consume(requestTokens, address(0)); + + rateLimiter = s_helper.getRateLimiter(); + assertEq(s_config.rate, rateLimiter.rate); + assertEq(s_config.capacity, rateLimiter.capacity); + assertEq(0, rateLimiter.tokens); + assertEq(s_config.isEnabled, rateLimiter.isEnabled); + } + + // Reverts + + function test_AggregateValueMaxCapacityExceeded_Revert() public { + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiter.AggregateValueMaxCapacityExceeded.selector, rateLimiter.capacity, rateLimiter.capacity + 1 + ) + ); + s_helper.consume(rateLimiter.capacity + 1, address(0)); + } + + function test_TokenMaxCapacityExceeded_Revert() public { + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiter.TokenMaxCapacityExceeded.selector, rateLimiter.capacity, rateLimiter.capacity + 1, s_token + ) + ); + s_helper.consume(rateLimiter.capacity + 1, s_token); + } + + function test_ConsumingMoreThanUint128_Revert() public { + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + + uint256 request = uint256(type(uint128).max) + 1; + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.AggregateValueMaxCapacityExceeded.selector, rateLimiter.capacity, request) + ); + s_helper.consume(request, address(0)); + } + + function test_AggregateValueRateLimitReached_Revert() public { + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + + uint256 overLimit = 20; + uint256 requestTokens1 = rateLimiter.capacity / 2; + uint256 requestTokens2 = rateLimiter.capacity / 2 + overLimit; + + uint256 waitInSeconds = overLimit / rateLimiter.rate; + + s_helper.consume(requestTokens1, address(0)); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiter.AggregateValueRateLimitReached.selector, waitInSeconds, rateLimiter.capacity - requestTokens1 + ) + ); + s_helper.consume(requestTokens2, address(0)); + } + + function test_TokenRateLimitReached_Revert() public { + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + + uint256 overLimit = 20; + uint256 requestTokens1 = rateLimiter.capacity / 2; + uint256 requestTokens2 = rateLimiter.capacity / 2 + overLimit; + + uint256 waitInSeconds = overLimit / rateLimiter.rate; + + s_helper.consume(requestTokens1, s_token); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiter.TokenRateLimitReached.selector, waitInSeconds, rateLimiter.capacity - requestTokens1, s_token + ) + ); + s_helper.consume(requestTokens2, s_token); + } + + function test_RateLimitReachedOverConsecutiveBlocks_Revert() public { + uint256 initBlockTime = BLOCK_TIME + 10000; + vm.warp(initBlockTime); + + RateLimiter.TokenBucket memory rateLimiter = s_helper.getRateLimiter(); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(rateLimiter.capacity); + + s_helper.consume(rateLimiter.capacity, address(0)); + + vm.warp(initBlockTime + 1); + + // Over rate limit by 1, force 1 second wait + uint256 overLimit = 1; + + vm.expectRevert(abi.encodeWithSelector(RateLimiter.AggregateValueRateLimitReached.selector, 1, rateLimiter.rate)); + s_helper.consume(rateLimiter.rate + overLimit, address(0)); + } +} diff --git a/contracts/src/v0.8/ccip/test/mocks/MockCommitStore.sol b/contracts/src/v0.8/ccip/test/mocks/MockCommitStore.sol new file mode 100644 index 00000000000..aff06016fa5 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/MockCommitStore.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ICommitStore} from "../../interfaces/ICommitStore.sol"; + +contract MockCommitStore is ICommitStore { + error PausedError(); + + uint64 private s_expectedNextSequenceNumber = 1; + + bool private s_paused = false; + + /// @inheritdoc ICommitStore + function verify( + bytes32[] calldata, + bytes32[] calldata, + uint256 + ) external view whenNotPaused returns (uint256 timestamp) { + return 1; + } + + function getExpectedNextSequenceNumber() external view returns (uint64) { + return s_expectedNextSequenceNumber; + } + + function setExpectedNextSequenceNumber(uint64 nextSeqNum) external { + s_expectedNextSequenceNumber = nextSeqNum; + } + + modifier whenNotPaused() { + if (paused()) revert PausedError(); + _; + } + + function paused() public view returns (bool) { + return s_paused; + } + + function pause() external { + s_paused = true; + } +} diff --git a/contracts/src/v0.8/ccip/test/mocks/MockE2EUSDCTokenMessenger.sol b/contracts/src/v0.8/ccip/test/mocks/MockE2EUSDCTokenMessenger.sol new file mode 100644 index 00000000000..9fa5cd1a66d --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/MockE2EUSDCTokenMessenger.sol @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2022, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.8.24; + +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; +import {ITokenMessenger} from "../../pools/USDC/ITokenMessenger.sol"; +import {IMessageTransmitterWithRelay} from "./interfaces/IMessageTransmitterWithRelay.sol"; + +// This contract mocks both the ITokenMessenger and IMessageTransmitter +// contracts involved with the Cross Chain Token Protocol. +contract MockE2EUSDCTokenMessenger is ITokenMessenger { + uint32 private immutable i_messageBodyVersion; + address private immutable i_transmitter; + + bytes32 public constant DESTINATION_TOKEN_MESSENGER = keccak256("i_destinationTokenMessenger"); + + uint64 public s_nonce; + + // Local Message Transmitter responsible for sending and receiving messages to/from remote domains + IMessageTransmitterWithRelay public immutable localMessageTransmitterWithRelay; + + constructor(uint32 version, address transmitter) { + i_messageBodyVersion = version; + s_nonce = 1; + i_transmitter = transmitter; + localMessageTransmitterWithRelay = IMessageTransmitterWithRelay(transmitter); + } + + // The mock function is based on the same function in https://github.com/circlefin/evm-cctp-contracts/blob/master/src/TokenMessenger.sol + function depositForBurnWithCaller( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller + ) external returns (uint64) { + IBurnMintERC20(burnToken).transferFrom(msg.sender, address(this), amount); + IBurnMintERC20(burnToken).burn(amount); + // Format message body + bytes memory _burnMessage = + abi.encodePacked(i_messageBodyVersion, burnToken, mintRecipient, amount, bytes32(uint256(uint160((msg.sender))))); + s_nonce = + _sendDepositForBurnMessage(destinationDomain, DESTINATION_TOKEN_MESSENGER, destinationCaller, _burnMessage); + emit DepositForBurn( + s_nonce, + burnToken, + amount, + msg.sender, + mintRecipient, + destinationDomain, + DESTINATION_TOKEN_MESSENGER, + destinationCaller + ); + return s_nonce; + } + + function messageBodyVersion() external view returns (uint32) { + return i_messageBodyVersion; + } + + function localMessageTransmitter() external view returns (address) { + return i_transmitter; + } + + /** + * @notice Sends a BurnMessage through the local message transmitter + * @dev calls local message transmitter's sendMessage() function if `_destinationCaller` == bytes32(0), + * or else calls sendMessageWithCaller(). + * @param _destinationDomain destination domain + * @param _destinationTokenMessenger address of registered TokenMessenger contract on destination domain, as bytes32 + * @param _destinationCaller caller on the destination domain, as bytes32. If `_destinationCaller` == bytes32(0), + * any address can call receiveMessage() on destination domain. + * @param _burnMessage formatted BurnMessage bytes (message body) + * @return nonce unique nonce reserved by message + */ + function _sendDepositForBurnMessage( + uint32 _destinationDomain, + bytes32 _destinationTokenMessenger, + bytes32 _destinationCaller, + bytes memory _burnMessage + ) internal returns (uint64 nonce) { + if (_destinationCaller == bytes32(0)) { + return localMessageTransmitterWithRelay.sendMessage(_destinationDomain, _destinationTokenMessenger, _burnMessage); + } else { + return localMessageTransmitterWithRelay.sendMessageWithCaller( + _destinationDomain, _destinationTokenMessenger, _destinationCaller, _burnMessage + ); + } + } +} diff --git a/contracts/src/v0.8/ccip/test/mocks/MockE2EUSDCTransmitter.sol b/contracts/src/v0.8/ccip/test/mocks/MockE2EUSDCTransmitter.sol new file mode 100644 index 00000000000..8e50bedea99 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/MockE2EUSDCTransmitter.sol @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2022, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity ^0.8.0; + +import {IMessageTransmitterWithRelay} from "./interfaces/IMessageTransmitterWithRelay.sol"; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; + +contract MockE2EUSDCTransmitter is IMessageTransmitterWithRelay { + // Indicated whether the receiveMessage() call should succeed. + bool public s_shouldSucceed; + uint32 private immutable i_version; + uint32 private immutable i_localDomain; + // Next available nonce from this source domain + uint64 public nextAvailableNonce; + + BurnMintERC677 internal immutable i_token; + + /** + * @notice Emitted when a new message is dispatched + * @param message Raw bytes of message + */ + event MessageSent(bytes message); + + constructor(uint32 _version, uint32 _localDomain, address token) { + i_version = _version; + i_localDomain = _localDomain; + s_shouldSucceed = true; + + i_token = BurnMintERC677(token); + } + + /// @param message The original message on the source chain + /// * Message format: + /// * Field Bytes Type Index + /// * version 4 uint32 0 + /// * sourceDomain 4 uint32 4 + /// * destinationDomain 4 uint32 8 + /// * nonce 8 uint64 12 + /// * sender 32 bytes32 20 + /// * recipient 32 bytes32 52 + /// * destinationCaller 32 bytes32 84 + /// * messageBody dynamic bytes 116 + function receiveMessage(bytes calldata message, bytes calldata) external returns (bool success) { + address recipient = address(bytes20(message[64:84])); + + // We always mint 1000e18 tokens to not complicate the test. + i_token.mint(recipient, 1000e18); + + return s_shouldSucceed; + } + + function setShouldSucceed(bool shouldSucceed) external { + s_shouldSucceed = shouldSucceed; + } + + function version() external view returns (uint32) { + return i_version; + } + + function localDomain() external view returns (uint32) { + return i_localDomain; + } + + /** + * This is based on similar function in https://github.com/circlefin/evm-cctp-contracts/blob/master/src/MessageTransmitter.sol + * @notice Send the message to the destination domain and recipient + * @dev Increment nonce, format the message, and emit `MessageSent` event with message information. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination chain as bytes32 + * @param messageBody Raw bytes content of message + * @return nonce reserved by message + */ + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external returns (uint64) { + bytes32 _emptyDestinationCaller = bytes32(0); + uint64 _nonce = _reserveAndIncrementNonce(); + bytes32 _messageSender = bytes32(uint256(uint160((msg.sender)))); + + _sendMessage(destinationDomain, recipient, _emptyDestinationCaller, _messageSender, _nonce, messageBody); + + return _nonce; + } + + /** + * @notice Send the message to the destination domain and recipient, for a specified `destinationCaller` on the + * destination domain. + * @dev Increment nonce, format the message, and emit `MessageSent` event with message information. + * WARNING: if the `destinationCaller` does not represent a valid address, then it will not be possible + * to broadcast the message on the destination domain. This is an advanced feature, and the standard + * sendMessage() should be preferred for use cases where a specific destination caller is not required. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination domain as bytes32 + * @param destinationCaller caller on the destination domain, as bytes32 + * @param messageBody Raw bytes content of message + * @return nonce reserved by message + */ + function sendMessageWithCaller( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + bytes calldata messageBody + ) external returns (uint64) { + require(destinationCaller != bytes32(0), "Destination caller must be nonzero"); + + uint64 _nonce = _reserveAndIncrementNonce(); + bytes32 _messageSender = bytes32(uint256(uint160((msg.sender)))); + + _sendMessage(destinationDomain, recipient, destinationCaller, _messageSender, _nonce, messageBody); + + return _nonce; + } + + /** + * Reserve and increment next available nonce + * @return nonce reserved + */ + function _reserveAndIncrementNonce() internal returns (uint64) { + uint64 _nonceReserved = nextAvailableNonce; + nextAvailableNonce = nextAvailableNonce + 1; + return _nonceReserved; + } + + /** + * @notice Send the message to the destination domain and recipient. If `_destinationCaller` is not equal to bytes32(0), + * the message can only be received on the destination chain when called by `_destinationCaller`. + * @dev Format the message and emit `MessageSent` event with message information. + * @param _destinationDomain Domain of destination chain + * @param _recipient Address of message recipient on destination domain as bytes32 + * @param _destinationCaller caller on the destination domain, as bytes32 + * @param _sender message sender, as bytes32 + * @param _nonce nonce reserved for message + * @param _messageBody Raw bytes content of message + */ + function _sendMessage( + uint32 _destinationDomain, + bytes32 _recipient, + bytes32 _destinationCaller, + bytes32 _sender, + uint64 _nonce, + bytes calldata _messageBody + ) internal { + require(_recipient != bytes32(0), "Recipient must be nonzero"); + // serialize message + bytes memory _message = abi.encodePacked( + i_version, i_localDomain, _destinationDomain, _nonce, _sender, _recipient, _destinationCaller, _messageBody + ); + + // Emit MessageSent event + emit MessageSent(_message); + } +} diff --git a/contracts/src/v0.8/ccip/test/mocks/MockRMN.sol b/contracts/src/v0.8/ccip/test/mocks/MockRMN.sol new file mode 100644 index 00000000000..3f7b0200e6f --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/MockRMN.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {RMN} from "../../RMN.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; +import {OwnerIsCreator} from "./../../../shared/access/OwnerIsCreator.sol"; + +/// @notice WARNING: This contract is to be only used for testing, all methods are unprotected. +contract MockRMN is IRMN { + error CustomError(bytes err); + + bytes private s_isCursedRevert; + + bool private s_globalCursed; + mapping(bytes16 subject => bool cursed) private s_cursedBySubject; + mapping(address commitStore => mapping(bytes32 root => bool blessed)) private s_blessedByRoot; + + function setTaggedRootBlessed(IRMN.TaggedRoot calldata taggedRoot, bool blessed) external { + s_blessedByRoot[taggedRoot.commitStore][taggedRoot.root] = blessed; + } + + function setGlobalCursed(bool cursed) external { + s_globalCursed = cursed; + } + + function setChainCursed(uint64 chainSelector, bool cursed) external { + s_cursedBySubject[bytes16(uint128(chainSelector))] = cursed; + } + + /// @notice Setting a revert error with length of 0 will disable reverts + /// @dev Useful to test revert handling of ARMProxy + function setIsCursedRevert(bytes calldata revertErr) external { + s_isCursedRevert = revertErr; + } + + // IRMN implementation follows + + function isCursed() external view returns (bool) { + if (s_isCursedRevert.length > 0) { + revert CustomError(s_isCursedRevert); + } + return s_globalCursed; + } + + function isCursed(bytes16 subject) external view returns (bool) { + if (s_isCursedRevert.length > 0) { + revert CustomError(s_isCursedRevert); + } + return s_globalCursed || s_cursedBySubject[subject]; + } + + function isBlessed(IRMN.TaggedRoot calldata taggedRoot) external view returns (bool) { + return s_blessedByRoot[taggedRoot.commitStore][taggedRoot.root]; + } +} diff --git a/contracts/src/v0.8/ccip/test/mocks/MockRMN1_0.sol b/contracts/src/v0.8/ccip/test/mocks/MockRMN1_0.sol new file mode 100644 index 00000000000..44ffc23b78f --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/MockRMN1_0.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IRMN} from "../../interfaces/IRMN.sol"; +import {OwnerIsCreator} from "./../../../shared/access/OwnerIsCreator.sol"; + +// Inlined from RMN 1.0 contract. +// solhint-disable gas-struct-packing +contract RMN { + struct Voter { + address blessVoteAddr; + address curseVoteAddr; + address curseUnvoteAddr; + uint8 blessWeight; + uint8 curseWeight; + } + + struct Config { + Voter[] voters; + uint16 blessWeightThreshold; + uint16 curseWeightThreshold; + } + + struct VersionedConfig { + Config config; + uint32 configVersion; + uint32 blockNumber; + } + + struct UnvoteToCurseRecord { + address curseVoteAddr; + bytes32 cursesHash; + bool forceUnvote; + } +} + +/// @dev Retained almost as-is from commit 88f285b94c23d0c684d337064758a5edde380fe2 for compatibility with offchain +/// tests and scripts. Internal structs of the RMN 1.0 contract that were depended on have been inlined. +/// @dev This contract should no longer be used for any new tests or scripts. +/// @notice WARNING: This contract is to be only used for testing, all methods are unprotected. +// TODO: remove this contract when tests and scripts are updated +contract MockRMN is IRMN, OwnerIsCreator { + error CustomError(bytes err); + + bool private s_curse; + bytes private s_err; + RMN.VersionedConfig private s_versionedConfig; + mapping(bytes16 subject => bool cursed) private s_curseBySubject; + + function isCursed() external view override returns (bool) { + if (s_err.length != 0) { + revert CustomError(s_err); + } + return s_curse; + } + + function isCursed(bytes16 subject) external view override returns (bool) { + if (s_err.length != 0) { + revert CustomError(s_err); + } + return s_curse || s_curseBySubject[subject]; + } + + function voteToCurse(bytes32) external { + s_curse = true; + } + + function voteToCurse(bytes32, bytes16 subject) external { + s_curseBySubject[subject] = true; + } + + function ownerUnvoteToCurse(RMN.UnvoteToCurseRecord[] memory) external { + s_curse = false; + } + + function ownerUnvoteToCurse(RMN.UnvoteToCurseRecord[] memory, bytes16 subject) external { + s_curseBySubject[subject] = false; + } + + function setRevert(bytes memory err) external { + s_err = err; + } + + function isBlessed(IRMN.TaggedRoot calldata) external view override returns (bool) { + return !s_curse; + } + + function getConfigDetails() external view returns (uint32 version, uint32 blockNumber, RMN.Config memory config) { + return (s_versionedConfig.configVersion, s_versionedConfig.blockNumber, s_versionedConfig.config); + } +} diff --git a/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol b/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol new file mode 100644 index 00000000000..87db0319514 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IAny2EVMMessageReceiver} from "../../interfaces/IAny2EVMMessageReceiver.sol"; +import {IRouter} from "../../interfaces/IRouter.sol"; +import {IRouterClient} from "../../interfaces/IRouterClient.sol"; + +import {CallWithExactGas} from "../../../shared/call/CallWithExactGas.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ERC165Checker} from + "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol"; + +contract MockCCIPRouter is IRouter, IRouterClient { + using SafeERC20 for IERC20; + using ERC165Checker for address; + + error InvalidAddress(bytes encodedAddress); + error InvalidExtraArgsTag(); + error ReceiverError(bytes err); + + event MessageExecuted(bytes32 messageId, uint64 sourceChainSelector, address offRamp, bytes32 calldataHash); + event MsgExecuted(bool success, bytes retData, uint256 gasUsed); + + uint16 public constant GAS_FOR_CALL_EXACT_CHECK = 5_000; + uint32 public constant DEFAULT_GAS_LIMIT = 200_000; + + uint256 internal s_mockFeeTokenAmount; //use setFee() to change to non-zero to test fees + + function routeMessage( + Client.Any2EVMMessage calldata message, + uint16 gasForCallExactCheck, + uint256 gasLimit, + address receiver + ) external returns (bool success, bytes memory retData, uint256 gasUsed) { + return _routeMessage(message, gasForCallExactCheck, gasLimit, receiver); + } + + function _routeMessage( + Client.Any2EVMMessage memory message, + uint16 gasForCallExactCheck, + uint256 gasLimit, + address receiver + ) internal returns (bool success, bytes memory retData, uint256 gasUsed) { + // Only send through the router if the receiver is a contract and implements the IAny2EVMMessageReceiver interface. + if (receiver.code.length == 0 || !receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId)) { + return (true, "", 0); + } + + bytes memory data = abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message); + + (success, retData, gasUsed) = CallWithExactGas._callWithExactGasSafeReturnData( + data, receiver, gasLimit, gasForCallExactCheck, Internal.MAX_RET_BYTES + ); + + // Event to assist testing, does not exist on real deployments + emit MsgExecuted(success, retData, gasUsed); + + // Real router event + emit MessageExecuted(message.messageId, message.sourceChainSelector, msg.sender, keccak256(data)); + return (success, retData, gasUsed); + } + + /// @notice Sends the tx locally to the receiver instead of on the destination chain. + /// @dev Ignores destinationChainSelector + /// @dev Returns a mock message ID, which is not calculated from the message contents in the + /// same way as the real message ID. + function ccipSend( + uint64 destinationChainSelector, + Client.EVM2AnyMessage calldata message + ) external payable returns (bytes32) { + if (message.receiver.length != 32) revert InvalidAddress(message.receiver); + uint256 decodedReceiver = abi.decode(message.receiver, (uint256)); + // We want to disallow sending to address(0) and to precompiles, which exist on address(1) through address(9). + if (decodedReceiver > type(uint160).max || decodedReceiver < 10) revert InvalidAddress(message.receiver); + + uint256 feeTokenAmount = getFee(destinationChainSelector, message); + if (message.feeToken == address(0)) { + if (msg.value < feeTokenAmount) revert InsufficientFeeTokenAmount(); + } else { + if (msg.value > 0) revert InvalidMsgValue(); + IERC20(message.feeToken).safeTransferFrom(msg.sender, address(this), feeTokenAmount); + } + + address receiver = address(uint160(decodedReceiver)); + uint256 gasLimit = _fromBytes(message.extraArgs).gasLimit; + bytes32 mockMsgId = keccak256(abi.encode(message)); + + Client.Any2EVMMessage memory executableMsg = Client.Any2EVMMessage({ + messageId: mockMsgId, + sourceChainSelector: 16015286601757825753, // Sepolia + sender: abi.encode(msg.sender), + data: message.data, + destTokenAmounts: message.tokenAmounts + }); + + for (uint256 i = 0; i < message.tokenAmounts.length; ++i) { + IERC20(message.tokenAmounts[i].token).safeTransferFrom(msg.sender, receiver, message.tokenAmounts[i].amount); + } + + (bool success, bytes memory retData,) = _routeMessage(executableMsg, GAS_FOR_CALL_EXACT_CHECK, gasLimit, receiver); + + if (!success) revert ReceiverError(retData); + + return mockMsgId; + } + + function _fromBytes(bytes calldata extraArgs) internal pure returns (Client.EVMExtraArgsV1 memory) { + if (extraArgs.length == 0) { + return Client.EVMExtraArgsV1({gasLimit: DEFAULT_GAS_LIMIT}); + } + if (bytes4(extraArgs) != Client.EVM_EXTRA_ARGS_V1_TAG) revert InvalidExtraArgsTag(); + return abi.decode(extraArgs[4:], (Client.EVMExtraArgsV1)); + } + + /// @notice Always returns true to make sure this check can be performed on any chain. + function isChainSupported(uint64) external pure returns (bool supported) { + return true; + } + + /// @notice Returns an empty array. + function getSupportedTokens(uint64) external pure returns (address[] memory tokens) { + return new address[](0); + } + + /// @notice Returns 0 as the fee is not supported in this mock contract. + function getFee(uint64, Client.EVM2AnyMessage memory) public view returns (uint256) { + return s_mockFeeTokenAmount; + } + + /// @notice Sets the fees returned by getFee but is only checked when using native fee tokens + function setFee(uint256 feeAmount) external { + s_mockFeeTokenAmount = feeAmount; + } + + /// @notice Always returns address(1234567890) + function getOnRamp(uint64 /* destChainSelector */ ) external pure returns (address onRampAddress) { + return address(1234567890); + } + + /// @notice Always returns true + function isOffRamp(uint64, /* sourceChainSelector */ address /* offRamp */ ) external pure returns (bool) { + return true; + } +} diff --git a/contracts/src/v0.8/ccip/test/mocks/MockUSDCTokenMessenger.sol b/contracts/src/v0.8/ccip/test/mocks/MockUSDCTokenMessenger.sol new file mode 100644 index 00000000000..562a9f467ff --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/MockUSDCTokenMessenger.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; +import {ITokenMessenger} from "../../pools/USDC/ITokenMessenger.sol"; + +// This contract mocks both the ITokenMessenger and IMessageTransmitter +// contracts involved with the Cross Chain Token Protocol. +contract MockUSDCTokenMessenger is ITokenMessenger { + uint32 private immutable i_messageBodyVersion; + address private immutable i_transmitter; + + bytes32 public constant DESTINATION_TOKEN_MESSENGER = keccak256("i_destinationTokenMessenger"); + + uint64 public s_nonce; + + constructor(uint32 version, address transmitter) { + i_messageBodyVersion = version; + s_nonce = 1; + i_transmitter = transmitter; + } + + function depositForBurnWithCaller( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller + ) external returns (uint64) { + IBurnMintERC20(burnToken).transferFrom(msg.sender, address(this), amount); + IBurnMintERC20(burnToken).burn(amount); + emit DepositForBurn( + s_nonce, + burnToken, + amount, + msg.sender, + mintRecipient, + destinationDomain, + DESTINATION_TOKEN_MESSENGER, + destinationCaller + ); + return s_nonce++; + } + + function messageBodyVersion() external view returns (uint32) { + return i_messageBodyVersion; + } + + function localMessageTransmitter() external view returns (address) { + return i_transmitter; + } +} diff --git a/contracts/src/v0.8/ccip/test/mocks/interfaces/IMessageTransmitterWithRelay.sol b/contracts/src/v0.8/ccip/test/mocks/interfaces/IMessageTransmitterWithRelay.sol new file mode 100644 index 00000000000..dc9c644e07a --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/interfaces/IMessageTransmitterWithRelay.sol @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity ^0.8.0; + +import {IMessageTransmitter} from "../../../pools/USDC/IMessageTransmitter.sol"; + +// This follows https://github.com/circlefin/evm-cctp-contracts/blob/master/src/interfaces/IMessageTransmitter.sol +interface IMessageTransmitterWithRelay is IMessageTransmitter { + /** + * @notice Sends an outgoing message from the source domain. + * @dev Increment nonce, format the message, and emit `MessageSent` event with message information. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination domain as bytes32 + * @param messageBody Raw bytes content of message + * @return nonce reserved by message + */ + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external returns (uint64); + + /** + * @notice Sends an outgoing message from the source domain, with a specified caller on the + * destination domain. + * @dev Increment nonce, format the message, and emit `MessageSent` event with message information. + * WARNING: if the `destinationCaller` does not represent a valid address as bytes32, then it will not be possible + * to broadcast the message on the destination domain. This is an advanced feature, and the standard + * sendMessage() should be preferred for use cases where a specific destination caller is not required. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination domain as bytes32 + * @param destinationCaller caller on the destination domain, as bytes32 + * @param messageBody Raw bytes content of message + * @return nonce reserved by message + */ + function sendMessageWithCaller( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + bytes calldata messageBody + ) external returns (uint64); +} diff --git a/contracts/src/v0.8/ccip/test/mocks/test/MockRouterTest.t.sol b/contracts/src/v0.8/ccip/test/mocks/test/MockRouterTest.t.sol new file mode 100644 index 00000000000..91798b494df --- /dev/null +++ b/contracts/src/v0.8/ccip/test/mocks/test/MockRouterTest.t.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.8.0; + +import {Client} from "../../../libraries/Client.sol"; + +import {TokenSetup} from "../../TokenSetup.t.sol"; +import {IRouter, IRouterClient, MockCCIPRouter} from "../MockRouter.sol"; + +import {IERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MockRouterTest is TokenSetup { + using SafeERC20 for IERC20; + + MockCCIPRouter public mockRouter; + + uint64 public constant mockChainSelector = 123456; + + Client.EVM2AnyMessage public message; + + function setUp() public override { + mockRouter = new MockCCIPRouter(); + + //Configure the Fee to 0.1 ether for native token fees + mockRouter.setFee(0.1 ether); + + deal(address(this), 100 ether); + + message.receiver = abi.encode(address(0x12345)); + message.data = abi.encode("Hello World"); + + s_sourceFeeToken = _deploySourceToken("sLINK", type(uint256).max, 18); + } + + function test_ccipSendWithInsufficientNativeTokens_Revert() public { + //Should revert because did not include sufficient eth to pay for fees + vm.expectRevert(IRouterClient.InsufficientFeeTokenAmount.selector); + mockRouter.ccipSend(mockChainSelector, message); + } + + function test_ccipSendWithSufficientNativeFeeTokens_Success() public { + //ccipSend with sufficient native tokens for fees + mockRouter.ccipSend{value: 0.1 ether}(mockChainSelector, message); + } + + function test_ccipSendWithInvalidMsgValue_Revert() public { + message.feeToken = address(1); //Set to non native-token fees + + vm.expectRevert(IRouterClient.InvalidMsgValue.selector); + mockRouter.ccipSend{value: 0.1 ether}(mockChainSelector, message); + } + + function test_ccipSendWithLinkFeeTokenbutInsufficientAllowance_Revert() public { + message.feeToken = s_sourceFeeToken; + + vm.expectRevert(bytes("ERC20: insufficient allowance")); + mockRouter.ccipSend(mockChainSelector, message); + } + + function test_ccipSendWithLinkFeeTokenAndValidMsgValue_Success() public { + message.feeToken = s_sourceFeeToken; + + vm.startPrank(OWNER, OWNER); + + IERC20(s_sourceFeeToken).safeApprove(address(mockRouter), type(uint256).max); + + mockRouter.ccipSend(mockChainSelector, message); + } +} diff --git a/contracts/src/v0.8/ccip/test/ocr/MultiOCR3Base.t.sol b/contracts/src/v0.8/ccip/test/ocr/MultiOCR3Base.t.sol new file mode 100644 index 00000000000..5b784bf7219 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/ocr/MultiOCR3Base.t.sol @@ -0,0 +1,921 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {MultiOCR3Base} from "../../ocr/MultiOCR3Base.sol"; +import {MultiOCR3Helper} from "../helpers/MultiOCR3Helper.sol"; +import {MultiOCR3BaseSetup} from "./MultiOCR3BaseSetup.t.sol"; + +import {Vm} from "forge-std/Vm.sol"; + +contract MultiOCR3Base_transmit is MultiOCR3BaseSetup { + bytes32 internal s_configDigest1; + bytes32 internal s_configDigest2; + bytes32 internal s_configDigest3; + + function setUp() public virtual override { + super.setUp(); + + s_configDigest1 = _getBasicConfigDigest(1, s_validSigners, s_validTransmitters); + s_configDigest2 = _getBasicConfigDigest(1, s_validSigners, s_validTransmitters); + s_configDigest3 = _getBasicConfigDigest(2, s_emptySigners, s_validTransmitters); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](3); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: s_configDigest1, + F: 1, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + ocrConfigs[1] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 1, + configDigest: s_configDigest2, + F: 2, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + ocrConfigs[2] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 2, + configDigest: s_configDigest3, + F: 1, + isSignatureVerificationEnabled: false, + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_TransmitSigners_gas_Success() public { + vm.pauseGasMetering(); + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + // F = 2, need 2 signatures + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, REPORT, reportContext, 2); + + s_multiOCR3.setTransmitOcrPluginType(0); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(0, s_configDigest1, uint64(uint256(s_configDigest1))); + + vm.startPrank(s_validTransmitters[1]); + vm.resumeGasMetering(); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, rawVs); + } + + function test_TransmitWithoutSignatureVerification_gas_Success() public { + vm.pauseGasMetering(); + bytes32[3] memory reportContext = [s_configDigest3, s_configDigest3, s_configDigest3]; + + s_multiOCR3.setTransmitOcrPluginType(2); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(2, s_configDigest3, uint64(uint256(s_configDigest3))); + + vm.startPrank(s_validTransmitters[0]); + vm.resumeGasMetering(); + s_multiOCR3.transmitWithoutSignatures(reportContext, REPORT); + } + + function test_Fuzz_TransmitSignersWithSignatures_Success(uint8 F, uint64 randomAddressOffset) public { + vm.pauseGasMetering(); + + F = uint8(bound(F, 1, 3)); + + // condition: signers.length > 3F + uint8 signersLength = 3 * F + 1; + address[] memory signers = new address[](signersLength); + address[] memory transmitters = new address[](signersLength); + uint256[] memory signerKeys = new uint256[](signersLength); + + // Force addresses to be unique (with a random offset for broader testing) + for (uint160 i = 0; i < signersLength; ++i) { + transmitters[i] = vm.addr(PRIVATE0 + randomAddressOffset + i); + // condition: non-zero oracle address + vm.assume(transmitters[i] != address(0)); + + // condition: non-repeating addresses (no clashes with transmitters) + signerKeys[i] = PRIVATE0 + randomAddressOffset + i + signersLength; + signers[i] = vm.addr(signerKeys[i]); + vm.assume(signers[i] != address(0)); + } + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 3, + configDigest: s_configDigest1, + F: F, + isSignatureVerificationEnabled: true, + signers: signers, + transmitters: transmitters + }); + s_multiOCR3.setOCR3Configs(ocrConfigs); + s_multiOCR3.setTransmitOcrPluginType(3); + + // Randomise picked transmitter with random offset + vm.startPrank(transmitters[randomAddressOffset % signersLength]); + + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + // condition: matches signature expectation for transmit + uint8 numSignatures = F + 1; + uint256[] memory pickedSignerKeys = new uint256[](numSignatures); + + // Randomise picked signers with random offset + for (uint256 i; i < numSignatures; ++i) { + pickedSignerKeys[i] = signerKeys[(i + randomAddressOffset) % numSignatures]; + } + + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(pickedSignerKeys, REPORT, reportContext, numSignatures); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(3, s_configDigest1, uint64(uint256(s_configDigest1))); + + vm.resumeGasMetering(); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, rawVs); + } + + // Reverts + function test_ForkedChain_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, REPORT, reportContext, 2); + + s_multiOCR3.setTransmitOcrPluginType(0); + + uint256 chain1 = block.chainid; + uint256 chain2 = chain1 + 1; + vm.chainId(chain2); + vm.expectRevert(abi.encodeWithSelector(MultiOCR3Base.ForkedChain.selector, chain1, chain2)); + + vm.startPrank(s_validTransmitters[0]); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, rawVs); + } + + function test_ZeroSignatures_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + s_multiOCR3.setTransmitOcrPluginType(0); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(MultiOCR3Base.WrongNumberOfSignatures.selector); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, new bytes32[](0), new bytes32[](0), bytes32("")); + } + + function test_TooManySignatures_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + // 1 signature too many + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, REPORT, reportContext, 6); + + s_multiOCR3.setTransmitOcrPluginType(1); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(MultiOCR3Base.WrongNumberOfSignatures.selector); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, rawVs); + } + + function test_InsufficientSignatures_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + // Missing 1 signature for unique report + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, REPORT, reportContext, 4); + + s_multiOCR3.setTransmitOcrPluginType(1); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(MultiOCR3Base.WrongNumberOfSignatures.selector); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, rawVs); + } + + function test_ConfigDigestMismatch_Revert() public { + bytes32 configDigest; + bytes32[3] memory reportContext = [configDigest, configDigest, configDigest]; + + (,,, bytes32 rawVs) = _getSignaturesForDigest(s_validSignerKeys, REPORT, reportContext, 2); + + s_multiOCR3.setTransmitOcrPluginType(0); + + vm.expectRevert(abi.encodeWithSelector(MultiOCR3Base.ConfigDigestMismatch.selector, s_configDigest1, configDigest)); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, new bytes32[](0), new bytes32[](0), rawVs); + } + + function test_SignatureOutOfRegistration_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](1); + + s_multiOCR3.setTransmitOcrPluginType(0); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(MultiOCR3Base.SignaturesOutOfRegistration.selector); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, bytes32("")); + } + + function test_UnAuthorizedTransmitter_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](2); + + s_multiOCR3.setTransmitOcrPluginType(0); + + vm.expectRevert(MultiOCR3Base.UnauthorizedTransmitter.selector); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, bytes32("")); + } + + function test_NonUniqueSignature_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + (bytes32[] memory rs, bytes32[] memory ss, uint8[] memory vs, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, REPORT, reportContext, 2); + + rs[1] = rs[0]; + ss[1] = ss[0]; + // Need to reset the rawVs to be valid + rawVs = bytes32(bytes1(vs[0] - 27)) | (bytes32(bytes1(vs[0] - 27)) >> 8); + + s_multiOCR3.setTransmitOcrPluginType(0); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(MultiOCR3Base.NonUniqueSignatures.selector); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, rawVs); + } + + function test_UnauthorizedSigner_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, REPORT, reportContext, 2); + + rs[0] = s_configDigest1; + ss = rs; + + s_multiOCR3.setTransmitOcrPluginType(0); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(MultiOCR3Base.UnauthorizedSigner.selector); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, rawVs); + } + + function test_UnconfiguredPlugin_Revert() public { + bytes32 configDigest; + bytes32[3] memory reportContext = [configDigest, configDigest, configDigest]; + + s_multiOCR3.setTransmitOcrPluginType(42); + + vm.expectRevert(MultiOCR3Base.UnauthorizedTransmitter.selector); + s_multiOCR3.transmitWithoutSignatures(reportContext, REPORT); + } + + function test_TransmitWithLessCalldataArgs_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + + s_multiOCR3.setTransmitOcrPluginType(0); + + // The transmit should fail, since we are trying to transmit without signatures when signatures are enabled + vm.startPrank(s_validTransmitters[1]); + + // report length + function selector + report length + abiencoded location of report value + report context words + uint256 receivedLength = REPORT.length + 4 + 5 * 32; + vm.expectRevert( + abi.encodeWithSelector( + MultiOCR3Base.WrongMessageLength.selector, + // Expecting inclusion of signature constant length components + receivedLength + 5 * 32, + receivedLength + ) + ); + s_multiOCR3.transmitWithoutSignatures(reportContext, REPORT); + } + + function test_TransmitWithExtraCalldataArgs_Revert() public { + bytes32[3] memory reportContext = [s_configDigest1, s_configDigest1, s_configDigest1]; + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](2); + + s_multiOCR3.setTransmitOcrPluginType(2); + + // The transmit should fail, since we are trying to transmit with signatures when signatures are disabled + vm.startPrank(s_validTransmitters[1]); + + // dynamic length + function selector + report length + abiencoded location of report value + report context words + // rawVs value, lengths of rs, ss, and start locations of rs & ss -> 5 words + uint256 receivedLength = REPORT.length + 4 + (5 * 32) + (5 * 32) + (2 * 32) + (2 * 32); + vm.expectRevert( + abi.encodeWithSelector( + MultiOCR3Base.WrongMessageLength.selector, + // Expecting exclusion of signature constant length components and rs, ss words + receivedLength - (5 * 32) - (4 * 32), + receivedLength + ) + ); + s_multiOCR3.transmitWithSignatures(reportContext, REPORT, rs, ss, bytes32("")); + } +} + +contract MultiOCR3Base_setOCR3Configs is MultiOCR3BaseSetup { + function test_SetConfigsZeroInput_Success() public { + vm.recordLogs(); + s_multiOCR3.setOCR3Configs(new MultiOCR3Base.OCRConfigArgs[](0)); + + // No logs emitted + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 0); + } + + function test_SetConfigWithSigners_Success() public { + uint8 F = 2; + + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(0)); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(F, s_validSigners, s_validTransmitters), + F: F, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + + vm.expectEmit(); + emit MultiOCR3Base.ConfigSet( + ocrConfigs[0].ocrPluginType, + ocrConfigs[0].configDigest, + ocrConfigs[0].signers, + ocrConfigs[0].transmitters, + ocrConfigs[0].F + ); + + vm.expectEmit(); + emit MultiOCR3Helper.AfterConfigSet(ocrConfigs[0].ocrPluginType); + + s_multiOCR3.setOCR3Configs(ocrConfigs); + + MultiOCR3Base.OCRConfig memory expectedConfig = MultiOCR3Base.OCRConfig({ + configInfo: MultiOCR3Base.ConfigInfo({ + configDigest: ocrConfigs[0].configDigest, + F: ocrConfigs[0].F, + n: uint8(ocrConfigs[0].signers.length), + isSignatureVerificationEnabled: ocrConfigs[0].isSignatureVerificationEnabled + }), + signers: s_validSigners, + transmitters: s_validTransmitters + }); + _assertOCRConfigEquality(s_multiOCR3.latestConfigDetails(0), expectedConfig); + } + + function test_SetConfigWithoutSigners_Success() public { + uint8 F = 1; + address[] memory signers = new address[](0); + + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(0)); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(F, signers, s_validTransmitters), + F: F, + isSignatureVerificationEnabled: false, + signers: signers, + transmitters: s_validTransmitters + }); + + vm.expectEmit(); + emit MultiOCR3Base.ConfigSet( + ocrConfigs[0].ocrPluginType, + ocrConfigs[0].configDigest, + ocrConfigs[0].signers, + ocrConfigs[0].transmitters, + ocrConfigs[0].F + ); + + vm.expectEmit(); + emit MultiOCR3Helper.AfterConfigSet(ocrConfigs[0].ocrPluginType); + + s_multiOCR3.setOCR3Configs(ocrConfigs); + + MultiOCR3Base.OCRConfig memory expectedConfig = MultiOCR3Base.OCRConfig({ + configInfo: MultiOCR3Base.ConfigInfo({ + configDigest: ocrConfigs[0].configDigest, + F: ocrConfigs[0].F, + n: uint8(ocrConfigs[0].signers.length), + isSignatureVerificationEnabled: ocrConfigs[0].isSignatureVerificationEnabled + }), + signers: signers, + transmitters: s_validTransmitters + }); + _assertOCRConfigEquality(s_multiOCR3.latestConfigDetails(0), expectedConfig); + } + + function test_SetConfigIgnoreSigners_Success() public { + uint8 F = 1; + + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(0)); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(F, new address[](0), s_validTransmitters), + F: F, + isSignatureVerificationEnabled: false, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + + vm.expectEmit(); + emit MultiOCR3Base.ConfigSet( + ocrConfigs[0].ocrPluginType, + ocrConfigs[0].configDigest, + s_emptySigners, + ocrConfigs[0].transmitters, + ocrConfigs[0].F + ); + + vm.expectEmit(); + emit MultiOCR3Helper.AfterConfigSet(ocrConfigs[0].ocrPluginType); + + s_multiOCR3.setOCR3Configs(ocrConfigs); + + MultiOCR3Base.OCRConfig memory expectedConfig = MultiOCR3Base.OCRConfig({ + configInfo: MultiOCR3Base.ConfigInfo({ + configDigest: ocrConfigs[0].configDigest, + F: ocrConfigs[0].F, + n: 0, + isSignatureVerificationEnabled: ocrConfigs[0].isSignatureVerificationEnabled + }), + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + _assertOCRConfigEquality(s_multiOCR3.latestConfigDetails(0), expectedConfig); + + // Verify no signer role is set + for (uint256 i = 0; i < s_validSigners.length; ++i) { + MultiOCR3Base.Oracle memory signerOracle = s_multiOCR3.getOracle(0, s_validSigners[i]); + assertEq(uint8(signerOracle.role), uint8(MultiOCR3Base.Role.Unset)); + } + } + + function test_SetMultipleConfigs_Success() public { + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(0)); + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(1)); + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(2)); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](3); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(2, s_validSigners, s_validTransmitters), + F: 2, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + ocrConfigs[1] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 1, + configDigest: _getBasicConfigDigest(1, s_validSigners, s_validTransmitters), + F: 1, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + ocrConfigs[2] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 2, + configDigest: _getBasicConfigDigest(1, s_partialSigners, s_partialTransmitters), + F: 1, + isSignatureVerificationEnabled: true, + signers: s_partialSigners, + transmitters: s_partialTransmitters + }); + + for (uint256 i; i < ocrConfigs.length; ++i) { + vm.expectEmit(); + emit MultiOCR3Base.ConfigSet( + ocrConfigs[i].ocrPluginType, + ocrConfigs[i].configDigest, + ocrConfigs[i].signers, + ocrConfigs[i].transmitters, + ocrConfigs[i].F + ); + + vm.expectEmit(); + emit MultiOCR3Helper.AfterConfigSet(ocrConfigs[i].ocrPluginType); + } + s_multiOCR3.setOCR3Configs(ocrConfigs); + + for (uint256 i; i < ocrConfigs.length; ++i) { + MultiOCR3Base.OCRConfig memory expectedConfig = MultiOCR3Base.OCRConfig({ + configInfo: MultiOCR3Base.ConfigInfo({ + configDigest: ocrConfigs[i].configDigest, + F: ocrConfigs[i].F, + n: uint8(ocrConfigs[i].signers.length), + isSignatureVerificationEnabled: ocrConfigs[i].isSignatureVerificationEnabled + }), + signers: ocrConfigs[i].signers, + transmitters: ocrConfigs[i].transmitters + }); + _assertOCRConfigEquality(s_multiOCR3.latestConfigDetails(ocrConfigs[i].ocrPluginType), expectedConfig); + } + + // pluginType 3 remains unconfigured + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(3)); + } + + function test_Fuzz_SetConfig_Success(MultiOCR3Base.OCRConfigArgs memory ocrConfig, uint64 randomAddressOffset) public { + // condition: cannot assume max oracle count + vm.assume(ocrConfig.transmitters.length <= 31); + vm.assume(ocrConfig.signers.length <= 31); + + // condition: F > 0 + ocrConfig.F = uint8(bound(ocrConfig.F, 1, 3)); + + uint256 transmittersLength = ocrConfig.transmitters.length; + + // Force addresses to be unique (with a random offset for broader testing) + for (uint160 i = 0; i < transmittersLength; ++i) { + ocrConfig.transmitters[i] = vm.addr(PRIVATE0 + randomAddressOffset + i); + // condition: non-zero oracle address + vm.assume(ocrConfig.transmitters[i] != address(0)); + } + + if (ocrConfig.signers.length == 0) { + ocrConfig.isSignatureVerificationEnabled = false; + } else { + ocrConfig.isSignatureVerificationEnabled = true; + + // condition: number of signers > 3F + vm.assume(ocrConfig.signers.length > 3 * ocrConfig.F); + + uint256 signersLength = ocrConfig.signers.length; + + // Force addresses to be unique - continuing generation with an offset after the transmitter addresses + for (uint160 i = 0; i < signersLength; ++i) { + ocrConfig.signers[i] = vm.addr(PRIVATE0 + randomAddressOffset + i + transmittersLength); + // condition: non-zero oracle address + vm.assume(ocrConfig.signers[i] != address(0)); + } + } + + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(ocrConfig.ocrPluginType)); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = ocrConfig; + + vm.expectEmit(); + emit MultiOCR3Base.ConfigSet( + ocrConfig.ocrPluginType, ocrConfig.configDigest, ocrConfig.signers, ocrConfig.transmitters, ocrConfig.F + ); + vm.expectEmit(); + emit MultiOCR3Helper.AfterConfigSet(ocrConfig.ocrPluginType); + s_multiOCR3.setOCR3Configs(ocrConfigs); + + MultiOCR3Base.OCRConfig memory expectedConfig = MultiOCR3Base.OCRConfig({ + configInfo: MultiOCR3Base.ConfigInfo({ + configDigest: ocrConfig.configDigest, + F: ocrConfig.F, + n: ocrConfig.isSignatureVerificationEnabled ? uint8(ocrConfig.signers.length) : 0, + isSignatureVerificationEnabled: ocrConfig.isSignatureVerificationEnabled + }), + signers: ocrConfig.signers, + transmitters: ocrConfig.transmitters + }); + _assertOCRConfigEquality(s_multiOCR3.latestConfigDetails(ocrConfig.ocrPluginType), expectedConfig); + } + + function test_UpdateConfigTransmittersWithoutSigners_Success() public { + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(0)); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(1, s_emptySigners, s_validTransmitters), + F: 1, + isSignatureVerificationEnabled: false, + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + s_multiOCR3.setOCR3Configs(ocrConfigs); + + address[] memory newTransmitters = s_partialSigners; + + ocrConfigs[0].F = 2; + ocrConfigs[0].configDigest = _getBasicConfigDigest(2, s_emptySigners, newTransmitters); + ocrConfigs[0].transmitters = newTransmitters; + + vm.expectEmit(); + emit MultiOCR3Base.ConfigSet( + ocrConfigs[0].ocrPluginType, + ocrConfigs[0].configDigest, + ocrConfigs[0].signers, + ocrConfigs[0].transmitters, + ocrConfigs[0].F + ); + vm.expectEmit(); + emit MultiOCR3Helper.AfterConfigSet(ocrConfigs[0].ocrPluginType); + + s_multiOCR3.setOCR3Configs(ocrConfigs); + + MultiOCR3Base.OCRConfig memory expectedConfig = MultiOCR3Base.OCRConfig({ + configInfo: MultiOCR3Base.ConfigInfo({ + configDigest: ocrConfigs[0].configDigest, + F: ocrConfigs[0].F, + n: uint8(ocrConfigs[0].signers.length), + isSignatureVerificationEnabled: ocrConfigs[0].isSignatureVerificationEnabled + }), + signers: s_emptySigners, + transmitters: newTransmitters + }); + _assertOCRConfigEquality(s_multiOCR3.latestConfigDetails(0), expectedConfig); + + // Verify oracle roles get correctly re-assigned + for (uint256 i; i < newTransmitters.length; ++i) { + MultiOCR3Base.Oracle memory transmitterOracle = s_multiOCR3.getOracle(0, newTransmitters[i]); + assertEq(transmitterOracle.index, i); + assertEq(uint8(transmitterOracle.role), uint8(MultiOCR3Base.Role.Transmitter)); + } + + // Verify old transmitters get correctly unset + for (uint256 i = newTransmitters.length; i < s_validTransmitters.length; ++i) { + MultiOCR3Base.Oracle memory transmitterOracle = s_multiOCR3.getOracle(0, s_validTransmitters[i]); + assertEq(uint8(transmitterOracle.role), uint8(MultiOCR3Base.Role.Unset)); + } + } + + function test_UpdateConfigSigners_Success() public { + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(0)); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(2, s_validSigners, s_validTransmitters), + F: 2, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + s_multiOCR3.setOCR3Configs(ocrConfigs); + + address[] memory newSigners = s_partialTransmitters; + address[] memory newTransmitters = s_partialSigners; + + ocrConfigs[0].F = 1; + ocrConfigs[0].configDigest = _getBasicConfigDigest(1, newSigners, newTransmitters); + ocrConfigs[0].signers = newSigners; + ocrConfigs[0].transmitters = newTransmitters; + + vm.expectEmit(); + emit MultiOCR3Base.ConfigSet( + ocrConfigs[0].ocrPluginType, + ocrConfigs[0].configDigest, + ocrConfigs[0].signers, + ocrConfigs[0].transmitters, + ocrConfigs[0].F + ); + vm.expectEmit(); + emit MultiOCR3Helper.AfterConfigSet(ocrConfigs[0].ocrPluginType); + + s_multiOCR3.setOCR3Configs(ocrConfigs); + + MultiOCR3Base.OCRConfig memory expectedConfig = MultiOCR3Base.OCRConfig({ + configInfo: MultiOCR3Base.ConfigInfo({ + configDigest: ocrConfigs[0].configDigest, + F: ocrConfigs[0].F, + n: uint8(ocrConfigs[0].signers.length), + isSignatureVerificationEnabled: ocrConfigs[0].isSignatureVerificationEnabled + }), + signers: newSigners, + transmitters: newTransmitters + }); + _assertOCRConfigEquality(s_multiOCR3.latestConfigDetails(0), expectedConfig); + + // Verify oracle roles get correctly re-assigned + for (uint256 i; i < newSigners.length; ++i) { + MultiOCR3Base.Oracle memory signerOracle = s_multiOCR3.getOracle(0, newSigners[i]); + assertEq(signerOracle.index, i); + assertEq(uint8(signerOracle.role), uint8(MultiOCR3Base.Role.Signer)); + + MultiOCR3Base.Oracle memory transmitterOracle = s_multiOCR3.getOracle(0, newTransmitters[i]); + assertEq(transmitterOracle.index, i); + assertEq(uint8(transmitterOracle.role), uint8(MultiOCR3Base.Role.Transmitter)); + } + + // Verify old signers / transmitters get correctly unset + for (uint256 i = newSigners.length; i < s_validSigners.length; ++i) { + MultiOCR3Base.Oracle memory signerOracle = s_multiOCR3.getOracle(0, s_validSigners[i]); + assertEq(uint8(signerOracle.role), uint8(MultiOCR3Base.Role.Unset)); + + MultiOCR3Base.Oracle memory transmitterOracle = s_multiOCR3.getOracle(0, s_validTransmitters[i]); + assertEq(uint8(transmitterOracle.role), uint8(MultiOCR3Base.Role.Unset)); + } + } + + // Reverts + + function test_RepeatTransmitterAddress_Revert() public { + address[] memory signers = s_validSigners; + address[] memory transmitters = s_validTransmitters; + transmitters[0] = signers[0]; + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(1, signers, transmitters), + F: 1, + isSignatureVerificationEnabled: true, + signers: signers, + transmitters: transmitters + }); + + vm.expectRevert( + abi.encodeWithSelector( + MultiOCR3Base.InvalidConfig.selector, MultiOCR3Base.InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS + ) + ); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_RepeatSignerAddress_Revert() public { + address[] memory signers = s_validSigners; + address[] memory transmitters = s_validTransmitters; + signers[1] = signers[0]; + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(1, signers, transmitters), + F: 1, + isSignatureVerificationEnabled: true, + signers: signers, + transmitters: transmitters + }); + + vm.expectRevert( + abi.encodeWithSelector( + MultiOCR3Base.InvalidConfig.selector, MultiOCR3Base.InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS + ) + ); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_SignerCannotBeZeroAddress_Revert() public { + uint8 F = 1; + address[] memory signers = new address[](3 * F + 1); + address[] memory transmitters = new address[](3 * F + 1); + for (uint160 i = 0; i < 3 * F + 1; ++i) { + signers[i] = address(i + 1); + transmitters[i] = address(i + 1000); + } + + signers[0] = address(0); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(F, signers, transmitters), + F: F, + isSignatureVerificationEnabled: true, + signers: signers, + transmitters: transmitters + }); + + vm.expectRevert(MultiOCR3Base.OracleCannotBeZeroAddress.selector); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_TransmitterCannotBeZeroAddress_Revert() public { + uint8 F = 1; + address[] memory signers = new address[](3 * F + 1); + address[] memory transmitters = new address[](3 * F + 1); + for (uint160 i = 0; i < 3 * F + 1; ++i) { + signers[i] = address(i + 1); + transmitters[i] = address(i + 1000); + } + + transmitters[0] = address(0); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(F, signers, transmitters), + F: F, + isSignatureVerificationEnabled: true, + signers: signers, + transmitters: transmitters + }); + + vm.expectRevert(MultiOCR3Base.OracleCannotBeZeroAddress.selector); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_StaticConfigChange_Revert() public { + uint8 F = 1; + + _assertOCRConfigUnconfigured(s_multiOCR3.latestConfigDetails(0)); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(F, s_validSigners, s_validTransmitters), + F: F, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + + s_multiOCR3.setOCR3Configs(ocrConfigs); + + // signature verification cannot change + ocrConfigs[0].isSignatureVerificationEnabled = false; + vm.expectRevert(abi.encodeWithSelector(MultiOCR3Base.StaticConfigCannotBeChanged.selector, 0)); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_FTooHigh_Revert() public { + address[] memory signers = new address[](0); + address[] memory transmitters = new address[](0); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(1, signers, transmitters), + F: 1, + isSignatureVerificationEnabled: true, + signers: signers, + transmitters: transmitters + }); + + vm.expectRevert( + abi.encodeWithSelector(MultiOCR3Base.InvalidConfig.selector, MultiOCR3Base.InvalidConfigErrorType.F_TOO_HIGH) + ); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_FMustBePositive_Revert() public { + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(0, s_validSigners, s_validTransmitters), + F: 0, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + + vm.expectRevert( + abi.encodeWithSelector( + MultiOCR3Base.InvalidConfig.selector, MultiOCR3Base.InvalidConfigErrorType.F_MUST_BE_POSITIVE + ) + ); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_TooManyTransmitters_Revert() public { + address[] memory signers = new address[](0); + address[] memory transmitters = new address[](32); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(10, signers, transmitters), + F: 10, + isSignatureVerificationEnabled: false, + signers: signers, + transmitters: transmitters + }); + + vm.expectRevert( + abi.encodeWithSelector( + MultiOCR3Base.InvalidConfig.selector, MultiOCR3Base.InvalidConfigErrorType.TOO_MANY_TRANSMITTERS + ) + ); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } + + function test_TooManySigners_Revert() public { + address[] memory signers = new address[](32); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: 0, + configDigest: _getBasicConfigDigest(1, signers, s_validTransmitters), + F: 1, + isSignatureVerificationEnabled: true, + signers: signers, + transmitters: s_validTransmitters + }); + + vm.expectRevert( + abi.encodeWithSelector( + MultiOCR3Base.InvalidConfig.selector, MultiOCR3Base.InvalidConfigErrorType.TOO_MANY_SIGNERS + ) + ); + s_multiOCR3.setOCR3Configs(ocrConfigs); + } +} diff --git a/contracts/src/v0.8/ccip/test/ocr/MultiOCR3BaseSetup.t.sol b/contracts/src/v0.8/ccip/test/ocr/MultiOCR3BaseSetup.t.sol new file mode 100644 index 00000000000..6f6219bc9b0 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/ocr/MultiOCR3BaseSetup.t.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {MultiOCR3Base} from "../../ocr/MultiOCR3Base.sol"; +import {BaseTest} from "../BaseTest.t.sol"; +import {MultiOCR3Helper} from "../helpers/MultiOCR3Helper.sol"; + +contract MultiOCR3BaseSetup is BaseTest { + // Signer private keys used for these test + uint256 internal constant PRIVATE0 = 0x7b2e97fe057e6de99d6872a2ef2abf52c9b4469bc848c2465ac3fcd8d336e81d; + + address[] internal s_validSigners; + address[] internal s_validTransmitters; + uint256[] internal s_validSignerKeys; + + address[] internal s_partialSigners; + address[] internal s_partialTransmitters; + uint256[] internal s_partialSignerKeys; + + address[] internal s_emptySigners; + + bytes internal constant REPORT = abi.encode("testReport"); + MultiOCR3Helper internal s_multiOCR3; + + function setUp() public virtual override { + BaseTest.setUp(); + + uint160 numSigners = 7; + s_validSignerKeys = new uint256[](numSigners); + s_validSigners = new address[](numSigners); + s_validTransmitters = new address[](numSigners); + + for (uint160 i; i < numSigners; ++i) { + s_validTransmitters[i] = address(4 + i); + s_validSignerKeys[i] = PRIVATE0 + i; + s_validSigners[i] = vm.addr(s_validSignerKeys[i]); + } + + s_partialSigners = new address[](4); + s_partialSignerKeys = new uint256[](4); + s_partialTransmitters = new address[](4); + for (uint256 i; i < s_partialSigners.length; ++i) { + s_partialSigners[i] = s_validSigners[i]; + s_partialSignerKeys[i] = s_validSignerKeys[i]; + s_partialTransmitters[i] = s_validTransmitters[i]; + } + + s_emptySigners = new address[](0); + + s_multiOCR3 = new MultiOCR3Helper(); + } + + /// @dev returns a mock config digest with config digest computation logic similar to OCR2Base + function _getBasicConfigDigest( + uint8 F, + address[] memory signers, + address[] memory transmitters + ) internal view returns (bytes32) { + bytes memory configBytes = abi.encode(""); + uint256 configVersion = 1; + + uint256 h = uint256( + keccak256( + abi.encode( + block.chainid, address(s_multiOCR3), signers, transmitters, F, configBytes, configVersion, configBytes + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00 + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } + + function _assertOCRConfigEquality( + MultiOCR3Base.OCRConfig memory configA, + MultiOCR3Base.OCRConfig memory configB + ) internal pure { + vm.assertEq(configA.configInfo.configDigest, configB.configInfo.configDigest); + vm.assertEq(configA.configInfo.F, configB.configInfo.F); + vm.assertEq(configA.configInfo.n, configB.configInfo.n); + vm.assertEq(configA.configInfo.isSignatureVerificationEnabled, configB.configInfo.isSignatureVerificationEnabled); + + vm.assertEq(configA.signers, configB.signers); + vm.assertEq(configA.transmitters, configB.transmitters); + } + + function _assertOCRConfigUnconfigured(MultiOCR3Base.OCRConfig memory config) internal pure { + assertEq(config.configInfo.configDigest, bytes32("")); + assertEq(config.signers.length, 0); + assertEq(config.transmitters.length, 0); + } + + function _getSignaturesForDigest( + uint256[] memory signerPrivateKeys, + bytes memory report, + bytes32[3] memory reportContext, + uint8 signatureCount + ) internal pure returns (bytes32[] memory rs, bytes32[] memory ss, uint8[] memory vs, bytes32 rawVs) { + rs = new bytes32[](signatureCount); + ss = new bytes32[](signatureCount); + vs = new uint8[](signatureCount); + + bytes32 reportDigest = keccak256(abi.encodePacked(keccak256(report), reportContext)); + + // Calculate signatures + for (uint256 i; i < signatureCount; ++i) { + (vs[i], rs[i], ss[i]) = vm.sign(signerPrivateKeys[i], reportDigest); + rawVs = rawVs | (bytes32(bytes1(vs[i] - 27)) >> (8 * i)); + } + + return (rs, ss, vs, rawVs); + } +} diff --git a/contracts/src/v0.8/ccip/test/ocr/OCR2Base.t.sol b/contracts/src/v0.8/ccip/test/ocr/OCR2Base.t.sol new file mode 100644 index 00000000000..7511ebdffae --- /dev/null +++ b/contracts/src/v0.8/ccip/test/ocr/OCR2Base.t.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {OCR2Abstract} from "../../ocr/OCR2Abstract.sol"; +import {OCR2Base} from "../../ocr/OCR2Base.sol"; +import {OCR2Helper} from "../helpers/OCR2Helper.sol"; +import {OCR2Setup} from "./OCR2Setup.t.sol"; + +contract OCR2BaseSetup is OCR2Setup { + OCR2Helper internal s_OCR2Base; + + bytes32[] internal s_rs; + bytes32[] internal s_ss; + bytes32 internal s_rawVs; + + uint40 internal s_latestEpochAndRound; + + function setUp() public virtual override { + OCR2Setup.setUp(); + s_OCR2Base = new OCR2Helper(); + + bytes32 testReportDigest = getTestReportDigest(); + + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](2); + uint8[] memory vs = new uint8[](2); + + // Calculate signatures + (vs[0], rs[0], ss[0]) = vm.sign(PRIVATE0, testReportDigest); + (vs[1], rs[1], ss[1]) = vm.sign(PRIVATE1, testReportDigest); + + s_rs = rs; + s_ss = ss; + s_rawVs = bytes32(bytes1(vs[0] - 27)) | (bytes32(bytes1(vs[1] - 27)) >> 8); + } + + function getBasicConfigDigest(uint8 f, uint64 currentConfigCount) internal view returns (bytes32) { + bytes memory configBytes = abi.encode(""); + return s_OCR2Base.configDigestFromConfigData( + block.chainid, + address(s_OCR2Base), + currentConfigCount + 1, + s_valid_signers, + s_valid_transmitters, + f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + } + + function getTestReportDigest() internal view returns (bytes32) { + bytes32 configDigest = getBasicConfigDigest(s_f, 0); + bytes32[3] memory reportContext = [configDigest, configDigest, configDigest]; + return keccak256(abi.encodePacked(keccak256(REPORT), reportContext)); + } + + function getBasicConfigDigest( + address contractAddress, + uint8 f, + uint64 currentConfigCount, + bytes memory onchainConfig + ) internal view returns (bytes32) { + return s_OCR2Base.configDigestFromConfigData( + block.chainid, + contractAddress, + currentConfigCount + 1, + s_valid_signers, + s_valid_transmitters, + f, + onchainConfig, + s_offchainConfigVersion, + abi.encode("") + ); + } +} + +contract OCR2Base_transmit is OCR2BaseSetup { + bytes32 internal s_configDigest; + + function setUp() public virtual override { + OCR2BaseSetup.setUp(); + bytes memory configBytes = abi.encode(""); + + s_configDigest = getBasicConfigDigest(s_f, 0); + s_OCR2Base.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, configBytes, s_offchainConfigVersion, configBytes + ); + } + + function test_Transmit2SignersSuccess_gas() public { + vm.pauseGasMetering(); + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + vm.startPrank(s_valid_transmitters[0]); + vm.resumeGasMetering(); + s_OCR2Base.transmit(reportContext, REPORT, s_rs, s_ss, s_rawVs); + } + + // Reverts + + function test_ForkedChain_Revert() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + uint256 chain1 = block.chainid; + uint256 chain2 = chain1 + 1; + vm.chainId(chain2); + vm.expectRevert(abi.encodeWithSelector(OCR2Base.ForkedChain.selector, chain1, chain2)); + vm.startPrank(s_valid_transmitters[0]); + s_OCR2Base.transmit(reportContext, REPORT, s_rs, s_ss, s_rawVs); + } + + function test_WrongNumberOfSignatures_Revert() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + vm.expectRevert(OCR2Base.WrongNumberOfSignatures.selector); + s_OCR2Base.transmit(reportContext, REPORT, new bytes32[](0), new bytes32[](0), s_rawVs); + } + + function test_ConfigDigestMismatch_Revert() public { + bytes32 configDigest; + bytes32[3] memory reportContext = [configDigest, configDigest, configDigest]; + + vm.expectRevert(abi.encodeWithSelector(OCR2Base.ConfigDigestMismatch.selector, s_configDigest, configDigest)); + s_OCR2Base.transmit(reportContext, REPORT, new bytes32[](0), new bytes32[](0), s_rawVs); + } + + function test_SignatureOutOfRegistration_Revert() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](1); + + vm.expectRevert(OCR2Base.SignaturesOutOfRegistration.selector); + s_OCR2Base.transmit(reportContext, REPORT, rs, ss, s_rawVs); + } + + function test_UnAuthorizedTransmitter_Revert() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](2); + + vm.expectRevert(OCR2Base.UnauthorizedTransmitter.selector); + s_OCR2Base.transmit(reportContext, REPORT, rs, ss, s_rawVs); + } + + function test_NonUniqueSignature_Revert() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + bytes32[] memory rs = s_rs; + bytes32[] memory ss = s_ss; + + rs[1] = rs[0]; + ss[1] = ss[0]; + // Need to reset the rawVs to be valid + bytes32 rawVs = bytes32(bytes1(uint8(28) - 27)) | (bytes32(bytes1(uint8(28) - 27)) >> 8); + + vm.startPrank(s_valid_transmitters[0]); + vm.expectRevert(OCR2Base.NonUniqueSignatures.selector); + s_OCR2Base.transmit(reportContext, REPORT, rs, ss, rawVs); + } + + function test_UnauthorizedSigner_Revert() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + bytes32[] memory rs = new bytes32[](2); + rs[0] = s_configDigest; + bytes32[] memory ss = rs; + + vm.startPrank(s_valid_transmitters[0]); + vm.expectRevert(OCR2Base.UnauthorizedSigner.selector); + s_OCR2Base.transmit(reportContext, REPORT, rs, ss, s_rawVs); + } +} + +contract OCR2Base_setOCR2Config is OCR2BaseSetup { + function test_SetConfigSuccess_gas() public { + vm.pauseGasMetering(); + bytes memory configBytes = abi.encode(""); + uint32 configCount = 0; + + bytes32 configDigest = getBasicConfigDigest(s_f, configCount++); + + address[] memory transmitters = s_OCR2Base.getTransmitters(); + assertEq(0, transmitters.length); + + vm.expectEmit(); + emit OCR2Abstract.ConfigSet( + 0, + configDigest, + configCount, + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + + s_OCR2Base.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, configBytes, s_offchainConfigVersion, configBytes + ); + + transmitters = s_OCR2Base.getTransmitters(); + assertEq(s_valid_transmitters, transmitters); + + configDigest = getBasicConfigDigest(s_f, configCount++); + + vm.expectEmit(); + emit OCR2Abstract.ConfigSet( + uint32(block.number), + configDigest, + configCount, + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + vm.resumeGasMetering(); + s_OCR2Base.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, configBytes, s_offchainConfigVersion, configBytes + ); + } + + // Reverts + function test_RepeatAddress_Revert() public { + address[] memory signers = new address[](10); + signers[0] = address(1245678); + address[] memory transmitters = new address[](10); + transmitters[0] = signers[0]; + + vm.expectRevert( + abi.encodeWithSelector(OCR2Base.InvalidConfig.selector, OCR2Base.InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS) + ); + s_OCR2Base.setOCR2Config(signers, transmitters, 2, abi.encode(""), 100, abi.encode("")); + } + + function test_SingerCannotBeZeroAddress_Revert() public { + uint256 f = 1; + address[] memory signers = new address[](3 * f + 1); + address[] memory transmitters = new address[](3 * f + 1); + for (uint160 i = 0; i < 3 * f + 1; ++i) { + signers[i] = address(i + 1); + transmitters[i] = address(i + 1000); + } + + signers[0] = address(0); + + vm.expectRevert(OCR2Base.OracleCannotBeZeroAddress.selector); + s_OCR2Base.setOCR2Config(signers, transmitters, uint8(f), abi.encode(""), 100, abi.encode("")); + } + + function test_TransmitterCannotBeZeroAddress_Revert() public { + uint256 f = 1; + address[] memory signers = new address[](3 * f + 1); + address[] memory transmitters = new address[](3 * f + 1); + for (uint160 i = 0; i < 3 * f + 1; ++i) { + signers[i] = address(i + 1); + transmitters[i] = address(i + 1000); + } + + transmitters[0] = address(0); + + vm.expectRevert(OCR2Base.OracleCannotBeZeroAddress.selector); + s_OCR2Base.setOCR2Config(signers, transmitters, uint8(f), abi.encode(""), 100, abi.encode("")); + } + + function test_OracleOutOfRegister_Revert() public { + address[] memory signers = new address[](10); + address[] memory transmitters = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + OCR2Base.InvalidConfig.selector, OCR2Base.InvalidConfigErrorType.NUM_SIGNERS_NOT_NUM_TRANSMITTERS + ) + ); + s_OCR2Base.setOCR2Config(signers, transmitters, 2, abi.encode(""), 100, abi.encode("")); + } + + function test_FTooHigh_Revert() public { + address[] memory signers = new address[](0); + uint8 f = 1; + + vm.expectRevert(abi.encodeWithSelector(OCR2Base.InvalidConfig.selector, OCR2Base.InvalidConfigErrorType.F_TOO_HIGH)); + s_OCR2Base.setOCR2Config(signers, new address[](0), f, abi.encode(""), 100, abi.encode("")); + } + + function test_FMustBePositive_Revert() public { + uint8 f = 0; + + vm.expectRevert( + abi.encodeWithSelector(OCR2Base.InvalidConfig.selector, OCR2Base.InvalidConfigErrorType.F_MUST_BE_POSITIVE) + ); + s_OCR2Base.setOCR2Config(new address[](0), new address[](0), f, abi.encode(""), 100, abi.encode("")); + } + + function test_TooManySigners_Revert() public { + address[] memory signers = new address[](32); + + vm.expectRevert( + abi.encodeWithSelector(OCR2Base.InvalidConfig.selector, OCR2Base.InvalidConfigErrorType.TOO_MANY_SIGNERS) + ); + s_OCR2Base.setOCR2Config(signers, new address[](0), 0, abi.encode(""), 100, abi.encode("")); + } +} diff --git a/contracts/src/v0.8/ccip/test/ocr/OCR2BaseNoChecks.t.sol b/contracts/src/v0.8/ccip/test/ocr/OCR2BaseNoChecks.t.sol new file mode 100644 index 00000000000..fd4cf3fc9e7 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/ocr/OCR2BaseNoChecks.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {OCR2BaseNoChecks} from "../../ocr/OCR2BaseNoChecks.sol"; +import {OCR2NoChecksHelper} from "../helpers/OCR2NoChecksHelper.sol"; +import {OCR2Setup} from "./OCR2Setup.t.sol"; + +contract OCR2BaseNoChecksSetup is OCR2Setup { + OCR2NoChecksHelper internal s_OCR2Base; + + bytes32[] internal s_rs; + bytes32[] internal s_ss; + bytes32 internal s_rawVs; + + function setUp() public virtual override { + OCR2Setup.setUp(); + s_OCR2Base = new OCR2NoChecksHelper(); + } + + function getBasicConfigDigest(uint8 f, uint64 currentConfigCount) internal view returns (bytes32) { + bytes memory configBytes = abi.encode(""); + return s_OCR2Base.configDigestFromConfigData( + block.chainid, + address(s_OCR2Base), + currentConfigCount + 1, + s_valid_signers, + s_valid_transmitters, + f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + } +} + +contract OCR2BaseNoChecks_transmit is OCR2BaseNoChecksSetup { + bytes32 internal s_configDigest; + + function setUp() public virtual override { + OCR2BaseNoChecksSetup.setUp(); + bytes memory configBytes = abi.encode(""); + + s_configDigest = getBasicConfigDigest(s_f, 0); + s_OCR2Base.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, configBytes, s_offchainConfigVersion, configBytes + ); + } + + function test_TransmitSuccess_gas() public { + vm.pauseGasMetering(); + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + vm.startPrank(s_valid_transmitters[0]); + vm.resumeGasMetering(); + s_OCR2Base.transmit(reportContext, REPORT, s_rs, s_ss, s_rawVs); + } + + // Reverts + + function test_ForkedChain_Revert() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + uint256 chain1 = block.chainid; + uint256 chain2 = chain1 + 1; + vm.chainId(chain2); + vm.expectRevert(abi.encodeWithSelector(OCR2BaseNoChecks.ForkedChain.selector, chain1, chain2)); + vm.startPrank(s_valid_transmitters[0]); + s_OCR2Base.transmit(reportContext, REPORT, s_rs, s_ss, s_rawVs); + } + + function test_ConfigDigestMismatch_Revert() public { + bytes32 configDigest; + + bytes32[3] memory reportContext = [configDigest, configDigest, configDigest]; + + vm.expectRevert( + abi.encodeWithSelector(OCR2BaseNoChecks.ConfigDigestMismatch.selector, s_configDigest, configDigest) + ); + s_OCR2Base.transmit(reportContext, REPORT, new bytes32[](0), new bytes32[](0), s_rawVs); + } + + function test_UnAuthorizedTransmitter_Revert() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + bytes32[] memory rs = new bytes32[](3); + bytes32[] memory ss = new bytes32[](3); + + vm.expectRevert(OCR2BaseNoChecks.UnauthorizedTransmitter.selector); + s_OCR2Base.transmit(reportContext, REPORT, rs, ss, s_rawVs); + } +} + +contract OCR2BaseNoChecks_setOCR2Config is OCR2BaseNoChecksSetup { + event ConfigSet( + uint32 previousConfigBlockNumber, + bytes32 configDigest, + uint64 configCount, + address[] signers, + address[] transmitters, + uint8 f, + bytes onchainConfig, + uint64 offchainConfigVersion, + bytes offchainConfig + ); + + function test_SetConfigSuccess_gas() public { + vm.pauseGasMetering(); + bytes memory configBytes = abi.encode(""); + uint32 configCount = 0; + + bytes32 configDigest = getBasicConfigDigest(s_f, configCount++); + + address[] memory transmitters = s_OCR2Base.getTransmitters(); + assertEq(0, transmitters.length); + + vm.expectEmit(); + emit ConfigSet( + 0, + configDigest, + configCount, + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + + s_OCR2Base.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, configBytes, s_offchainConfigVersion, configBytes + ); + + transmitters = s_OCR2Base.getTransmitters(); + assertEq(s_valid_transmitters, transmitters); + + configDigest = getBasicConfigDigest(s_f, configCount++); + + vm.expectEmit(); + emit ConfigSet( + uint32(block.number), + configDigest, + configCount, + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + vm.resumeGasMetering(); + s_OCR2Base.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, configBytes, s_offchainConfigVersion, configBytes + ); + } + + // Reverts + function test_RepeatAddress_Revert() public { + address[] memory signers = new address[](4); + address[] memory transmitters = new address[](4); + transmitters[0] = address(1245678); + transmitters[1] = address(1245678); + transmitters[2] = address(1245678); + transmitters[3] = address(1245678); + + vm.expectRevert( + abi.encodeWithSelector( + OCR2BaseNoChecks.InvalidConfig.selector, OCR2BaseNoChecks.InvalidConfigErrorType.REPEATED_ORACLE_ADDRESS + ) + ); + s_OCR2Base.setOCR2Config(signers, transmitters, 1, abi.encode(""), 100, abi.encode("")); + } + + function test_FMustBePositive_Revert() public { + uint8 f = 0; + + vm.expectRevert( + abi.encodeWithSelector( + OCR2BaseNoChecks.InvalidConfig.selector, OCR2BaseNoChecks.InvalidConfigErrorType.F_MUST_BE_POSITIVE + ) + ); + s_OCR2Base.setOCR2Config(new address[](0), new address[](0), f, abi.encode(""), 100, abi.encode("")); + } + + function test_TransmitterCannotBeZeroAddress_Revert() public { + uint256 f = 1; + address[] memory signers = new address[](3 * f + 1); + address[] memory transmitters = new address[](3 * f + 1); + for (uint160 i = 0; i < 3 * f + 1; ++i) { + signers[i] = address(i + 1); + transmitters[i] = address(i + 1000); + } + + transmitters[0] = address(0); + + vm.expectRevert(OCR2BaseNoChecks.OracleCannotBeZeroAddress.selector); + s_OCR2Base.setOCR2Config(signers, transmitters, uint8(f), abi.encode(""), 100, abi.encode("")); + } + + function test_TooManyTransmitter_Revert() public { + address[] memory transmitters = new address[](100); + + vm.expectRevert( + abi.encodeWithSelector( + OCR2BaseNoChecks.InvalidConfig.selector, OCR2BaseNoChecks.InvalidConfigErrorType.TOO_MANY_TRANSMITTERS + ) + ); + s_OCR2Base.setOCR2Config(new address[](0), transmitters, 0, abi.encode(""), 100, abi.encode("")); + } +} diff --git a/contracts/src/v0.8/ccip/test/ocr/OCR2Setup.t.sol b/contracts/src/v0.8/ccip/test/ocr/OCR2Setup.t.sol new file mode 100644 index 00000000000..e4be8ffa29b --- /dev/null +++ b/contracts/src/v0.8/ccip/test/ocr/OCR2Setup.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Test} from "forge-std/Test.sol"; + +contract OCR2Setup is Test { + uint256 internal constant PRIVATE0 = 0x7b2e97fe057e6de99d6872a2ef2abf52c9b4469bc848c2465ac3fcd8d336e81d; + uint256 internal constant PRIVATE1 = 0xab56160806b05ef1796789248e1d7f34a6465c5280899159d645218cd216cee6; + uint256 internal constant PRIVATE2 = 0x6ec7caa8406a49b76736602810e0a2871959fbbb675e23a8590839e4717f1f7f; + uint256 internal constant PRIVATE3 = 0x80f14b11da94ae7f29d9a7713ea13dc838e31960a5c0f2baf45ed458947b730a; + + address[] internal s_valid_signers; + address[] internal s_valid_transmitters; + + uint64 internal constant s_offchainConfigVersion = 3; + uint8 internal constant s_f = 1; + bytes internal constant REPORT = abi.encode("testReport"); + + function setUp() public virtual { + s_valid_transmitters = new address[](4); + for (uint160 i = 0; i < 4; ++i) { + s_valid_transmitters[i] = address(4 + i); + } + + s_valid_signers = new address[](4); + s_valid_signers[0] = vm.addr(PRIVATE0); //0xc110458BE52CaA6bB68E66969C3218A4D9Db0211 + s_valid_signers[1] = vm.addr(PRIVATE1); //0xc110a19c08f1da7F5FfB281dc93630923F8E3719 + s_valid_signers[2] = vm.addr(PRIVATE2); //0xc110fdF6e8fD679C7Cc11602d1cd829211A18e9b + s_valid_signers[3] = vm.addr(PRIVATE3); //0xc11028017c9b445B6bF8aE7da951B5cC28B326C0 + } +} diff --git a/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMMultiOffRamp.t.sol b/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMMultiOffRamp.t.sol new file mode 100644 index 00000000000..43899cbfd69 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMMultiOffRamp.t.sol @@ -0,0 +1,3429 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ICommitStore} from "../../interfaces/ICommitStore.sol"; +import {IMessageInterceptor} from "../../interfaces/IMessageInterceptor.sol"; +import {IPriceRegistry} from "../../interfaces/IPriceRegistry.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; +import {ITokenAdminRegistry} from "../../interfaces/ITokenAdminRegistry.sol"; + +import {CallWithExactGas} from "../../../shared/call/CallWithExactGas.sol"; +import {NonceManager} from "../../NonceManager.sol"; +import {PriceRegistry} from "../../PriceRegistry.sol"; +import {RMN} from "../../RMN.sol"; +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {MerkleMultiProof} from "../../libraries/MerkleMultiProof.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {MultiOCR3Base} from "../../ocr/MultiOCR3Base.sol"; +import {EVM2EVMMultiOffRamp} from "../../offRamp/EVM2EVMMultiOffRamp.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {EVM2EVMMultiOffRampHelper} from "../helpers/EVM2EVMMultiOffRampHelper.sol"; +import {EVM2EVMOffRampHelper} from "../helpers/EVM2EVMOffRampHelper.sol"; +import {MaybeRevertingBurnMintTokenPool} from "../helpers/MaybeRevertingBurnMintTokenPool.sol"; +import {MessageInterceptorHelper} from "../helpers/MessageInterceptorHelper.sol"; +import {ConformingReceiver} from "../helpers/receivers/ConformingReceiver.sol"; +import {MaybeRevertMessageReceiver} from "../helpers/receivers/MaybeRevertMessageReceiver.sol"; +import {MaybeRevertMessageReceiverNo165} from "../helpers/receivers/MaybeRevertMessageReceiverNo165.sol"; +import {ReentrancyAbuserMultiRamp} from "../helpers/receivers/ReentrancyAbuserMultiRamp.sol"; +import {EVM2EVMMultiOffRampSetup} from "./EVM2EVMMultiOffRampSetup.t.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract EVM2EVMMultiOffRamp_constructor is EVM2EVMMultiOffRampSetup { + function test_Constructor_Success() public { + EVM2EVMMultiOffRamp.StaticConfig memory staticConfig = EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry), + nonceManager: address(s_inboundNonceManager) + }); + EVM2EVMMultiOffRamp.DynamicConfig memory dynamicConfig = + _generateDynamicMultiOffRampConfig(address(s_destRouter), address(s_priceRegistry)); + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](2); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: ON_RAMP_ADDRESS_1, + isEnabled: true + }); + sourceChainConfigs[1] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1 + 1, + onRamp: ON_RAMP_ADDRESS_2, + isEnabled: true + }); + + EVM2EVMMultiOffRamp.SourceChainConfig memory expectedSourceChainConfig1 = + EVM2EVMMultiOffRamp.SourceChainConfig({isEnabled: true, minSeqNr: 1, onRamp: sourceChainConfigs[0].onRamp}); + + EVM2EVMMultiOffRamp.SourceChainConfig memory expectedSourceChainConfig2 = + EVM2EVMMultiOffRamp.SourceChainConfig({isEnabled: true, minSeqNr: 1, onRamp: sourceChainConfigs[1].onRamp}); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.StaticConfigSet(staticConfig); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.DynamicConfigSet(dynamicConfig); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainSelectorAdded(SOURCE_CHAIN_SELECTOR_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainConfigSet(SOURCE_CHAIN_SELECTOR_1, expectedSourceChainConfig1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainSelectorAdded(SOURCE_CHAIN_SELECTOR_1 + 1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainConfigSet(SOURCE_CHAIN_SELECTOR_1 + 1, expectedSourceChainConfig2); + + s_offRamp = new EVM2EVMMultiOffRampHelper(staticConfig, dynamicConfig, sourceChainConfigs); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Execution), + configDigest: s_configDigestExec, + F: s_F, + isSignatureVerificationEnabled: false, + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + + s_offRamp.setOCR3Configs(ocrConfigs); + + // Static config + EVM2EVMMultiOffRamp.StaticConfig memory gotStaticConfig = s_offRamp.getStaticConfig(); + assertEq(staticConfig.chainSelector, gotStaticConfig.chainSelector); + assertEq(staticConfig.rmnProxy, gotStaticConfig.rmnProxy); + assertEq(staticConfig.tokenAdminRegistry, gotStaticConfig.tokenAdminRegistry); + + // Dynamic config + EVM2EVMMultiOffRamp.DynamicConfig memory gotDynamicConfig = s_offRamp.getDynamicConfig(); + _assertSameConfig(dynamicConfig, gotDynamicConfig); + + // OCR Config + MultiOCR3Base.OCRConfig memory expectedOCRConfig = MultiOCR3Base.OCRConfig({ + configInfo: MultiOCR3Base.ConfigInfo({ + configDigest: ocrConfigs[0].configDigest, + F: ocrConfigs[0].F, + n: 0, + isSignatureVerificationEnabled: ocrConfigs[0].isSignatureVerificationEnabled + }), + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + MultiOCR3Base.OCRConfig memory gotOCRConfig = s_offRamp.latestConfigDetails(uint8(Internal.OCRPluginType.Execution)); + _assertOCRConfigEquality(expectedOCRConfig, gotOCRConfig); + + _assertSourceChainConfigEquality( + s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR_1), expectedSourceChainConfig1 + ); + _assertSourceChainConfigEquality( + s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR_1 + 1), expectedSourceChainConfig2 + ); + + // OffRamp initial values + assertEq("EVM2EVMMultiOffRamp 1.6.0-dev", s_offRamp.typeAndVersion()); + assertEq(OWNER, s_offRamp.owner()); + assertEq(0, s_offRamp.getLatestPriceSequenceNumber()); + } + + // Revert + function test_ZeroOnRampAddress_Revert() public { + uint64[] memory sourceChainSelectors = new uint64[](1); + sourceChainSelectors[0] = SOURCE_CHAIN_SELECTOR_1; + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](1); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: new bytes(0), + isEnabled: true + }); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroAddressNotAllowed.selector); + + s_offRamp = new EVM2EVMMultiOffRampHelper( + EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry), + nonceManager: address(s_inboundNonceManager) + }), + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)), + sourceChainConfigs + ); + } + + function test_SourceChainSelector_Revert() public { + uint64[] memory sourceChainSelectors = new uint64[](1); + sourceChainSelectors[0] = SOURCE_CHAIN_SELECTOR_1; + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](1); + sourceChainConfigs[0] = + EVM2EVMMultiOffRamp.SourceChainConfigArgs({sourceChainSelector: 0, onRamp: ON_RAMP_ADDRESS_1, isEnabled: true}); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroChainSelectorNotAllowed.selector); + + s_offRamp = new EVM2EVMMultiOffRampHelper( + EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry), + nonceManager: address(s_inboundNonceManager) + }), + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)), + sourceChainConfigs + ); + } + + function test_ZeroRMNProxy_Revert() public { + uint64[] memory sourceChainSelectors = new uint64[](1); + sourceChainSelectors[0] = SOURCE_CHAIN_SELECTOR_1; + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](0); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroAddressNotAllowed.selector); + + s_offRamp = new EVM2EVMMultiOffRampHelper( + EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + rmnProxy: ZERO_ADDRESS, + tokenAdminRegistry: address(s_tokenAdminRegistry), + nonceManager: address(s_inboundNonceManager) + }), + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)), + sourceChainConfigs + ); + } + + function test_ZeroChainSelector_Revert() public { + uint64[] memory sourceChainSelectors = new uint64[](1); + sourceChainSelectors[0] = SOURCE_CHAIN_SELECTOR_1; + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](0); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroChainSelectorNotAllowed.selector); + + s_offRamp = new EVM2EVMMultiOffRampHelper( + EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: 0, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry), + nonceManager: address(s_inboundNonceManager) + }), + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)), + sourceChainConfigs + ); + } + + function test_ZeroTokenAdminRegistry_Revert() public { + uint64[] memory sourceChainSelectors = new uint64[](1); + sourceChainSelectors[0] = SOURCE_CHAIN_SELECTOR_1; + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](0); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroAddressNotAllowed.selector); + + s_offRamp = new EVM2EVMMultiOffRampHelper( + EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: ZERO_ADDRESS, + nonceManager: address(s_inboundNonceManager) + }), + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)), + sourceChainConfigs + ); + } + + function test_ZeroNonceManager_Revert() public { + uint64[] memory sourceChainSelectors = new uint64[](1); + sourceChainSelectors[0] = SOURCE_CHAIN_SELECTOR_1; + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](0); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroAddressNotAllowed.selector); + + s_offRamp = new EVM2EVMMultiOffRampHelper( + EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry), + nonceManager: ZERO_ADDRESS + }), + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)), + sourceChainConfigs + ); + } +} + +contract EVM2EVMMultiOffRamp_setDynamicConfig is EVM2EVMMultiOffRampSetup { + function test_SetDynamicConfig_Success() public { + EVM2EVMMultiOffRamp.DynamicConfig memory dynamicConfig = + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.DynamicConfigSet(dynamicConfig); + + s_offRamp.setDynamicConfig(dynamicConfig); + + EVM2EVMMultiOffRamp.DynamicConfig memory newConfig = s_offRamp.getDynamicConfig(); + _assertSameConfig(dynamicConfig, newConfig); + } + + function test_SetDynamicConfigWithValidator_Success() public { + EVM2EVMMultiOffRamp.DynamicConfig memory dynamicConfig = + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)); + dynamicConfig.messageValidator = address(s_inboundMessageValidator); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.DynamicConfigSet(dynamicConfig); + + s_offRamp.setDynamicConfig(dynamicConfig); + + EVM2EVMMultiOffRamp.DynamicConfig memory newConfig = s_offRamp.getDynamicConfig(); + _assertSameConfig(dynamicConfig, newConfig); + } + + // Reverts + + function test_NonOwner_Revert() public { + vm.startPrank(STRANGER); + EVM2EVMMultiOffRamp.DynamicConfig memory dynamicConfig = + _generateDynamicMultiOffRampConfig(USER_3, address(s_priceRegistry)); + + vm.expectRevert("Only callable by owner"); + + s_offRamp.setDynamicConfig(dynamicConfig); + } + + function test_RouterZeroAddress_Revert() public { + EVM2EVMMultiOffRamp.DynamicConfig memory dynamicConfig = + _generateDynamicMultiOffRampConfig(ZERO_ADDRESS, address(s_priceRegistry)); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroAddressNotAllowed.selector); + + s_offRamp.setDynamicConfig(dynamicConfig); + } + + function test_PriceRegistryZeroAddress_Revert() public { + EVM2EVMMultiOffRamp.DynamicConfig memory dynamicConfig = _generateDynamicMultiOffRampConfig(USER_3, ZERO_ADDRESS); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroAddressNotAllowed.selector); + + s_offRamp.setDynamicConfig(dynamicConfig); + } +} + +contract EVM2EVMMultiOffRamp_ccipReceive is EVM2EVMMultiOffRampSetup { + // Reverts + + function test_Reverts() public { + Client.Any2EVMMessage memory message = + _convertToGeneralMessage(_generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1)); + vm.expectRevert(); + s_offRamp.ccipReceive(message); + } +} + +contract EVM2EVMMultiOffRamp_executeSingleReport is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 1); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_3, 1); + } + + function test_SingleMessageNoTokens_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + + messages[0].header.nonce++; + messages[0].header.sequenceNumber++; + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + uint64 nonceBefore = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender); + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + assertGt(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender), nonceBefore); + } + + function test_SingleMessageNoTokensUnordered_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + messages[0].header.nonce = 0; + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + // Nonce never increments on unordered messages. + uint64 nonceBefore = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender); + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + assertEq( + s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender), + nonceBefore, + "nonce must remain unchanged on unordered messages" + ); + + messages[0].header.sequenceNumber++; + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + // Nonce never increments on unordered messages. + nonceBefore = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender); + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + assertEq( + s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender), + nonceBefore, + "nonce must remain unchanged on unordered messages" + ); + } + + function test_SingleMessageNoTokensOtherChain_Success() public { + Internal.Any2EVMRampMessage[] memory messagesChain1 = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messagesChain1), new uint256[](0) + ); + + uint64 nonceChain1 = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messagesChain1[0].sender); + assertGt(nonceChain1, 0); + + Internal.Any2EVMRampMessage[] memory messagesChain2 = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_3, ON_RAMP_ADDRESS_3); + assertEq(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_3, messagesChain2[0].sender), 0); + + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_3, messagesChain2), new uint256[](0) + ); + assertGt(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_3, messagesChain2[0].sender), 0); + + // Other chain's nonce is unaffected + assertEq(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messagesChain1[0].sender), nonceChain1); + } + + function test_ReceiverError_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + bytes memory realError1 = new bytes(2); + realError1[0] = 0xbe; + realError1[1] = 0xef; + s_reverting_receiver.setErr(realError1); + + messages[0].receiver = address(s_reverting_receiver); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, realError1) + ) + ); + // Nonce should increment on non-strict + assertEq(uint64(0), s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(OWNER))); + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + assertEq(uint64(1), s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(OWNER))); + } + + function test_SkippedIncorrectNonce_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + messages[0].header.nonce++; + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit NonceManager.SkippedIncorrectNonce( + messages[0].header.sourceChainSelector, messages[0].header.nonce, messages[0].sender + ); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + } + + function test_SkippedIncorrectNonceStillExecutes_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + messages[1].header.nonce++; + messages[1].header.messageId = Internal._hash(messages[1], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit NonceManager.SkippedIncorrectNonce(SOURCE_CHAIN_SELECTOR_1, messages[1].header.nonce, messages[1].sender); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + } + + function test__execute_SkippedAlreadyExecutedMessage_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SkippedAlreadyExecutedMessage(SOURCE_CHAIN_SELECTOR_1, messages[0].header.sequenceNumber); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + } + + function test__execute_SkippedAlreadyExecutedMessageUnordered_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + messages[0].header.nonce = 0; + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SkippedAlreadyExecutedMessage(SOURCE_CHAIN_SELECTOR_1, messages[0].header.sequenceNumber); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + } + + // Send a message to a contract that does not implement the CCIPReceiver interface + // This should execute successfully. + function test_SingleMessageToNonCCIPReceiver_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + MaybeRevertMessageReceiverNo165 newReceiver = new MaybeRevertMessageReceiverNo165(true); + messages[0].receiver = address(newReceiver); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + } + + function test_SingleMessagesNoTokensSuccess_gas() public { + vm.pauseGasMetering(); + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + Internal.ExecutionReportSingleChain memory report = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.resumeGasMetering(); + s_offRamp.executeSingleReport(report, new uint256[](0)); + } + + function test_TwoMessagesWithTokensSuccess_gas() public { + vm.pauseGasMetering(); + Internal.Any2EVMRampMessage[] memory messages = + _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + // Set message 1 to use another receiver to simulate more fair gas costs + messages[1].receiver = address(s_secondary_receiver); + messages[1].header.messageId = Internal._hash(messages[1], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[1].header.sequenceNumber, + messages[1].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + Internal.ExecutionReportSingleChain memory report = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.resumeGasMetering(); + s_offRamp.executeSingleReport(report, new uint256[](0)); + } + + function test_TwoMessagesWithTokensAndGE_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + // Set message 1 to use another receiver to simulate more fair gas costs + messages[1].receiver = address(s_secondary_receiver); + messages[1].header.messageId = Internal._hash(messages[1], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[1].header.sequenceNumber, + messages[1].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + assertEq(uint64(0), s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(OWNER))); + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), _getGasLimitsFromMessages(messages) + ); + assertEq(uint64(2), s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(OWNER))); + } + + function test_Fuzz_InterleavingOrderedAndUnorderedMessages_Success(bool[7] memory orderings) public { + Internal.Any2EVMRampMessage[] memory messages = new Internal.Any2EVMRampMessage[](orderings.length); + // number of tokens needs to be capped otherwise we hit UnsupportedNumberOfTokens. + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](3); + for (uint256 i = 0; i < 3; ++i) { + tokenAmounts[i].token = s_sourceTokens[i % s_sourceTokens.length]; + tokenAmounts[i].amount = 1e18; + } + uint64 expectedNonce = 0; + for (uint256 i = 0; i < orderings.length; ++i) { + messages[i] = + _generateAny2EVMMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, uint64(i + 1), tokenAmounts, !orderings[i]); + if (orderings[i]) { + messages[i].header.nonce = ++expectedNonce; + } + messages[i].header.messageId = Internal._hash(messages[i], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[i].header.sequenceNumber, + messages[i].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + } + + uint64 nonceBefore = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(OWNER)); + assertEq(uint64(0), nonceBefore, "nonce before exec should be 0"); + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), _getGasLimitsFromMessages(messages) + ); + // all executions should succeed. + for (uint256 i = 0; i < orderings.length; ++i) { + assertEq( + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, messages[i].header.sequenceNumber)), + uint256(Internal.MessageExecutionState.SUCCESS) + ); + } + assertEq( + nonceBefore + expectedNonce, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, abi.encode(OWNER)) + ); + } + + function test_InvalidSourcePoolAddress_Success() public { + address fakePoolAddress = address(0x0000000000333333); + + Internal.Any2EVMRampMessage[] memory messages = + _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + messages[0].tokenAmounts[0].sourcePoolAddress = abi.encode(fakePoolAddress); + + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + messages[1].header.messageId = Internal._hash(messages[1], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.TokenHandlingError.selector, + abi.encodeWithSelector(TokenPool.InvalidSourcePoolAddress.selector, abi.encode(fakePoolAddress)) + ) + ); + + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + } + + function test_WithCurseOnAnotherSourceChain_Success() public { + s_mockRMN.setChainCursed(SOURCE_CHAIN_SELECTOR_2, true); + s_offRamp.executeSingleReport( + _generateReportFromMessages( + SOURCE_CHAIN_SELECTOR_1, _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1) + ), + new uint256[](0) + ); + } + + // Reverts + + function test_MismatchingDestChainSelector_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_3, ON_RAMP_ADDRESS_3); + messages[0].header.destChainSelector = DEST_CHAIN_SELECTOR + 1; + + Internal.ExecutionReportSingleChain memory executionReport = + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.InvalidMessageDestChainSelector.selector, messages[0].header.destChainSelector + ) + ); + s_offRamp.executeSingleReport(executionReport, new uint256[](0)); + } + + function test_MismatchingOnRampRoot_Revert() public { + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 0); + + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + EVM2EVMMultiOffRamp.CommitReport memory commitReport = _constructCommitReport( + // Root against mismatching on ramp + Internal._hash(messages[0], ON_RAMP_ADDRESS_3) + ); + _commit(commitReport, s_latestSequenceNumber); + + Internal.ExecutionReportSingleChain memory executionReport = + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.RootNotCommitted.selector, SOURCE_CHAIN_SELECTOR_1)); + s_offRamp.executeSingleReport(executionReport, new uint256[](0)); + } + + function test_Unhealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.CursedByRMN.selector, SOURCE_CHAIN_SELECTOR_1)); + s_offRamp.executeSingleReport( + _generateReportFromMessages( + SOURCE_CHAIN_SELECTOR_1, _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1) + ), + new uint256[](0) + ); + // Uncurse should succeed + s_mockRMN.setGlobalCursed(false); + s_offRamp.executeSingleReport( + _generateReportFromMessages( + SOURCE_CHAIN_SELECTOR_1, _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1) + ), + new uint256[](0) + ); + } + + function test_UnhealthySingleChainCurse_Revert() public { + s_mockRMN.setChainCursed(SOURCE_CHAIN_SELECTOR_1, true); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.CursedByRMN.selector, SOURCE_CHAIN_SELECTOR_1)); + s_offRamp.executeSingleReport( + _generateReportFromMessages( + SOURCE_CHAIN_SELECTOR_1, _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1) + ), + new uint256[](0) + ); + // Uncurse should succeed + s_mockRMN.setChainCursed(SOURCE_CHAIN_SELECTOR_1, false); + s_offRamp.executeSingleReport( + _generateReportFromMessages( + SOURCE_CHAIN_SELECTOR_1, _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1) + ), + new uint256[](0) + ); + } + + function test_UnexpectedTokenData_Revert() public { + Internal.ExecutionReportSingleChain memory report = _generateReportFromMessages( + SOURCE_CHAIN_SELECTOR_1, _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1) + ); + report.offchainTokenData = new bytes[][](report.messages.length + 1); + + vm.expectRevert(EVM2EVMMultiOffRamp.UnexpectedTokenData.selector); + + s_offRamp.executeSingleReport(report, new uint256[](0)); + } + + function test_EmptyReport_Revert() public { + vm.expectRevert(EVM2EVMMultiOffRamp.EmptyReport.selector); + s_offRamp.executeSingleReport( + Internal.ExecutionReportSingleChain({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + proofs: new bytes32[](0), + proofFlagBits: 0, + messages: new Internal.Any2EVMRampMessage[](0), + offchainTokenData: new bytes[][](0) + }), + new uint256[](0) + ); + } + + function test_RootNotCommitted_Revert() public { + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 0); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.RootNotCommitted.selector, SOURCE_CHAIN_SELECTOR_1)); + + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), _getGasLimitsFromMessages(messages) + ); + } + + function test_ManualExecutionNotYetEnabled_Revert() public { + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, BLOCK_TIME); + + vm.expectRevert( + abi.encodeWithSelector(EVM2EVMMultiOffRamp.ManualExecutionNotYetEnabled.selector, SOURCE_CHAIN_SELECTOR_1) + ); + + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + s_offRamp.executeSingleReport( + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), _getGasLimitsFromMessages(messages) + ); + } + + function test_NonExistingSourceChain_Revert() public { + uint64 newSourceChainSelector = SOURCE_CHAIN_SELECTOR_1 + 1; + bytes memory newOnRamp = abi.encode(ON_RAMP_ADDRESS, 1); + + Internal.Any2EVMRampMessage[] memory messages = _generateSingleBasicMessage(newSourceChainSelector, newOnRamp); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.SourceChainNotEnabled.selector, newSourceChainSelector)); + s_offRamp.executeSingleReport(_generateReportFromMessages(newSourceChainSelector, messages), new uint256[](0)); + } + + function test_DisabledSourceChain_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_2, ON_RAMP_ADDRESS_2); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.SourceChainNotEnabled.selector, SOURCE_CHAIN_SELECTOR_2)); + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_2, messages), new uint256[](0)); + } + + function test_TokenDataMismatch_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + Internal.ExecutionReportSingleChain memory report = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + report.offchainTokenData[0] = new bytes[](messages[0].tokenAmounts.length + 1); + + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.TokenDataMismatch.selector, SOURCE_CHAIN_SELECTOR_1, messages[0].header.sequenceNumber + ) + ); + s_offRamp.executeSingleReport(report, new uint256[](0)); + } + + function test_RouterYULCall_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + // gas limit too high, Router's external call should revert + messages[0].gasLimit = 1e36; + messages[0].receiver = address(new ConformingReceiver(address(s_destRouter), s_destFeeToken)); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + Internal.ExecutionReportSingleChain memory executionReport = + _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector(CallWithExactGas.NotEnoughGasForCall.selector) + ); + s_offRamp.executeSingleReport(executionReport, new uint256[](0)); + } + + function test_RetryFailedMessageWithoutManualExecution_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + bytes memory realError1 = new bytes(2); + realError1[0] = 0xbe; + realError1[1] = 0xef; + s_reverting_receiver.setErr(realError1); + + messages[0].receiver = address(s_reverting_receiver); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, realError1) + ) + ); + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.AlreadyAttempted.selector, SOURCE_CHAIN_SELECTOR_1, messages[0].header.sequenceNumber + ) + ); + s_offRamp.executeSingleReport(_generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[](0)); + } + + function _constructCommitReport(bytes32 merkleRoot) internal view returns (EVM2EVMMultiOffRamp.CommitReport memory) { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: EVM2EVMMultiOffRamp.Interval(1, 2), + merkleRoot: merkleRoot + }); + + return EVM2EVMMultiOffRamp.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + merkleRoots: roots + }); + } +} + +contract EVM2EVMMultiOffRamp_executeSingleMessage is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + vm.startPrank(address(s_offRamp)); + } + + function test_executeSingleMessage_NoTokens_Success() public { + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_executeSingleMessage_WithTokens_Success() public { + Internal.Any2EVMRampMessage memory message = + _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1)[0]; + bytes[] memory offchainTokenData = new bytes[](message.tokenAmounts.length); + + vm.expectCall( + s_destPoolByToken[s_destTokens[0]], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: message.sender, + receiver: message.receiver, + amount: message.tokenAmounts[0].amount, + localToken: abi.decode(message.tokenAmounts[0].destTokenAddress, (address)), + remoteChainSelector: SOURCE_CHAIN_SELECTOR_1, + sourcePoolAddress: message.tokenAmounts[0].sourcePoolAddress, + sourcePoolData: message.tokenAmounts[0].extraData, + offchainTokenData: offchainTokenData[0] + }) + ) + ); + + s_offRamp.executeSingleMessage(message, offchainTokenData); + } + + function test_executeSingleMessage_WithValidation_Success() public { + vm.stopPrank(); + vm.startPrank(OWNER); + _enableInboundMessageValidator(); + vm.startPrank(address(s_offRamp)); + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_NonContract_Success() public { + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + message.receiver = STRANGER; + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_NonContractWithTokens_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + vm.expectEmit(); + emit TokenPool.Released(address(s_offRamp), STRANGER, amounts[0]); + vm.expectEmit(); + emit TokenPool.Minted(address(s_offRamp), STRANGER, amounts[1]); + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1, amounts); + message.receiver = STRANGER; + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + // Reverts + + function test_TokenHandlingError_Revert() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + + bytes memory errorMessage = "Random token pool issue"; + + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1, amounts); + s_maybeRevertingPool.setShouldRevert(errorMessage); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.TokenHandlingError.selector, errorMessage)); + + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_ZeroGasDONExecution_Revert() public { + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + message.gasLimit = 0; + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.ReceiverError.selector, "")); + + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_MessageSender_Revert() public { + vm.stopPrank(); + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + vm.expectRevert(EVM2EVMMultiOffRamp.CanOnlySelfCall.selector); + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_executeSingleMessage_WithFailingValidation_Revert() public { + vm.stopPrank(); + vm.startPrank(OWNER); + _enableInboundMessageValidator(); + vm.startPrank(address(s_offRamp)); + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + s_inboundMessageValidator.setMessageIdValidationState(message.header.messageId, true); + vm.expectRevert( + abi.encodeWithSelector( + IMessageInterceptor.MessageValidationError.selector, + abi.encodeWithSelector(IMessageInterceptor.MessageValidationError.selector, bytes("Invalid message")) + ) + ); + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_executeSingleMessage_WithFailingValidationNoRouterCall_Revert() public { + vm.stopPrank(); + vm.startPrank(OWNER); + _enableInboundMessageValidator(); + vm.startPrank(address(s_offRamp)); + + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + + // Setup the receiver to a non-CCIP Receiver, which will skip the Router call (but should still perform the validation) + MaybeRevertMessageReceiverNo165 newReceiver = new MaybeRevertMessageReceiverNo165(true); + message.receiver = address(newReceiver); + message.header.messageId = Internal._hash(message, ON_RAMP_ADDRESS_1); + + s_inboundMessageValidator.setMessageIdValidationState(message.header.messageId, true); + vm.expectRevert( + abi.encodeWithSelector( + IMessageInterceptor.MessageValidationError.selector, + abi.encodeWithSelector(IMessageInterceptor.MessageValidationError.selector, bytes("Invalid message")) + ) + ); + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } +} + +contract EVM2EVMMultiOffRamp_batchExecute is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 1); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_3, 1); + } + + function test_SingleReport_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + uint64 nonceBefore = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender); + s_offRamp.batchExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[][](1)); + + assertGt(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender), nonceBefore); + } + + function test_MultipleReportsSameChain_Success() public { + Internal.Any2EVMRampMessage[] memory messages1 = new Internal.Any2EVMRampMessage[](2); + Internal.Any2EVMRampMessage[] memory messages2 = new Internal.Any2EVMRampMessage[](1); + + messages1[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + messages1[1] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 2); + messages2[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 3); + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages1); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages2); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages1[0].header.sourceChainSelector, + messages1[0].header.sequenceNumber, + messages1[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages1[1].header.sourceChainSelector, + messages1[1].header.sequenceNumber, + messages1[1].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages2[0].header.sourceChainSelector, + messages2[0].header.sequenceNumber, + messages2[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + uint64 nonceBefore = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages1[0].sender); + s_offRamp.batchExecute(reports, new uint256[][](2)); + assertGt(s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages1[0].sender), nonceBefore); + } + + function test_MultipleReportsDifferentChains_Success() public { + Internal.Any2EVMRampMessage[] memory messages1 = new Internal.Any2EVMRampMessage[](2); + Internal.Any2EVMRampMessage[] memory messages2 = new Internal.Any2EVMRampMessage[](1); + + messages1[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + messages1[1] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 2); + messages2[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_3, ON_RAMP_ADDRESS_3, 1); + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages1); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_3, messages2); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages1[0].header.sourceChainSelector, + messages1[0].header.sequenceNumber, + messages1[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages1[1].header.sourceChainSelector, + messages1[1].header.sequenceNumber, + messages1[1].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages2[0].header.sourceChainSelector, + messages2[0].header.sequenceNumber, + messages2[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.batchExecute(reports, new uint256[][](2)); + + uint64 nonceChain1 = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages1[0].sender); + uint64 nonceChain3 = s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_3, messages2[0].sender); + + assertTrue(nonceChain1 != nonceChain3); + assertGt(nonceChain1, 0); + assertGt(nonceChain3, 0); + } + + function test_MultipleReportsSkipDuplicate_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SkippedAlreadyExecutedMessage(SOURCE_CHAIN_SELECTOR_1, messages[0].header.sequenceNumber); + + s_offRamp.batchExecute(reports, new uint256[][](2)); + } + + // Reverts + function test_ZeroReports_Revert() public { + vm.expectRevert(EVM2EVMMultiOffRamp.EmptyReport.selector); + s_offRamp.batchExecute(new Internal.ExecutionReportSingleChain[](0), new uint256[][](1)); + } + + function test_Unhealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.CursedByRMN.selector, SOURCE_CHAIN_SELECTOR_1)); + s_offRamp.batchExecute( + _generateBatchReportFromMessages( + SOURCE_CHAIN_SELECTOR_1, _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1) + ), + new uint256[][](1) + ); + // Uncurse should succeed + s_mockRMN.setGlobalCursed(false); + s_offRamp.batchExecute( + _generateBatchReportFromMessages( + SOURCE_CHAIN_SELECTOR_1, _generateMessagesWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1) + ), + new uint256[][](1) + ); + } + + function test_OutOfBoundsGasLimitsAccess_Revert() public { + Internal.Any2EVMRampMessage[] memory messages1 = new Internal.Any2EVMRampMessage[](2); + Internal.Any2EVMRampMessage[] memory messages2 = new Internal.Any2EVMRampMessage[](1); + + messages1[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + messages1[1] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 2); + messages2[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 3); + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages1); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages2); + + vm.expectRevert(); + s_offRamp.batchExecute(reports, new uint256[][](1)); + } +} + +contract EVM2EVMMultiOffRamp_manuallyExecute is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 1); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_3, 1); + } + + function test_manuallyExecute_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + messages[0].receiver = address(s_reverting_receiver); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + s_offRamp.batchExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[][](1)); + + s_reverting_receiver.setRevert(false); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = new uint256[](messages.length); + s_offRamp.manuallyExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), gasLimitOverrides); + } + + function test_manuallyExecute_WithGasOverride_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + messages[0].receiver = address(s_reverting_receiver); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + s_offRamp.batchExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[][](1)); + + s_reverting_receiver.setRevert(false); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = _getGasLimitsFromMessages(messages); + gasLimitOverrides[0][0] += 1; + + s_offRamp.manuallyExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), gasLimitOverrides); + } + + function test_manuallyExecute_DoesNotRevertIfUntouched_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + messages[0].receiver = address(s_reverting_receiver); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + assertEq( + messages[0].header.nonce - 1, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender) + ); + + s_reverting_receiver.setRevert(true); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, "") + ) + ); + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = _getGasLimitsFromMessages(messages); + + s_offRamp.manuallyExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), gasLimitOverrides); + + assertEq( + messages[0].header.nonce, s_inboundNonceManager.getInboundNonce(SOURCE_CHAIN_SELECTOR_1, messages[0].sender) + ); + } + + function test_manuallyExecute_WithMultiReportGasOverride_Success() public { + Internal.Any2EVMRampMessage[] memory messages1 = new Internal.Any2EVMRampMessage[](3); + Internal.Any2EVMRampMessage[] memory messages2 = new Internal.Any2EVMRampMessage[](2); + + for (uint64 i = 0; i < 3; ++i) { + messages1[i] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, i + 1); + messages1[i].receiver = address(s_reverting_receiver); + messages1[i].header.messageId = Internal._hash(messages1[i], ON_RAMP_ADDRESS_1); + } + + for (uint64 i = 0; i < 2; ++i) { + messages2[i] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_3, ON_RAMP_ADDRESS_3, i + 1); + messages2[i].receiver = address(s_reverting_receiver); + messages2[i].header.messageId = Internal._hash(messages2[i], ON_RAMP_ADDRESS_3); + } + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages1); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_3, messages2); + + s_offRamp.batchExecute(reports, new uint256[][](2)); + + s_reverting_receiver.setRevert(false); + + uint256[][] memory gasLimitOverrides = new uint256[][](2); + gasLimitOverrides[0] = _getGasLimitsFromMessages(messages1); + gasLimitOverrides[1] = _getGasLimitsFromMessages(messages2); + + for (uint256 i = 0; i < 3; ++i) { + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages1[i].header.sequenceNumber, + messages1[i].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + gasLimitOverrides[0][i] += 1; + } + + for (uint256 i = 0; i < 2; ++i) { + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_3, + messages2[i].header.sequenceNumber, + messages2[i].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + gasLimitOverrides[1][i] += 1; + } + + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + } + + function test_manuallyExecute_WithPartialMessages_Success() public { + Internal.Any2EVMRampMessage[] memory messages = new Internal.Any2EVMRampMessage[](3); + + for (uint64 i = 0; i < 3; ++i) { + messages[i] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, i + 1); + } + messages[1].receiver = address(s_reverting_receiver); + messages[1].header.messageId = Internal._hash(messages[1], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[1].header.sequenceNumber, + messages[1].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, bytes("")) + ) + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[2].header.sequenceNumber, + messages[2].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.batchExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[][](1)); + + s_reverting_receiver.setRevert(false); + + // Only the 2nd message reverted + Internal.Any2EVMRampMessage[] memory newMessages = new Internal.Any2EVMRampMessage[](1); + newMessages[0] = messages[1]; + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = _getGasLimitsFromMessages(newMessages); + gasLimitOverrides[0][0] += 1; + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + newMessages[0].header.sequenceNumber, + newMessages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + s_offRamp.manuallyExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, newMessages), gasLimitOverrides); + } + + function test_manuallyExecute_LowGasLimit_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + messages[0].gasLimit = 1; + messages[0].receiver = address(new ConformingReceiver(address(s_destRouter), s_destFeeToken)); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector(EVM2EVMMultiOffRamp.ReceiverError.selector, "") + ); + s_offRamp.batchExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[][](1)); + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = new uint256[](1); + gasLimitOverrides[0][0] = 100_000; + + vm.expectEmit(); + emit ConformingReceiver.MessageReceived(); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + s_offRamp.manuallyExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), gasLimitOverrides); + } + + // Reverts + + function test_manuallyExecute_ForkedChain_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + Internal.ExecutionReportSingleChain[] memory reports = + _generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + uint256 chain1 = block.chainid; + uint256 chain2 = chain1 + 1; + vm.chainId(chain2); + vm.expectRevert(abi.encodeWithSelector(MultiOCR3Base.ForkedChain.selector, chain1, chain2)); + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = _getGasLimitsFromMessages(messages); + + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + } + + function test_ManualExecGasLimitMismatchSingleReport_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = new Internal.Any2EVMRampMessage[](2); + messages[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + messages[1] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 2); + + Internal.ExecutionReportSingleChain[] memory reports = + _generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + // No overrides for report + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, new uint256[][](0)); + + // No messages + uint256[][] memory gasLimitOverrides = new uint256[][](1); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + + // 1 message missing + gasLimitOverrides[0] = new uint256[](1); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + + // 1 message in excess + gasLimitOverrides[0] = new uint256[](3); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + } + + function test_manuallyExecute_GasLimitMismatchMultipleReports_Revert() public { + Internal.Any2EVMRampMessage[] memory messages1 = new Internal.Any2EVMRampMessage[](2); + Internal.Any2EVMRampMessage[] memory messages2 = new Internal.Any2EVMRampMessage[](1); + + messages1[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + messages1[1] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 2); + messages2[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_3, ON_RAMP_ADDRESS_3, 1); + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages1); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_3, messages2); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, new uint256[][](0)); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, new uint256[][](1)); + + uint256[][] memory gasLimitOverrides = new uint256[][](2); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + + // 2nd report empty + gasLimitOverrides[0] = new uint256[](2); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + + // 1st report empty + gasLimitOverrides[0] = new uint256[](0); + gasLimitOverrides[1] = new uint256[](1); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + + // 1st report oversized + gasLimitOverrides[0] = new uint256[](3); + + vm.expectRevert(EVM2EVMMultiOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(reports, gasLimitOverrides); + } + + function test_ManualExecInvalidGasLimit_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = _getGasLimitsFromMessages(messages); + gasLimitOverrides[0][0]--; + + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.InvalidManualExecutionGasLimit.selector, SOURCE_CHAIN_SELECTOR_1, 0, gasLimitOverrides[0][0] + ) + ); + s_offRamp.manuallyExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), gasLimitOverrides); + } + + function test_manuallyExecute_FailedTx_Revert() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + + messages[0].receiver = address(s_reverting_receiver); + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + s_offRamp.batchExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), new uint256[][](1)); + + s_reverting_receiver.setRevert(true); + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = _getGasLimitsFromMessages(messages); + + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.ExecutionError.selector, + messages[0].header.messageId, + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, bytes("")) + ) + ) + ); + s_offRamp.manuallyExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), gasLimitOverrides); + } + + function test_manuallyExecute_ReentrancyFails() public { + uint256 tokenAmount = 1e9; + IERC20 tokenToAbuse = IERC20(s_destFeeToken); + + // This needs to be deployed before the source chain message is sent + // because we need the address for the receiver. + ReentrancyAbuserMultiRamp receiver = new ReentrancyAbuserMultiRamp(address(s_destRouter), s_offRamp); + uint256 balancePre = tokenToAbuse.balanceOf(address(receiver)); + + // For this test any message will be flagged as correct by the + // commitStore. In a real scenario the abuser would have to actually + // send the message that they want to replay. + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + messages[0].tokenAmounts = new Internal.RampTokenAmount[](1); + messages[0].tokenAmounts[0] = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[s_sourceFeeToken]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[s_sourceFeeToken]), + extraData: "", + amount: tokenAmount + }); + + messages[0].receiver = address(receiver); + + messages[0].header.messageId = Internal._hash(messages[0], ON_RAMP_ADDRESS_1); + + Internal.ExecutionReportSingleChain memory report = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + // sets the report to be repeated on the ReentrancyAbuser to be able to replay + receiver.setPayload(report); + + uint256[][] memory gasLimitOverrides = new uint256[][](1); + gasLimitOverrides[0] = _getGasLimitsFromMessages(messages); + + // The first entry should be fine and triggers the second entry. This one fails + // but since it's an inner tx of the first one it is caught in the try-catch. + // This means the first tx is marked `FAILURE` with the error message of the second tx. + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.ReceiverError.selector, + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.AlreadyExecuted.selector, + messages[0].header.sourceChainSelector, + messages[0].header.sequenceNumber + ) + ) + ); + + s_offRamp.manuallyExecute(_generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages), gasLimitOverrides); + + // Since the tx failed we don't release the tokens + assertEq(tokenToAbuse.balanceOf(address(receiver)), balancePre); + } +} + +contract EVM2EVMMultiOffRamp_execute is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 1); + } + + // Asserts that execute completes + function test_SingleReport_Success() public { + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + Internal.ExecutionReportSingleChain[] memory reports = + _generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + SOURCE_CHAIN_SELECTOR_1, + messages[0].header.sequenceNumber, + messages[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted( + uint8(Internal.OCRPluginType.Execution), s_configDigestExec, uint64(uint256(s_configDigestExec)) + ); + + _execute(reports); + } + + function test_MultipleReports_Success() public { + Internal.Any2EVMRampMessage[] memory messages1 = new Internal.Any2EVMRampMessage[](2); + Internal.Any2EVMRampMessage[] memory messages2 = new Internal.Any2EVMRampMessage[](1); + + messages1[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + messages1[1] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 2); + messages2[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 3); + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages1); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages2); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages1[0].header.sourceChainSelector, + messages1[0].header.sequenceNumber, + messages1[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages1[1].header.sourceChainSelector, + messages1[1].header.sequenceNumber, + messages1[1].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages2[0].header.sourceChainSelector, + messages2[0].header.sequenceNumber, + messages2[0].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted( + uint8(Internal.OCRPluginType.Execution), s_configDigestExec, uint64(uint256(s_configDigestExec)) + ); + + _execute(reports); + } + + function test_LargeBatch_Success() public { + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](10); + for (uint64 i = 0; i < reports.length; ++i) { + Internal.Any2EVMRampMessage[] memory messages = new Internal.Any2EVMRampMessage[](3); + messages[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1 + i * 3); + messages[1] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 2 + i * 3); + messages[2] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 3 + i * 3); + + reports[i] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + } + + for (uint64 i = 0; i < reports.length; ++i) { + for (uint64 j = 0; j < reports[i].messages.length; ++j) { + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + reports[i].messages[j].header.sourceChainSelector, + reports[i].messages[j].header.sequenceNumber, + reports[i].messages[j].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + } + } + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted( + uint8(Internal.OCRPluginType.Execution), s_configDigestExec, uint64(uint256(s_configDigestExec)) + ); + + _execute(reports); + } + + function test_MultipleReportsWithPartialValidationFailures_Success() public { + _enableInboundMessageValidator(); + + Internal.Any2EVMRampMessage[] memory messages1 = new Internal.Any2EVMRampMessage[](2); + Internal.Any2EVMRampMessage[] memory messages2 = new Internal.Any2EVMRampMessage[](1); + + messages1[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1); + messages1[1] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 2); + messages2[0] = _generateAny2EVMMessageNoTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 3); + + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](2); + reports[0] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages1); + reports[1] = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages2); + + s_inboundMessageValidator.setMessageIdValidationState(messages1[0].header.messageId, true); + s_inboundMessageValidator.setMessageIdValidationState(messages2[0].header.messageId, true); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages1[0].header.sourceChainSelector, + messages1[0].header.sequenceNumber, + messages1[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + IMessageInterceptor.MessageValidationError.selector, + abi.encodeWithSelector(IMessageInterceptor.MessageValidationError.selector, bytes("Invalid message")) + ) + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages1[1].header.sourceChainSelector, + messages1[1].header.sequenceNumber, + messages1[1].header.messageId, + Internal.MessageExecutionState.SUCCESS, + "" + ); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.ExecutionStateChanged( + messages2[0].header.sourceChainSelector, + messages2[0].header.sequenceNumber, + messages2[0].header.messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + IMessageInterceptor.MessageValidationError.selector, + abi.encodeWithSelector(IMessageInterceptor.MessageValidationError.selector, bytes("Invalid message")) + ) + ); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted( + uint8(Internal.OCRPluginType.Execution), s_configDigestExec, uint64(uint256(s_configDigestExec)) + ); + + _execute(reports); + } + + // Reverts + + function test_UnauthorizedTransmitter_Revert() public { + bytes32[3] memory reportContext = [s_configDigestExec, s_configDigestExec, s_configDigestExec]; + + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + Internal.ExecutionReportSingleChain[] memory reports = + _generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.expectRevert(MultiOCR3Base.UnauthorizedTransmitter.selector); + s_offRamp.execute(reportContext, abi.encode(reports)); + } + + function test_NoConfig_Revert() public { + _redeployOffRampWithNoOCRConfigs(); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 1); + + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + Internal.ExecutionReportSingleChain[] memory reports = + _generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + bytes32[3] memory reportContext = [bytes32(""), s_configDigestExec, s_configDigestExec]; + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(MultiOCR3Base.UnauthorizedTransmitter.selector); + s_offRamp.execute(reportContext, abi.encode(reports)); + } + + function test_NoConfigWithOtherConfigPresent_Revert() public { + _redeployOffRampWithNoOCRConfigs(); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 1); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Commit), + configDigest: s_configDigestCommit, + F: s_F, + isSignatureVerificationEnabled: false, + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + s_offRamp.setOCR3Configs(ocrConfigs); + + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + Internal.ExecutionReportSingleChain[] memory reports = + _generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + bytes32[3] memory reportContext = [bytes32(""), s_configDigestExec, s_configDigestExec]; + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(MultiOCR3Base.UnauthorizedTransmitter.selector); + s_offRamp.execute(reportContext, abi.encode(reports)); + } + + function test_WrongConfigWithSigners_Revert() public { + _redeployOffRampWithNoOCRConfigs(); + s_offRamp.setVerifyOverrideResult(SOURCE_CHAIN_SELECTOR_1, 1); + + s_configDigestExec = _getBasicConfigDigest(1, s_validSigners, s_validTransmitters); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Execution), + configDigest: s_configDigestExec, + F: s_F, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + s_offRamp.setOCR3Configs(ocrConfigs); + + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + Internal.ExecutionReportSingleChain[] memory reports = + _generateBatchReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.expectRevert(); + _execute(reports); + } + + function test_ZeroReports_Revert() public { + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](0); + + vm.expectRevert(EVM2EVMMultiOffRamp.EmptyReport.selector); + _execute(reports); + } + + function test_IncorrectArrayType_Revert() public { + bytes32[3] memory reportContext = [s_configDigestExec, s_configDigestExec, s_configDigestExec]; + + uint256[] memory wrongData = new uint256[](1); + wrongData[0] = 1; + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(); + s_offRamp.execute(reportContext, abi.encode(wrongData)); + } + + function test_NonArray_Revert() public { + bytes32[3] memory reportContext = [s_configDigestExec, s_configDigestExec, s_configDigestExec]; + + Internal.Any2EVMRampMessage[] memory messages = + _generateSingleBasicMessage(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1); + Internal.ExecutionReportSingleChain memory report = _generateReportFromMessages(SOURCE_CHAIN_SELECTOR_1, messages); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(); + s_offRamp.execute(reportContext, abi.encode(report)); + } +} + +contract EVM2EVMMultiOffRamp_getExecutionState is EVM2EVMMultiOffRampSetup { + mapping(uint64 sourceChainSelector => mapping(uint64 seqNum => Internal.MessageExecutionState state)) internal + s_differentialExecutionState; + + /// forge-config: default.fuzz.runs = 32 + /// forge-config: ccip.fuzz.runs = 32 + function test_Fuzz_Differential_Success( + uint64 sourceChainSelector, + uint16[500] memory seqNums, + uint8[500] memory values + ) public { + for (uint256 i = 0; i < seqNums.length; ++i) { + // Only use the first three slots. This makes sure existing slots get overwritten + // as the tests uses 500 sequence numbers. + uint16 seqNum = seqNums[i] % 386; + Internal.MessageExecutionState state = Internal.MessageExecutionState(values[i] % 4); + s_differentialExecutionState[sourceChainSelector][seqNum] = state; + s_offRamp.setExecutionStateHelper(sourceChainSelector, seqNum, state); + assertEq(uint256(state), uint256(s_offRamp.getExecutionState(sourceChainSelector, seqNum))); + } + + for (uint256 i = 0; i < seqNums.length; ++i) { + uint16 seqNum = seqNums[i] % 386; + Internal.MessageExecutionState expectedState = s_differentialExecutionState[sourceChainSelector][seqNum]; + assertEq(uint256(expectedState), uint256(s_offRamp.getExecutionState(sourceChainSelector, seqNum))); + } + } + + function test_GetExecutionState_Success() public { + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 0, Internal.MessageExecutionState.FAILURE); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3); + + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 1, Internal.MessageExecutionState.FAILURE); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3 + (3 << 2)); + + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 1, Internal.MessageExecutionState.IN_PROGRESS); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3 + (1 << 2)); + + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 2, Internal.MessageExecutionState.FAILURE); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3 + (1 << 2) + (3 << 4)); + + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 127, Internal.MessageExecutionState.IN_PROGRESS); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3 + (1 << 2) + (3 << 4) + (1 << 254)); + + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 128, Internal.MessageExecutionState.SUCCESS); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3 + (1 << 2) + (3 << 4) + (1 << 254)); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 1), 2); + + assertEq( + uint256(Internal.MessageExecutionState.FAILURE), uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, 0)) + ); + assertEq( + uint256(Internal.MessageExecutionState.IN_PROGRESS), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, 1)) + ); + assertEq( + uint256(Internal.MessageExecutionState.FAILURE), uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, 2)) + ); + assertEq( + uint256(Internal.MessageExecutionState.IN_PROGRESS), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, 127)) + ); + assertEq( + uint256(Internal.MessageExecutionState.SUCCESS), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, 128)) + ); + } + + function test_GetDifferentChainExecutionState_Success() public { + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 0, Internal.MessageExecutionState.FAILURE); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1 + 1, 0), 0); + + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 127, Internal.MessageExecutionState.IN_PROGRESS); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3 + (1 << 254)); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1 + 1, 0), 0); + + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, 128, Internal.MessageExecutionState.SUCCESS); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3 + (1 << 254)); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 1), 2); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1 + 1, 0), 0); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1 + 1, 1), 0); + + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1 + 1, 127, Internal.MessageExecutionState.FAILURE); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 0), 3 + (1 << 254)); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, 1), 2); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1 + 1, 0), (3 << 254)); + assertEq(s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1 + 1, 1), 0); + + assertEq( + uint256(Internal.MessageExecutionState.FAILURE), uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, 0)) + ); + assertEq( + uint256(Internal.MessageExecutionState.IN_PROGRESS), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, 127)) + ); + assertEq( + uint256(Internal.MessageExecutionState.SUCCESS), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, 128)) + ); + + assertEq( + uint256(Internal.MessageExecutionState.UNTOUCHED), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1 + 1, 0)) + ); + assertEq( + uint256(Internal.MessageExecutionState.FAILURE), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1 + 1, 127)) + ); + assertEq( + uint256(Internal.MessageExecutionState.UNTOUCHED), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1 + 1, 128)) + ); + } + + function test_FillExecutionState_Success() public { + for (uint64 i = 0; i < 384; ++i) { + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, i, Internal.MessageExecutionState.FAILURE); + } + + for (uint64 i = 0; i < 384; ++i) { + assertEq( + uint256(Internal.MessageExecutionState.FAILURE), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, i)) + ); + } + + for (uint64 i = 0; i < 3; ++i) { + assertEq(type(uint256).max, s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, i)); + } + + for (uint64 i = 0; i < 384; ++i) { + s_offRamp.setExecutionStateHelper(SOURCE_CHAIN_SELECTOR_1, i, Internal.MessageExecutionState.IN_PROGRESS); + } + + for (uint64 i = 0; i < 384; ++i) { + assertEq( + uint256(Internal.MessageExecutionState.IN_PROGRESS), + uint256(s_offRamp.getExecutionState(SOURCE_CHAIN_SELECTOR_1, i)) + ); + } + + for (uint64 i = 0; i < 3; ++i) { + // 0x555... == 0b101010101010..... + assertEq( + 0x5555555555555555555555555555555555555555555555555555555555555555, + s_offRamp.getExecutionStateBitMap(SOURCE_CHAIN_SELECTOR_1, i) + ); + } + } +} + +contract EVM2EVMMultiOffRamp_trialExecute is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + } + + function test_trialExecute_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1, amounts); + IERC20 dstToken0 = IERC20(s_destTokens[0]); + uint256 startingBalance = dstToken0.balanceOf(message.receiver); + + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + assertEq(uint256(Internal.MessageExecutionState.SUCCESS), uint256(newState)); + assertEq("", err); + + // Check that the tokens were transferred + assertEq(startingBalance + amounts[0], dstToken0.balanceOf(message.receiver)); + } + + function test_TokenHandlingErrorIsCaught_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + + IERC20 dstToken0 = IERC20(s_destTokens[0]); + uint256 startingBalance = dstToken0.balanceOf(OWNER); + + bytes memory errorMessage = "Random token pool issue"; + + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1, amounts); + s_maybeRevertingPool.setShouldRevert(errorMessage); + + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(newState)); + assertEq(abi.encodeWithSelector(EVM2EVMMultiOffRamp.TokenHandlingError.selector, errorMessage), err); + + // Expect the balance to remain the same + assertEq(startingBalance, dstToken0.balanceOf(OWNER)); + } + + function test_RateLimitError_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + + bytes memory errorMessage = abi.encodeWithSelector(RateLimiter.BucketOverfilled.selector); + + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1, amounts); + s_maybeRevertingPool.setShouldRevert(errorMessage); + + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(newState)); + assertEq(abi.encodeWithSelector(EVM2EVMMultiOffRamp.TokenHandlingError.selector, errorMessage), err); + } + + // TODO test actual pool exists but isn't compatible instead of just no pool + function test_TokenPoolIsNotAContract_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 10000; + Internal.Any2EVMRampMessage memory message = + _generateAny2EVMMessageWithTokens(SOURCE_CHAIN_SELECTOR_1, ON_RAMP_ADDRESS_1, 1, amounts); + + // Happy path, pool is correct + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + + assertEq(uint256(Internal.MessageExecutionState.SUCCESS), uint256(newState)); + assertEq("", err); + + // address 0 has no contract + assertEq(address(0).code.length, 0); + + message.tokenAmounts[0] = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(address(0)), + destTokenAddress: abi.encode(address(0)), + extraData: "", + amount: message.tokenAmounts[0].amount + }); + + message.header.messageId = Internal._hash(message, ON_RAMP_ADDRESS_1); + + // Unhappy path, no revert but marked as failed. + (newState, err) = s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(newState)); + assertEq(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, abi.encode(address(0))), err); + + address notAContract = makeAddr("not_a_contract"); + + message.tokenAmounts[0] = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(address(0)), + destTokenAddress: abi.encode(notAContract), + extraData: "", + amount: message.tokenAmounts[0].amount + }); + + message.header.messageId = Internal._hash(message, ON_RAMP_ADDRESS_1); + + (newState, err) = s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(newState)); + assertEq(abi.encodeWithSelector(EVM2EVMMultiOffRamp.NotACompatiblePool.selector, address(0)), err); + } +} + +contract EVM2EVMMultiOffRamp__releaseOrMintSingleToken is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + } + + function test__releaseOrMintSingleToken_Success() public { + uint256 amount = 123123; + address token = s_sourceTokens[0]; + bytes memory originalSender = abi.encode(OWNER); + bytes memory offchainTokenData = abi.encode(keccak256("offchainTokenData")); + + IERC20 dstToken1 = IERC20(s_destTokenBySourceToken[token]); + uint256 startingBalance = dstToken1.balanceOf(OWNER); + + Internal.RampTokenAmount memory tokenAmount = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[token]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[token]), + extraData: "", + amount: amount + }); + + vm.expectCall( + s_destPoolBySourceToken[token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: originalSender, + receiver: OWNER, + amount: amount, + localToken: s_destTokenBySourceToken[token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR_1, + sourcePoolAddress: tokenAmount.sourcePoolAddress, + sourcePoolData: tokenAmount.extraData, + offchainTokenData: offchainTokenData + }) + ) + ); + + s_offRamp.releaseOrMintSingleToken(tokenAmount, originalSender, OWNER, SOURCE_CHAIN_SELECTOR_1, offchainTokenData); + + assertEq(startingBalance + amount, dstToken1.balanceOf(OWNER)); + } + + function test__releaseOrMintSingleToken_NotACompatiblePool_Revert() public { + uint256 amount = 123123; + address token = s_sourceTokens[0]; + address destToken = s_destTokenBySourceToken[token]; + vm.label(destToken, "destToken"); + bytes memory originalSender = abi.encode(OWNER); + bytes memory offchainTokenData = abi.encode(keccak256("offchainTokenData")); + + Internal.RampTokenAmount memory tokenAmount = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[token]), + destTokenAddress: abi.encode(destToken), + extraData: "", + amount: amount + }); + + // Address(0) should always revert + address returnedPool = address(0); + + vm.mockCall( + address(s_tokenAdminRegistry), + abi.encodeWithSelector(ITokenAdminRegistry.getPool.selector, destToken), + abi.encode(returnedPool) + ); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.NotACompatiblePool.selector, returnedPool)); + + s_offRamp.releaseOrMintSingleToken(tokenAmount, originalSender, OWNER, SOURCE_CHAIN_SELECTOR_1, offchainTokenData); + + // A contract that doesn't support the interface should also revert + returnedPool = address(s_offRamp); + + vm.mockCall( + address(s_tokenAdminRegistry), + abi.encodeWithSelector(ITokenAdminRegistry.getPool.selector, destToken), + abi.encode(returnedPool) + ); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.NotACompatiblePool.selector, returnedPool)); + + s_offRamp.releaseOrMintSingleToken(tokenAmount, originalSender, OWNER, SOURCE_CHAIN_SELECTOR_1, offchainTokenData); + } + + function test__releaseOrMintSingleToken_TokenHandlingError_revert_Revert() public { + address receiver = makeAddr("receiver"); + uint256 amount = 123123; + address token = s_sourceTokens[0]; + address destToken = s_destTokenBySourceToken[token]; + bytes memory originalSender = abi.encode(OWNER); + bytes memory offchainTokenData = abi.encode(keccak256("offchainTokenData")); + + Internal.RampTokenAmount memory tokenAmount = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[token]), + destTokenAddress: abi.encode(destToken), + extraData: "", + amount: amount + }); + + bytes memory revertData = "call reverted :o"; + + vm.mockCallRevert(destToken, abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount), revertData); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.TokenHandlingError.selector, revertData)); + s_offRamp.releaseOrMintSingleToken( + tokenAmount, originalSender, receiver, SOURCE_CHAIN_SELECTOR_1, offchainTokenData + ); + } +} + +contract EVM2EVMMultiOffRamp_releaseOrMintTokens is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + } + + function test_releaseOrMintTokens_Success() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + IERC20 dstToken1 = IERC20(s_destFeeToken); + uint256 startingBalance = dstToken1.balanceOf(OWNER); + uint256 amount1 = 100; + srcTokenAmounts[0].amount = amount1; + + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + offchainTokenData[0] = abi.encode(0x12345678); + + Internal.RampTokenAmount[] memory sourceTokenAmounts = _getDefaultSourceTokenData(srcTokenAmounts); + + vm.expectCall( + s_destPoolBySourceToken[srcTokenAmounts[0].token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + receiver: OWNER, + amount: srcTokenAmounts[0].amount, + localToken: s_destTokenBySourceToken[srcTokenAmounts[0].token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR_1, + sourcePoolAddress: sourceTokenAmounts[0].sourcePoolAddress, + sourcePoolData: sourceTokenAmounts[0].extraData, + offchainTokenData: offchainTokenData[0] + }) + ) + ); + + s_offRamp.releaseOrMintTokens( + sourceTokenAmounts, abi.encode(OWNER), OWNER, SOURCE_CHAIN_SELECTOR_1, offchainTokenData + ); + + assertEq(startingBalance + amount1, dstToken1.balanceOf(OWNER)); + } + + function test_releaseOrMintTokens_destDenominatedDecimals_Success() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + address destToken = s_destFeeToken; + uint256 amount = 100; + uint256 destinationDenominationMultiplier = 1000; + srcTokenAmounts[0].amount = amount; + + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + + Internal.RampTokenAmount[] memory sourceTokenAmounts = _getDefaultSourceTokenData(srcTokenAmounts); + + // Since the pool call is mocked, we manually release funds to the offRamp + deal(destToken, address(s_offRamp), amount * destinationDenominationMultiplier); + + vm.mockCall( + s_destPoolBySourceToken[srcTokenAmounts[0].token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + receiver: OWNER, + amount: amount, + localToken: s_destTokenBySourceToken[srcTokenAmounts[0].token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR_1, + sourcePoolAddress: sourceTokenAmounts[0].sourcePoolAddress, + sourcePoolData: sourceTokenAmounts[0].extraData, + offchainTokenData: offchainTokenData[0] + }) + ), + abi.encode(amount * destinationDenominationMultiplier) + ); + + Client.EVMTokenAmount[] memory destTokenAmounts = s_offRamp.releaseOrMintTokens( + sourceTokenAmounts, abi.encode(OWNER), OWNER, SOURCE_CHAIN_SELECTOR_1, offchainTokenData + ); + + assertEq(destTokenAmounts[0].amount, amount * destinationDenominationMultiplier); + assertEq(destTokenAmounts[0].token, destToken); + } + + // Revert + + function test_TokenHandlingError_Reverts() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + + bytes memory unknownError = bytes("unknown error"); + s_maybeRevertingPool.setShouldRevert(unknownError); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.TokenHandlingError.selector, unknownError)); + + s_offRamp.releaseOrMintTokens( + _getDefaultSourceTokenData(srcTokenAmounts), + abi.encode(OWNER), + OWNER, + SOURCE_CHAIN_SELECTOR_1, + new bytes[](srcTokenAmounts.length) + ); + } + + function test_releaseOrMintTokens_InvalidDataLengthReturnData_Revert() public { + uint256 amount = 100; + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + srcTokenAmounts[0].amount = amount; + + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + Internal.RampTokenAmount[] memory sourceTokenAmounts = _getDefaultSourceTokenData(srcTokenAmounts); + + vm.mockCall( + s_destPoolBySourceToken[srcTokenAmounts[0].token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + receiver: OWNER, + amount: amount, + localToken: s_destTokenBySourceToken[srcTokenAmounts[0].token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR_1, + sourcePoolAddress: sourceTokenAmounts[0].sourcePoolAddress, + sourcePoolData: sourceTokenAmounts[0].extraData, + offchainTokenData: offchainTokenData[0] + }) + ), + // Includes the amount twice, this will revert due to the return data being to long + abi.encode(amount, amount) + ); + + vm.expectRevert( + abi.encodeWithSelector(EVM2EVMMultiOffRamp.InvalidDataLength.selector, Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, 64) + ); + + s_offRamp.releaseOrMintTokens( + sourceTokenAmounts, abi.encode(OWNER), OWNER, SOURCE_CHAIN_SELECTOR_1, offchainTokenData + ); + } + + function test_releaseOrMintTokens_InvalidEVMAddress_Revert() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + Internal.RampTokenAmount[] memory sourceTokenAmounts = _getDefaultSourceTokenData(srcTokenAmounts); + bytes memory wrongAddress = abi.encode(address(1000), address(10000), address(10000)); + + sourceTokenAmounts[0].destTokenAddress = wrongAddress; + + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, wrongAddress)); + + s_offRamp.releaseOrMintTokens( + sourceTokenAmounts, abi.encode(OWNER), OWNER, SOURCE_CHAIN_SELECTOR_1, offchainTokenData + ); + } + + function test__releaseOrMintTokens_PoolIsNotAPool_Reverts() public { + // The offRamp is a contract, but not a pool + address fakePoolAddress = address(s_offRamp); + + Internal.RampTokenAmount[] memory sourceTokenAmounts = new Internal.RampTokenAmount[](1); + sourceTokenAmounts[0] = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(fakePoolAddress), + destTokenAddress: abi.encode(s_offRamp), + extraData: "", + amount: 1 + }); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.NotACompatiblePool.selector, address(0))); + s_offRamp.releaseOrMintTokens(sourceTokenAmounts, abi.encode(OWNER), OWNER, SOURCE_CHAIN_SELECTOR_1, new bytes[](1)); + } + + function test_releaseOrMintTokens_PoolDoesNotSupportDest_Reverts() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + uint256 amount1 = 100; + srcTokenAmounts[0].amount = amount1; + + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + offchainTokenData[0] = abi.encode(0x12345678); + + Internal.RampTokenAmount[] memory sourceTokenAmounts = _getDefaultSourceTokenData(srcTokenAmounts); + + vm.expectCall( + s_destPoolBySourceToken[srcTokenAmounts[0].token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + receiver: OWNER, + amount: srcTokenAmounts[0].amount, + localToken: s_destTokenBySourceToken[srcTokenAmounts[0].token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR_3, + sourcePoolAddress: sourceTokenAmounts[0].sourcePoolAddress, + sourcePoolData: sourceTokenAmounts[0].extraData, + offchainTokenData: offchainTokenData[0] + }) + ) + ); + vm.expectRevert(); + s_offRamp.releaseOrMintTokens( + sourceTokenAmounts, abi.encode(OWNER), OWNER, SOURCE_CHAIN_SELECTOR_3, offchainTokenData + ); + } + + /// forge-config: default.fuzz.runs = 32 + /// forge-config: ccip.fuzz.runs = 1024 + // Uint256 gives a good range of values to test, both inside and outside of the eth address space. + function test_Fuzz__releaseOrMintTokens_AnyRevertIsCaught_Success(uint256 destPool) public { + // Input 447301751254033913445893214690834296930546521452, which is 0x4E59B44847B379578588920CA78FBF26C0B4956C + // triggers some Create2Deployer and causes it to fail + vm.assume(destPool != 447301751254033913445893214690834296930546521452); + bytes memory unusedVar = abi.encode(makeAddr("unused")); + Internal.RampTokenAmount[] memory sourceTokenAmounts = new Internal.RampTokenAmount[](1); + sourceTokenAmounts[0] = Internal.RampTokenAmount({ + sourcePoolAddress: unusedVar, + destTokenAddress: abi.encode(destPool), + extraData: unusedVar, + amount: 1 + }); + + try s_offRamp.releaseOrMintTokens( + sourceTokenAmounts, abi.encode(OWNER), OWNER, SOURCE_CHAIN_SELECTOR_1, new bytes[](1) + ) {} catch (bytes memory reason) { + // Any revert should be a TokenHandlingError, InvalidEVMAddress, InvalidDataLength or NoContract as those are caught by the offramp + assertTrue( + bytes4(reason) == EVM2EVMMultiOffRamp.TokenHandlingError.selector + || bytes4(reason) == Internal.InvalidEVMAddress.selector + || bytes4(reason) == EVM2EVMMultiOffRamp.InvalidDataLength.selector + || bytes4(reason) == CallWithExactGas.NoContract.selector + || bytes4(reason) == EVM2EVMMultiOffRamp.NotACompatiblePool.selector, + "Expected TokenHandlingError or InvalidEVMAddress" + ); + + if (destPool > type(uint160).max) { + assertEq(reason, abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, abi.encode(destPool))); + } + } + } +} + +contract EVM2EVMMultiOffRamp_applySourceChainConfigUpdates is EVM2EVMMultiOffRampSetup { + function test_ApplyZeroUpdates_Success() public { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](0); + + vm.recordLogs(); + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + + // No logs emitted + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 0); + + // assertEq(s_offRamp.getSourceChainSelectors().length, 0); + } + + function test_AddNewChain_Success() public { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](1); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: ON_RAMP_ADDRESS_1, + isEnabled: true + }); + + EVM2EVMMultiOffRamp.SourceChainConfig memory expectedSourceChainConfig = + EVM2EVMMultiOffRamp.SourceChainConfig({isEnabled: true, minSeqNr: 1, onRamp: ON_RAMP_ADDRESS_1}); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainSelectorAdded(SOURCE_CHAIN_SELECTOR_1); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainConfigSet(SOURCE_CHAIN_SELECTOR_1, expectedSourceChainConfig); + + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + + _assertSourceChainConfigEquality(s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR_1), expectedSourceChainConfig); + } + + function test_ReplaceExistingChain_Success() public { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](1); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: ON_RAMP_ADDRESS_1, + isEnabled: true + }); + + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + + sourceChainConfigs[0].isEnabled = false; + EVM2EVMMultiOffRamp.SourceChainConfig memory expectedSourceChainConfig = + EVM2EVMMultiOffRamp.SourceChainConfig({isEnabled: false, minSeqNr: 1, onRamp: ON_RAMP_ADDRESS_1}); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainConfigSet(SOURCE_CHAIN_SELECTOR_1, expectedSourceChainConfig); + + vm.recordLogs(); + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + + // No log emitted for chain selector added (only for setting the config) + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 1); + + _assertSourceChainConfigEquality(s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR_1), expectedSourceChainConfig); + + // uint64[] memory resultSourceChainSelectors = s_offRamp.getSourceChainSelectors(); + // assertEq(resultSourceChainSelectors.length, 1); + // assertEq(resultSourceChainSelectors[0], SOURCE_CHAIN_SELECTOR_1); + } + + function test_AddMultipleChains_Success() public { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](3); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: abi.encode(ON_RAMP_ADDRESS_1, 0), + isEnabled: true + }); + sourceChainConfigs[1] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1 + 1, + onRamp: abi.encode(ON_RAMP_ADDRESS_1, 1), + isEnabled: false + }); + sourceChainConfigs[2] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1 + 2, + onRamp: abi.encode(ON_RAMP_ADDRESS_1, 2), + isEnabled: true + }); + + EVM2EVMMultiOffRamp.SourceChainConfig[] memory expectedSourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfig[](3); + for (uint256 i = 0; i < 3; ++i) { + expectedSourceChainConfigs[i] = EVM2EVMMultiOffRamp.SourceChainConfig({ + isEnabled: sourceChainConfigs[i].isEnabled, + minSeqNr: 1, + onRamp: abi.encode(ON_RAMP_ADDRESS_1, i) + }); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainSelectorAdded(sourceChainConfigs[i].sourceChainSelector); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainConfigSet( + sourceChainConfigs[i].sourceChainSelector, expectedSourceChainConfigs[i] + ); + } + + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + + for (uint256 i = 0; i < 3; ++i) { + _assertSourceChainConfigEquality( + s_offRamp.getSourceChainConfig(sourceChainConfigs[i].sourceChainSelector), expectedSourceChainConfigs[i] + ); + } + } + + function test_Fuzz_applySourceChainConfigUpdate_Success( + EVM2EVMMultiOffRamp.SourceChainConfigArgs memory sourceChainConfigArgs + ) public { + // Skip invalid inputs + vm.assume(sourceChainConfigArgs.sourceChainSelector != 0); + vm.assume(sourceChainConfigArgs.onRamp.length != 0); + + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](2); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: ON_RAMP_ADDRESS_1, + isEnabled: true + }); + sourceChainConfigs[1] = sourceChainConfigArgs; + + // Handle cases when an update occurs + bool isNewChain = sourceChainConfigs[1].sourceChainSelector != SOURCE_CHAIN_SELECTOR_1; + if (!isNewChain) { + sourceChainConfigs[1].onRamp = sourceChainConfigs[0].onRamp; + } + + EVM2EVMMultiOffRamp.SourceChainConfig memory expectedSourceChainConfig = EVM2EVMMultiOffRamp.SourceChainConfig({ + isEnabled: sourceChainConfigArgs.isEnabled, + minSeqNr: 1, + onRamp: sourceChainConfigArgs.onRamp + }); + + if (isNewChain) { + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainSelectorAdded(sourceChainConfigArgs.sourceChainSelector); + } + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.SourceChainConfigSet(sourceChainConfigArgs.sourceChainSelector, expectedSourceChainConfig); + + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + + _assertSourceChainConfigEquality( + s_offRamp.getSourceChainConfig(sourceChainConfigArgs.sourceChainSelector), expectedSourceChainConfig + ); + } + + // Reverts + + function test_ZeroOnRampAddress_Revert() public { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](1); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: new bytes(0), + isEnabled: true + }); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroAddressNotAllowed.selector); + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + } + + function test_ZeroSourceChainSelector_Revert() public { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](1); + sourceChainConfigs[0] = + EVM2EVMMultiOffRamp.SourceChainConfigArgs({sourceChainSelector: 0, onRamp: ON_RAMP_ADDRESS_1, isEnabled: true}); + + vm.expectRevert(EVM2EVMMultiOffRamp.ZeroChainSelectorNotAllowed.selector); + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + } + + function test_ReplaceExistingChainOnRamp_Revert() public { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](1); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: ON_RAMP_ADDRESS_1, + isEnabled: true + }); + + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + + sourceChainConfigs[0].onRamp = ON_RAMP_ADDRESS_2; + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.InvalidStaticConfig.selector, SOURCE_CHAIN_SELECTOR_1)); + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + } +} + +contract EVM2EVMMultiOffRamp_commit is EVM2EVMMultiOffRampSetup { + uint64 internal s_maxInterval = 12; + + function setUp() public virtual override { + super.setUp(); + _setupMultipleOffRamps(); + + s_latestSequenceNumber = uint64(uint256(s_configDigestCommit)); + } + + function test_ReportAndPriceUpdate_Success() public { + EVM2EVMMultiOffRamp.CommitReport memory commitReport = _constructCommitReport(); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.CommitReportAccepted(commitReport); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber); + + _commit(commitReport, s_latestSequenceNumber); + + assertEq(s_maxInterval + 1, s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR).minSeqNr); + assertEq(s_latestSequenceNumber, s_offRamp.getLatestPriceSequenceNumber()); + } + + function test_ReportOnlyRootSuccess_gas() public { + uint64 max1 = 931; + bytes32 root = "Only a single root"; + + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: EVM2EVMMultiOffRamp.Interval(1, max1), + merkleRoot: root + }); + + EVM2EVMMultiOffRamp.CommitReport memory commitReport = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.CommitReportAccepted(commitReport); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber); + + _commit(commitReport, s_latestSequenceNumber); + + assertEq(max1 + 1, s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR).minSeqNr); + assertEq(0, s_offRamp.getLatestPriceSequenceNumber()); + assertEq(block.timestamp, s_offRamp.getMerkleRoot(SOURCE_CHAIN_SELECTOR_1, root)); + } + + function test_StaleReportWithRoot_Success() public { + uint64 maxSeq = 12; + uint224 tokenStartPrice = + IPriceRegistry(s_offRamp.getDynamicConfig().priceRegistry).getTokenPrice(s_sourceFeeToken).value; + + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: EVM2EVMMultiOffRamp.Interval(1, maxSeq), + merkleRoot: "stale report 1" + }); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.CommitReportAccepted(commitReport); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber); + + _commit(commitReport, s_latestSequenceNumber); + + assertEq(maxSeq + 1, s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR).minSeqNr); + assertEq(0, s_offRamp.getLatestPriceSequenceNumber()); + + commitReport.merkleRoots[0].interval = EVM2EVMMultiOffRamp.Interval(maxSeq + 1, maxSeq * 2); + commitReport.merkleRoots[0].merkleRoot = "stale report 2"; + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.CommitReportAccepted(commitReport); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber); + + _commit(commitReport, s_latestSequenceNumber); + + assertEq(maxSeq * 2 + 1, s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR).minSeqNr); + assertEq(0, s_offRamp.getLatestPriceSequenceNumber()); + assertEq( + tokenStartPrice, IPriceRegistry(s_offRamp.getDynamicConfig().priceRegistry).getTokenPrice(s_sourceFeeToken).value + ); + } + + function test_OnlyTokenPriceUpdates_Success() public { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](0); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = EVM2EVMMultiOffRamp.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + merkleRoots: roots + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, 4e18, block.timestamp); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber); + + _commit(commitReport, s_latestSequenceNumber); + + assertEq(s_latestSequenceNumber, s_offRamp.getLatestPriceSequenceNumber()); + } + + function test_OnlyGasPriceUpdates_Success() public { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](0); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = EVM2EVMMultiOffRamp.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + merkleRoots: roots + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, 4e18, block.timestamp); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber); + + _commit(commitReport, s_latestSequenceNumber); + assertEq(s_latestSequenceNumber, s_offRamp.getLatestPriceSequenceNumber()); + } + + function test_PriceSequenceNumberCleared_Success() public { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](0); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = EVM2EVMMultiOffRamp.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + merkleRoots: roots + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, 4e18, block.timestamp); + _commit(commitReport, s_latestSequenceNumber); + + assertEq(s_latestSequenceNumber, s_offRamp.getLatestPriceSequenceNumber()); + + vm.startPrank(OWNER); + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Execution), + configDigest: s_configDigestExec, + F: s_F, + isSignatureVerificationEnabled: false, + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + s_offRamp.setOCR3Configs(ocrConfigs); + + // Execution plugin OCR config should not clear latest epoch and round + assertEq(s_latestSequenceNumber, s_offRamp.getLatestPriceSequenceNumber()); + + // Commit plugin config should clear latest epoch & round + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Commit), + configDigest: s_configDigestCommit, + F: s_F, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + s_offRamp.setOCR3Configs(ocrConfigs); + + assertEq(0, s_offRamp.getLatestPriceSequenceNumber()); + + // The same sequence number can be reported again + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, 4e18, block.timestamp); + + _commit(commitReport, s_latestSequenceNumber); + } + + function test_ValidPriceUpdateThenStaleReportWithRoot_Success() public { + uint64 maxSeq = 12; + uint224 tokenPrice1 = 4e18; + uint224 tokenPrice2 = 5e18; + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](0); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = EVM2EVMMultiOffRamp.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, tokenPrice1), + merkleRoots: roots + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, tokenPrice1, block.timestamp); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber); + + _commit(commitReport, s_latestSequenceNumber); + assertEq(s_latestSequenceNumber, s_offRamp.getLatestPriceSequenceNumber()); + + roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: EVM2EVMMultiOffRamp.Interval(1, maxSeq), + merkleRoot: "stale report" + }); + commitReport.priceUpdates = getSingleTokenPriceUpdateStruct(s_sourceFeeToken, tokenPrice2); + commitReport.merkleRoots = roots; + + vm.expectEmit(); + emit EVM2EVMMultiOffRamp.CommitReportAccepted(commitReport); + + vm.expectEmit(); + emit MultiOCR3Base.Transmitted(uint8(Internal.OCRPluginType.Commit), s_configDigestCommit, s_latestSequenceNumber); + + _commit(commitReport, s_latestSequenceNumber); + + assertEq(maxSeq + 1, s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR).minSeqNr); + assertEq( + tokenPrice1, IPriceRegistry(s_offRamp.getDynamicConfig().priceRegistry).getTokenPrice(s_sourceFeeToken).value + ); + assertEq(s_latestSequenceNumber, s_offRamp.getLatestPriceSequenceNumber()); + } + + // Reverts + + function test_UnauthorizedTransmitter_Revert() public { + EVM2EVMMultiOffRamp.CommitReport memory commitReport = _constructCommitReport(); + + bytes32[3] memory reportContext = + [s_configDigestCommit, bytes32(uint256(s_latestSequenceNumber)), s_configDigestCommit]; + + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, abi.encode(commitReport), reportContext, s_F + 1); + + vm.expectRevert(MultiOCR3Base.UnauthorizedTransmitter.selector); + s_offRamp.commit(reportContext, abi.encode(commitReport), rs, ss, rawVs); + } + + function test_NoConfig_Revert() public { + _redeployOffRampWithNoOCRConfigs(); + + EVM2EVMMultiOffRamp.CommitReport memory commitReport = _constructCommitReport(); + + bytes32[3] memory reportContext = [bytes32(""), s_configDigestCommit, s_configDigestCommit]; + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, abi.encode(commitReport), reportContext, s_F + 1); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(); + s_offRamp.commit(reportContext, abi.encode(commitReport), rs, ss, rawVs); + } + + function test_NoConfigWithOtherConfigPresent_Revert() public { + _redeployOffRampWithNoOCRConfigs(); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Execution), + configDigest: s_configDigestExec, + F: s_F, + isSignatureVerificationEnabled: false, + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + s_offRamp.setOCR3Configs(ocrConfigs); + + EVM2EVMMultiOffRamp.CommitReport memory commitReport = _constructCommitReport(); + + bytes32[3] memory reportContext = [bytes32(""), s_configDigestCommit, s_configDigestCommit]; + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, abi.encode(commitReport), reportContext, s_F + 1); + + vm.startPrank(s_validTransmitters[0]); + vm.expectRevert(); + s_offRamp.commit(reportContext, abi.encode(commitReport), rs, ss, rawVs); + } + + function test_WrongConfigWithoutSigners_Revert() public { + _redeployOffRampWithNoOCRConfigs(); + + EVM2EVMMultiOffRamp.CommitReport memory commitReport = _constructCommitReport(); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](1); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Commit), + configDigest: s_configDigestCommit, + F: s_F, + isSignatureVerificationEnabled: false, + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + s_offRamp.setOCR3Configs(ocrConfigs); + + vm.expectRevert(); + _commit(commitReport, s_latestSequenceNumber); + } + + function test_Unhealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: EVM2EVMMultiOffRamp.Interval(1, 2), + merkleRoot: "Only a single root" + }); + + EVM2EVMMultiOffRamp.CommitReport memory commitReport = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.CursedByRMN.selector, roots[0].sourceChainSelector)); + _commit(commitReport, s_latestSequenceNumber); + } + + function test_InvalidRootRevert() public { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: EVM2EVMMultiOffRamp.Interval(1, 4), + merkleRoot: bytes32(0) + }); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + vm.expectRevert(EVM2EVMMultiOffRamp.InvalidRoot.selector); + _commit(commitReport, s_latestSequenceNumber); + } + + function test_InvalidInterval_Revert() public { + EVM2EVMMultiOffRamp.Interval memory interval = EVM2EVMMultiOffRamp.Interval(2, 2); + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: interval, + merkleRoot: bytes32(0) + }); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + vm.expectRevert( + abi.encodeWithSelector(EVM2EVMMultiOffRamp.InvalidInterval.selector, roots[0].sourceChainSelector, interval) + ); + _commit(commitReport, s_latestSequenceNumber); + } + + function test_InvalidIntervalMinLargerThanMax_Revert() public { + s_offRamp.getSourceChainConfig(SOURCE_CHAIN_SELECTOR); + EVM2EVMMultiOffRamp.Interval memory interval = EVM2EVMMultiOffRamp.Interval(1, 0); + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: interval, + merkleRoot: bytes32(0) + }); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + vm.expectRevert( + abi.encodeWithSelector(EVM2EVMMultiOffRamp.InvalidInterval.selector, roots[0].sourceChainSelector, interval) + ); + _commit(commitReport, s_latestSequenceNumber); + } + + function test_ZeroEpochAndRound_Revert() public { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](0); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = EVM2EVMMultiOffRamp.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + merkleRoots: roots + }); + + vm.expectRevert(EVM2EVMMultiOffRamp.StaleCommitReport.selector); + _commit(commitReport, 0); + } + + function test_OnlyPriceUpdateStaleReport_Revert() public { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](0); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = EVM2EVMMultiOffRamp.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + merkleRoots: roots + }); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated(s_sourceFeeToken, 4e18, block.timestamp); + _commit(commitReport, s_latestSequenceNumber); + + vm.expectRevert(EVM2EVMMultiOffRamp.StaleCommitReport.selector); + _commit(commitReport, s_latestSequenceNumber); + } + + function test_SourceChainNotEnabled_Revert() public { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: 0, + interval: EVM2EVMMultiOffRamp.Interval(1, 2), + merkleRoot: "Only a single root" + }); + + EVM2EVMMultiOffRamp.CommitReport memory commitReport = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOffRamp.SourceChainNotEnabled.selector, 0)); + _commit(commitReport, s_latestSequenceNumber); + } + + function test_RootAlreadyCommitted_Revert() public { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: EVM2EVMMultiOffRamp.Interval(1, 2), + merkleRoot: "Only a single root" + }); + EVM2EVMMultiOffRamp.CommitReport memory commitReport = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + _commit(commitReport, s_latestSequenceNumber); + commitReport.merkleRoots[0].interval = EVM2EVMMultiOffRamp.Interval(3, 3); + + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMMultiOffRamp.RootAlreadyCommitted.selector, roots[0].sourceChainSelector, roots[0].merkleRoot + ) + ); + _commit(commitReport, ++s_latestSequenceNumber); + } + + function _constructCommitReport() internal view returns (EVM2EVMMultiOffRamp.CommitReport memory) { + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + interval: EVM2EVMMultiOffRamp.Interval(1, s_maxInterval), + merkleRoot: "test #2" + }); + + return EVM2EVMMultiOffRamp.CommitReport({ + priceUpdates: getSingleTokenPriceUpdateStruct(s_sourceFeeToken, 4e18), + merkleRoots: roots + }); + } +} + +contract EVM2EVMMultiOffRamp_resetUnblessedRoots is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupRealRMN(); + _deployOffRamp(s_destRouter, s_realRMN, s_inboundNonceManager); + _setupMultipleOffRamps(); + } + + function test_ResetUnblessedRoots_Success() public { + EVM2EVMMultiOffRamp.UnblessedRoot[] memory rootsToReset = new EVM2EVMMultiOffRamp.UnblessedRoot[](3); + rootsToReset[0] = EVM2EVMMultiOffRamp.UnblessedRoot({sourceChainSelector: SOURCE_CHAIN_SELECTOR, merkleRoot: "1"}); + rootsToReset[1] = EVM2EVMMultiOffRamp.UnblessedRoot({sourceChainSelector: SOURCE_CHAIN_SELECTOR, merkleRoot: "2"}); + rootsToReset[2] = EVM2EVMMultiOffRamp.UnblessedRoot({sourceChainSelector: SOURCE_CHAIN_SELECTOR, merkleRoot: "3"}); + + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](3); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + interval: EVM2EVMMultiOffRamp.Interval(1, 2), + merkleRoot: rootsToReset[0].merkleRoot + }); + roots[1] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + interval: EVM2EVMMultiOffRamp.Interval(3, 4), + merkleRoot: rootsToReset[1].merkleRoot + }); + roots[2] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + interval: EVM2EVMMultiOffRamp.Interval(5, 5), + merkleRoot: rootsToReset[2].merkleRoot + }); + + EVM2EVMMultiOffRamp.CommitReport memory report = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + + _commit(report, ++s_latestSequenceNumber); + + IRMN.TaggedRoot[] memory blessedTaggedRoots = new IRMN.TaggedRoot[](1); + blessedTaggedRoots[0] = IRMN.TaggedRoot({commitStore: address(s_offRamp), root: rootsToReset[1].merkleRoot}); + + vm.startPrank(BLESS_VOTE_ADDR); + s_realRMN.voteToBless(blessedTaggedRoots); + + vm.expectEmit(false, false, false, true); + emit EVM2EVMMultiOffRamp.RootRemoved(rootsToReset[0].merkleRoot); + + vm.expectEmit(false, false, false, true); + emit EVM2EVMMultiOffRamp.RootRemoved(rootsToReset[2].merkleRoot); + + vm.startPrank(OWNER); + s_offRamp.resetUnblessedRoots(rootsToReset); + + assertEq(0, s_offRamp.getMerkleRoot(SOURCE_CHAIN_SELECTOR, rootsToReset[0].merkleRoot)); + assertEq(BLOCK_TIME, s_offRamp.getMerkleRoot(SOURCE_CHAIN_SELECTOR, rootsToReset[1].merkleRoot)); + assertEq(0, s_offRamp.getMerkleRoot(SOURCE_CHAIN_SELECTOR, rootsToReset[2].merkleRoot)); + } + + // Reverts + + function test_OnlyOwner_Revert() public { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + EVM2EVMMultiOffRamp.UnblessedRoot[] memory rootsToReset = new EVM2EVMMultiOffRamp.UnblessedRoot[](0); + s_offRamp.resetUnblessedRoots(rootsToReset); + } +} + +contract EVM2EVMMultiOffRamp_verify is EVM2EVMMultiOffRampSetup { + function setUp() public virtual override { + super.setUp(); + _setupRealRMN(); + _deployOffRamp(s_destRouter, s_realRMN, s_inboundNonceManager); + _setupMultipleOffRamps(); + } + + function test_NotBlessed_Success() public { + bytes32[] memory leaves = new bytes32[](1); + leaves[0] = "root"; + + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + interval: EVM2EVMMultiOffRamp.Interval(1, 2), + merkleRoot: leaves[0] + }); + EVM2EVMMultiOffRamp.CommitReport memory report = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + _commit(report, ++s_latestSequenceNumber); + bytes32[] memory proofs = new bytes32[](0); + // We have not blessed this root, should return 0. + uint256 timestamp = s_offRamp.verify(SOURCE_CHAIN_SELECTOR, leaves, proofs, 0); + assertEq(uint256(0), timestamp); + } + + function test_Blessed_Success() public { + bytes32[] memory leaves = new bytes32[](1); + leaves[0] = "root"; + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + interval: EVM2EVMMultiOffRamp.Interval(1, 2), + merkleRoot: leaves[0] + }); + EVM2EVMMultiOffRamp.CommitReport memory report = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + _commit(report, ++s_latestSequenceNumber); + // Bless that root. + IRMN.TaggedRoot[] memory taggedRoots = new IRMN.TaggedRoot[](1); + taggedRoots[0] = IRMN.TaggedRoot({commitStore: address(s_offRamp), root: leaves[0]}); + vm.startPrank(BLESS_VOTE_ADDR); + s_realRMN.voteToBless(taggedRoots); + bytes32[] memory proofs = new bytes32[](0); + uint256 timestamp = s_offRamp.verify(SOURCE_CHAIN_SELECTOR, leaves, proofs, 0); + assertEq(BLOCK_TIME, timestamp); + } + + function test_NotBlessedWrongChainSelector_Success() public { + bytes32[] memory leaves = new bytes32[](1); + leaves[0] = "root"; + EVM2EVMMultiOffRamp.MerkleRoot[] memory roots = new EVM2EVMMultiOffRamp.MerkleRoot[](1); + roots[0] = EVM2EVMMultiOffRamp.MerkleRoot({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + interval: EVM2EVMMultiOffRamp.Interval(1, 2), + merkleRoot: leaves[0] + }); + + EVM2EVMMultiOffRamp.CommitReport memory report = + EVM2EVMMultiOffRamp.CommitReport({priceUpdates: getEmptyPriceUpdates(), merkleRoots: roots}); + _commit(report, ++s_latestSequenceNumber); + + // Bless that root. + IRMN.TaggedRoot[] memory taggedRoots = new IRMN.TaggedRoot[](1); + taggedRoots[0] = IRMN.TaggedRoot({commitStore: address(s_offRamp), root: leaves[0]}); + vm.startPrank(BLESS_VOTE_ADDR); + s_realRMN.voteToBless(taggedRoots); + + bytes32[] memory proofs = new bytes32[](0); + uint256 timestamp = s_offRamp.verify(SOURCE_CHAIN_SELECTOR + 1, leaves, proofs, 0); + assertEq(uint256(0), timestamp); + } + + // Reverts + + function test_TooManyLeaves_Revert() public { + bytes32[] memory leaves = new bytes32[](258); + bytes32[] memory proofs = new bytes32[](0); + vm.expectRevert(MerkleMultiProof.InvalidProof.selector); + s_offRamp.verify(SOURCE_CHAIN_SELECTOR, leaves, proofs, 0); + } +} diff --git a/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMMultiOffRampSetup.t.sol b/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMMultiOffRampSetup.t.sol new file mode 100644 index 00000000000..507e966a70a --- /dev/null +++ b/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMMultiOffRampSetup.t.sol @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IAny2EVMMessageReceiver} from "../../interfaces/IAny2EVMMessageReceiver.sol"; + +import {IAny2EVMOffRamp} from "../../interfaces/IAny2EVMOffRamp.sol"; +import {ICommitStore} from "../../interfaces/ICommitStore.sol"; +import {IRMN} from "../../interfaces/IRMN.sol"; + +import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol"; +import {NonceManager} from "../../NonceManager.sol"; +import {RMN} from "../../RMN.sol"; +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {MultiOCR3Base} from "../../ocr/MultiOCR3Base.sol"; +import {EVM2EVMMultiOffRamp} from "../../offRamp/EVM2EVMMultiOffRamp.sol"; +import {EVM2EVMOffRamp} from "../../offRamp/EVM2EVMOffRamp.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {TokenSetup} from "../TokenSetup.t.sol"; +import {EVM2EVMMultiOffRampHelper} from "../helpers/EVM2EVMMultiOffRampHelper.sol"; +import {EVM2EVMOffRampHelper} from "../helpers/EVM2EVMOffRampHelper.sol"; +import {MaybeRevertingBurnMintTokenPool} from "../helpers/MaybeRevertingBurnMintTokenPool.sol"; +import {MessageInterceptorHelper} from "../helpers/MessageInterceptorHelper.sol"; +import {MaybeRevertMessageReceiver} from "../helpers/receivers/MaybeRevertMessageReceiver.sol"; +import {MockCommitStore} from "../mocks/MockCommitStore.sol"; +import {MultiOCR3BaseSetup} from "../ocr/MultiOCR3BaseSetup.t.sol"; +import {PriceRegistrySetup} from "../priceRegistry/PriceRegistry.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract EVM2EVMMultiOffRampSetup is TokenSetup, PriceRegistrySetup, MultiOCR3BaseSetup { + uint64 internal constant SOURCE_CHAIN_SELECTOR_1 = SOURCE_CHAIN_SELECTOR; + uint64 internal constant SOURCE_CHAIN_SELECTOR_2 = 6433500567565415381; + uint64 internal constant SOURCE_CHAIN_SELECTOR_3 = 4051577828743386545; + + bytes internal constant ON_RAMP_ADDRESS_1 = abi.encode(ON_RAMP_ADDRESS); + bytes internal constant ON_RAMP_ADDRESS_2 = abi.encode(0xaA3f843Cf8E33B1F02dd28303b6bD87B1aBF8AE4); + bytes internal constant ON_RAMP_ADDRESS_3 = abi.encode(0x71830C37Cb193e820de488Da111cfbFcC680a1b9); + + address internal constant BLESS_VOTE_ADDR = address(8888); + + IAny2EVMMessageReceiver internal s_receiver; + IAny2EVMMessageReceiver internal s_secondary_receiver; + MaybeRevertMessageReceiver internal s_reverting_receiver; + + MaybeRevertingBurnMintTokenPool internal s_maybeRevertingPool; + + EVM2EVMMultiOffRampHelper internal s_offRamp; + MessageInterceptorHelper internal s_inboundMessageValidator; + NonceManager internal s_inboundNonceManager; + RMN internal s_realRMN; + address internal s_sourceTokenPool = makeAddr("sourceTokenPool"); + + bytes32 internal s_configDigestExec; + bytes32 internal s_configDigestCommit; + uint64 internal constant s_offchainConfigVersion = 3; + uint8 internal constant s_F = 1; + + uint64 internal s_latestSequenceNumber; + + function setUp() public virtual override(TokenSetup, PriceRegistrySetup, MultiOCR3BaseSetup) { + TokenSetup.setUp(); + PriceRegistrySetup.setUp(); + MultiOCR3BaseSetup.setUp(); + + s_inboundMessageValidator = new MessageInterceptorHelper(); + s_receiver = new MaybeRevertMessageReceiver(false); + s_secondary_receiver = new MaybeRevertMessageReceiver(false); + s_reverting_receiver = new MaybeRevertMessageReceiver(true); + + s_maybeRevertingPool = MaybeRevertingBurnMintTokenPool(s_destPoolByToken[s_destTokens[1]]); + s_inboundNonceManager = new NonceManager(new address[](0)); + + _deployOffRamp(s_destRouter, s_mockRMN, s_inboundNonceManager); + } + + function _deployOffRamp(Router router, IRMN rmnProxy, NonceManager nonceManager) internal { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](0); + + s_offRamp = new EVM2EVMMultiOffRampHelper( + EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + rmnProxy: address(rmnProxy), + tokenAdminRegistry: address(s_tokenAdminRegistry), + nonceManager: address(nonceManager) + }), + _generateDynamicMultiOffRampConfig(address(router), address(s_priceRegistry)), + sourceChainConfigs + ); + + s_configDigestExec = _getBasicConfigDigest(s_F, s_emptySigners, s_validTransmitters); + s_configDigestCommit = _getBasicConfigDigest(s_F, s_validSigners, s_validTransmitters); + + MultiOCR3Base.OCRConfigArgs[] memory ocrConfigs = new MultiOCR3Base.OCRConfigArgs[](2); + ocrConfigs[0] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Execution), + configDigest: s_configDigestExec, + F: s_F, + isSignatureVerificationEnabled: false, + signers: s_emptySigners, + transmitters: s_validTransmitters + }); + ocrConfigs[1] = MultiOCR3Base.OCRConfigArgs({ + ocrPluginType: uint8(Internal.OCRPluginType.Commit), + configDigest: s_configDigestCommit, + F: s_F, + isSignatureVerificationEnabled: true, + signers: s_validSigners, + transmitters: s_validTransmitters + }); + + s_offRamp.setDynamicConfig(_generateDynamicMultiOffRampConfig(address(router), address(s_priceRegistry))); + s_offRamp.setOCR3Configs(ocrConfigs); + + address[] memory authorizedCallers = new address[](1); + authorizedCallers[0] = address(s_offRamp); + NonceManager(nonceManager).applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: authorizedCallers, removedCallers: new address[](0)}) + ); + + address[] memory priceUpdaters = new address[](1); + priceUpdaters[0] = address(s_offRamp); + s_priceRegistry.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: priceUpdaters, removedCallers: new address[](0)}) + ); + } + + // TODO: function can be made common across OffRampSetup and MultiOffRampSetup + function _deploySingleLaneOffRamp( + ICommitStore commitStore, + Router router, + address prevOffRamp, + uint64 sourceChainSelector, + address onRampAddress + ) internal returns (EVM2EVMOffRampHelper) { + EVM2EVMOffRampHelper offRamp = new EVM2EVMOffRampHelper( + EVM2EVMOffRamp.StaticConfig({ + commitStore: address(commitStore), + chainSelector: DEST_CHAIN_SELECTOR, + sourceChainSelector: sourceChainSelector, + onRamp: onRampAddress, + prevOffRamp: prevOffRamp, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + getInboundRateLimiterConfig() + ); + offRamp.setOCR2Config( + s_validSigners, + s_validTransmitters, + s_F, + abi.encode(_generateDynamicOffRampConfig(address(router), address(s_priceRegistry))), + s_offchainConfigVersion, + abi.encode("") + ); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](0); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](2); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: sourceChainSelector, offRamp: address(s_offRamp)}); + offRampUpdates[1] = Router.OffRamp({sourceChainSelector: sourceChainSelector, offRamp: address(prevOffRamp)}); + s_destRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + EVM2EVMOffRamp.RateLimitToken[] memory tokensToAdd = new EVM2EVMOffRamp.RateLimitToken[](s_sourceTokens.length); + for (uint256 i = 0; i < s_sourceTokens.length; ++i) { + tokensToAdd[i] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceTokens[i], destToken: s_destTokens[i]}); + } + offRamp.updateRateLimitTokens(new EVM2EVMOffRamp.RateLimitToken[](0), tokensToAdd); + + return offRamp; + } + + function _setupMultipleOffRamps() internal { + EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs = + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](3); + sourceChainConfigs[0] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_1, + onRamp: ON_RAMP_ADDRESS_1, + isEnabled: true + }); + sourceChainConfigs[1] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_2, + onRamp: ON_RAMP_ADDRESS_2, + isEnabled: false + }); + sourceChainConfigs[2] = EVM2EVMMultiOffRamp.SourceChainConfigArgs({ + sourceChainSelector: SOURCE_CHAIN_SELECTOR_3, + onRamp: ON_RAMP_ADDRESS_3, + isEnabled: true + }); + _setupMultipleOffRampsFromConfigs(sourceChainConfigs); + } + + function _setupMultipleOffRampsFromConfigs(EVM2EVMMultiOffRamp.SourceChainConfigArgs[] memory sourceChainConfigs) + internal + { + s_offRamp.applySourceChainConfigUpdates(sourceChainConfigs); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](0); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](2 * sourceChainConfigs.length); + + for (uint256 i = 0; i < sourceChainConfigs.length; ++i) { + uint64 sourceChainSelector = sourceChainConfigs[i].sourceChainSelector; + + offRampUpdates[2 * i] = Router.OffRamp({sourceChainSelector: sourceChainSelector, offRamp: address(s_offRamp)}); + offRampUpdates[2 * i + 1] = Router.OffRamp({ + sourceChainSelector: sourceChainSelector, + offRamp: s_inboundNonceManager.getPreviousRamps(sourceChainSelector).prevOffRamp + }); + } + + s_destRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + function _generateDynamicOffRampConfig( + address router, + address priceRegistry + ) internal pure returns (EVM2EVMOffRamp.DynamicConfig memory) { + return EVM2EVMOffRamp.DynamicConfig({ + permissionLessExecutionThresholdSeconds: PERMISSION_LESS_EXECUTION_THRESHOLD_SECONDS, + router: router, + priceRegistry: priceRegistry, + maxNumberOfTokensPerMsg: MAX_TOKENS_LENGTH, + maxDataBytes: MAX_DATA_SIZE, + maxPoolReleaseOrMintGas: MAX_TOKEN_POOL_RELEASE_OR_MINT_GAS, + maxTokenTransferGas: MAX_TOKEN_POOL_TRANSFER_GAS + }); + } + + function _generateDynamicMultiOffRampConfig( + address router, + address priceRegistry + ) internal pure returns (EVM2EVMMultiOffRamp.DynamicConfig memory) { + return EVM2EVMMultiOffRamp.DynamicConfig({ + permissionLessExecutionThresholdSeconds: PERMISSION_LESS_EXECUTION_THRESHOLD_SECONDS, + router: router, + priceRegistry: priceRegistry, + messageValidator: address(0), + maxPoolReleaseOrMintGas: MAX_TOKEN_POOL_RELEASE_OR_MINT_GAS, + maxTokenTransferGas: MAX_TOKEN_POOL_TRANSFER_GAS + }); + } + + function _convertToGeneralMessage(Internal.Any2EVMRampMessage memory original) + internal + view + returns (Client.Any2EVMMessage memory message) + { + uint256 numberOfTokens = original.tokenAmounts.length; + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](numberOfTokens); + + for (uint256 i = 0; i < numberOfTokens; ++i) { + Internal.RampTokenAmount memory tokenAmount = original.tokenAmounts[i]; + + address destPoolAddress = abi.decode(tokenAmount.destTokenAddress, (address)); + TokenPool pool = TokenPool(destPoolAddress); + destTokenAmounts[i].token = address(pool.getToken()); + destTokenAmounts[i].amount = tokenAmount.amount; + } + + return Client.Any2EVMMessage({ + messageId: original.header.messageId, + sourceChainSelector: original.header.sourceChainSelector, + sender: abi.encode(original.sender), + data: original.data, + destTokenAmounts: destTokenAmounts + }); + } + + function _generateAny2EVMMessageNoTokens( + uint64 sourceChainSelector, + bytes memory onRamp, + uint64 sequenceNumber + ) internal view returns (Internal.Any2EVMRampMessage memory) { + return _generateAny2EVMMessage(sourceChainSelector, onRamp, sequenceNumber, new Client.EVMTokenAmount[](0), false); + } + + function _generateAny2EVMMessageWithTokens( + uint64 sourceChainSelector, + bytes memory onRamp, + uint64 sequenceNumber, + uint256[] memory amounts + ) internal view returns (Internal.Any2EVMRampMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + tokenAmounts[i].amount = amounts[i]; + } + return _generateAny2EVMMessage(sourceChainSelector, onRamp, sequenceNumber, tokenAmounts, false); + } + + function _generateAny2EVMMessage( + uint64 sourceChainSelector, + bytes memory onRamp, + uint64 sequenceNumber, + Client.EVMTokenAmount[] memory tokenAmounts, + bool allowOutOfOrderExecution + ) internal view returns (Internal.Any2EVMRampMessage memory) { + bytes memory data = abi.encode(0); + + Internal.RampTokenAmount[] memory rampTokenAmounts = new Internal.RampTokenAmount[](tokenAmounts.length); + + // Correctly set the TokenDataPayload for each token. Tokens have to be set up in the TokenSetup. + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + rampTokenAmounts[i] = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[tokenAmounts[i].token]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[tokenAmounts[i].token]), + extraData: "", + amount: tokenAmounts[i].amount + }); + } + + Internal.Any2EVMRampMessage memory message = Internal.Any2EVMRampMessage({ + header: Internal.RampMessageHeader({ + messageId: "", + sourceChainSelector: sourceChainSelector, + destChainSelector: DEST_CHAIN_SELECTOR, + sequenceNumber: sequenceNumber, + nonce: allowOutOfOrderExecution ? 0 : sequenceNumber + }), + sender: abi.encode(OWNER), + data: data, + receiver: address(s_receiver), + tokenAmounts: rampTokenAmounts, + gasLimit: GAS_LIMIT + }); + + message.header.messageId = Internal._hash(message, onRamp); + + return message; + } + + function _generateSingleBasicMessage( + uint64 sourceChainSelector, + bytes memory onRamp + ) internal view returns (Internal.Any2EVMRampMessage[] memory) { + Internal.Any2EVMRampMessage[] memory messages = new Internal.Any2EVMRampMessage[](1); + messages[0] = _generateAny2EVMMessageNoTokens(sourceChainSelector, onRamp, 1); + return messages; + } + + function _generateMessagesWithTokens( + uint64 sourceChainSelector, + bytes memory onRamp + ) internal view returns (Internal.Any2EVMRampMessage[] memory) { + Internal.Any2EVMRampMessage[] memory messages = new Internal.Any2EVMRampMessage[](2); + Client.EVMTokenAmount[] memory tokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + tokenAmounts[0].amount = 1e18; + tokenAmounts[1].amount = 5e18; + messages[0] = _generateAny2EVMMessage(sourceChainSelector, onRamp, 1, tokenAmounts, false); + messages[1] = _generateAny2EVMMessage(sourceChainSelector, onRamp, 2, tokenAmounts, false); + + return messages; + } + + function _generateReportFromMessages( + uint64 sourceChainSelector, + Internal.Any2EVMRampMessage[] memory messages + ) internal pure returns (Internal.ExecutionReportSingleChain memory) { + bytes[][] memory offchainTokenData = new bytes[][](messages.length); + + for (uint256 i = 0; i < messages.length; ++i) { + offchainTokenData[i] = new bytes[](messages[i].tokenAmounts.length); + } + + return Internal.ExecutionReportSingleChain({ + sourceChainSelector: sourceChainSelector, + proofs: new bytes32[](0), + proofFlagBits: 2 ** 256 - 1, + messages: messages, + offchainTokenData: offchainTokenData + }); + } + + function _generateBatchReportFromMessages( + uint64 sourceChainSelector, + Internal.Any2EVMRampMessage[] memory messages + ) internal pure returns (Internal.ExecutionReportSingleChain[] memory) { + Internal.ExecutionReportSingleChain[] memory reports = new Internal.ExecutionReportSingleChain[](1); + reports[0] = _generateReportFromMessages(sourceChainSelector, messages); + return reports; + } + + function _getGasLimitsFromMessages(Internal.Any2EVMRampMessage[] memory messages) + internal + pure + returns (uint256[] memory) + { + uint256[] memory gasLimits = new uint256[](messages.length); + for (uint256 i = 0; i < messages.length; ++i) { + gasLimits[i] = messages[i].gasLimit; + } + + return gasLimits; + } + + function _assertSameConfig( + EVM2EVMMultiOffRamp.DynamicConfig memory a, + EVM2EVMMultiOffRamp.DynamicConfig memory b + ) public pure { + assertEq(a.permissionLessExecutionThresholdSeconds, b.permissionLessExecutionThresholdSeconds); + assertEq(a.router, b.router); + assertEq(a.maxPoolReleaseOrMintGas, b.maxPoolReleaseOrMintGas); + assertEq(a.maxTokenTransferGas, b.maxTokenTransferGas); + assertEq(a.messageValidator, b.messageValidator); + assertEq(a.priceRegistry, b.priceRegistry); + } + + function _assertSourceChainConfigEquality( + EVM2EVMMultiOffRamp.SourceChainConfig memory config1, + EVM2EVMMultiOffRamp.SourceChainConfig memory config2 + ) internal pure { + assertEq(config1.isEnabled, config2.isEnabled); + assertEq(config1.minSeqNr, config2.minSeqNr); + assertEq(config1.onRamp, config2.onRamp); + } + + function _getDefaultSourceTokenData(Client.EVMTokenAmount[] memory srcTokenAmounts) + internal + view + returns (Internal.RampTokenAmount[] memory) + { + Internal.RampTokenAmount[] memory sourceTokenData = new Internal.RampTokenAmount[](srcTokenAmounts.length); + for (uint256 i = 0; i < srcTokenAmounts.length; ++i) { + sourceTokenData[i] = Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[srcTokenAmounts[i].token]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[srcTokenAmounts[i].token]), + extraData: "", + amount: srcTokenAmounts[i].amount + }); + } + return sourceTokenData; + } + + function _enableInboundMessageValidator() internal { + EVM2EVMMultiOffRamp.DynamicConfig memory dynamicConfig = s_offRamp.getDynamicConfig(); + dynamicConfig.messageValidator = address(s_inboundMessageValidator); + s_offRamp.setDynamicConfig(dynamicConfig); + } + + function _redeployOffRampWithNoOCRConfigs() internal { + s_offRamp = new EVM2EVMMultiOffRampHelper( + EVM2EVMMultiOffRamp.StaticConfig({ + chainSelector: DEST_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry), + nonceManager: address(s_inboundNonceManager) + }), + _generateDynamicMultiOffRampConfig(address(s_destRouter), address(s_priceRegistry)), + new EVM2EVMMultiOffRamp.SourceChainConfigArgs[](0) + ); + + address[] memory authorizedCallers = new address[](1); + authorizedCallers[0] = address(s_offRamp); + s_inboundNonceManager.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: authorizedCallers, removedCallers: new address[](0)}) + ); + _setupMultipleOffRamps(); + + address[] memory priceUpdaters = new address[](1); + priceUpdaters[0] = address(s_offRamp); + s_priceRegistry.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: priceUpdaters, removedCallers: new address[](0)}) + ); + } + + function _setupRealRMN() internal { + RMN.Voter[] memory voters = new RMN.Voter[](1); + voters[0] = + RMN.Voter({blessVoteAddr: BLESS_VOTE_ADDR, curseVoteAddr: address(9999), blessWeight: 1, curseWeight: 1}); + // Overwrite base mock rmn with real. + s_realRMN = new RMN(RMN.Config({voters: voters, blessWeightThreshold: 1, curseWeightThreshold: 1})); + } + + function _commit(EVM2EVMMultiOffRamp.CommitReport memory commitReport, uint64 sequenceNumber) internal { + bytes32[3] memory reportContext = [s_configDigestCommit, bytes32(uint256(sequenceNumber)), s_configDigestCommit]; + + (bytes32[] memory rs, bytes32[] memory ss,, bytes32 rawVs) = + _getSignaturesForDigest(s_validSignerKeys, abi.encode(commitReport), reportContext, s_F + 1); + + vm.startPrank(s_validTransmitters[0]); + s_offRamp.commit(reportContext, abi.encode(commitReport), rs, ss, rawVs); + } + + function _execute(Internal.ExecutionReportSingleChain[] memory reports) internal { + bytes32[3] memory reportContext = [s_configDigestExec, s_configDigestExec, s_configDigestExec]; + + vm.startPrank(s_validTransmitters[0]); + s_offRamp.execute(reportContext, abi.encode(reports)); + } +} diff --git a/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMOffRamp.t.sol b/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMOffRamp.t.sol new file mode 100644 index 00000000000..e94184e3c5e --- /dev/null +++ b/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMOffRamp.t.sol @@ -0,0 +1,1986 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ICommitStore} from "../../interfaces/ICommitStore.sol"; +import {IPoolV1} from "../../interfaces/IPool.sol"; +import {ITokenAdminRegistry} from "../../interfaces/ITokenAdminRegistry.sol"; + +import {CallWithExactGas} from "../../../shared/call/CallWithExactGas.sol"; + +import {GenericReceiver} from "../../../shared/test/testhelpers/GenericReceiver.sol"; +import {AggregateRateLimiter} from "../../AggregateRateLimiter.sol"; +import {RMN} from "../../RMN.sol"; +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {OCR2Abstract} from "../../ocr/OCR2Abstract.sol"; +import {EVM2EVMOffRamp} from "../../offRamp/EVM2EVMOffRamp.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {EVM2EVMOffRampHelper} from "../helpers/EVM2EVMOffRampHelper.sol"; +import {MaybeRevertingBurnMintTokenPool} from "../helpers/MaybeRevertingBurnMintTokenPool.sol"; +import {ConformingReceiver} from "../helpers/receivers/ConformingReceiver.sol"; +import {MaybeRevertMessageReceiver} from "../helpers/receivers/MaybeRevertMessageReceiver.sol"; +import {MaybeRevertMessageReceiverNo165} from "../helpers/receivers/MaybeRevertMessageReceiverNo165.sol"; +import {ReentrancyAbuser} from "../helpers/receivers/ReentrancyAbuser.sol"; +import {MockCommitStore} from "../mocks/MockCommitStore.sol"; +import {OCR2Base} from "../ocr/OCR2Base.t.sol"; +import {OCR2BaseNoChecks} from "../ocr/OCR2BaseNoChecks.t.sol"; +import {EVM2EVMOffRampSetup} from "./EVM2EVMOffRampSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract EVM2EVMOffRamp_constructor is EVM2EVMOffRampSetup { + function test_Constructor_Success() public { + EVM2EVMOffRamp.StaticConfig memory staticConfig = EVM2EVMOffRamp.StaticConfig({ + commitStore: address(s_mockCommitStore), + chainSelector: DEST_CHAIN_SELECTOR, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + onRamp: ON_RAMP_ADDRESS, + prevOffRamp: address(0), + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }); + EVM2EVMOffRamp.DynamicConfig memory dynamicConfig = + generateDynamicOffRampConfig(address(s_destRouter), address(s_priceRegistry)); + + s_offRamp = new EVM2EVMOffRampHelper(staticConfig, getInboundRateLimiterConfig()); + + s_offRamp.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + + // Static config + EVM2EVMOffRamp.StaticConfig memory gotStaticConfig = s_offRamp.getStaticConfig(); + assertEq(staticConfig.commitStore, gotStaticConfig.commitStore); + assertEq(staticConfig.sourceChainSelector, gotStaticConfig.sourceChainSelector); + assertEq(staticConfig.chainSelector, gotStaticConfig.chainSelector); + assertEq(staticConfig.onRamp, gotStaticConfig.onRamp); + assertEq(staticConfig.prevOffRamp, gotStaticConfig.prevOffRamp); + assertEq(staticConfig.tokenAdminRegistry, gotStaticConfig.tokenAdminRegistry); + + // Dynamic config + EVM2EVMOffRamp.DynamicConfig memory gotDynamicConfig = s_offRamp.getDynamicConfig(); + _assertSameConfig(dynamicConfig, gotDynamicConfig); + + (uint32 configCount, uint32 blockNumber,) = s_offRamp.latestConfigDetails(); + assertEq(1, configCount); + assertEq(block.number, blockNumber); + + // OffRamp initial values + assertEq("EVM2EVMOffRamp 1.5.0-dev", s_offRamp.typeAndVersion()); + assertEq(OWNER, s_offRamp.owner()); + } + + // Revert + function test_ZeroOnRampAddress_Revert() public { + vm.expectRevert(EVM2EVMOffRamp.ZeroAddressNotAllowed.selector); + + s_offRamp = new EVM2EVMOffRampHelper( + EVM2EVMOffRamp.StaticConfig({ + commitStore: address(s_mockCommitStore), + chainSelector: DEST_CHAIN_SELECTOR, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + onRamp: ZERO_ADDRESS, + prevOffRamp: address(0), + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + RateLimiter.Config({isEnabled: true, rate: 1e20, capacity: 1e20}) + ); + } + + function test_CommitStoreAlreadyInUse_Revert() public { + s_mockCommitStore.setExpectedNextSequenceNumber(2); + + vm.expectRevert(EVM2EVMOffRamp.CommitStoreAlreadyInUse.selector); + + s_offRamp = new EVM2EVMOffRampHelper( + EVM2EVMOffRamp.StaticConfig({ + commitStore: address(s_mockCommitStore), + chainSelector: DEST_CHAIN_SELECTOR, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + onRamp: ON_RAMP_ADDRESS, + prevOffRamp: address(0), + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + getInboundRateLimiterConfig() + ); + } +} + +contract EVM2EVMOffRamp_setDynamicConfig is EVM2EVMOffRampSetup { + function test_SetDynamicConfig_Success() public { + EVM2EVMOffRamp.StaticConfig memory staticConfig = s_offRamp.getStaticConfig(); + EVM2EVMOffRamp.DynamicConfig memory dynamicConfig = generateDynamicOffRampConfig(USER_3, address(s_priceRegistry)); + bytes memory onchainConfig = abi.encode(dynamicConfig); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ConfigSet(staticConfig, dynamicConfig); + + vm.expectEmit(); + uint32 configCount = 1; + emit OCR2Abstract.ConfigSet( + uint32(block.number), + getBasicConfigDigest(address(s_offRamp), s_f, configCount, onchainConfig), + configCount + 1, + s_valid_signers, + s_valid_transmitters, + s_f, + onchainConfig, + s_offchainConfigVersion, + abi.encode("") + ); + + s_offRamp.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, onchainConfig, s_offchainConfigVersion, abi.encode("") + ); + + EVM2EVMOffRamp.DynamicConfig memory newConfig = s_offRamp.getDynamicConfig(); + _assertSameConfig(dynamicConfig, newConfig); + } + + function test_NonOwner_Revert() public { + vm.startPrank(STRANGER); + EVM2EVMOffRamp.DynamicConfig memory dynamicConfig = generateDynamicOffRampConfig(USER_3, address(s_priceRegistry)); + + vm.expectRevert("Only callable by owner"); + + s_offRamp.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + } + + function test_RouterZeroAddress_Revert() public { + EVM2EVMOffRamp.DynamicConfig memory dynamicConfig = generateDynamicOffRampConfig(ZERO_ADDRESS, ZERO_ADDRESS); + + vm.expectRevert(EVM2EVMOffRamp.ZeroAddressNotAllowed.selector); + + s_offRamp.setOCR2Config( + s_valid_signers, s_valid_transmitters, s_f, abi.encode(dynamicConfig), s_offchainConfigVersion, abi.encode("") + ); + } +} + +contract EVM2EVMOffRamp_metadataHash is EVM2EVMOffRampSetup { + function test_MetadataHash_Success() public view { + bytes32 h = s_offRamp.metadataHash(); + assertEq( + h, + keccak256( + abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, ON_RAMP_ADDRESS) + ) + ); + } +} + +contract EVM2EVMOffRamp_ccipReceive is EVM2EVMOffRampSetup { + // Reverts + + function test_Reverts() public { + Client.Any2EVMMessage memory message = _convertToGeneralMessage(_generateAny2EVMMessageNoTokens(1)); + vm.expectRevert(); + s_offRamp.ccipReceive(message); + } +} + +contract EVM2EVMOffRamp_execute is EVM2EVMOffRampSetup { + error PausedError(); + + function _generateMsgWithoutTokens( + uint256 gasLimit, + bytes memory messageData + ) internal view returns (Internal.EVM2EVMMessage memory) { + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageNoTokens(1); + message.gasLimit = gasLimit; + message.data = messageData; + message.messageId = Internal._hash( + message, + keccak256( + abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, ON_RAMP_ADDRESS) + ) + ); + return message; + } + + function test_Fuzz_trialExecuteWithoutTokens_Success(bytes4 funcSelector, bytes memory messageData) public { + vm.assume( + funcSelector != GenericReceiver.setRevert.selector && funcSelector != GenericReceiver.setErr.selector + && funcSelector != 0x5100fc21 && funcSelector != 0x00000000 // s_toRevert(), which is public and therefore has a function selector + ); + + // Convert bytes4 into bytes memory to use in the message + Internal.EVM2EVMMessage memory message = _generateMsgWithoutTokens(GAS_LIMIT, messageData); + + // Convert an Internal.EVM2EVMMessage into a Client.Any2EVMMessage digestable by the client + Client.Any2EVMMessage memory receivedMessage = _convertToGeneralMessage(message); + bytes memory expectedCallData = + abi.encodeWithSelector(MaybeRevertMessageReceiver.ccipReceive.selector, receivedMessage); + + vm.expectCall(address(s_receiver), expectedCallData); + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + assertEq(uint256(Internal.MessageExecutionState.SUCCESS), uint256(newState)); + assertEq("", err); + } + + function test_Fuzz_trialExecuteWithTokens_Success(uint16 tokenAmount, bytes calldata messageData) public { + vm.assume(tokenAmount != 0); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = uint256(tokenAmount); + amounts[1] = uint256(tokenAmount); + + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageWithTokens(1, amounts); + // console.log(message.length); + message.data = messageData; + + IERC20 dstToken0 = IERC20(s_destTokens[0]); + uint256 startingBalance = dstToken0.balanceOf(message.receiver); + + vm.expectCall(s_destTokens[0], abi.encodeWithSelector(IERC20.transfer.selector, address(s_receiver), amounts[0])); + + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + assertEq(uint256(Internal.MessageExecutionState.SUCCESS), uint256(newState)); + assertEq("", err); + + // Check that the tokens were transferred + assertEq(startingBalance + amounts[0], dstToken0.balanceOf(message.receiver)); + } + + function test_Fuzz_getSenderNonce(uint8 trialExecutions) public { + vm.assume(trialExecutions > 1); + + Internal.EVM2EVMMessage[] memory messages; + + if (trialExecutions == 1) { + messages = new Internal.EVM2EVMMessage[](1); + messages[0] = _generateAny2EVMMessageNoTokens(0); + } else { + messages = _generateSingleBasicMessage(); + } + + // Fuzz the number of calls from the sender to ensure that getSenderNonce works + for (uint256 i = 1; i < trialExecutions; ++i) { + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + messages[0].nonce++; + messages[0].sequenceNumber++; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + } + + messages[0].nonce = 0; + messages[0].sequenceNumber = 0; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + uint64 nonceBefore = s_offRamp.getSenderNonce(messages[0].sender); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(s_offRamp.getSenderNonce(messages[0].sender), nonceBefore, "sender nonce is not as expected"); + } + + function test_Fuzz_getSenderNonceWithPrevOffRamp_Success(uint8 trialExecutions) public { + vm.assume(trialExecutions > 1); + // Fuzz a random nonce for getSenderNonce + test_Fuzz_getSenderNonce(trialExecutions); + + address prevOffRamp = address(s_offRamp); + deployOffRamp(s_mockCommitStore, s_destRouter, prevOffRamp); + + // Make sure the off-ramp address has changed by querying the static config + assertNotEq(address(s_offRamp), prevOffRamp); + EVM2EVMOffRamp.StaticConfig memory staticConfig = s_offRamp.getStaticConfig(); + assertEq(staticConfig.prevOffRamp, prevOffRamp, "Previous offRamp does not match expected address"); + + // Since i_prevOffRamp != address(0) and senderNonce == 0, there should be a call to the previous offRamp + vm.expectCall(prevOffRamp, abi.encodeWithSelector(s_offRamp.getSenderNonce.selector, OWNER)); + uint256 currentSenderNonce = s_offRamp.getSenderNonce(OWNER); + assertNotEq(currentSenderNonce, 0, "Sender nonce should not be zero"); + assertEq(currentSenderNonce, trialExecutions - 1, "Sender Nonce does not match expected trial executions"); + + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + currentSenderNonce = s_offRamp.getSenderNonce(OWNER); + assertEq(currentSenderNonce, trialExecutions - 1, "Sender Nonce on new offramp does not match expected executions"); + } + + function test_SingleMessageNoTokens_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + messages[0].nonce++; + messages[0].sequenceNumber++; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + uint64 nonceBefore = s_offRamp.getSenderNonce(messages[0].sender); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertGt(s_offRamp.getSenderNonce(messages[0].sender), nonceBefore); + } + + function test_SingleMessageNoTokensUnordered_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].nonce = 0; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + // Nonce never increments on unordered messages. + uint64 nonceBefore = s_offRamp.getSenderNonce(messages[0].sender); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq( + s_offRamp.getSenderNonce(messages[0].sender), nonceBefore, "nonce must remain unchanged on unordered messages" + ); + + messages[0].sequenceNumber++; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + // Nonce never increments on unordered messages. + nonceBefore = s_offRamp.getSenderNonce(messages[0].sender); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq( + s_offRamp.getSenderNonce(messages[0].sender), nonceBefore, "nonce must remain unchanged on unordered messages" + ); + } + + function test_ReceiverError_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + bytes memory realError1 = new bytes(2); + realError1[0] = 0xbe; + realError1[1] = 0xef; + s_reverting_receiver.setErr(realError1); + + messages[0].receiver = address(s_reverting_receiver); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, + messages[0].messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, realError1) + ) + ); + // Nonce should increment on non-strict + assertEq(uint64(0), s_offRamp.getSenderNonce(address(OWNER))); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(uint64(1), s_offRamp.getSenderNonce(address(OWNER))); + } + + function test_StrictUntouchedToSuccess_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + messages[0].strict = true; + messages[0].receiver = address(s_receiver); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + // Nonce should increment on a strict untouched -> success. + assertEq(uint64(0), s_offRamp.getSenderNonce(address(OWNER))); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(uint64(1), s_offRamp.getSenderNonce(address(OWNER))); + } + + function test_SkippedIncorrectNonce_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + messages[0].nonce++; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.SkippedIncorrectNonce(messages[0].nonce, messages[0].sender); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } + + function test_SkippedIncorrectNonceStillExecutes_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateMessagesWithTokens(); + + messages[1].nonce++; + messages[1].messageId = Internal._hash(messages[1], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + vm.expectEmit(); + emit EVM2EVMOffRamp.SkippedIncorrectNonce(messages[1].nonce, messages[1].sender); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } + + function test__execute_SkippedAlreadyExecutedMessage_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + vm.expectEmit(); + emit EVM2EVMOffRamp.SkippedAlreadyExecutedMessage(messages[0].sequenceNumber); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } + + function test__execute_SkippedAlreadyExecutedMessageUnordered_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].nonce = 0; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + vm.expectEmit(); + emit EVM2EVMOffRamp.SkippedAlreadyExecutedMessage(messages[0].sequenceNumber); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } + + // Send a message to a contract that does not implement the CCIPReceiver interface + // This should execute successfully. + function test_SingleMessageToNonCCIPReceiver_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + MaybeRevertMessageReceiverNo165 newReceiver = new MaybeRevertMessageReceiverNo165(true); + messages[0].receiver = address(newReceiver); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } + + function test_SingleMessagesNoTokensSuccess_gas() public { + vm.pauseGasMetering(); + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + Internal.ExecutionReport memory report = _generateReportFromMessages(messages); + + vm.resumeGasMetering(); + s_offRamp.execute(report, new uint256[](0)); + } + + function test_TwoMessagesWithTokensSuccess_gas() public { + vm.pauseGasMetering(); + Internal.EVM2EVMMessage[] memory messages = _generateMessagesWithTokens(); + // Set message 1 to use another receiver to simulate more fair gas costs + messages[1].receiver = address(s_secondary_receiver); + messages[1].messageId = Internal._hash(messages[1], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[1].sequenceNumber, messages[1].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + Internal.ExecutionReport memory report = _generateReportFromMessages(messages); + + vm.resumeGasMetering(); + s_offRamp.execute(report, new uint256[](0)); + } + + function test_TwoMessagesWithTokensAndGE_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateMessagesWithTokens(); + // Set message 1 to use another receiver to simulate more fair gas costs + messages[1].receiver = address(s_secondary_receiver); + messages[1].messageId = Internal._hash(messages[1], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[1].sequenceNumber, messages[1].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + assertEq(uint64(0), s_offRamp.getSenderNonce(OWNER)); + s_offRamp.execute(_generateReportFromMessages(messages), _getGasLimitsFromMessages(messages)); + assertEq(uint64(2), s_offRamp.getSenderNonce(OWNER)); + } + + function test_Fuzz_InterleavingOrderedAndUnorderedMessages_Success(bool[7] memory orderings) public { + Internal.EVM2EVMMessage[] memory messages = new Internal.EVM2EVMMessage[](orderings.length); + // number of tokens needs to be capped otherwise we hit UnsupportedNumberOfTokens. + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](3); + for (uint256 i = 0; i < 3; ++i) { + tokenAmounts[i].token = s_sourceTokens[i % s_sourceTokens.length]; + tokenAmounts[i].amount = 1e18; + } + uint64 expectedNonce = 0; + for (uint256 i = 0; i < orderings.length; ++i) { + messages[i] = _generateAny2EVMMessage(uint64(i + 1), tokenAmounts, !orderings[i]); + if (orderings[i]) { + messages[i].nonce = ++expectedNonce; + } + messages[i].messageId = Internal._hash(messages[i], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[i].sequenceNumber, messages[i].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + } + + uint64 nonceBefore = s_offRamp.getSenderNonce(OWNER); + assertEq(uint64(0), nonceBefore, "nonce before exec should be 0"); + s_offRamp.execute(_generateReportFromMessages(messages), _getGasLimitsFromMessages(messages)); + // all executions should succeed. + for (uint256 i = 0; i < orderings.length; ++i) { + assertEq( + uint256(s_offRamp.getExecutionState(messages[i].sequenceNumber)), + uint256(Internal.MessageExecutionState.SUCCESS) + ); + } + assertEq(nonceBefore + expectedNonce, s_offRamp.getSenderNonce(OWNER)); + } + + function test_InvalidSourcePoolAddress_Success() public { + address fakePoolAddress = address(0x0000000000333333); + + Internal.EVM2EVMMessage[] memory messages = _generateMessagesWithTokens(); + messages[0].sourceTokenData[0] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(fakePoolAddress), + destTokenAddress: abi.encode(s_destTokenBySourceToken[messages[0].tokenAmounts[0].token]), + extraData: "" + }) + ); + + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + messages[1].messageId = Internal._hash(messages[1], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, + messages[0].messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMOffRamp.TokenHandlingError.selector, + abi.encodeWithSelector(TokenPool.InvalidSourcePoolAddress.selector, abi.encode(fakePoolAddress)) + ) + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } + + // Reverts + + function test_InvalidMessageId_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].nonce++; + // MessageID no longer matches hash. + Internal.ExecutionReport memory executionReport = _generateReportFromMessages(messages); + vm.expectRevert(EVM2EVMOffRamp.InvalidMessageId.selector); + s_offRamp.execute(executionReport, new uint256[](0)); + } + + function test_Paused_Revert() public { + s_mockCommitStore.pause(); + vm.expectRevert(PausedError.selector); + s_offRamp.execute(_generateReportFromMessages(_generateMessagesWithTokens()), new uint256[](0)); + } + + function test_Unhealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + vm.expectRevert(EVM2EVMOffRamp.CursedByRMN.selector); + s_offRamp.execute(_generateReportFromMessages(_generateMessagesWithTokens()), new uint256[](0)); + // Uncurse should succeed + s_mockRMN.setGlobalCursed(false); + s_offRamp.execute(_generateReportFromMessages(_generateMessagesWithTokens()), new uint256[](0)); + } + + function test_UnexpectedTokenData_Revert() public { + Internal.ExecutionReport memory report = _generateReportFromMessages(_generateSingleBasicMessage()); + report.offchainTokenData = new bytes[][](report.messages.length + 1); + + vm.expectRevert(EVM2EVMOffRamp.UnexpectedTokenData.selector); + + s_offRamp.execute(report, new uint256[](0)); + } + + function test_EmptyReport_Revert() public { + vm.expectRevert(EVM2EVMOffRamp.EmptyReport.selector); + s_offRamp.execute( + Internal.ExecutionReport({ + proofs: new bytes32[](0), + proofFlagBits: 0, + messages: new Internal.EVM2EVMMessage[](0), + offchainTokenData: new bytes[][](0) + }), + new uint256[](0) + ); + } + + function test_RootNotCommitted_Revert() public { + vm.mockCall(address(s_mockCommitStore), abi.encodeWithSelector(ICommitStore.verify.selector), abi.encode(0)); + vm.expectRevert(EVM2EVMOffRamp.RootNotCommitted.selector); + + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + s_offRamp.execute(_generateReportFromMessages(messages), _getGasLimitsFromMessages(messages)); + vm.clearMockedCalls(); + } + + function test_ManualExecutionNotYetEnabled_Revert() public { + vm.mockCall( + address(s_mockCommitStore), abi.encodeWithSelector(ICommitStore.verify.selector), abi.encode(BLOCK_TIME) + ); + vm.expectRevert(EVM2EVMOffRamp.ManualExecutionNotYetEnabled.selector); + + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + s_offRamp.execute(_generateReportFromMessages(messages), _getGasLimitsFromMessages(messages)); + vm.clearMockedCalls(); + } + + function test_InvalidSourceChain_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].sourceChainSelector = SOURCE_CHAIN_SELECTOR + 1; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.InvalidSourceChain.selector, SOURCE_CHAIN_SELECTOR + 1)); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } + + function test_UnsupportedNumberOfTokens_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + Client.EVMTokenAmount[] memory newTokens = new Client.EVMTokenAmount[](MAX_TOKENS_LENGTH + 1); + messages[0].tokenAmounts = newTokens; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + Internal.ExecutionReport memory report = _generateReportFromMessages(messages); + + vm.expectRevert( + abi.encodeWithSelector(EVM2EVMOffRamp.UnsupportedNumberOfTokens.selector, messages[0].sequenceNumber) + ); + s_offRamp.execute(report, new uint256[](0)); + } + + function test_TokenDataMismatch_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + Internal.ExecutionReport memory report = _generateReportFromMessages(messages); + + report.offchainTokenData[0] = new bytes[](messages[0].tokenAmounts.length + 1); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.TokenDataMismatch.selector, messages[0].sequenceNumber)); + s_offRamp.execute(report, new uint256[](0)); + } + + function test_MessageTooLarge_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].data = new bytes(MAX_DATA_SIZE + 1); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + Internal.ExecutionReport memory executionReport = _generateReportFromMessages(messages); + vm.expectRevert( + abi.encodeWithSelector(EVM2EVMOffRamp.MessageTooLarge.selector, MAX_DATA_SIZE, messages[0].data.length) + ); + s_offRamp.execute(executionReport, new uint256[](0)); + } + + function test_RouterYULCall_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + // gas limit too high, Router's external call should revert + messages[0].gasLimit = 1e36; + messages[0].receiver = address(new ConformingReceiver(address(s_destRouter), s_destFeeToken)); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + Internal.ExecutionReport memory executionReport = _generateReportFromMessages(messages); + + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMOffRamp.ExecutionError.selector, abi.encodeWithSelector(CallWithExactGas.NotEnoughGasForCall.selector) + ) + ); + s_offRamp.execute(executionReport, new uint256[](0)); + } + + function test_RetryFailedMessageWithoutManualExecution_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + bytes memory realError1 = new bytes(2); + realError1[0] = 0xbe; + realError1[1] = 0xef; + s_reverting_receiver.setErr(realError1); + + messages[0].receiver = address(s_reverting_receiver); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, + messages[0].messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, realError1) + ) + ); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.AlreadyAttempted.selector, messages[0].sequenceNumber)); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } +} + +contract EVM2EVMOffRamp_execute_upgrade is EVM2EVMOffRampSetup { + EVM2EVMOffRampHelper internal s_prevOffRamp; + + function setUp() public virtual override { + super.setUp(); + + s_prevOffRamp = s_offRamp; + + deployOffRamp(s_mockCommitStore, s_destRouter, address(s_prevOffRamp)); + } + + function test_V2_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + } + + function test_V2SenderNoncesReadsPreviousRamp_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + uint64 startNonce = s_offRamp.getSenderNonce(messages[0].sender); + + for (uint64 i = 1; i < 4; ++i) { + s_prevOffRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + messages[0].nonce++; + messages[0].sequenceNumber++; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + assertEq(startNonce + i, s_offRamp.getSenderNonce(messages[0].sender)); + } + } + + function test_V2NonceStartsAtV1Nonce_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + uint64 startNonce = s_offRamp.getSenderNonce(messages[0].sender); + + s_prevOffRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + assertEq(startNonce + 1, s_offRamp.getSenderNonce(messages[0].sender)); + + messages[0].nonce++; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(startNonce + 2, s_offRamp.getSenderNonce(messages[0].sender)); + + messages[0].nonce++; + messages[0].sequenceNumber++; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(startNonce + 3, s_offRamp.getSenderNonce(messages[0].sender)); + } + + function test_V2NonceNewSenderStartsAtZero_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_prevOffRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + address newSender = address(1234567); + messages[0].sender = newSender; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + // new sender nonce in new offramp should go from 0 -> 1 + assertEq(s_offRamp.getSenderNonce(newSender), 0); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(s_offRamp.getSenderNonce(newSender), 1); + } + + function test_V2OffRampNonceSkipsIfMsgInFlight_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + address newSender = address(1234567); + messages[0].sender = newSender; + messages[0].nonce = 2; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + uint64 startNonce = s_offRamp.getSenderNonce(messages[0].sender); + + // new offramp sees msg nonce higher than senderNonce + // it waits for previous offramp to execute + vm.expectEmit(); + emit EVM2EVMOffRamp.SkippedSenderWithPreviousRampMessageInflight(messages[0].nonce, newSender); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(startNonce, s_offRamp.getSenderNonce(messages[0].sender)); + + messages[0].nonce = 1; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + // previous offramp executes msg and increases nonce + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + s_prevOffRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(startNonce + 1, s_offRamp.getSenderNonce(messages[0].sender)); + + messages[0].nonce = 2; + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + // new offramp is able to execute + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + assertEq(startNonce + 2, s_offRamp.getSenderNonce(messages[0].sender)); + } +} + +contract EVM2EVMOffRamp_executeSingleMessage is EVM2EVMOffRampSetup { + function setUp() public virtual override { + super.setUp(); + vm.startPrank(address(s_offRamp)); + } + + function test_executeSingleMessage_NoTokens_Success() public { + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageNoTokens(1); + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_executeSingleMessage_WithTokens_Success() public { + Internal.EVM2EVMMessage memory message = _generateMessagesWithTokens()[0]; + bytes[] memory offchainTokenData = new bytes[](message.tokenAmounts.length); + Internal.SourceTokenData memory sourceTokenData = abi.decode(message.sourceTokenData[0], (Internal.SourceTokenData)); + + vm.expectCall( + s_destPoolByToken[s_destTokens[0]], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(message.sender), + receiver: message.receiver, + amount: message.tokenAmounts[0].amount, + localToken: s_destTokenBySourceToken[message.tokenAmounts[0].token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: "" + }) + ) + ); + + s_offRamp.executeSingleMessage(message, offchainTokenData); + } + + function test_executeSingleMessage_ZeroGasZeroData_Success() public { + uint256 gasLimit = 0; + Internal.EVM2EVMMessage memory message = _generateMsgWithoutTokens(gasLimit); + Client.Any2EVMMessage memory receiverMsg = _convertToGeneralMessage(message); + + // expect 0 calls to be made as no gas is provided + vm.expectCall( + address(s_destRouter), + abi.encodeCall(Router.routeMessage, (receiverMsg, Internal.GAS_FOR_CALL_EXACT_CHECK, gasLimit, message.receiver)), + 0 + ); + + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + + // Ensure we encoded it properly, and didn't simply expect the wrong call + gasLimit = 200_000; + message = _generateMsgWithoutTokens(gasLimit); + receiverMsg = _convertToGeneralMessage(message); + + vm.expectCall( + address(s_destRouter), + abi.encodeCall(Router.routeMessage, (receiverMsg, Internal.GAS_FOR_CALL_EXACT_CHECK, gasLimit, message.receiver)), + 1 + ); + + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function _generateMsgWithoutTokens(uint256 gasLimit) internal view returns (Internal.EVM2EVMMessage memory) { + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageNoTokens(1); + message.gasLimit = gasLimit; + message.data = ""; + message.messageId = Internal._hash( + message, + keccak256( + abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, ON_RAMP_ADDRESS) + ) + ); + return message; + } + + function test_NonContract_Success() public { + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageNoTokens(1); + message.receiver = STRANGER; + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_NonContractWithTokens_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + vm.expectEmit(); + emit TokenPool.Released(address(s_offRamp), STRANGER, amounts[0]); + vm.expectEmit(); + emit TokenPool.Minted(address(s_offRamp), STRANGER, amounts[1]); + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageWithTokens(1, amounts); + message.receiver = STRANGER; + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + // Reverts + + function test_TokenHandlingError_Revert() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + + bytes memory errorMessage = "Random token pool issue"; + + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageWithTokens(1, amounts); + s_maybeRevertingPool.setShouldRevert(errorMessage); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.TokenHandlingError.selector, errorMessage)); + + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_ZeroGasDONExecution_Revert() public { + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageNoTokens(1); + message.gasLimit = 0; + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.ReceiverError.selector, "")); + + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } + + function test_MessageSender_Revert() public { + vm.stopPrank(); + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageNoTokens(1); + vm.expectRevert(EVM2EVMOffRamp.CanOnlySelfCall.selector); + s_offRamp.executeSingleMessage(message, new bytes[](message.tokenAmounts.length)); + } +} + +contract EVM2EVMOffRamp__report is EVM2EVMOffRampSetup { + // Asserts that execute completes + function test_Report_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + Internal.ExecutionReport memory report = _generateReportFromMessages(messages); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + s_offRamp.report(abi.encode(report)); + } +} + +contract EVM2EVMOffRamp_manuallyExecute is EVM2EVMOffRampSetup { + function test_ManualExec_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].receiver = address(s_reverting_receiver); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + s_reverting_receiver.setRevert(false); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), new uint256[](messages.length)); + } + + function test_manuallyExecute_DoesNotRevertIfUntouched_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].receiver = address(s_reverting_receiver); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + assertEq(messages[0].nonce - 1, s_offRamp.getSenderNonce(messages[0].sender)); + + s_reverting_receiver.setRevert(true); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, + messages[0].messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, "") + ) + ); + + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), new uint256[](1)); + + assertEq(messages[0].nonce, s_offRamp.getSenderNonce(messages[0].sender)); + } + + function test_ManualExecWithGasOverride_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].receiver = address(s_reverting_receiver); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + s_reverting_receiver.setRevert(false); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + + uint256[] memory gasLimitOverrides = _getGasLimitsFromMessages(messages); + gasLimitOverrides[0] += 1; + + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), gasLimitOverrides); + } + + function test_LowGasLimitManualExec_Success() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].gasLimit = 1; + messages[0].receiver = address(new ConformingReceiver(address(s_destRouter), s_destFeeToken)); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, + messages[0].messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector(EVM2EVMOffRamp.ReceiverError.selector, "") + ); + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + uint256[] memory gasLimitOverrides = new uint256[](1); + gasLimitOverrides[0] = 100_000; + + vm.expectEmit(); + emit MaybeRevertMessageReceiver.MessageReceived(); + + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, messages[0].messageId, Internal.MessageExecutionState.SUCCESS, "" + ); + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), gasLimitOverrides); + } + + function test_ManualExecForkedChain_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + Internal.ExecutionReport memory report = _generateReportFromMessages(messages); + uint256 chain1 = block.chainid; + uint256 chain2 = chain1 + 1; + vm.chainId(chain2); + vm.expectRevert(abi.encodeWithSelector(OCR2BaseNoChecks.ForkedChain.selector, chain1, chain2)); + + s_offRamp.manuallyExecute(report, _getGasLimitsFromMessages(messages)); + } + + function test_ManualExecGasLimitMismatch_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + vm.expectRevert(EVM2EVMOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), new uint256[](0)); + + vm.expectRevert(EVM2EVMOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), new uint256[](messages.length - 1)); + + vm.expectRevert(EVM2EVMOffRamp.ManualExecutionGasLimitMismatch.selector); + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), new uint256[](messages.length + 1)); + } + + function test_ManualExecInvalidGasLimit_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + uint256[] memory gasLimits = _getGasLimitsFromMessages(messages); + gasLimits[0]--; + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.InvalidManualExecutionGasLimit.selector, 0, gasLimits[0])); + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), gasLimits); + } + + function test_ManualExecFailedTx_Revert() public { + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + + messages[0].receiver = address(s_reverting_receiver); + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + s_offRamp.execute(_generateReportFromMessages(messages), new uint256[](0)); + + s_reverting_receiver.setRevert(true); + + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMOffRamp.ExecutionError.selector, + abi.encodeWithSelector( + EVM2EVMOffRamp.ReceiverError.selector, + abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, bytes("")) + ) + ) + ); + s_offRamp.manuallyExecute(_generateReportFromMessages(messages), _getGasLimitsFromMessages(messages)); + } + + function test_ReentrancyManualExecuteFails() public { + uint256 tokenAmount = 1e9; + IERC20 tokenToAbuse = IERC20(s_destFeeToken); + + // This needs to be deployed before the source chain message is sent + // because we need the address for the receiver. + ReentrancyAbuser receiver = new ReentrancyAbuser(address(s_destRouter), s_offRamp); + uint256 balancePre = tokenToAbuse.balanceOf(address(receiver)); + + // For this test any message will be flagged as correct by the + // commitStore. In a real scenario the abuser would have to actually + // send the message that they want to replay. + Internal.EVM2EVMMessage[] memory messages = _generateSingleBasicMessage(); + messages[0].tokenAmounts = new Client.EVMTokenAmount[](1); + messages[0].tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceFeeToken, amount: tokenAmount}); + messages[0].receiver = address(receiver); + messages[0].sourceTokenData = new bytes[](1); + messages[0].sourceTokenData[0] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[s_sourceFeeToken]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[s_sourceFeeToken]), + extraData: "" + }) + ); + + messages[0].messageId = Internal._hash(messages[0], s_offRamp.metadataHash()); + + Internal.ExecutionReport memory report = _generateReportFromMessages(messages); + + // sets the report to be repeated on the ReentrancyAbuser to be able to replay + receiver.setPayload(report); + + // The first entry should be fine and triggers the second entry. This one fails + // but since it's an inner tx of the first one it is caught in the try-catch. + // This means the first tx is marked `FAILURE` with the error message of the second tx. + vm.expectEmit(); + emit EVM2EVMOffRamp.ExecutionStateChanged( + messages[0].sequenceNumber, + messages[0].messageId, + Internal.MessageExecutionState.FAILURE, + abi.encodeWithSelector( + EVM2EVMOffRamp.ReceiverError.selector, + abi.encodeWithSelector(EVM2EVMOffRamp.AlreadyExecuted.selector, messages[0].sequenceNumber) + ) + ); + + s_offRamp.manuallyExecute(report, _getGasLimitsFromMessages(messages)); + + // Since the tx failed we don't release the tokens + assertEq(tokenToAbuse.balanceOf(address(receiver)), balancePre); + } +} + +contract EVM2EVMOffRamp_getExecutionState is EVM2EVMOffRampSetup { + mapping(uint64 seqNum => Internal.MessageExecutionState state) internal s_differentialExecutionState; + + /// forge-config: default.fuzz.runs = 32 + /// forge-config: ccip.fuzz.runs = 32 + function test_Fuzz_Differential_Success(uint16[500] memory seqNums, uint8[500] memory values) public { + for (uint256 i = 0; i < seqNums.length; ++i) { + // Only use the first three slots. This makes sure existing slots get overwritten + // as the tests uses 500 sequence numbers. + uint16 seqNum = seqNums[i] % 386; + Internal.MessageExecutionState state = Internal.MessageExecutionState(values[i] % 4); + s_differentialExecutionState[seqNum] = state; + s_offRamp.setExecutionStateHelper(seqNum, state); + assertEq(uint256(state), uint256(s_offRamp.getExecutionState(seqNum))); + } + + for (uint256 i = 0; i < seqNums.length; ++i) { + uint16 seqNum = seqNums[i] % 386; + Internal.MessageExecutionState expectedState = s_differentialExecutionState[seqNum]; + assertEq(uint256(expectedState), uint256(s_offRamp.getExecutionState(seqNum))); + } + } + + function test_GetExecutionState_Success() public { + s_offRamp.setExecutionStateHelper(0, Internal.MessageExecutionState.FAILURE); + assertEq(s_offRamp.getExecutionStateBitMap(0), 3); + + s_offRamp.setExecutionStateHelper(1, Internal.MessageExecutionState.FAILURE); + assertEq(s_offRamp.getExecutionStateBitMap(0), 3 + (3 << 2)); + + s_offRamp.setExecutionStateHelper(1, Internal.MessageExecutionState.IN_PROGRESS); + assertEq(s_offRamp.getExecutionStateBitMap(0), 3 + (1 << 2)); + + s_offRamp.setExecutionStateHelper(2, Internal.MessageExecutionState.FAILURE); + assertEq(s_offRamp.getExecutionStateBitMap(0), 3 + (1 << 2) + (3 << 4)); + + s_offRamp.setExecutionStateHelper(127, Internal.MessageExecutionState.IN_PROGRESS); + assertEq(s_offRamp.getExecutionStateBitMap(0), 3 + (1 << 2) + (3 << 4) + (1 << 254)); + + s_offRamp.setExecutionStateHelper(128, Internal.MessageExecutionState.SUCCESS); + assertEq(s_offRamp.getExecutionStateBitMap(0), 3 + (1 << 2) + (3 << 4) + (1 << 254)); + assertEq(s_offRamp.getExecutionStateBitMap(1), 2); + + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(s_offRamp.getExecutionState(0))); + assertEq(uint256(Internal.MessageExecutionState.IN_PROGRESS), uint256(s_offRamp.getExecutionState(1))); + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(s_offRamp.getExecutionState(2))); + assertEq(uint256(Internal.MessageExecutionState.IN_PROGRESS), uint256(s_offRamp.getExecutionState(127))); + assertEq(uint256(Internal.MessageExecutionState.SUCCESS), uint256(s_offRamp.getExecutionState(128))); + } + + function test_FillExecutionState_Success() public { + for (uint64 i = 0; i < 384; ++i) { + s_offRamp.setExecutionStateHelper(i, Internal.MessageExecutionState.FAILURE); + } + + for (uint64 i = 0; i < 384; ++i) { + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(s_offRamp.getExecutionState(i))); + } + + for (uint64 i = 0; i < 3; ++i) { + assertEq(type(uint256).max, s_offRamp.getExecutionStateBitMap(i)); + } + + for (uint64 i = 0; i < 384; ++i) { + s_offRamp.setExecutionStateHelper(i, Internal.MessageExecutionState.IN_PROGRESS); + } + + for (uint64 i = 0; i < 384; ++i) { + assertEq(uint256(Internal.MessageExecutionState.IN_PROGRESS), uint256(s_offRamp.getExecutionState(i))); + } + + for (uint64 i = 0; i < 3; ++i) { + // 0x555... == 0b101010101010..... + assertEq(0x5555555555555555555555555555555555555555555555555555555555555555, s_offRamp.getExecutionStateBitMap(i)); + } + } +} + +contract EVM2EVMOffRamp__trialExecute is EVM2EVMOffRampSetup { + function test_trialExecute_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageWithTokens(1, amounts); + IERC20 dstToken0 = IERC20(s_destTokens[0]); + uint256 startingBalance = dstToken0.balanceOf(message.receiver); + + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + assertEq(uint256(Internal.MessageExecutionState.SUCCESS), uint256(newState)); + assertEq("", err); + + // Check that the tokens were transferred + assertEq(startingBalance + amounts[0], dstToken0.balanceOf(message.receiver)); + } + + function test_TokenHandlingErrorIsCaught_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + + IERC20 dstToken0 = IERC20(s_destTokens[0]); + uint256 startingBalance = dstToken0.balanceOf(OWNER); + + bytes memory errorMessage = "Random token pool issue"; + + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageWithTokens(1, amounts); + s_maybeRevertingPool.setShouldRevert(errorMessage); + + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(newState)); + assertEq(abi.encodeWithSelector(EVM2EVMOffRamp.TokenHandlingError.selector, errorMessage), err); + + // Expect the balance to remain the same + assertEq(startingBalance, dstToken0.balanceOf(OWNER)); + } + + function test_RateLimitError_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000; + amounts[1] = 50; + + bytes memory errorMessage = abi.encodeWithSelector(RateLimiter.BucketOverfilled.selector); + + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageWithTokens(1, amounts); + s_maybeRevertingPool.setShouldRevert(errorMessage); + + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(newState)); + assertEq(abi.encodeWithSelector(EVM2EVMOffRamp.TokenHandlingError.selector, errorMessage), err); + } + + function test_TokenPoolIsNotAContract_Success() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 10000; + Internal.EVM2EVMMessage memory message = _generateAny2EVMMessageWithTokens(1, amounts); + + // Happy path, pool is correct + (Internal.MessageExecutionState newState, bytes memory err) = + s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + + assertEq(uint256(Internal.MessageExecutionState.SUCCESS), uint256(newState)); + assertEq("", err); + + // address 0 has no contract + assertEq(address(0).code.length, 0); + message.sourceTokenData[0] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(address(0)), + destTokenAddress: abi.encode(address(0)), + extraData: "" + }) + ); + + message.messageId = Internal._hash( + message, + keccak256( + abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, ON_RAMP_ADDRESS) + ) + ); + + // Unhappy path, no revert but marked as failed. + (newState, err) = s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(newState)); + assertEq(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, abi.encode(address(0))), err); + + address notAContract = makeAddr("not_a_contract"); + + message.sourceTokenData[0] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(address(0)), + destTokenAddress: abi.encode(notAContract), + extraData: "" + }) + ); + + message.messageId = Internal._hash( + message, + keccak256( + abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, ON_RAMP_ADDRESS) + ) + ); + + (newState, err) = s_offRamp.trialExecute(message, new bytes[](message.tokenAmounts.length)); + + assertEq(uint256(Internal.MessageExecutionState.FAILURE), uint256(newState)); + assertEq(abi.encodeWithSelector(EVM2EVMOffRamp.NotACompatiblePool.selector, address(0)), err); + } +} + +contract EVM2EVMOffRamp__releaseOrMintToken is EVM2EVMOffRampSetup { + function test__releaseOrMintToken_Success() public { + uint256 amount = 123123; + address token = s_sourceTokens[0]; + bytes memory originalSender = abi.encode(OWNER); + bytes memory offchainTokenData = abi.encode(keccak256("offchainTokenData")); + + IERC20 dstToken1 = IERC20(s_destTokenBySourceToken[token]); + uint256 startingBalance = dstToken1.balanceOf(OWNER); + + Internal.SourceTokenData memory sourceTokenData = Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[token]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[token]), + extraData: "" + }); + + vm.expectCall( + s_destPoolBySourceToken[token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: originalSender, + receiver: OWNER, + amount: amount, + localToken: s_destTokenBySourceToken[token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData + }) + ) + ); + + s_offRamp.releaseOrMintToken(amount, originalSender, OWNER, sourceTokenData, offchainTokenData); + + assertEq(startingBalance + amount, dstToken1.balanceOf(OWNER)); + } + + function test__releaseOrMintToken_NotACompatiblePool_Revert() public { + uint256 amount = 123123; + address token = s_sourceTokens[0]; + address destToken = s_destTokenBySourceToken[token]; + vm.label(destToken, "destToken"); + bytes memory originalSender = abi.encode(OWNER); + bytes memory offchainTokenData = abi.encode(keccak256("offchainTokenData")); + + Internal.SourceTokenData memory sourceTokenData = Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[token]), + destTokenAddress: abi.encode(destToken), + extraData: "" + }); + + // Address(0) should always revert + address returnedPool = address(0); + + vm.mockCall( + address(s_tokenAdminRegistry), + abi.encodeWithSelector(ITokenAdminRegistry.getPool.selector, destToken), + abi.encode(returnedPool) + ); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.NotACompatiblePool.selector, returnedPool)); + + s_offRamp.releaseOrMintToken(amount, originalSender, OWNER, sourceTokenData, offchainTokenData); + + // A contract that doesn't support the interface should also revert + returnedPool = address(s_offRamp); + + vm.mockCall( + address(s_tokenAdminRegistry), + abi.encodeWithSelector(ITokenAdminRegistry.getPool.selector, destToken), + abi.encode(returnedPool) + ); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.NotACompatiblePool.selector, returnedPool)); + + s_offRamp.releaseOrMintToken(amount, originalSender, OWNER, sourceTokenData, offchainTokenData); + } + + function test__releaseOrMintToken_TokenHandlingError_revert_Revert() public { + address receiver = makeAddr("receiver"); + uint256 amount = 123123; + address token = s_sourceTokens[0]; + address destToken = s_destTokenBySourceToken[token]; + bytes memory originalSender = abi.encode(OWNER); + bytes memory offchainTokenData = abi.encode(keccak256("offchainTokenData")); + + Internal.SourceTokenData memory sourceTokenData = Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[token]), + destTokenAddress: abi.encode(destToken), + extraData: "" + }); + + bytes memory revertData = "call reverted :o"; + + vm.mockCallRevert(destToken, abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount), revertData); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.TokenHandlingError.selector, revertData)); + s_offRamp.releaseOrMintToken(amount, originalSender, receiver, sourceTokenData, offchainTokenData); + } +} + +contract EVM2EVMOffRamp__releaseOrMintTokens is EVM2EVMOffRampSetup { + function test_releaseOrMintTokens_Success() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + IERC20 dstToken1 = IERC20(s_destFeeToken); + uint256 startingBalance = dstToken1.balanceOf(OWNER); + uint256 amount1 = 100; + srcTokenAmounts[0].amount = amount1; + + bytes memory originalSender = abi.encode(OWNER); + + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + offchainTokenData[0] = abi.encode(0x12345678); + + bytes[] memory encodedSourceTokenData = _getDefaultSourceTokenData(srcTokenAmounts); + Internal.SourceTokenData memory sourceTokenData = abi.decode(encodedSourceTokenData[0], (Internal.SourceTokenData)); + + vm.expectCall( + s_destPoolBySourceToken[srcTokenAmounts[0].token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: originalSender, + receiver: OWNER, + amount: srcTokenAmounts[0].amount, + localToken: s_destTokenBySourceToken[srcTokenAmounts[0].token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData[0] + }) + ) + ); + + s_offRamp.releaseOrMintTokens(srcTokenAmounts, originalSender, OWNER, encodedSourceTokenData, offchainTokenData); + + assertEq(startingBalance + amount1, dstToken1.balanceOf(OWNER)); + } + + function test_releaseOrMintTokens_destDenominatedDecimals_Success() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + address destToken = s_destFeeToken; + uint256 amount = 100; + uint256 destinationDenominationMultiplier = 1000; + srcTokenAmounts[0].amount = amount; + + bytes memory originalSender = abi.encode(OWNER); + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + bytes[] memory encodedSourceTokenData = _getDefaultSourceTokenData(srcTokenAmounts); + Internal.SourceTokenData memory sourceTokenData = abi.decode(encodedSourceTokenData[0], (Internal.SourceTokenData)); + + // Since the pool call is mocked, we manually release funds to the offRamp + deal(destToken, address(s_offRamp), amount * destinationDenominationMultiplier); + + vm.mockCall( + s_destPoolBySourceToken[srcTokenAmounts[0].token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: originalSender, + receiver: OWNER, + amount: amount, + localToken: s_destTokenBySourceToken[srcTokenAmounts[0].token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData[0] + }) + ), + abi.encode(amount * destinationDenominationMultiplier) + ); + + Client.EVMTokenAmount[] memory destTokenAmounts = + s_offRamp.releaseOrMintTokens(srcTokenAmounts, originalSender, OWNER, encodedSourceTokenData, offchainTokenData); + + assertEq(destTokenAmounts[0].amount, amount * destinationDenominationMultiplier); + assertEq(destTokenAmounts[0].token, destToken); + } + + function test_OverValueWithARLOff_Success() public { + // Set a high price to trip the ARL + uint224 tokenPrice = 3 ** 128; + Internal.PriceUpdates memory priceUpdates = getSingleTokenPriceUpdateStruct(s_destFeeToken, tokenPrice); + s_priceRegistry.updatePrices(priceUpdates); + + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + uint256 amount1 = 100; + srcTokenAmounts[0].amount = amount1; + + bytes memory originalSender = abi.encode(OWNER); + + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + offchainTokenData[0] = abi.encode(0x12345678); + + bytes[] memory sourceTokenData = _getDefaultSourceTokenData(srcTokenAmounts); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiter.AggregateValueMaxCapacityExceeded.selector, + getInboundRateLimiterConfig().capacity, + (amount1 * tokenPrice) / 1e18 + ) + ); + + // // Expect to fail from ARL + s_offRamp.releaseOrMintTokens(srcTokenAmounts, originalSender, OWNER, sourceTokenData, offchainTokenData); + + // Configure ARL off for token + EVM2EVMOffRamp.RateLimitToken[] memory removes = new EVM2EVMOffRamp.RateLimitToken[](1); + removes[0] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceFeeToken, destToken: s_destFeeToken}); + s_offRamp.updateRateLimitTokens(removes, new EVM2EVMOffRamp.RateLimitToken[](0)); + + // Expect the call now succeeds + s_offRamp.releaseOrMintTokens(srcTokenAmounts, originalSender, OWNER, sourceTokenData, offchainTokenData); + } + + // Revert + + function test_TokenHandlingError_Reverts() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + + bytes memory unknownError = bytes("unknown error"); + s_maybeRevertingPool.setShouldRevert(unknownError); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.TokenHandlingError.selector, unknownError)); + + s_offRamp.releaseOrMintTokens( + srcTokenAmounts, + abi.encode(OWNER), + OWNER, + _getDefaultSourceTokenData(srcTokenAmounts), + new bytes[](srcTokenAmounts.length) + ); + } + + function test_releaseOrMintTokens_InvalidDataLengthReturnData_Revert() public { + uint256 amount = 100; + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + srcTokenAmounts[0].amount = amount; + + bytes memory originalSender = abi.encode(OWNER); + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + bytes[] memory encodedSourceTokenData = _getDefaultSourceTokenData(srcTokenAmounts); + Internal.SourceTokenData memory sourceTokenData = abi.decode(encodedSourceTokenData[0], (Internal.SourceTokenData)); + + vm.mockCall( + s_destPoolBySourceToken[srcTokenAmounts[0].token], + abi.encodeWithSelector( + LockReleaseTokenPool.releaseOrMint.selector, + Pool.ReleaseOrMintInV1({ + originalSender: originalSender, + receiver: OWNER, + amount: amount, + localToken: s_destTokenBySourceToken[srcTokenAmounts[0].token], + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData[0] + }) + ), + // Includes the amount twice, this will revert due to the return data being to long + abi.encode(amount, amount) + ); + + vm.expectRevert( + abi.encodeWithSelector(EVM2EVMOffRamp.InvalidDataLength.selector, Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, 64) + ); + + s_offRamp.releaseOrMintTokens(srcTokenAmounts, originalSender, OWNER, encodedSourceTokenData, offchainTokenData); + } + + function test_releaseOrMintTokens_InvalidEVMAddress_Revert() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + + bytes memory originalSender = abi.encode(OWNER); + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + bytes[] memory sourceTokenData = _getDefaultSourceTokenData(srcTokenAmounts); + bytes memory wrongAddress = abi.encode(address(1000), address(10000), address(10000)); + + sourceTokenData[0] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[srcTokenAmounts[0].token]), + destTokenAddress: wrongAddress, + extraData: "" + }) + ); + + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, wrongAddress)); + + s_offRamp.releaseOrMintTokens(srcTokenAmounts, originalSender, OWNER, sourceTokenData, offchainTokenData); + } + + function test_RateLimitErrors_Reverts() public { + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + + bytes[] memory rateLimitErrors = new bytes[](5); + rateLimitErrors[0] = abi.encodeWithSelector(RateLimiter.BucketOverfilled.selector); + rateLimitErrors[1] = + abi.encodeWithSelector(RateLimiter.AggregateValueMaxCapacityExceeded.selector, uint256(100), uint256(1000)); + rateLimitErrors[2] = + abi.encodeWithSelector(RateLimiter.AggregateValueRateLimitReached.selector, uint256(42), 1, s_sourceTokens[0]); + rateLimitErrors[3] = abi.encodeWithSelector( + RateLimiter.TokenMaxCapacityExceeded.selector, uint256(100), uint256(1000), s_sourceTokens[0] + ); + rateLimitErrors[4] = + abi.encodeWithSelector(RateLimiter.TokenRateLimitReached.selector, uint256(42), 1, s_sourceTokens[0]); + + for (uint256 i = 0; i < rateLimitErrors.length; ++i) { + s_maybeRevertingPool.setShouldRevert(rateLimitErrors[i]); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.TokenHandlingError.selector, rateLimitErrors[i])); + + s_offRamp.releaseOrMintTokens( + srcTokenAmounts, + abi.encode(OWNER), + OWNER, + _getDefaultSourceTokenData(srcTokenAmounts), + new bytes[](srcTokenAmounts.length) + ); + } + } + + function test__releaseOrMintTokens_NotACompatiblePool_Reverts() public { + address fakePoolAddress = makeAddr("Doesn't exist"); + + bytes[] memory sourceTokenData = new bytes[](1); + sourceTokenData[0] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(fakePoolAddress), + destTokenAddress: abi.encode(fakePoolAddress), + extraData: "" + }) + ); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOffRamp.NotACompatiblePool.selector, address(0))); + s_offRamp.releaseOrMintTokens( + new Client.EVMTokenAmount[](1), abi.encode(makeAddr("original_sender")), OWNER, sourceTokenData, new bytes[](1) + ); + } + + function test_PriceNotFoundForToken_Reverts() public { + // Set token price to 0 + s_priceRegistry.updatePrices(getSingleTokenPriceUpdateStruct(s_destFeeToken, 0)); + + Client.EVMTokenAmount[] memory srcTokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + uint256 amount1 = 100; + srcTokenAmounts[0].amount = amount1; + + bytes memory originalSender = abi.encode(OWNER); + + bytes[] memory offchainTokenData = new bytes[](srcTokenAmounts.length); + offchainTokenData[0] = abi.encode(0x12345678); + + bytes[] memory sourceTokenData = _getDefaultSourceTokenData(srcTokenAmounts); + + vm.expectRevert(abi.encodeWithSelector(AggregateRateLimiter.PriceNotFoundForToken.selector, s_destFeeToken)); + + s_offRamp.releaseOrMintTokens(srcTokenAmounts, originalSender, OWNER, sourceTokenData, offchainTokenData); + } + + /// forge-config: default.fuzz.runs = 32 + /// forge-config: ccip.fuzz.runs = 1024 + // Uint256 gives a good range of values to test, both inside and outside of the eth address space. + function test_Fuzz__releaseOrMintTokens_AnyRevertIsCaught_Success(uint256 destPool) public { + // Input 447301751254033913445893214690834296930546521452, which is 0x4E59B44847B379578588920CA78FBF26C0B4956C + // triggers some Create2Deployer and causes it to fail + vm.assume(destPool != 447301751254033913445893214690834296930546521452); + bytes memory unusedVar = abi.encode(makeAddr("unused")); + bytes[] memory sourceTokenData = new bytes[](1); + sourceTokenData[0] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: unusedVar, + destTokenAddress: abi.encode(destPool), + extraData: unusedVar + }) + ); + + try s_offRamp.releaseOrMintTokens(new Client.EVMTokenAmount[](1), unusedVar, OWNER, sourceTokenData, new bytes[](1)) + {} catch (bytes memory reason) { + // Any revert should be a TokenHandlingError, InvalidEVMAddress, InvalidDataLength or NoContract as those are caught by the offramp + assertTrue( + bytes4(reason) == EVM2EVMOffRamp.TokenHandlingError.selector + || bytes4(reason) == Internal.InvalidEVMAddress.selector + || bytes4(reason) == EVM2EVMOffRamp.InvalidDataLength.selector + || bytes4(reason) == CallWithExactGas.NoContract.selector + || bytes4(reason) == EVM2EVMOffRamp.NotACompatiblePool.selector, + "Expected TokenHandlingError or InvalidEVMAddress" + ); + + if (destPool > type(uint160).max) { + assertEq(reason, abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, abi.encode(destPool))); + } + } + } +} + +contract EVM2EVMOffRamp_getAllRateLimitTokens is EVM2EVMOffRampSetup { + function test_GetAllRateLimitTokens_Success() public view { + (address[] memory sourceTokens, address[] memory destTokens) = s_offRamp.getAllRateLimitTokens(); + + for (uint256 i = 0; i < s_sourceTokens.length; ++i) { + assertEq(s_sourceTokens[i], sourceTokens[i]); + assertEq(s_destTokens[i], destTokens[i]); + } + } +} + +contract EVM2EVMOffRamp_updateRateLimitTokens is EVM2EVMOffRampSetup { + function setUp() public virtual override { + super.setUp(); + // Clear rate limit tokens state + EVM2EVMOffRamp.RateLimitToken[] memory remove = new EVM2EVMOffRamp.RateLimitToken[](s_sourceTokens.length); + for (uint256 i = 0; i < s_sourceTokens.length; ++i) { + remove[i] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceTokens[i], destToken: s_destTokens[i]}); + } + s_offRamp.updateRateLimitTokens(remove, new EVM2EVMOffRamp.RateLimitToken[](0)); + } + + function test_updateRateLimitTokens_Success() public { + EVM2EVMOffRamp.RateLimitToken[] memory adds = new EVM2EVMOffRamp.RateLimitToken[](2); + adds[0] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceTokens[0], destToken: s_destTokens[0]}); + adds[1] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceTokens[1], destToken: s_destTokens[1]}); + + for (uint256 i = 0; i < adds.length; ++i) { + vm.expectEmit(); + emit EVM2EVMOffRamp.TokenAggregateRateLimitAdded(adds[i].sourceToken, adds[i].destToken); + } + + s_offRamp.updateRateLimitTokens(new EVM2EVMOffRamp.RateLimitToken[](0), adds); + + (address[] memory sourceTokens, address[] memory destTokens) = s_offRamp.getAllRateLimitTokens(); + + for (uint256 i = 0; i < adds.length; ++i) { + assertEq(adds[i].sourceToken, sourceTokens[i]); + assertEq(adds[i].destToken, destTokens[i]); + } + } + + function test_updateRateLimitTokens_AddsAndRemoves_Success() public { + EVM2EVMOffRamp.RateLimitToken[] memory adds = new EVM2EVMOffRamp.RateLimitToken[](3); + adds[0] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceTokens[0], destToken: s_destTokens[0]}); + adds[1] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceTokens[1], destToken: s_destTokens[1]}); + // Add a duplicate, this should not revert the tx + adds[2] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceTokens[1], destToken: s_destTokens[1]}); + + EVM2EVMOffRamp.RateLimitToken[] memory removes = new EVM2EVMOffRamp.RateLimitToken[](1); + removes[0] = adds[0]; + + for (uint256 i = 0; i < adds.length - 1; ++i) { + vm.expectEmit(); + emit EVM2EVMOffRamp.TokenAggregateRateLimitAdded(adds[i].sourceToken, adds[i].destToken); + } + + s_offRamp.updateRateLimitTokens(removes, adds); + + for (uint256 i = 0; i < removes.length; ++i) { + vm.expectEmit(); + emit EVM2EVMOffRamp.TokenAggregateRateLimitRemoved(removes[i].sourceToken, removes[i].destToken); + } + + s_offRamp.updateRateLimitTokens(removes, new EVM2EVMOffRamp.RateLimitToken[](0)); + + (address[] memory sourceTokens, address[] memory destTokens) = s_offRamp.getAllRateLimitTokens(); + + assertEq(1, sourceTokens.length); + assertEq(adds[1].sourceToken, sourceTokens[0]); + + assertEq(1, destTokens.length); + assertEq(adds[1].destToken, destTokens[0]); + } + + function test_Fuzz_UpdateRateLimitTokens(uint8 numTokens) public { + // Needs to be more than 1 so that the division doesn't round down and the even makes the comparisons simpler + vm.assume(numTokens > 1 && numTokens % 2 == 0); + + // Clear the Rate limit tokens array so the test can start from a baseline + (address[] memory sourceTokens, address[] memory destTokens) = s_offRamp.getAllRateLimitTokens(); + EVM2EVMOffRamp.RateLimitToken[] memory removes = new EVM2EVMOffRamp.RateLimitToken[](sourceTokens.length); + for (uint256 x = 0; x < removes.length; x++) { + removes[x] = EVM2EVMOffRamp.RateLimitToken({sourceToken: sourceTokens[x], destToken: destTokens[x]}); + } + s_offRamp.updateRateLimitTokens(removes, new EVM2EVMOffRamp.RateLimitToken[](0)); + + // Sanity check that the rateLimitTokens were successfully cleared + (sourceTokens, destTokens) = s_offRamp.getAllRateLimitTokens(); + assertEq(sourceTokens.length, 0, "sourceTokenLength should be zero"); + + EVM2EVMOffRamp.RateLimitToken[] memory adds = new EVM2EVMOffRamp.RateLimitToken[](numTokens); + + for (uint256 x = 0; x < numTokens; x++) { + address tokenAddr = vm.addr(x + 1); + + // Create an array of several fake tokens to add which are deployed on the same address on both chains for simplicity + adds[x] = EVM2EVMOffRamp.RateLimitToken({sourceToken: tokenAddr, destToken: tokenAddr}); + } + + // Attempt to add the tokens to the RateLimitToken Array + s_offRamp.updateRateLimitTokens(new EVM2EVMOffRamp.RateLimitToken[](0), adds); + + // Retrieve them from storage and make sure that they all match the expected adds + (sourceTokens, destTokens) = s_offRamp.getAllRateLimitTokens(); + + for (uint256 x = 0; x < sourceTokens.length; x++) { + // Check that the tokens match the ones we generated earlier + assertEq(sourceTokens[x], adds[x].sourceToken, "Source token doesn't match add"); + assertEq(destTokens[x], adds[x].sourceToken, "dest Token doesn't match add"); + } + + // Attempt to remove half of the numTokens by removing the second half of the list and copying it to a removes array + removes = new EVM2EVMOffRamp.RateLimitToken[](adds.length / 2); + + for (uint256 x = 0; x < adds.length / 2; x++) { + removes[x] = adds[x + (adds.length / 2)]; + } + + // Attempt to update again, this time adding nothing and removing the second half of the tokens + s_offRamp.updateRateLimitTokens(removes, new EVM2EVMOffRamp.RateLimitToken[](0)); + + (sourceTokens, destTokens) = s_offRamp.getAllRateLimitTokens(); + assertEq(sourceTokens.length, adds.length / 2, "Current Rate limit token length is not half of the original adds"); + for (uint256 x = 0; x < sourceTokens.length; x++) { + // Check that the tokens match the ones we generated earlier and didn't remove in the previous step + assertEq(sourceTokens[x], adds[x].sourceToken, "Source token doesn't match add after removes"); + assertEq(destTokens[x], adds[x].destToken, "dest Token doesn't match add after removes"); + } + } + + // Reverts + + function test_updateRateLimitTokens_NonOwner_Revert() public { + EVM2EVMOffRamp.RateLimitToken[] memory addsAndRemoves = new EVM2EVMOffRamp.RateLimitToken[](4); + + vm.startPrank(STRANGER); + + vm.expectRevert("Only callable by owner"); + + s_offRamp.updateRateLimitTokens(addsAndRemoves, addsAndRemoves); + } +} diff --git a/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMOffRampSetup.t.sol b/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMOffRampSetup.t.sol new file mode 100644 index 00000000000..053869b88a6 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/offRamp/EVM2EVMOffRampSetup.t.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IAny2EVMMessageReceiver} from "../../interfaces/IAny2EVMMessageReceiver.sol"; +import {ICommitStore} from "../../interfaces/ICommitStore.sol"; +import {IPoolV1} from "../../interfaces/IPool.sol"; + +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {EVM2EVMOffRamp} from "../../offRamp/EVM2EVMOffRamp.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {TokenSetup} from "../TokenSetup.t.sol"; +import {EVM2EVMOffRampHelper} from "../helpers/EVM2EVMOffRampHelper.sol"; +import {MaybeRevertingBurnMintTokenPool} from "../helpers/MaybeRevertingBurnMintTokenPool.sol"; +import {MaybeRevertMessageReceiver} from "../helpers/receivers/MaybeRevertMessageReceiver.sol"; +import {MockCommitStore} from "../mocks/MockCommitStore.sol"; +import {OCR2BaseSetup} from "../ocr/OCR2Base.t.sol"; +import {PriceRegistrySetup} from "../priceRegistry/PriceRegistry.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract EVM2EVMOffRampSetup is TokenSetup, PriceRegistrySetup, OCR2BaseSetup { + MockCommitStore internal s_mockCommitStore; + IAny2EVMMessageReceiver internal s_receiver; + IAny2EVMMessageReceiver internal s_secondary_receiver; + MaybeRevertMessageReceiver internal s_reverting_receiver; + + MaybeRevertingBurnMintTokenPool internal s_maybeRevertingPool; + + EVM2EVMOffRampHelper internal s_offRamp; + address internal s_sourceTokenPool = makeAddr("sourceTokenPool"); + + function setUp() public virtual override(TokenSetup, PriceRegistrySetup, OCR2BaseSetup) { + TokenSetup.setUp(); + PriceRegistrySetup.setUp(); + OCR2BaseSetup.setUp(); + + s_mockCommitStore = new MockCommitStore(); + s_receiver = new MaybeRevertMessageReceiver(false); + s_secondary_receiver = new MaybeRevertMessageReceiver(false); + s_reverting_receiver = new MaybeRevertMessageReceiver(true); + + s_maybeRevertingPool = MaybeRevertingBurnMintTokenPool(s_destPoolByToken[s_destTokens[1]]); + + deployOffRamp(s_mockCommitStore, s_destRouter, address(0)); + } + + function deployOffRamp(ICommitStore commitStore, Router router, address prevOffRamp) internal { + s_offRamp = new EVM2EVMOffRampHelper( + EVM2EVMOffRamp.StaticConfig({ + commitStore: address(commitStore), + chainSelector: DEST_CHAIN_SELECTOR, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + onRamp: ON_RAMP_ADDRESS, + prevOffRamp: prevOffRamp, + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + getInboundRateLimiterConfig() + ); + s_offRamp.setOCR2Config( + s_valid_signers, + s_valid_transmitters, + s_f, + abi.encode(generateDynamicOffRampConfig(address(router), address(s_priceRegistry))), + s_offchainConfigVersion, + abi.encode("") + ); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](0); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](2); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: address(s_offRamp)}); + offRampUpdates[1] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: address(prevOffRamp)}); + s_destRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + EVM2EVMOffRamp.RateLimitToken[] memory tokensToAdd = new EVM2EVMOffRamp.RateLimitToken[](s_sourceTokens.length); + for (uint256 i = 0; i < s_sourceTokens.length; ++i) { + tokensToAdd[i] = EVM2EVMOffRamp.RateLimitToken({sourceToken: s_sourceTokens[i], destToken: s_destTokens[i]}); + } + s_offRamp.updateRateLimitTokens(new EVM2EVMOffRamp.RateLimitToken[](0), tokensToAdd); + } + + function generateDynamicOffRampConfig( + address router, + address priceRegistry + ) internal pure returns (EVM2EVMOffRamp.DynamicConfig memory) { + return EVM2EVMOffRamp.DynamicConfig({ + permissionLessExecutionThresholdSeconds: PERMISSION_LESS_EXECUTION_THRESHOLD_SECONDS, + router: router, + priceRegistry: priceRegistry, + maxNumberOfTokensPerMsg: MAX_TOKENS_LENGTH, + maxDataBytes: MAX_DATA_SIZE, + maxPoolReleaseOrMintGas: MAX_TOKEN_POOL_RELEASE_OR_MINT_GAS, + maxTokenTransferGas: MAX_TOKEN_POOL_TRANSFER_GAS + }); + } + + function _convertToGeneralMessage(Internal.EVM2EVMMessage memory original) + internal + view + returns (Client.Any2EVMMessage memory message) + { + uint256 numberOfTokens = original.tokenAmounts.length; + Client.EVMTokenAmount[] memory destTokenAmounts = new Client.EVMTokenAmount[](numberOfTokens); + + for (uint256 i = 0; i < numberOfTokens; ++i) { + Internal.SourceTokenData memory sourceTokenData = + abi.decode(original.sourceTokenData[i], (Internal.SourceTokenData)); + + address destPoolAddress = abi.decode(sourceTokenData.destTokenAddress, (address)); + TokenPool pool = TokenPool(destPoolAddress); + destTokenAmounts[i].token = address(pool.getToken()); + destTokenAmounts[i].amount = original.tokenAmounts[i].amount; + } + + return Client.Any2EVMMessage({ + messageId: original.messageId, + sourceChainSelector: original.sourceChainSelector, + sender: abi.encode(original.sender), + data: original.data, + destTokenAmounts: destTokenAmounts + }); + } + + function _generateAny2EVMMessageNoTokens(uint64 sequenceNumber) + internal + view + returns (Internal.EVM2EVMMessage memory) + { + return _generateAny2EVMMessage(sequenceNumber, new Client.EVMTokenAmount[](0), false); + } + + function _generateAny2EVMMessageWithTokens( + uint64 sequenceNumber, + uint256[] memory amounts + ) internal view returns (Internal.EVM2EVMMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + tokenAmounts[i].amount = amounts[i]; + } + return _generateAny2EVMMessage(sequenceNumber, tokenAmounts, false); + } + + function _generateAny2EVMMessage( + uint64 sequenceNumber, + Client.EVMTokenAmount[] memory tokenAmounts, + bool allowOutOfOrderExecution + ) internal view returns (Internal.EVM2EVMMessage memory) { + bytes memory data = abi.encode(0); + Internal.EVM2EVMMessage memory message = Internal.EVM2EVMMessage({ + sequenceNumber: sequenceNumber, + sender: OWNER, + nonce: allowOutOfOrderExecution ? 0 : sequenceNumber, + gasLimit: GAS_LIMIT, + strict: false, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + receiver: address(s_receiver), + data: data, + tokenAmounts: tokenAmounts, + sourceTokenData: new bytes[](tokenAmounts.length), + feeToken: s_destFeeToken, + feeTokenAmount: uint256(0), + messageId: "" + }); + + // Correctly set the TokenDataPayload for each token. Tokens have to be set up in the TokenSetup. + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + message.sourceTokenData[i] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[tokenAmounts[i].token]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[tokenAmounts[i].token]), + extraData: "" + }) + ); + } + + message.messageId = Internal._hash( + message, + keccak256( + abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, ON_RAMP_ADDRESS) + ) + ); + + return message; + } + + function _generateSingleBasicMessage() internal view returns (Internal.EVM2EVMMessage[] memory) { + Internal.EVM2EVMMessage[] memory messages = new Internal.EVM2EVMMessage[](1); + messages[0] = _generateAny2EVMMessageNoTokens(1); + return messages; + } + + function _generateMessagesWithTokens() internal view returns (Internal.EVM2EVMMessage[] memory) { + Internal.EVM2EVMMessage[] memory messages = new Internal.EVM2EVMMessage[](2); + Client.EVMTokenAmount[] memory tokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + tokenAmounts[0].amount = 1e18; + tokenAmounts[1].amount = 5e18; + messages[0] = _generateAny2EVMMessage(1, tokenAmounts, false); + messages[1] = _generateAny2EVMMessage(2, tokenAmounts, false); + + return messages; + } + + function _generateReportFromMessages(Internal.EVM2EVMMessage[] memory messages) + internal + pure + returns (Internal.ExecutionReport memory) + { + bytes[][] memory offchainTokenData = new bytes[][](messages.length); + + for (uint256 i = 0; i < messages.length; ++i) { + offchainTokenData[i] = new bytes[](messages[i].tokenAmounts.length); + } + + return Internal.ExecutionReport({ + proofs: new bytes32[](0), + proofFlagBits: 2 ** 256 - 1, + messages: messages, + offchainTokenData: offchainTokenData + }); + } + + function _getGasLimitsFromMessages(Internal.EVM2EVMMessage[] memory messages) + internal + pure + returns (uint256[] memory) + { + uint256[] memory gasLimits = new uint256[](messages.length); + for (uint256 i = 0; i < messages.length; ++i) { + gasLimits[i] = messages[i].gasLimit; + } + + return gasLimits; + } + + function _assertSameConfig(EVM2EVMOffRamp.DynamicConfig memory a, EVM2EVMOffRamp.DynamicConfig memory b) public pure { + assertEq(a.permissionLessExecutionThresholdSeconds, b.permissionLessExecutionThresholdSeconds); + assertEq(a.router, b.router); + assertEq(a.priceRegistry, b.priceRegistry); + assertEq(a.maxNumberOfTokensPerMsg, b.maxNumberOfTokensPerMsg); + assertEq(a.maxDataBytes, b.maxDataBytes); + assertEq(a.maxPoolReleaseOrMintGas, b.maxPoolReleaseOrMintGas); + assertEq(a.maxTokenTransferGas, b.maxTokenTransferGas); + } + + function _getDefaultSourceTokenData(Client.EVMTokenAmount[] memory srcTokenAmounts) + internal + view + returns (bytes[] memory) + { + bytes[] memory sourceTokenData = new bytes[](srcTokenAmounts.length); + for (uint256 i = 0; i < srcTokenAmounts.length; ++i) { + sourceTokenData[i] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[srcTokenAmounts[i].token]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[srcTokenAmounts[i].token]), + extraData: "" + }) + ); + } + return sourceTokenData; + } +} diff --git a/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMMultiOnRamp.t.sol b/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMMultiOnRamp.t.sol new file mode 100644 index 00000000000..bc7fac95be6 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMMultiOnRamp.t.sol @@ -0,0 +1,720 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IMessageInterceptor} from "../../interfaces/IMessageInterceptor.sol"; +import {ITokenAdminRegistry} from "../../interfaces/ITokenAdminRegistry.sol"; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {MultiAggregateRateLimiter} from "../../MultiAggregateRateLimiter.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {USDPriceWith18Decimals} from "../../libraries/USDPriceWith18Decimals.sol"; +import {EVM2EVMMultiOnRamp} from "../../onRamp/EVM2EVMMultiOnRamp.sol"; +import {EVM2EVMOnRamp} from "../../onRamp/EVM2EVMOnRamp.sol"; +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {EVM2EVMOnRampHelper} from "../helpers/EVM2EVMOnRampHelper.sol"; +import {MaybeRevertingBurnMintTokenPool} from "../helpers/MaybeRevertingBurnMintTokenPool.sol"; +import {MessageInterceptorHelper} from "../helpers/MessageInterceptorHelper.sol"; +import "./EVM2EVMMultiOnRampSetup.t.sol"; + +contract EVM2EVMMultiOnRamp_constructor is EVM2EVMMultiOnRampSetup { + function test_Constructor_Success() public { + EVM2EVMMultiOnRamp.StaticConfig memory staticConfig = EVM2EVMMultiOnRamp.StaticConfig({ + chainSelector: SOURCE_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + nonceManager: address(s_outboundNonceManager), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }); + EVM2EVMMultiOnRamp.DynamicConfig memory dynamicConfig = + _generateDynamicMultiOnRampConfig(address(s_sourceRouter), address(s_priceRegistry)); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.ConfigSet(staticConfig, dynamicConfig); + + _deployOnRamp( + SOURCE_CHAIN_SELECTOR, address(s_sourceRouter), address(s_outboundNonceManager), address(s_tokenAdminRegistry) + ); + + EVM2EVMMultiOnRamp.StaticConfig memory gotStaticConfig = s_onRamp.getStaticConfig(); + _assertStaticConfigsEqual(staticConfig, gotStaticConfig); + + EVM2EVMMultiOnRamp.DynamicConfig memory gotDynamicConfig = s_onRamp.getDynamicConfig(); + _assertDynamicConfigsEqual(dynamicConfig, gotDynamicConfig); + + // Initial values + assertEq("EVM2EVMMultiOnRamp 1.6.0-dev", s_onRamp.typeAndVersion()); + assertEq(OWNER, s_onRamp.owner()); + assertEq(1, s_onRamp.getExpectedNextSequenceNumber(DEST_CHAIN_SELECTOR)); + } + + function test_Constructor_InvalidConfigChainSelectorEqZero_Revert() public { + vm.expectRevert(EVM2EVMMultiOnRamp.InvalidConfig.selector); + new EVM2EVMMultiOnRampHelper( + EVM2EVMMultiOnRamp.StaticConfig({ + chainSelector: 0, + rmnProxy: address(s_mockRMN), + nonceManager: address(s_outboundNonceManager), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + _generateDynamicMultiOnRampConfig(address(s_sourceRouter), address(s_priceRegistry)) + ); + } + + function test_Constructor_InvalidConfigRMNProxyEqAddressZero_Revert() public { + vm.expectRevert(EVM2EVMMultiOnRamp.InvalidConfig.selector); + s_onRamp = new EVM2EVMMultiOnRampHelper( + EVM2EVMMultiOnRamp.StaticConfig({ + chainSelector: SOURCE_CHAIN_SELECTOR, + rmnProxy: address(0), + nonceManager: address(s_outboundNonceManager), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + _generateDynamicMultiOnRampConfig(address(s_sourceRouter), address(s_priceRegistry)) + ); + } + + function test_Constructor_InvalidConfigNonceManagerEqAddressZero_Revert() public { + vm.expectRevert(EVM2EVMMultiOnRamp.InvalidConfig.selector); + new EVM2EVMMultiOnRampHelper( + EVM2EVMMultiOnRamp.StaticConfig({ + chainSelector: SOURCE_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + nonceManager: address(0), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + _generateDynamicMultiOnRampConfig(address(s_sourceRouter), address(s_priceRegistry)) + ); + } + + function test_Constructor_InvalidConfigTokenAdminRegistryEqAddressZero_Revert() public { + vm.expectRevert(EVM2EVMMultiOnRamp.InvalidConfig.selector); + new EVM2EVMMultiOnRampHelper( + EVM2EVMMultiOnRamp.StaticConfig({ + chainSelector: SOURCE_CHAIN_SELECTOR, + rmnProxy: address(s_mockRMN), + nonceManager: address(s_outboundNonceManager), + tokenAdminRegistry: address(0) + }), + _generateDynamicMultiOnRampConfig(address(s_sourceRouter), address(s_priceRegistry)) + ); + } +} + +contract EVM2EVMMultiOnRamp_forwardFromRouter is EVM2EVMMultiOnRampSetup { + struct LegacyExtraArgs { + uint256 gasLimit; + bool strict; + } + + function setUp() public virtual override { + super.setUp(); + + address[] memory feeTokens = new address[](1); + feeTokens[0] = s_sourceTokens[1]; + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + + // Since we'll mostly be testing for valid calls from the router we'll + // mock all calls to be originating from the router and re-mock in + // tests that require failure. + vm.startPrank(address(s_sourceRouter)); + } + + function test_ForwardFromRouterSuccessCustomExtraArgs() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT * 2})); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouterSuccessLegacyExtraArgs() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = + abi.encodeWithSelector(Client.EVM_EXTRA_ARGS_V1_TAG, LegacyExtraArgs({gasLimit: GAS_LIMIT * 2, strict: true})); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + // We expect the message to be emitted with strict = false. + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouterSuccessEmptyExtraArgs() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = ""; + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + // We expect the message to be emitted with strict = false. + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouter_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouterExtraArgsV2_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = abi.encodeWithSelector( + Client.EVM_EXTRA_ARGS_V2_TAG, Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT * 2, allowOutOfOrderExecution: false}) + ); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouterExtraArgsV2AllowOutOfOrderTrue_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = abi.encodeWithSelector( + Client.EVM_EXTRA_ARGS_V2_TAG, Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT * 2, allowOutOfOrderExecution: true}) + ); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ShouldIncrementSeqNumAndNonce_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + for (uint64 i = 1; i < 4; ++i) { + uint64 nonceBefore = s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER); + uint64 sequenceNumberBefore = s_onRamp.getExpectedNextSequenceNumber(DEST_CHAIN_SELECTOR) - 1; + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, i, i, 0, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + uint64 nonceAfter = s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER); + uint64 sequenceNumberAfter = s_onRamp.getExpectedNextSequenceNumber(DEST_CHAIN_SELECTOR) - 1; + assertEq(nonceAfter, nonceBefore + 1); + assertEq(sequenceNumberAfter, sequenceNumberBefore + 1); + } + } + + function test_ShouldIncrementNonceOnlyOnOrdered_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = abi.encodeWithSelector( + Client.EVM_EXTRA_ARGS_V2_TAG, Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT * 2, allowOutOfOrderExecution: true}) + ); + + for (uint64 i = 1; i < 4; ++i) { + uint64 nonceBefore = s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER); + uint64 sequenceNumberBefore = s_onRamp.getExpectedNextSequenceNumber(DEST_CHAIN_SELECTOR) - 1; + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, i, i, 0, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + uint64 nonceAfter = s_outboundNonceManager.getOutboundNonce(DEST_CHAIN_SELECTOR, OWNER); + uint64 sequenceNumberAfter = s_onRamp.getExpectedNextSequenceNumber(DEST_CHAIN_SELECTOR) - 1; + assertEq(nonceAfter, nonceBefore); + assertEq(sequenceNumberAfter, sequenceNumberBefore + 1); + } + } + + function test_ShouldStoreLinkFees() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.FeePaid(s_sourceFeeToken, feeAmount); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + + assertEq(IERC20(s_sourceFeeToken).balanceOf(address(s_onRamp)), feeAmount); + } + + function test_ShouldStoreNonLinkFees() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = s_sourceTokens[1]; + + uint256 feeAmount = 1234567890; + IERC20(s_sourceTokens[1]).transferFrom(OWNER, address(s_onRamp), feeAmount); + + // Calculate conversion done by prices contract + uint256 feeTokenPrice = s_priceRegistry.getTokenPrice(s_sourceTokens[1]).value; + uint256 linkTokenPrice = s_priceRegistry.getTokenPrice(s_sourceFeeToken).value; + uint256 conversionRate = (feeTokenPrice * 1e18) / linkTokenPrice; + uint256 expectedJuels = (feeAmount * conversionRate) / 1e18; + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.FeePaid(s_sourceTokens[1], expectedJuels); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + + assertEq(IERC20(s_sourceTokens[1]).balanceOf(address(s_onRamp)), feeAmount); + } + + // Make sure any valid sender, receiver and feeAmount can be handled. + // @TODO Temporarily setting lower fuzz run as 256 triggers snapshot gas off by 1 error. + // https://github.com/foundry-rs/foundry/issues/5689 + /// forge-dynamicConfig: default.fuzz.runs = 32 + /// forge-dynamicConfig: ccip.fuzz.runs = 32 + function test_Fuzz_ForwardFromRouter_Success(address originalSender, address receiver, uint96 feeTokenAmount) public { + // To avoid RouterMustSetOriginalSender + vm.assume(originalSender != address(0)); + vm.assume(uint160(receiver) >= Internal.PRECOMPILE_SPACE); + feeTokenAmount = uint96(bound(feeTokenAmount, 0, MAX_MSG_FEES_JUELS)); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.receiver = abi.encode(receiver); + + // Make sure the tokens are in the contract + deal(s_sourceFeeToken, address(s_onRamp), feeTokenAmount); + + Internal.EVM2AnyRampMessage memory expectedEvent = _messageToEvent(message, 1, 1, feeTokenAmount, originalSender); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.FeePaid(s_sourceFeeToken, feeTokenAmount); + vm.expectEmit(false, false, false, true); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, expectedEvent); + + // Assert the message Id is correct + assertEq( + expectedEvent.header.messageId, + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeTokenAmount, originalSender) + ); + } + + function test_forwardFromRouter_WithValidation_Success() public { + _enableOutboundMessageValidator(); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT * 2})); + uint256 feeAmount = 1234567890; + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 1e18; + message.tokenAmounts[0].token = s_sourceTokens[0]; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + s_outboundMessageValidator.setMessageIdValidationState(keccak256(abi.encode(message)), false); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.CCIPSendRequested(DEST_CHAIN_SELECTOR, _messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + // Reverts + + function test_Paused_Revert() public { + // We pause by disabling the whitelist + vm.stopPrank(); + vm.startPrank(OWNER); + address router = address(0); + s_onRamp.setDynamicConfig(_generateDynamicMultiOnRampConfig(router, address(2))); + vm.expectRevert(EVM2EVMMultiOnRamp.MustBeCalledByRouter.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), 0, OWNER); + } + + function test_InvalidExtraArgsTag_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = bytes("bad args"); + + vm.expectRevert(EVM2EVMMultiOnRamp.InvalidExtraArgsTag.selector); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_Permissions_Revert() public { + vm.stopPrank(); + vm.startPrank(OWNER); + vm.expectRevert(EVM2EVMMultiOnRamp.MustBeCalledByRouter.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), 0, OWNER); + } + + function test_OriginalSender_Revert() public { + vm.expectRevert(EVM2EVMMultiOnRamp.RouterMustSetOriginalSender.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), 0, address(0)); + } + + function test_MessageValidationError_Revert() public { + _enableOutboundMessageValidator(); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT * 2})); + uint256 feeAmount = 1234567890; + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 1e18; + message.tokenAmounts[0].token = s_sourceTokens[0]; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + s_outboundMessageValidator.setMessageIdValidationState(keccak256(abi.encode(message)), true); + + vm.expectRevert( + abi.encodeWithSelector(IMessageInterceptor.MessageValidationError.selector, bytes("Invalid message")) + ); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_CannotSendZeroTokens_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 0; + message.tokenAmounts[0].token = s_sourceTokens[0]; + vm.expectRevert(EVM2EVMMultiOnRamp.CannotSendZeroTokens.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, STRANGER); + } + + function test_UnsupportedToken_Revert() public { + address wrongToken = address(1); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].token = wrongToken; + message.tokenAmounts[0].amount = 1; + + // We need to set the price of this new token to be able to reach + // the proper revert point. This must be called by the owner. + vm.stopPrank(); + vm.startPrank(OWNER); + + Internal.PriceUpdates memory priceUpdates = getSingleTokenPriceUpdateStruct(wrongToken, 1); + s_priceRegistry.updatePrices(priceUpdates); + + // Change back to the router + vm.startPrank(address(s_sourceRouter)); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOnRamp.UnsupportedToken.selector, wrongToken)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_forwardFromRouter_UnsupportedToken_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 1; + message.tokenAmounts[0].token = address(1); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOnRamp.UnsupportedToken.selector, message.tokenAmounts[0].token)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_MesssageFeeTooHigh_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + vm.expectRevert( + abi.encodeWithSelector(PriceRegistry.MessageFeeTooHigh.selector, MAX_MSG_FEES_JUELS + 1, MAX_MSG_FEES_JUELS) + ); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, MAX_MSG_FEES_JUELS + 1, OWNER); + } + + function test_SourceTokenDataTooLarge_Revert() public { + address sourceETH = s_sourceTokens[1]; + vm.stopPrank(); + vm.startPrank(OWNER); + + MaybeRevertingBurnMintTokenPool newPool = new MaybeRevertingBurnMintTokenPool( + BurnMintERC677(sourceETH), new address[](0), address(s_mockRMN), address(s_sourceRouter) + ); + BurnMintERC677(sourceETH).grantMintAndBurnRoles(address(newPool)); + deal(address(sourceETH), address(newPool), type(uint256).max); + + // Add TokenPool to OnRamp + s_tokenAdminRegistry.setPool(sourceETH, address(newPool)); + + // Allow chain in TokenPool + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_destTokenPool), + remoteTokenAddress: abi.encode(s_destToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + newPool.applyChainUpdates(chainUpdates); + + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(address(sourceETH), 1000); + + // No data set, should succeed + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Set max data length, should succeed + vm.startPrank(OWNER); + newPool.setSourceTokenData(new bytes(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES)); + + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Set data to max length +1, should revert + vm.startPrank(OWNER); + newPool.setSourceTokenData(new bytes(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + 1)); + + vm.startPrank(address(s_sourceRouter)); + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.SourceTokenDataTooLarge.selector, sourceETH)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Set token config to allow larger data + vm.startPrank(OWNER); + PriceRegistry.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + _generateTokenTransferFeeConfigArgs(1, 1); + tokenTransferFeeConfigArgs[0].destChainSelector = DEST_CHAIN_SELECTOR; + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token = sourceETH; + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig = PriceRegistry + .TokenTransferFeeConfig({ + minFeeUSDCents: 1, + maxFeeUSDCents: 0, + deciBps: 0, + destGasOverhead: 0, + destBytesOverhead: uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) + 32, + isEnabled: true + }); + s_priceRegistry.applyTokenTransferFeeConfigUpdates( + tokenTransferFeeConfigArgs, new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](0) + ); + + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Set the token data larger than the configured token data, should revert + vm.startPrank(OWNER); + newPool.setSourceTokenData(new bytes(uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) + 32 + 1)); + + vm.startPrank(address(s_sourceRouter)); + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.SourceTokenDataTooLarge.selector, sourceETH)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } +} + +contract EVM2EVMMultiOnRamp_getSupportedTokens is EVM2EVMMultiOnRampSetup { + function test_GetSupportedTokens_Revert() public { + vm.expectRevert(EVM2EVMMultiOnRamp.GetSupportedTokensFunctionalityRemovedCheckAdminRegistry.selector); + s_onRamp.getSupportedTokens(DEST_CHAIN_SELECTOR); + } +} + +contract EVM2EVMMultiOnRamp_getFee is EVM2EVMMultiOnRampSetup { + using USDPriceWith18Decimals for uint224; + + function test_EmptyMessage_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = testTokens[i]; + + uint256 feeAmount = s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + uint256 expectedFeeAmount = s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + + assertEq(expectedFeeAmount, feeAmount); + } + } + + function test_SingleTokenMessage_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + uint256 tokenAmount = 10000e18; + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, tokenAmount); + message.feeToken = testTokens[i]; + + uint256 feeAmount = s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + uint256 expectedFeeAmount = s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + + assertEq(expectedFeeAmount, feeAmount); + } + } + + // Reverts + + function test_Unhealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMMultiOnRamp.CursedByRMN.selector, DEST_CHAIN_SELECTOR)); + s_onRamp.getFee(DEST_CHAIN_SELECTOR, _generateEmptyMessage()); + } + + function test_EnforceOutOfOrder_Revert() public { + // Update dynamic config to enforce allowOutOfOrderExecution = true. + vm.stopPrank(); + vm.startPrank(OWNER); + + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + destChainConfigArgs[0].destChainConfig.enforceOutOfOrder = true; + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + vm.stopPrank(); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + // Empty extraArgs to should revert since it enforceOutOfOrder is true. + message.extraArgs = ""; + + vm.expectRevert(PriceRegistry.ExtraArgOutOfOrderExecutionMustBeTrue.selector); + s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + } +} + +contract EVM2EVMMultiOnRamp_setDynamicConfig is EVM2EVMMultiOnRampSetup { + function test_SetDynamicConfig_Success() public { + EVM2EVMMultiOnRamp.StaticConfig memory staticConfig = s_onRamp.getStaticConfig(); + EVM2EVMMultiOnRamp.DynamicConfig memory newConfig = EVM2EVMMultiOnRamp.DynamicConfig({ + router: address(2134), + priceRegistry: address(23423), + messageValidator: makeAddr("messageValidator"), + feeAggregator: FEE_AGGREGATOR + }); + + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.ConfigSet(staticConfig, newConfig); + + s_onRamp.setDynamicConfig(newConfig); + + EVM2EVMMultiOnRamp.DynamicConfig memory gotDynamicConfig = s_onRamp.getDynamicConfig(); + assertEq(newConfig.router, gotDynamicConfig.router); + assertEq(newConfig.priceRegistry, gotDynamicConfig.priceRegistry); + } + + // Reverts + + function test_SetConfigInvalidConfigPriceRegistryEqAddressZero_Revert() public { + EVM2EVMMultiOnRamp.DynamicConfig memory newConfig = EVM2EVMMultiOnRamp.DynamicConfig({ + router: address(2134), + priceRegistry: address(0), + feeAggregator: FEE_AGGREGATOR, + messageValidator: makeAddr("messageValidator") + }); + + vm.expectRevert(EVM2EVMMultiOnRamp.InvalidConfig.selector); + s_onRamp.setDynamicConfig(newConfig); + } + + function test_SetConfigInvalidConfig_Revert() public { + EVM2EVMMultiOnRamp.DynamicConfig memory newConfig = EVM2EVMMultiOnRamp.DynamicConfig({ + router: address(1), + priceRegistry: address(23423), + messageValidator: address(0), + feeAggregator: FEE_AGGREGATOR + }); + + // Invalid price reg reverts. + newConfig.priceRegistry = address(0); + vm.expectRevert(EVM2EVMMultiOnRamp.InvalidConfig.selector); + s_onRamp.setDynamicConfig(newConfig); + } + + function test_SetConfigInvalidConfigFeeAggregatorEqAddressZero_Revert() public { + EVM2EVMMultiOnRamp.DynamicConfig memory newConfig = EVM2EVMMultiOnRamp.DynamicConfig({ + router: address(2134), + priceRegistry: address(23423), + messageValidator: address(0), + feeAggregator: address(0) + }); + vm.expectRevert(EVM2EVMMultiOnRamp.InvalidConfig.selector); + s_onRamp.setDynamicConfig(newConfig); + } + + function test_SetConfigOnlyOwner_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + s_onRamp.setDynamicConfig(_generateDynamicMultiOnRampConfig(address(1), address(2))); + vm.startPrank(ADMIN); + vm.expectRevert("Only callable by owner"); + s_onRamp.setDynamicConfig(_generateDynamicMultiOnRampConfig(address(1), address(2))); + } +} + +contract EVM2EVMMultiOnRamp_withdrawFeeTokens is EVM2EVMMultiOnRampSetup { + mapping(address => uint256) internal s_nopFees; + + function setUp() public virtual override { + super.setUp(); + + // Since we'll mostly be testing for valid calls from the router we'll + // mock all calls to be originating from the router and re-mock in + // tests that require failure. + vm.startPrank(address(s_sourceRouter)); + + uint256 feeAmount = 1234567890; + + // Send a bunch of messages, increasing the juels in the contract + for (uint256 i = 0; i < s_sourceFeeTokens.length; ++i) { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = s_sourceFeeTokens[i % s_sourceFeeTokens.length]; + uint256 newFeeTokenBalance = IERC20(message.feeToken).balanceOf(address(s_onRamp)) + feeAmount; + deal(message.feeToken, address(s_onRamp), newFeeTokenBalance); + s_nopFees[message.feeToken] = newFeeTokenBalance; + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + } + + function test_Fuzz_WithdrawFeeTokens_Success(uint256[5] memory amounts) public { + vm.startPrank(OWNER); + address[] memory feeTokens = new address[](amounts.length); + for (uint256 i = 0; i < amounts.length; ++i) { + vm.assume(amounts[i] > 0); + feeTokens[i] = _deploySourceToken("", amounts[i], 18); + IERC20(feeTokens[i]).transfer(address(s_onRamp), amounts[i]); + } + + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + + for (uint256 i = 0; i < feeTokens.length; ++i) { + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.FeeTokenWithdrawn(FEE_AGGREGATOR, feeTokens[i], amounts[i]); + } + + s_onRamp.withdrawFeeTokens(); + + for (uint256 i = 0; i < feeTokens.length; ++i) { + assertEq(IERC20(feeTokens[i]).balanceOf(FEE_AGGREGATOR), amounts[i]); + assertEq(IERC20(feeTokens[i]).balanceOf(address(s_onRamp)), 0); + } + } + + function test_WithdrawFeeTokens_Success() public { + vm.expectEmit(); + emit EVM2EVMMultiOnRamp.FeeTokenWithdrawn(FEE_AGGREGATOR, s_sourceFeeToken, s_nopFees[s_sourceFeeToken]); + + s_onRamp.withdrawFeeTokens(); + + assertEq(IERC20(s_sourceFeeToken).balanceOf(FEE_AGGREGATOR), s_nopFees[s_sourceFeeToken]); + assertEq(IERC20(s_sourceFeeToken).balanceOf(address(s_onRamp)), 0); + } +} + +contract EVM2EVMMultiOnRamp_getTokenPool is EVM2EVMMultiOnRampSetup { + function test_GetTokenPool_Success() public view { + assertEq( + s_sourcePoolByToken[s_sourceTokens[0]], + address(s_onRamp.getPoolBySourceToken(DEST_CHAIN_SELECTOR, IERC20(s_sourceTokens[0]))) + ); + assertEq( + s_sourcePoolByToken[s_sourceTokens[1]], + address(s_onRamp.getPoolBySourceToken(DEST_CHAIN_SELECTOR, IERC20(s_sourceTokens[1]))) + ); + + address wrongToken = address(123); + address nonExistentPool = address(s_onRamp.getPoolBySourceToken(DEST_CHAIN_SELECTOR, IERC20(wrongToken))); + + assertEq(address(0), nonExistentPool); + } +} diff --git a/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMMultiOnRampSetup.t.sol b/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMMultiOnRampSetup.t.sol new file mode 100644 index 00000000000..f085185753d --- /dev/null +++ b/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMMultiOnRampSetup.t.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPoolV1} from "../../interfaces/IPool.sol"; + +import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol"; +import {NonceManager} from "../../NonceManager.sol"; +import {PriceRegistry} from "../../PriceRegistry.sol"; +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {EVM2EVMMultiOnRamp} from "../../onRamp/EVM2EVMMultiOnRamp.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {TokenSetup} from "../TokenSetup.t.sol"; +import {EVM2EVMMultiOnRampHelper} from "../helpers/EVM2EVMMultiOnRampHelper.sol"; +import {MessageInterceptorHelper} from "../helpers/MessageInterceptorHelper.sol"; +import {PriceRegistryFeeSetup} from "../priceRegistry/PriceRegistry.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract EVM2EVMMultiOnRampSetup is TokenSetup, PriceRegistryFeeSetup { + uint256 internal immutable i_tokenAmount0 = 9; + uint256 internal immutable i_tokenAmount1 = 7; + + bytes32 internal s_metadataHash; + + EVM2EVMMultiOnRampHelper internal s_onRamp; + MessageInterceptorHelper internal s_outboundMessageValidator; + address[] internal s_offRamps; + NonceManager internal s_outboundNonceManager; + + function setUp() public virtual override(TokenSetup, PriceRegistryFeeSetup) { + TokenSetup.setUp(); + PriceRegistryFeeSetup.setUp(); + + s_outboundMessageValidator = new MessageInterceptorHelper(); + s_outboundNonceManager = new NonceManager(new address[](0)); + (s_onRamp, s_metadataHash) = _deployOnRamp( + SOURCE_CHAIN_SELECTOR, address(s_sourceRouter), address(s_outboundNonceManager), address(s_tokenAdminRegistry) + ); + + s_offRamps = new address[](2); + s_offRamps[0] = address(10); + s_offRamps[1] = address(11); + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](2); + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: address(s_onRamp)}); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: s_offRamps[0]}); + offRampUpdates[1] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: s_offRamps[1]}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + + // Pre approve the first token so the gas estimates of the tests + // only cover actual gas usage from the ramps + IERC20(s_sourceTokens[0]).approve(address(s_sourceRouter), 2 ** 128); + IERC20(s_sourceTokens[1]).approve(address(s_sourceRouter), 2 ** 128); + } + + function _generateTokenMessage() public view returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + tokenAmounts[0].amount = i_tokenAmount0; + tokenAmounts[1].amount = i_tokenAmount1; + return Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + } + + function _messageToEvent( + Client.EVM2AnyMessage memory message, + uint64 seqNum, + uint64 nonce, + uint256 feeTokenAmount, + address originalSender + ) public view returns (Internal.EVM2AnyRampMessage memory) { + return _messageToEvent( + message, + SOURCE_CHAIN_SELECTOR, + DEST_CHAIN_SELECTOR, + seqNum, + nonce, + feeTokenAmount, + originalSender, + s_metadataHash, + s_tokenAdminRegistry + ); + } + + function _generateDynamicMultiOnRampConfig( + address router, + address priceRegistry + ) internal pure returns (EVM2EVMMultiOnRamp.DynamicConfig memory) { + return EVM2EVMMultiOnRamp.DynamicConfig({ + router: router, + priceRegistry: priceRegistry, + messageValidator: address(0), + feeAggregator: FEE_AGGREGATOR + }); + } + + // Slicing is only available for calldata. So we have to build a new bytes array. + function _removeFirst4Bytes(bytes memory data) internal pure returns (bytes memory) { + bytes memory result = new bytes(data.length - 4); + for (uint256 i = 4; i < data.length; ++i) { + result[i - 4] = data[i]; + } + return result; + } + + function _deployOnRamp( + uint64 sourceChainSelector, + address sourceRouter, + address nonceManager, + address tokenAdminRegistry + ) internal returns (EVM2EVMMultiOnRampHelper, bytes32 metadataHash) { + EVM2EVMMultiOnRampHelper onRamp = new EVM2EVMMultiOnRampHelper( + EVM2EVMMultiOnRamp.StaticConfig({ + chainSelector: sourceChainSelector, + rmnProxy: address(s_mockRMN), + nonceManager: nonceManager, + tokenAdminRegistry: tokenAdminRegistry + }), + _generateDynamicMultiOnRampConfig(sourceRouter, address(s_priceRegistry)) + ); + + address[] memory authorizedCallers = new address[](1); + authorizedCallers[0] = address(onRamp); + + NonceManager(nonceManager).applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: authorizedCallers, removedCallers: new address[](0)}) + ); + + return ( + onRamp, + keccak256(abi.encode(Internal.EVM_2_ANY_MESSAGE_HASH, sourceChainSelector, DEST_CHAIN_SELECTOR, address(onRamp))) + ); + } + + function _enableOutboundMessageValidator() internal { + (, address msgSender,) = vm.readCallers(); + + bool resetPrank = false; + + if (msgSender != OWNER) { + vm.stopPrank(); + vm.startPrank(OWNER); + resetPrank = true; + } + + EVM2EVMMultiOnRamp.DynamicConfig memory dynamicConfig = s_onRamp.getDynamicConfig(); + dynamicConfig.messageValidator = address(s_outboundMessageValidator); + s_onRamp.setDynamicConfig(dynamicConfig); + + if (resetPrank) { + vm.stopPrank(); + vm.startPrank(msgSender); + } + } + + function _assertStaticConfigsEqual( + EVM2EVMMultiOnRamp.StaticConfig memory a, + EVM2EVMMultiOnRamp.StaticConfig memory b + ) internal pure { + assertEq(a.chainSelector, b.chainSelector); + assertEq(a.rmnProxy, b.rmnProxy); + assertEq(a.tokenAdminRegistry, b.tokenAdminRegistry); + } + + function _assertDynamicConfigsEqual( + EVM2EVMMultiOnRamp.DynamicConfig memory a, + EVM2EVMMultiOnRamp.DynamicConfig memory b + ) internal pure { + assertEq(a.router, b.router); + assertEq(a.priceRegistry, b.priceRegistry); + } +} diff --git a/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMOnRamp.t.sol b/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMOnRamp.t.sol new file mode 100644 index 00000000000..197a87b7081 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMOnRamp.t.sol @@ -0,0 +1,1986 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITokenAdminRegistry} from "../../interfaces/ITokenAdminRegistry.sol"; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {AggregateRateLimiter} from "../../AggregateRateLimiter.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {USDPriceWith18Decimals} from "../../libraries/USDPriceWith18Decimals.sol"; +import {EVM2EVMOnRamp} from "../../onRamp/EVM2EVMOnRamp.sol"; +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {MaybeRevertingBurnMintTokenPool} from "../helpers/MaybeRevertingBurnMintTokenPool.sol"; +import "./EVM2EVMOnRampSetup.t.sol"; + +contract EVM2EVMOnRamp_constructor is EVM2EVMOnRampSetup { + function test_Constructor_Success() public { + EVM2EVMOnRamp.StaticConfig memory staticConfig = EVM2EVMOnRamp.StaticConfig({ + linkToken: s_sourceTokens[0], + chainSelector: SOURCE_CHAIN_SELECTOR, + destChainSelector: DEST_CHAIN_SELECTOR, + defaultTxGasLimit: GAS_LIMIT, + maxNopFeesJuels: MAX_NOP_FEES_JUELS, + prevOnRamp: address(0), + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }); + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = + generateDynamicOnRampConfig(address(s_sourceRouter), address(s_priceRegistry)); + + vm.expectEmit(); + emit EVM2EVMOnRamp.ConfigSet(staticConfig, dynamicConfig); + + s_onRamp = new EVM2EVMOnRampHelper( + staticConfig, + dynamicConfig, + getOutboundRateLimiterConfig(), + s_feeTokenConfigArgs, + s_tokenTransferFeeConfigArgs, + getNopsAndWeights() + ); + + EVM2EVMOnRamp.StaticConfig memory gotStaticConfig = s_onRamp.getStaticConfig(); + assertEq(staticConfig.linkToken, gotStaticConfig.linkToken); + assertEq(staticConfig.chainSelector, gotStaticConfig.chainSelector); + assertEq(staticConfig.destChainSelector, gotStaticConfig.destChainSelector); + assertEq(staticConfig.defaultTxGasLimit, gotStaticConfig.defaultTxGasLimit); + assertEq(staticConfig.maxNopFeesJuels, gotStaticConfig.maxNopFeesJuels); + assertEq(staticConfig.prevOnRamp, gotStaticConfig.prevOnRamp); + assertEq(staticConfig.rmnProxy, gotStaticConfig.rmnProxy); + + EVM2EVMOnRamp.DynamicConfig memory gotDynamicConfig = s_onRamp.getDynamicConfig(); + assertEq(dynamicConfig.router, gotDynamicConfig.router); + assertEq(dynamicConfig.maxNumberOfTokensPerMsg, gotDynamicConfig.maxNumberOfTokensPerMsg); + assertEq(dynamicConfig.destGasOverhead, gotDynamicConfig.destGasOverhead); + assertEq(dynamicConfig.destGasPerPayloadByte, gotDynamicConfig.destGasPerPayloadByte); + assertEq(dynamicConfig.priceRegistry, gotDynamicConfig.priceRegistry); + assertEq(dynamicConfig.maxDataBytes, gotDynamicConfig.maxDataBytes); + assertEq(dynamicConfig.maxPerMsgGasLimit, gotDynamicConfig.maxPerMsgGasLimit); + + // Initial values + assertEq("EVM2EVMOnRamp 1.5.0-dev", s_onRamp.typeAndVersion()); + assertEq(OWNER, s_onRamp.owner()); + assertEq(1, s_onRamp.getExpectedNextSequenceNumber()); + } +} + +contract EVM2EVMOnRamp_payNops_fuzz is EVM2EVMOnRampSetup { + function test_Fuzz_NopPayNops_Success(uint96 nopFeesJuels) public { + (EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights, uint256 weightsTotal) = s_onRamp.getNops(); + // To avoid NoFeesToPay + vm.assume(nopFeesJuels > weightsTotal); + vm.assume(nopFeesJuels < MAX_NOP_FEES_JUELS); + + // Set Nop fee juels + deal(s_sourceFeeToken, address(s_onRamp), nopFeesJuels); + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), nopFeesJuels, OWNER); + + vm.startPrank(OWNER); + + uint256 totalJuels = s_onRamp.getNopFeesJuels(); + s_onRamp.payNops(); + for (uint256 i = 0; i < nopsAndWeights.length; ++i) { + uint256 expectedPayout = (totalJuels * nopsAndWeights[i].weight) / weightsTotal; + assertEq(IERC20(s_sourceFeeToken).balanceOf(nopsAndWeights[i].nop), expectedPayout); + } + } +} + +contract EVM2EVMNopsFeeSetup is EVM2EVMOnRampSetup { + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + // Since we'll mostly be testing for valid calls from the router we'll + // mock all calls to be originating from the router and re-mock in + // tests that require failure. + vm.startPrank(address(s_sourceRouter)); + + uint256 feeAmount = 1234567890; + uint256 numberOfMessages = 5; + + // Send a bunch of messages, increasing the juels in the contract + for (uint256 i = 0; i < numberOfMessages; ++i) { + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), feeAmount, OWNER); + } + + assertEq(s_onRamp.getNopFeesJuels(), feeAmount * numberOfMessages); + assertEq(IERC20(s_sourceFeeToken).balanceOf(address(s_onRamp)), feeAmount * numberOfMessages); + } +} + +contract EVM2EVMOnRamp_payNops is EVM2EVMNopsFeeSetup { + function test_OwnerPayNops_Success() public { + vm.startPrank(OWNER); + + uint256 totalJuels = s_onRamp.getNopFeesJuels(); + s_onRamp.payNops(); + (EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights, uint256 weightsTotal) = s_onRamp.getNops(); + for (uint256 i = 0; i < nopsAndWeights.length; ++i) { + uint256 expectedPayout = (nopsAndWeights[i].weight * totalJuels) / weightsTotal; + assertEq(IERC20(s_sourceFeeToken).balanceOf(nopsAndWeights[i].nop), expectedPayout); + } + } + + function test_AdminPayNops_Success() public { + vm.startPrank(ADMIN); + + uint256 totalJuels = s_onRamp.getNopFeesJuels(); + s_onRamp.payNops(); + (EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights, uint256 weightsTotal) = s_onRamp.getNops(); + for (uint256 i = 0; i < nopsAndWeights.length; ++i) { + uint256 expectedPayout = (nopsAndWeights[i].weight * totalJuels) / weightsTotal; + assertEq(IERC20(s_sourceFeeToken).balanceOf(nopsAndWeights[i].nop), expectedPayout); + } + } + + function test_NopPayNops_Success() public { + vm.startPrank(getNopsAndWeights()[0].nop); + + uint256 totalJuels = s_onRamp.getNopFeesJuels(); + s_onRamp.payNops(); + (EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights, uint256 weightsTotal) = s_onRamp.getNops(); + for (uint256 i = 0; i < nopsAndWeights.length; ++i) { + uint256 expectedPayout = (nopsAndWeights[i].weight * totalJuels) / weightsTotal; + assertEq(IERC20(s_sourceFeeToken).balanceOf(nopsAndWeights[i].nop), expectedPayout); + } + } + + function test_PayNopsSuccessAfterSetNops() public { + vm.startPrank(OWNER); + + // set 2 nops, 1 from previous, 1 new + address prevNop = getNopsAndWeights()[0].nop; + address newNop = STRANGER; + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = new EVM2EVMOnRamp.NopAndWeight[](2); + nopsAndWeights[0] = EVM2EVMOnRamp.NopAndWeight({nop: prevNop, weight: 1}); + nopsAndWeights[1] = EVM2EVMOnRamp.NopAndWeight({nop: newNop, weight: 1}); + s_onRamp.setNops(nopsAndWeights); + + // refill OnRamp nops fees + vm.startPrank(address(s_sourceRouter)); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), feeAmount, OWNER); + + vm.startPrank(newNop); + uint256 prevNopBalance = IERC20(s_sourceFeeToken).balanceOf(prevNop); + uint256 totalJuels = s_onRamp.getNopFeesJuels(); + + s_onRamp.payNops(); + + assertEq(totalJuels / 2 + prevNopBalance, IERC20(s_sourceFeeToken).balanceOf(prevNop)); + assertEq(totalJuels / 2, IERC20(s_sourceFeeToken).balanceOf(newNop)); + } + + // Reverts + + function test_InsufficientBalance_Revert() public { + vm.startPrank(address(s_onRamp)); + IERC20(s_sourceFeeToken).transfer(OWNER, IERC20(s_sourceFeeToken).balanceOf(address(s_onRamp))); + vm.startPrank(OWNER); + vm.expectRevert(EVM2EVMOnRamp.InsufficientBalance.selector); + s_onRamp.payNops(); + } + + function test_WrongPermissions_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert(EVM2EVMOnRamp.OnlyCallableByOwnerOrAdminOrNop.selector); + s_onRamp.payNops(); + } + + function test_NoFeesToPay_Revert() public { + vm.startPrank(OWNER); + s_onRamp.payNops(); + vm.expectRevert(EVM2EVMOnRamp.NoFeesToPay.selector); + s_onRamp.payNops(); + } + + function test_NoNopsToPay_Revert() public { + vm.startPrank(OWNER); + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = new EVM2EVMOnRamp.NopAndWeight[](0); + s_onRamp.setNops(nopsAndWeights); + vm.expectRevert(EVM2EVMOnRamp.NoNopsToPay.selector); + s_onRamp.payNops(); + } +} + +contract EVM2EVMOnRamp_linkAvailableForPayment is EVM2EVMNopsFeeSetup { + function test_LinkAvailableForPayment_Success() public { + uint256 totalJuels = s_onRamp.getNopFeesJuels(); + uint256 linkBalance = IERC20(s_sourceFeeToken).balanceOf(address(s_onRamp)); + + assertEq(int256(linkBalance - totalJuels), s_onRamp.linkAvailableForPayment()); + + vm.startPrank(OWNER); + s_onRamp.payNops(); + + assertEq(int256(linkBalance - totalJuels), s_onRamp.linkAvailableForPayment()); + } + + function test_InsufficientLinkBalance_Success() public { + uint256 totalJuels = s_onRamp.getNopFeesJuels(); + uint256 linkBalance = IERC20(s_sourceFeeToken).balanceOf(address(s_onRamp)); + + vm.startPrank(address(s_onRamp)); + + uint256 linkRemaining = 1; + IERC20(s_sourceFeeToken).transfer(OWNER, linkBalance - linkRemaining); + + vm.startPrank(STRANGER); + assertEq(int256(linkRemaining) - int256(totalJuels), s_onRamp.linkAvailableForPayment()); + } +} + +contract EVM2EVMOnRamp_forwardFromRouter is EVM2EVMOnRampSetup { + struct LegacyExtraArgs { + uint256 gasLimit; + bool strict; + } + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + address[] memory feeTokens = new address[](1); + feeTokens[0] = s_sourceTokens[1]; + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + + // Since we'll mostly be testing for valid calls from the router we'll + // mock all calls to be originating from the router and re-mock in + // tests that require failure. + vm.startPrank(address(s_sourceRouter)); + } + + function test_ForwardFromRouterSuccessCustomExtraArgs() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT * 2})); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouterSuccessLegacyExtraArgs() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = + abi.encodeWithSelector(Client.EVM_EXTRA_ARGS_V1_TAG, LegacyExtraArgs({gasLimit: GAS_LIMIT * 2, strict: true})); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + // We expect the message to be emitted with strict = false. + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouter_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouterExtraArgsV2_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = abi.encodeWithSelector( + Client.EVM_EXTRA_ARGS_V2_TAG, Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT * 2, allowOutOfOrderExecution: true}) + ); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_ForwardFromRouterExtraArgsV2AllowOutOfOrderTrue_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = abi.encodeWithSelector( + Client.EVM_EXTRA_ARGS_V2_TAG, Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT * 2, allowOutOfOrderExecution: true}) + ); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, feeAmount, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + + function test_Fuzz_EnforceOutOfOrder(bool enforce, bool allowOutOfOrderExecution) public { + // Update dynamic config to enforce allowOutOfOrderExecution = defaultVal. + vm.stopPrank(); + vm.startPrank(OWNER); + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = s_onRamp.getDynamicConfig(); + s_onRamp.setDynamicConfig( + EVM2EVMOnRamp.DynamicConfig({ + router: dynamicConfig.router, + maxNumberOfTokensPerMsg: dynamicConfig.maxNumberOfTokensPerMsg, + destGasOverhead: dynamicConfig.destGasOverhead, + destGasPerPayloadByte: dynamicConfig.destGasPerPayloadByte, + destDataAvailabilityOverheadGas: dynamicConfig.destDataAvailabilityOverheadGas, + destGasPerDataAvailabilityByte: dynamicConfig.destGasPerDataAvailabilityByte, + destDataAvailabilityMultiplierBps: dynamicConfig.destDataAvailabilityMultiplierBps, + priceRegistry: dynamicConfig.priceRegistry, + maxDataBytes: dynamicConfig.maxDataBytes, + maxPerMsgGasLimit: dynamicConfig.maxPerMsgGasLimit, + defaultTokenFeeUSDCents: dynamicConfig.defaultTokenFeeUSDCents, + defaultTokenDestGasOverhead: dynamicConfig.defaultTokenDestGasOverhead, + defaultTokenDestBytesOverhead: dynamicConfig.defaultTokenDestBytesOverhead, + enforceOutOfOrder: enforce + }) + ); + vm.stopPrank(); + + vm.startPrank(address(s_sourceRouter)); + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = abi.encodeWithSelector( + Client.EVM_EXTRA_ARGS_V2_TAG, + Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT * 2, allowOutOfOrderExecution: allowOutOfOrderExecution}) + ); + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + if (enforce) { + // If enforcement is on, only true should be allowed. + if (allowOutOfOrderExecution) { + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, feeAmount, OWNER)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } else { + vm.expectRevert(EVM2EVMOnRamp.ExtraArgOutOfOrderExecutionMustBeTrue.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + } else { + // no enforcement should allow any value. + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, feeAmount, OWNER)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } + } + + function test_ShouldIncrementSeqNumAndNonce_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + for (uint64 i = 1; i < 4; ++i) { + uint64 nonceBefore = s_onRamp.getSenderNonce(OWNER); + uint64 sequenceNumberBefore = s_onRamp.getSequenceNumber(); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, i, i, 0, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + uint64 nonceAfter = s_onRamp.getSenderNonce(OWNER); + uint64 sequenceNumberAfter = s_onRamp.getSequenceNumber(); + assertEq(nonceAfter, nonceBefore + 1); + assertEq(sequenceNumberAfter, sequenceNumberBefore + 1); + } + } + + function test_ShouldIncrementNonceOnlyOnOrdered_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = abi.encodeWithSelector( + Client.EVM_EXTRA_ARGS_V2_TAG, Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT * 2, allowOutOfOrderExecution: true}) + ); + + for (uint64 i = 1; i < 4; ++i) { + uint64 nonceBefore = s_onRamp.getSenderNonce(OWNER); + uint64 sequenceNumberBefore = s_onRamp.getSequenceNumber(); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, i, i, 0, OWNER)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + uint64 nonceAfter = s_onRamp.getSenderNonce(OWNER); + uint64 sequenceNumberAfter = s_onRamp.getSequenceNumber(); + assertEq(nonceAfter, nonceBefore); + assertEq(sequenceNumberAfter, sequenceNumberBefore + 1); + } + } + + function test_forwardFromRouter_ShouldStoreLinkFees_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + + assertEq(IERC20(s_sourceFeeToken).balanceOf(address(s_onRamp)), feeAmount); + assertEq(s_onRamp.getNopFeesJuels(), feeAmount); + } + + function test_ShouldStoreNonLinkFees() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = s_sourceTokens[1]; + + uint256 feeAmount = 1234567890; + IERC20(s_sourceTokens[1]).transferFrom(OWNER, address(s_onRamp), feeAmount); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + + assertEq(IERC20(s_sourceTokens[1]).balanceOf(address(s_onRamp)), feeAmount); + + // Calculate conversion done by prices contract + uint256 feeTokenPrice = s_priceRegistry.getTokenPrice(s_sourceTokens[1]).value; + uint256 linkTokenPrice = s_priceRegistry.getTokenPrice(s_sourceFeeToken).value; + uint256 conversionRate = (feeTokenPrice * 1e18) / linkTokenPrice; + uint256 expectedJuels = (feeAmount * conversionRate) / 1e18; + + assertEq(s_onRamp.getNopFeesJuels(), expectedJuels); + } + + // Make sure any valid sender, receiver and feeAmount can be handled. + // @TODO Temporarily setting lower fuzz run as 256 triggers snapshot gas off by 1 error. + // https://github.com/foundry-rs/foundry/issues/5689 + /// forge-config: default.fuzz.runs = 32 + /// forge-config: ccip.fuzz.runs = 32 + function test_Fuzz_ForwardFromRouter_Success(address originalSender, address receiver, uint96 feeTokenAmount) public { + // To avoid RouterMustSetOriginalSender + vm.assume(originalSender != address(0)); + vm.assume(uint160(receiver) >= Internal.PRECOMPILE_SPACE); + vm.assume(feeTokenAmount <= MAX_NOP_FEES_JUELS); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.receiver = abi.encode(receiver); + + // Make sure the tokens are in the contract + deal(s_sourceFeeToken, address(s_onRamp), feeTokenAmount); + + Internal.EVM2EVMMessage memory expectedEvent = _messageToEvent(message, 1, 1, feeTokenAmount, originalSender); + + vm.expectEmit(false, false, false, true); + emit EVM2EVMOnRamp.CCIPSendRequested(expectedEvent); + + // Assert the message Id is correct + assertEq( + expectedEvent.messageId, s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeTokenAmount, originalSender) + ); + // Assert the fee token amount is correctly assigned to the nop fee pool + assertEq(feeTokenAmount, s_onRamp.getNopFeesJuels()); + } + + function test_OverValueWithARLOff_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 10; + message.tokenAmounts[0].token = s_sourceTokens[0]; + + IERC20(s_sourceTokens[0]).approve(address(s_onRamp), 10); + + vm.startPrank(OWNER); + // Set a high price to trip the ARL + uint224 tokenPrice = 3 ** 128; + Internal.PriceUpdates memory priceUpdates = getSingleTokenPriceUpdateStruct(s_sourceTokens[0], tokenPrice); + s_priceRegistry.updatePrices(priceUpdates); + vm.startPrank(address(s_sourceRouter)); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiter.AggregateValueMaxCapacityExceeded.selector, + getOutboundRateLimiterConfig().capacity, + (message.tokenAmounts[0].amount * tokenPrice) / 1e18 + ) + ); + // Expect to fail from ARL + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Configure ARL off for token + EVM2EVMOnRamp.TokenTransferFeeConfig memory tokenTransferFeeConfig = + s_onRamp.getTokenTransferFeeConfig(s_sourceTokens[0]); + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + new EVM2EVMOnRamp.TokenTransferFeeConfigArgs[](1); + tokenTransferFeeConfigArgs[0] = EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: s_sourceTokens[0], + minFeeUSDCents: tokenTransferFeeConfig.minFeeUSDCents, + maxFeeUSDCents: tokenTransferFeeConfig.maxFeeUSDCents, + deciBps: tokenTransferFeeConfig.deciBps, + destGasOverhead: tokenTransferFeeConfig.destGasOverhead, + destBytesOverhead: tokenTransferFeeConfig.destBytesOverhead, + aggregateRateLimitEnabled: false + }); + vm.startPrank(OWNER); + s_onRamp.setTokenTransferFeeConfig(tokenTransferFeeConfigArgs, new address[](0)); + + vm.startPrank(address(s_sourceRouter)); + // Expect the call now succeeds + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + // Reverts + + function test_Paused_Revert() public { + // We pause by disabling the whitelist + vm.stopPrank(); + vm.startPrank(OWNER); + address router = address(0); + s_onRamp.setDynamicConfig(generateDynamicOnRampConfig(router, address(2))); + vm.expectRevert(EVM2EVMOnRamp.MustBeCalledByRouter.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), 0, OWNER); + } + + function test_InvalidExtraArgsTag_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = bytes("bad args"); + + vm.expectRevert(EVM2EVMOnRamp.InvalidExtraArgsTag.selector); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_Unhealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + vm.expectRevert(EVM2EVMOnRamp.CursedByRMN.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), 0, OWNER); + } + + function test_Permissions_Revert() public { + vm.stopPrank(); + vm.startPrank(OWNER); + vm.expectRevert(EVM2EVMOnRamp.MustBeCalledByRouter.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), 0, OWNER); + } + + function test_OriginalSender_Revert() public { + vm.expectRevert(EVM2EVMOnRamp.RouterMustSetOriginalSender.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), 0, address(0)); + } + + function test_MessageTooLarge_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.data = new bytes(MAX_DATA_SIZE + 1); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.MessageTooLarge.selector, MAX_DATA_SIZE, message.data.length)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, STRANGER); + } + + function test_TooManyTokens_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + uint256 tooMany = MAX_TOKENS_LENGTH + 1; + message.tokenAmounts = new Client.EVMTokenAmount[](tooMany); + vm.expectRevert(EVM2EVMOnRamp.UnsupportedNumberOfTokens.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, STRANGER); + } + + function test_CannotSendZeroTokens_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 0; + message.tokenAmounts[0].token = s_sourceTokens[0]; + vm.expectRevert(EVM2EVMOnRamp.CannotSendZeroTokens.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, STRANGER); + } + + function test_UnsupportedToken_Revert() public { + address wrongToken = address(1); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].token = wrongToken; + message.tokenAmounts[0].amount = 1; + + // We need to set the price of this new token to be able to reach + // the proper revert point. This must be called by the owner. + vm.stopPrank(); + vm.startPrank(OWNER); + + Internal.PriceUpdates memory priceUpdates = getSingleTokenPriceUpdateStruct(wrongToken, 1); + s_priceRegistry.updatePrices(priceUpdates); + + // Change back to the router + vm.startPrank(address(s_sourceRouter)); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.UnsupportedToken.selector, wrongToken)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_MaxCapacityExceeded_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 2 ** 128; + message.tokenAmounts[0].token = s_sourceTokens[0]; + + IERC20(s_sourceTokens[0]).approve(address(s_onRamp), 2 ** 128); + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiter.AggregateValueMaxCapacityExceeded.selector, + getOutboundRateLimiterConfig().capacity, + (message.tokenAmounts[0].amount * s_sourceTokenPrices[0]) / 1e18 + ) + ); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_PriceNotFoundForToken_Revert() public { + // Set token price to 0 + vm.stopPrank(); + vm.startPrank(OWNER); + s_priceRegistry.updatePrices(getSingleTokenPriceUpdateStruct(CUSTOM_TOKEN, 0)); + + vm.startPrank(address(s_sourceRouter)); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].token = CUSTOM_TOKEN; + message.tokenAmounts[0].amount = 1; + + vm.expectRevert(abi.encodeWithSelector(AggregateRateLimiter.PriceNotFoundForToken.selector, CUSTOM_TOKEN)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + // Asserts gasLimit must be <=maxGasLimit + function test_MessageGasLimitTooHigh_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: MAX_GAS_LIMIT + 1})); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.MessageGasLimitTooHigh.selector)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_InvalidAddressEncodePacked_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.receiver = abi.encodePacked(address(234)); + + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, message.receiver)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 1, OWNER); + } + + function test_InvalidAddress_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.receiver = abi.encode(type(uint208).max); + + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, message.receiver)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 1, OWNER); + } + + // We disallow sending to addresses 0-9. + function test_ZeroAddressReceiver_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + for (uint160 i = 0; i < 10; ++i) { + message.receiver = abi.encode(address(i)); + + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, message.receiver)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 1, OWNER); + } + } + + function test_MaxFeeBalanceReached_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + vm.expectRevert(EVM2EVMOnRamp.MaxFeeBalanceReached.selector); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, MAX_NOP_FEES_JUELS + 1, OWNER); + } + + function test_InvalidChainSelector_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint64 wrongChainSelector = DEST_CHAIN_SELECTOR + 1; + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.InvalidChainSelector.selector, wrongChainSelector)); + + s_onRamp.forwardFromRouter(wrongChainSelector, message, 1, OWNER); + } + + function test_SourceTokenDataTooLarge_Revert() public { + address sourceETH = s_sourceTokens[1]; + vm.stopPrank(); + vm.startPrank(OWNER); + + MaybeRevertingBurnMintTokenPool newPool = new MaybeRevertingBurnMintTokenPool( + BurnMintERC677(sourceETH), new address[](0), address(s_mockRMN), address(s_sourceRouter) + ); + BurnMintERC677(sourceETH).grantMintAndBurnRoles(address(newPool)); + deal(address(sourceETH), address(newPool), type(uint256).max); + + // Add TokenPool to OnRamp + s_tokenAdminRegistry.setPool(sourceETH, address(newPool)); + + // Allow chain in TokenPool + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_destTokenPool), + remoteTokenAddress: abi.encode(s_destToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + newPool.applyChainUpdates(chainUpdates); + + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(address(sourceETH), 1000); + + // No data set, should succeed + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Set max data length, should succeed + vm.startPrank(OWNER); + newPool.setSourceTokenData(new bytes(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES)); + + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Set data to max length +1, should revert + vm.startPrank(OWNER); + newPool.setSourceTokenData(new bytes(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + 1)); + + vm.startPrank(address(s_sourceRouter)); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.SourceTokenDataTooLarge.selector, sourceETH)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Set token config to allow larger data + vm.startPrank(OWNER); + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + new EVM2EVMOnRamp.TokenTransferFeeConfigArgs[](1); + tokenTransferFeeConfigArgs[0] = EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: sourceETH, + minFeeUSDCents: 1, + maxFeeUSDCents: 0, + deciBps: 0, + destGasOverhead: 0, + destBytesOverhead: uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) + 32, + aggregateRateLimitEnabled: false + }); + s_onRamp.setTokenTransferFeeConfig(tokenTransferFeeConfigArgs, new address[](0)); + + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + // Set the token data larger than the configured token data, should revert + vm.startPrank(OWNER); + newPool.setSourceTokenData(new bytes(uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) + 32 + 1)); + + vm.startPrank(address(s_sourceRouter)); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.SourceTokenDataTooLarge.selector, sourceETH)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_forwardFromRouter_UnsupportedToken_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 1; + message.tokenAmounts[0].token = address(1); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.UnsupportedToken.selector, message.tokenAmounts[0].token)); + + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + } + + function test_EnforceOutOfOrder_Revert() public { + // Update dynamic config to enforce allowOutOfOrderExecution = true. + vm.stopPrank(); + vm.startPrank(OWNER); + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = s_onRamp.getDynamicConfig(); + s_onRamp.setDynamicConfig( + EVM2EVMOnRamp.DynamicConfig({ + router: dynamicConfig.router, + maxNumberOfTokensPerMsg: dynamicConfig.maxNumberOfTokensPerMsg, + destGasOverhead: dynamicConfig.destGasOverhead, + destGasPerPayloadByte: dynamicConfig.destGasPerPayloadByte, + destDataAvailabilityOverheadGas: dynamicConfig.destDataAvailabilityOverheadGas, + destGasPerDataAvailabilityByte: dynamicConfig.destGasPerDataAvailabilityByte, + destDataAvailabilityMultiplierBps: dynamicConfig.destDataAvailabilityMultiplierBps, + priceRegistry: dynamicConfig.priceRegistry, + maxDataBytes: dynamicConfig.maxDataBytes, + maxPerMsgGasLimit: dynamicConfig.maxPerMsgGasLimit, + defaultTokenFeeUSDCents: dynamicConfig.defaultTokenFeeUSDCents, + defaultTokenDestGasOverhead: dynamicConfig.defaultTokenDestGasOverhead, + defaultTokenDestBytesOverhead: dynamicConfig.defaultTokenDestBytesOverhead, + enforceOutOfOrder: true + }) + ); + vm.stopPrank(); + + vm.startPrank(address(s_sourceRouter)); + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + // Empty extraArgs to should revert since it enforceOutOfOrder is true. + message.extraArgs = ""; + uint256 feeAmount = 1234567890; + IERC20(s_sourceFeeToken).transferFrom(OWNER, address(s_onRamp), feeAmount); + + vm.expectRevert(EVM2EVMOnRamp.ExtraArgOutOfOrderExecutionMustBeTrue.selector); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, feeAmount, OWNER); + } +} + +contract EVM2EVMOnRamp_forwardFromRouter_upgrade is EVM2EVMOnRampSetup { + uint256 internal constant FEE_AMOUNT = 1234567890; + EVM2EVMOnRampHelper internal s_prevOnRamp; + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + s_prevOnRamp = s_onRamp; + + s_onRamp = new EVM2EVMOnRampHelper( + EVM2EVMOnRamp.StaticConfig({ + linkToken: s_sourceTokens[0], + chainSelector: SOURCE_CHAIN_SELECTOR, + destChainSelector: DEST_CHAIN_SELECTOR, + defaultTxGasLimit: GAS_LIMIT, + maxNopFeesJuels: MAX_NOP_FEES_JUELS, + prevOnRamp: address(s_prevOnRamp), + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + generateDynamicOnRampConfig(address(s_sourceRouter), address(s_priceRegistry)), + getOutboundRateLimiterConfig(), + s_feeTokenConfigArgs, + s_tokenTransferFeeConfigArgs, + getNopsAndWeights() + ); + s_onRamp.setAdmin(ADMIN); + + s_metadataHash = keccak256( + abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, address(s_onRamp)) + ); + + vm.startPrank(address(s_sourceRouter)); + } + + function test_V2_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, FEE_AMOUNT, OWNER)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + } + + function test_V2SenderNoncesReadsPreviousRamp_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + uint64 startNonce = s_onRamp.getSenderNonce(OWNER); + + for (uint64 i = 1; i < 4; ++i) { + s_prevOnRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, 0, OWNER); + + assertEq(startNonce + i, s_onRamp.getSenderNonce(OWNER)); + } + } + + function test_V2NonceStartsAtV1Nonce_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint64 startNonce = s_onRamp.getSenderNonce(OWNER); + + // send 1 message from previous onramp + s_prevOnRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + + assertEq(startNonce + 1, s_onRamp.getSenderNonce(OWNER)); + + // new onramp nonce should start from 2, while sequence number start from 1 + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, startNonce + 2, FEE_AMOUNT, OWNER)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + + assertEq(startNonce + 2, s_onRamp.getSenderNonce(OWNER)); + + // after another send, nonce should be 3, and sequence number be 2 + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 2, startNonce + 3, FEE_AMOUNT, OWNER)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + + assertEq(startNonce + 3, s_onRamp.getSenderNonce(OWNER)); + } + + function test_V2NonceNewSenderStartsAtZero_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + // send 1 message from previous onramp from OWNER + s_prevOnRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, OWNER); + + address newSender = address(1234567); + // new onramp nonce should start from 1 for new sender + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(_messageToEvent(message, 1, 1, FEE_AMOUNT, newSender)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, message, FEE_AMOUNT, newSender); + } +} + +contract EVM2EVMOnRamp_getFeeSetup is EVM2EVMOnRampSetup { + uint224 internal s_feeTokenPrice; + uint224 internal s_wrappedTokenPrice; + uint224 internal s_customTokenPrice; + + address internal s_selfServeTokenDefaultPricing = makeAddr("self-serve-token-default-pricing"); + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + + // Add additional pool addresses for test tokens to mark them as supported + s_tokenAdminRegistry.proposeAdministrator(s_sourceRouter.getWrappedNative(), OWNER); + s_tokenAdminRegistry.acceptAdminRole(s_sourceRouter.getWrappedNative()); + s_tokenAdminRegistry.proposeAdministrator(CUSTOM_TOKEN, OWNER); + s_tokenAdminRegistry.acceptAdminRole(CUSTOM_TOKEN); + + LockReleaseTokenPool wrappedNativePool = new LockReleaseTokenPool( + IERC20(s_sourceRouter.getWrappedNative()), new address[](0), address(s_mockRMN), true, address(s_sourceRouter) + ); + + TokenPool.ChainUpdate[] memory wrappedNativeChainUpdate = new TokenPool.ChainUpdate[](1); + wrappedNativeChainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(address(111111)), + remoteTokenAddress: abi.encode(s_destToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + wrappedNativePool.applyChainUpdates(wrappedNativeChainUpdate); + s_tokenAdminRegistry.setPool(s_sourceRouter.getWrappedNative(), address(wrappedNativePool)); + + LockReleaseTokenPool customPool = new LockReleaseTokenPool( + IERC20(CUSTOM_TOKEN), new address[](0), address(s_mockRMN), true, address(s_sourceRouter) + ); + TokenPool.ChainUpdate[] memory customChainUpdate = new TokenPool.ChainUpdate[](1); + customChainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(makeAddr("random")), + remoteTokenAddress: abi.encode(s_destToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + customPool.applyChainUpdates(customChainUpdate); + s_tokenAdminRegistry.setPool(CUSTOM_TOKEN, address(customPool)); + + s_feeTokenPrice = s_sourceTokenPrices[0]; + s_wrappedTokenPrice = s_sourceTokenPrices[2]; + s_customTokenPrice = CUSTOM_TOKEN_PRICE; + + // Ensure the self-serve token is set up on the admin registry + vm.mockCall( + address(s_tokenAdminRegistry), + abi.encodeWithSelector(ITokenAdminRegistry.getPool.selector, s_selfServeTokenDefaultPricing), + abi.encode(makeAddr("self-serve-pool")) + ); + } + + function calcUSDValueFromTokenAmount(uint224 tokenPrice, uint256 tokenAmount) internal pure returns (uint256) { + return (tokenPrice * tokenAmount) / 1e18; + } + + function applyBpsRatio(uint256 tokenAmount, uint16 ratio) internal pure returns (uint256) { + return (tokenAmount * ratio) / 1e5; + } + + function configUSDCentToWei(uint256 usdCent) internal pure returns (uint256) { + return usdCent * 1e16; + } +} + +contract EVM2EVMOnRamp_getDataAvailabilityCost is EVM2EVMOnRamp_getFeeSetup { + function test_EmptyMessageCalculatesDataAvailabilityCost_Success() public view { + uint256 dataAvailabilityCostUSD = s_onRamp.getDataAvailabilityCost(USD_PER_DATA_AVAILABILITY_GAS, 0, 0, 0); + + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = s_onRamp.getDynamicConfig(); + + uint256 dataAvailabilityGas = dynamicConfig.destDataAvailabilityOverheadGas + + dynamicConfig.destGasPerDataAvailabilityByte * Internal.MESSAGE_FIXED_BYTES; + uint256 expectedDataAvailabilityCostUSD = + USD_PER_DATA_AVAILABILITY_GAS * dataAvailabilityGas * dynamicConfig.destDataAvailabilityMultiplierBps * 1e14; + + assertEq(expectedDataAvailabilityCostUSD, dataAvailabilityCostUSD); + } + + function test_SimpleMessageCalculatesDataAvailabilityCost_Success() public view { + uint256 dataAvailabilityCostUSD = s_onRamp.getDataAvailabilityCost(USD_PER_DATA_AVAILABILITY_GAS, 100, 5, 50); + + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = s_onRamp.getDynamicConfig(); + + uint256 dataAvailabilityLengthBytes = + Internal.MESSAGE_FIXED_BYTES + 100 + (5 * Internal.MESSAGE_FIXED_BYTES_PER_TOKEN) + 50; + uint256 dataAvailabilityGas = dynamicConfig.destDataAvailabilityOverheadGas + + dynamicConfig.destGasPerDataAvailabilityByte * dataAvailabilityLengthBytes; + uint256 expectedDataAvailabilityCostUSD = + USD_PER_DATA_AVAILABILITY_GAS * dataAvailabilityGas * dynamicConfig.destDataAvailabilityMultiplierBps * 1e14; + + assertEq(expectedDataAvailabilityCostUSD, dataAvailabilityCostUSD); + } + + function test_Fuzz_ZeroDataAvailabilityGasPriceAlwaysCalculatesZeroDataAvailabilityCost_Success( + uint64 messageDataLength, + uint32 numberOfTokens, + uint32 tokenTransferBytesOverhead + ) public view { + uint256 dataAvailabilityCostUSD = + s_onRamp.getDataAvailabilityCost(0, messageDataLength, numberOfTokens, tokenTransferBytesOverhead); + + assertEq(0, dataAvailabilityCostUSD); + } + + function test_Fuzz_CalculateDataAvailabilityCost_Success( + uint32 destDataAvailabilityOverheadGas, + uint16 destGasPerDataAvailabilityByte, + uint16 destDataAvailabilityMultiplierBps, + uint112 dataAvailabilityGasPrice, + uint64 messageDataLength, + uint32 numberOfTokens, + uint32 tokenTransferBytesOverhead + ) public { + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = s_onRamp.getDynamicConfig(); + dynamicConfig.destDataAvailabilityOverheadGas = destDataAvailabilityOverheadGas; + dynamicConfig.destGasPerDataAvailabilityByte = destGasPerDataAvailabilityByte; + dynamicConfig.destDataAvailabilityMultiplierBps = destDataAvailabilityMultiplierBps; + s_onRamp.setDynamicConfig(dynamicConfig); + + uint256 dataAvailabilityCostUSD = s_onRamp.getDataAvailabilityCost( + dataAvailabilityGasPrice, messageDataLength, numberOfTokens, tokenTransferBytesOverhead + ); + + uint256 dataAvailabilityLengthBytes = Internal.MESSAGE_FIXED_BYTES + messageDataLength + + (numberOfTokens * Internal.MESSAGE_FIXED_BYTES_PER_TOKEN) + tokenTransferBytesOverhead; + + uint256 dataAvailabilityGas = + destDataAvailabilityOverheadGas + destGasPerDataAvailabilityByte * dataAvailabilityLengthBytes; + uint256 expectedDataAvailabilityCostUSD = + dataAvailabilityGasPrice * dataAvailabilityGas * destDataAvailabilityMultiplierBps * 1e14; + + assertEq(expectedDataAvailabilityCostUSD, dataAvailabilityCostUSD); + } +} + +contract EVM2EVMOnRamp_getSupportedTokens is EVM2EVMOnRampSetup { + function test_GetSupportedTokens_Revert() public { + vm.expectRevert(EVM2EVMOnRamp.GetSupportedTokensFunctionalityRemovedCheckAdminRegistry.selector); + s_onRamp.getSupportedTokens(DEST_CHAIN_SELECTOR); + } +} + +contract EVM2EVMOnRamp_getTokenTransferCost is EVM2EVMOnRamp_getFeeSetup { + using USDPriceWith18Decimals for uint224; + + function test_NoTokenTransferChargesZeroFee_Success() public view { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + assertEq(0, feeUSDWei); + assertEq(0, destGasOverhead); + assertEq(0, destBytesOverhead); + } + + function test__getTokenTransferCost_selfServeUsesDefaults_Success() public view { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_selfServeTokenDefaultPricing, 1000); + + // Get config to assert it isn't set + EVM2EVMOnRamp.TokenTransferFeeConfig memory transferFeeConfig = + s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token); + + assertFalse(transferFeeConfig.isEnabled); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + // Assert that the default values are used + assertEq(uint256(DEFAULT_TOKEN_FEE_USD_CENTS) * 1e16, feeUSDWei); + assertEq(DEFAULT_TOKEN_DEST_GAS_OVERHEAD, destGasOverhead); + assertEq(DEFAULT_TOKEN_BYTES_OVERHEAD, destBytesOverhead); + } + + function test_SmallTokenTransferChargesMinFeeAndGas_Success() public view { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, 1000); + EVM2EVMOnRamp.TokenTransferFeeConfig memory transferFeeConfig = + s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + assertEq(configUSDCentToWei(transferFeeConfig.minFeeUSDCents), feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, destBytesOverhead); + } + + function test_ZeroAmountTokenTransferChargesMinFeeAndGas_Success() public view { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, 0); + EVM2EVMOnRamp.TokenTransferFeeConfig memory transferFeeConfig = + s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + assertEq(configUSDCentToWei(transferFeeConfig.minFeeUSDCents), feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, destBytesOverhead); + } + + function test_LargeTokenTransferChargesMaxFeeAndGas_Success() public view { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, 1e36); + EVM2EVMOnRamp.TokenTransferFeeConfig memory transferFeeConfig = + s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + assertEq(configUSDCentToWei(transferFeeConfig.maxFeeUSDCents), feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, destBytesOverhead); + } + + function test_FeeTokenBpsFee_Success() public view { + uint256 tokenAmount = 10000e18; + + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, tokenAmount); + EVM2EVMOnRamp.TokenTransferFeeConfig memory transferFeeConfig = + s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + uint256 usdWei = calcUSDValueFromTokenAmount(s_feeTokenPrice, tokenAmount); + uint256 bpsUSDWei = applyBpsRatio(usdWei, s_tokenTransferFeeConfigArgs[0].deciBps); + + assertEq(bpsUSDWei, feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, destBytesOverhead); + } + + function test_WETHTokenBpsFee_Success() public view { + uint256 tokenAmount = 100e18; + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](1), + feeToken: s_sourceRouter.getWrappedNative(), + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceRouter.getWrappedNative(), amount: tokenAmount}); + + EVM2EVMOnRamp.TokenTransferFeeConfig memory transferFeeConfig = + s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_wrappedTokenPrice, message.tokenAmounts); + + uint256 usdWei = calcUSDValueFromTokenAmount(s_wrappedTokenPrice, tokenAmount); + uint256 bpsUSDWei = applyBpsRatio(usdWei, s_tokenTransferFeeConfigArgs[1].deciBps); + + assertEq(bpsUSDWei, feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(transferFeeConfig.destBytesOverhead, destBytesOverhead); + } + + function test_CustomTokenBpsFee_Success() public view { + uint256 tokenAmount = 200000e18; + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](1), + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: CUSTOM_TOKEN, amount: tokenAmount}); + + EVM2EVMOnRamp.TokenTransferFeeConfig memory transferFeeConfig = + s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + uint256 usdWei = calcUSDValueFromTokenAmount(s_customTokenPrice, tokenAmount); + uint256 bpsUSDWei = applyBpsRatio(usdWei, s_tokenTransferFeeConfigArgs[2].deciBps); + + assertEq(bpsUSDWei, feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(transferFeeConfig.destBytesOverhead, destBytesOverhead); + } + + function test_ZeroFeeConfigChargesMinFee_Success() public { + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + new EVM2EVMOnRamp.TokenTransferFeeConfigArgs[](1); + tokenTransferFeeConfigArgs[0] = EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: s_sourceFeeToken, + minFeeUSDCents: 1, + maxFeeUSDCents: 0, + deciBps: 0, + destGasOverhead: 0, + destBytesOverhead: uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES), + aggregateRateLimitEnabled: true + }); + s_onRamp.setTokenTransferFeeConfig(tokenTransferFeeConfigArgs, new address[](0)); + + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, 1e36); + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + // if token charges 0 bps, it should cost minFee to transfer + assertEq(configUSDCentToWei(tokenTransferFeeConfigArgs[0].minFeeUSDCents), feeUSDWei); + assertEq(0, destGasOverhead); + assertEq(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, destBytesOverhead); + } + + function test_Fuzz_TokenTransferFeeDuplicateTokens_Success(uint256 transfers, uint256 amount) public view { + // It shouldn't be possible to pay materially lower fees by splitting up the transfers. + // Note it is possible to pay higher fees since the minimum fees are added. + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = s_onRamp.getDynamicConfig(); + transfers = bound(transfers, 1, dynamicConfig.maxNumberOfTokensPerMsg); + // Cap amount to avoid overflow + amount = bound(amount, 0, 1e36); + Client.EVMTokenAmount[] memory multiple = new Client.EVMTokenAmount[](transfers); + for (uint256 i = 0; i < transfers; ++i) { + multiple[i] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: amount}); + } + Client.EVMTokenAmount[] memory single = new Client.EVMTokenAmount[](1); + single[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: amount * transfers}); + + address feeToken = s_sourceRouter.getWrappedNative(); + + (uint256 feeSingleUSDWei, uint32 gasOverheadSingle, uint32 bytesOverheadSingle) = + s_onRamp.getTokenTransferCost(feeToken, s_wrappedTokenPrice, single); + (uint256 feeMultipleUSDWei, uint32 gasOverheadMultiple, uint32 bytesOverheadMultiple) = + s_onRamp.getTokenTransferCost(feeToken, s_wrappedTokenPrice, multiple); + + // Note that there can be a rounding error once per split. + assertTrue(feeMultipleUSDWei >= (feeSingleUSDWei - dynamicConfig.maxNumberOfTokensPerMsg)); + assertEq(gasOverheadMultiple, gasOverheadSingle * transfers); + assertEq(bytesOverheadMultiple, bytesOverheadSingle * transfers); + } + + function test_MixedTokenTransferFee_Success() public view { + address[3] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative(), CUSTOM_TOKEN]; + uint224[3] memory tokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice, s_customTokenPrice]; + EVM2EVMOnRamp.TokenTransferFeeConfig[3] memory tokenTransferFeeConfigs = [ + s_onRamp.getTokenTransferFeeConfig(testTokens[0]), + s_onRamp.getTokenTransferFeeConfig(testTokens[1]), + s_onRamp.getTokenTransferFeeConfig(testTokens[2]) + ]; + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](3), + feeToken: s_sourceRouter.getWrappedNative(), + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + uint256 expectedTotalGas = 0; + uint256 expectedTotalBytes = 0; + + // Start with small token transfers, total bps fee is lower than min token transfer fee + for (uint256 i = 0; i < testTokens.length; ++i) { + message.tokenAmounts[i] = Client.EVMTokenAmount({token: testTokens[i], amount: 1e14}); + expectedTotalGas += s_onRamp.getTokenTransferFeeConfig(testTokens[i]).destGasOverhead; + uint32 dstBytesOverhead = s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[i].token).destBytesOverhead; + expectedTotalBytes += dstBytesOverhead == 0 ? uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) : dstBytesOverhead; + } + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_wrappedTokenPrice, message.tokenAmounts); + + uint256 expectedFeeUSDWei = 0; + for (uint256 i = 0; i < testTokens.length; ++i) { + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[i].minFeeUSDCents); + } + + assertEq(expectedFeeUSDWei, feeUSDWei); + assertEq(expectedTotalGas, destGasOverhead); + assertEq(expectedTotalBytes, destBytesOverhead); + + // Set 1st token transfer to a meaningful amount so its bps fee is now between min and max fee + message.tokenAmounts[0] = Client.EVMTokenAmount({token: testTokens[0], amount: 10000e18}); + + (feeUSDWei, destGasOverhead, destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_wrappedTokenPrice, message.tokenAmounts); + expectedFeeUSDWei = applyBpsRatio( + calcUSDValueFromTokenAmount(tokenPrices[0], message.tokenAmounts[0].amount), tokenTransferFeeConfigs[0].deciBps + ); + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[1].minFeeUSDCents); + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[2].minFeeUSDCents); + + assertEq(expectedFeeUSDWei, feeUSDWei); + assertEq(expectedTotalGas, destGasOverhead); + assertEq(expectedTotalBytes, destBytesOverhead); + + // Set 2nd token transfer to a large amount that is higher than maxFeeUSD + message.tokenAmounts[1] = Client.EVMTokenAmount({token: testTokens[1], amount: 1e36}); + + (feeUSDWei, destGasOverhead, destBytesOverhead) = + s_onRamp.getTokenTransferCost(message.feeToken, s_wrappedTokenPrice, message.tokenAmounts); + expectedFeeUSDWei = applyBpsRatio( + calcUSDValueFromTokenAmount(tokenPrices[0], message.tokenAmounts[0].amount), tokenTransferFeeConfigs[0].deciBps + ); + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[1].maxFeeUSDCents); + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[2].minFeeUSDCents); + + assertEq(expectedFeeUSDWei, feeUSDWei); + assertEq(expectedTotalGas, destGasOverhead); + assertEq(expectedTotalBytes, destBytesOverhead); + } + + // reverts + + function test_UnsupportedToken_Revert() public { + address NOT_SUPPORTED_TOKEN = address(123); + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(NOT_SUPPORTED_TOKEN, 200); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.UnsupportedToken.selector, NOT_SUPPORTED_TOKEN)); + + s_onRamp.getTokenTransferCost(message.feeToken, s_feeTokenPrice, message.tokenAmounts); + } +} + +contract EVM2EVMOnRamp_getFee is EVM2EVMOnRamp_getFeeSetup { + using USDPriceWith18Decimals for uint224; + + function test_EmptyMessage_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = testTokens[i]; + EVM2EVMOnRamp.FeeTokenConfig memory feeTokenConfig = s_onRamp.getFeeTokenConfig(message.feeToken); + + uint256 feeAmount = s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + + uint256 gasUsed = GAS_LIMIT + DEST_GAS_OVERHEAD; + uint256 gasFeeUSD = (gasUsed * feeTokenConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + uint256 messageFeeUSD = + (configUSDCentToWei(feeTokenConfig.networkFeeUSDCents) * feeTokenConfig.premiumMultiplierWeiPerEth); + uint256 dataAvailabilityFeeUSD = s_onRamp.getDataAvailabilityCost( + USD_PER_DATA_AVAILABILITY_GAS, message.data.length, message.tokenAmounts.length, 0 + ); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD + dataAvailabilityFeeUSD) / feeTokenPrices[i]; + assertEq(totalPriceInFeeToken, feeAmount); + } + } + + function test_ZeroDataAvailabilityMultiplier_Success() public { + EVM2EVMOnRamp.DynamicConfig memory dynamicConfig = s_onRamp.getDynamicConfig(); + dynamicConfig.destDataAvailabilityMultiplierBps = 0; + s_onRamp.setDynamicConfig(dynamicConfig); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + EVM2EVMOnRamp.FeeTokenConfig memory feeTokenConfig = s_onRamp.getFeeTokenConfig(message.feeToken); + + uint256 feeAmount = s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + + uint256 gasUsed = GAS_LIMIT + DEST_GAS_OVERHEAD; + uint256 gasFeeUSD = (gasUsed * feeTokenConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + uint256 messageFeeUSD = + (configUSDCentToWei(feeTokenConfig.networkFeeUSDCents) * feeTokenConfig.premiumMultiplierWeiPerEth); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD) / s_feeTokenPrice; + assertEq(totalPriceInFeeToken, feeAmount); + } + + function test_HighGasMessage_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + uint256 customGasLimit = MAX_GAS_LIMIT; + uint256 customDataSize = MAX_DATA_SIZE; + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: new bytes(customDataSize), + tokenAmounts: new Client.EVMTokenAmount[](0), + feeToken: testTokens[i], + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: customGasLimit})) + }); + + EVM2EVMOnRamp.FeeTokenConfig memory feeTokenConfig = s_onRamp.getFeeTokenConfig(message.feeToken); + uint256 feeAmount = s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + + uint256 gasUsed = customGasLimit + DEST_GAS_OVERHEAD + customDataSize * DEST_GAS_PER_PAYLOAD_BYTE; + uint256 gasFeeUSD = (gasUsed * feeTokenConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + uint256 messageFeeUSD = + (configUSDCentToWei(feeTokenConfig.networkFeeUSDCents) * feeTokenConfig.premiumMultiplierWeiPerEth); + uint256 dataAvailabilityFeeUSD = s_onRamp.getDataAvailabilityCost( + USD_PER_DATA_AVAILABILITY_GAS, message.data.length, message.tokenAmounts.length, 0 + ); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD + dataAvailabilityFeeUSD) / feeTokenPrices[i]; + assertEq(totalPriceInFeeToken, feeAmount); + } + } + + function test_SingleTokenMessage_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + uint256 tokenAmount = 10000e18; + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, tokenAmount); + message.feeToken = testTokens[i]; + EVM2EVMOnRamp.FeeTokenConfig memory feeTokenConfig = s_onRamp.getFeeTokenConfig(message.feeToken); + uint32 tokenGasOverhead = s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token).destGasOverhead; + uint32 destBytesOverhead = s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[0].token).destBytesOverhead; + uint32 tokenBytesOverhead = + destBytesOverhead == 0 ? uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) : destBytesOverhead; + + uint256 feeAmount = s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + + uint256 gasUsed = GAS_LIMIT + DEST_GAS_OVERHEAD + tokenGasOverhead; + uint256 gasFeeUSD = (gasUsed * feeTokenConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + (uint256 transferFeeUSD,,) = + s_onRamp.getTokenTransferCost(message.feeToken, feeTokenPrices[i], message.tokenAmounts); + uint256 messageFeeUSD = (transferFeeUSD * feeTokenConfig.premiumMultiplierWeiPerEth); + uint256 dataAvailabilityFeeUSD = s_onRamp.getDataAvailabilityCost( + USD_PER_DATA_AVAILABILITY_GAS, message.data.length, message.tokenAmounts.length, tokenBytesOverhead + ); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD + dataAvailabilityFeeUSD) / feeTokenPrices[i]; + assertEq(totalPriceInFeeToken, feeAmount); + } + } + + function test_MessageWithDataAndTokenTransfer_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + uint256 customGasLimit = 1_000_000; + uint256 feeTokenAmount = 10000e18; + uint256 customTokenAmount = 200000e18; + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](2), + feeToken: testTokens[i], + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: customGasLimit})) + }); + EVM2EVMOnRamp.FeeTokenConfig memory feeTokenConfig = s_onRamp.getFeeTokenConfig(message.feeToken); + + message.tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceFeeToken, amount: feeTokenAmount}); + message.tokenAmounts[1] = Client.EVMTokenAmount({token: CUSTOM_TOKEN, amount: customTokenAmount}); + message.data = "random bits and bytes that should be factored into the cost of the message"; + + uint32 tokenGasOverhead = 0; + uint32 tokenBytesOverhead = 0; + for (uint256 j = 0; j < message.tokenAmounts.length; ++j) { + tokenGasOverhead += s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[j].token).destGasOverhead; + uint32 destBytesOverhead = s_onRamp.getTokenTransferFeeConfig(message.tokenAmounts[j].token).destBytesOverhead; + tokenBytesOverhead += destBytesOverhead == 0 ? uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) : destBytesOverhead; + } + + uint256 feeAmount = s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + + uint256 gasUsed = + customGasLimit + DEST_GAS_OVERHEAD + message.data.length * DEST_GAS_PER_PAYLOAD_BYTE + tokenGasOverhead; + uint256 gasFeeUSD = (gasUsed * feeTokenConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + (uint256 transferFeeUSD,,) = + s_onRamp.getTokenTransferCost(message.feeToken, feeTokenPrices[i], message.tokenAmounts); + uint256 messageFeeUSD = (transferFeeUSD * feeTokenConfig.premiumMultiplierWeiPerEth); + uint256 dataAvailabilityFeeUSD = s_onRamp.getDataAvailabilityCost( + USD_PER_DATA_AVAILABILITY_GAS, message.data.length, message.tokenAmounts.length, tokenBytesOverhead + ); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD + dataAvailabilityFeeUSD) / feeTokenPrices[i]; + assertEq(totalPriceInFeeToken, feeAmount); + } + } + + // Reverts + + function test_NotAFeeToken_Revert() public { + address notAFeeToken = address(0x111111); + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(notAFeeToken, 1); + message.feeToken = notAFeeToken; + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.NotAFeeToken.selector, notAFeeToken)); + + s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + } + + function test_MessageTooLarge_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.data = new bytes(MAX_DATA_SIZE + 1); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.MessageTooLarge.selector, MAX_DATA_SIZE, message.data.length)); + + s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + } + + function test_TooManyTokens_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + uint256 tooMany = MAX_TOKENS_LENGTH + 1; + message.tokenAmounts = new Client.EVMTokenAmount[](tooMany); + vm.expectRevert(EVM2EVMOnRamp.UnsupportedNumberOfTokens.selector); + s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + } + + // Asserts gasLimit must be <=maxGasLimit + function test_MessageGasLimitTooHigh_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: MAX_GAS_LIMIT + 1})); + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.MessageGasLimitTooHigh.selector)); + s_onRamp.getFee(DEST_CHAIN_SELECTOR, message); + } +} + +contract EVM2EVMOnRamp_setNops is EVM2EVMOnRampSetup { + // Used because EnumerableMap doesn't guarantee order + mapping(address nop => uint256 weight) internal s_nopsToWeights; + + function test_SetNops_Success() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = getNopsAndWeights(); + nopsAndWeights[1].nop = USER_4; + nopsAndWeights[1].weight = 20; + for (uint256 i = 0; i < nopsAndWeights.length; ++i) { + s_nopsToWeights[nopsAndWeights[i].nop] = nopsAndWeights[i].weight; + } + + s_onRamp.setNops(nopsAndWeights); + + (EVM2EVMOnRamp.NopAndWeight[] memory actual,) = s_onRamp.getNops(); + for (uint256 i = 0; i < actual.length; ++i) { + assertEq(actual[i].weight, s_nopsToWeights[actual[i].nop]); + } + } + + function test_AdminCanSetNops_Success() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = getNopsAndWeights(); + // Should not revert + vm.startPrank(ADMIN); + s_onRamp.setNops(nopsAndWeights); + } + + function test_IncludesPayment_Success() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = getNopsAndWeights(); + nopsAndWeights[1].nop = USER_4; + nopsAndWeights[1].weight = 20; + uint32 totalWeight; + for (uint256 i = 0; i < nopsAndWeights.length; ++i) { + totalWeight += nopsAndWeights[i].weight; + s_nopsToWeights[nopsAndWeights[i].nop] = nopsAndWeights[i].weight; + } + + // Make sure a payout happens regardless of what the weights are set to + uint96 nopFeesJuels = totalWeight * 5; + // Set Nop fee juels + deal(s_sourceFeeToken, address(s_onRamp), nopFeesJuels); + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), nopFeesJuels, OWNER); + vm.startPrank(OWNER); + + // We don't care about the fee calculation logic in this test + // so we don't verify the amounts. We do verify the addresses to + // make sure the existing nops get paid and not the new ones. + EVM2EVMOnRamp.NopAndWeight[] memory existingNopsAndWeights = getNopsAndWeights(); + for (uint256 i = 0; i < existingNopsAndWeights.length; ++i) { + vm.expectEmit(true, false, false, false); + emit EVM2EVMOnRamp.NopPaid(existingNopsAndWeights[i].nop, 0); + } + + s_onRamp.setNops(nopsAndWeights); + + (EVM2EVMOnRamp.NopAndWeight[] memory actual,) = s_onRamp.getNops(); + for (uint256 i = 0; i < actual.length; ++i) { + assertEq(actual[i].weight, s_nopsToWeights[actual[i].nop]); + } + } + + function test_SetNopsRemovesOldNopsCompletely_Success() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = new EVM2EVMOnRamp.NopAndWeight[](0); + s_onRamp.setNops(nopsAndWeights); + (EVM2EVMOnRamp.NopAndWeight[] memory actual, uint256 totalWeight) = s_onRamp.getNops(); + assertEq(actual.length, 0); + assertEq(totalWeight, 0); + + address prevNop = getNopsAndWeights()[0].nop; + vm.startPrank(prevNop); + + // prev nop should not have permission to call payNops + vm.expectRevert(EVM2EVMOnRamp.OnlyCallableByOwnerOrAdminOrNop.selector); + s_onRamp.payNops(); + } + + // Reverts + + function test_NotEnoughFundsForPayout_Revert() public { + uint96 nopFeesJuels = MAX_NOP_FEES_JUELS; + // Set Nop fee juels but don't transfer LINK. This can happen when users + // pay in non-link tokens. + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), nopFeesJuels, OWNER); + vm.startPrank(OWNER); + + vm.expectRevert(EVM2EVMOnRamp.InsufficientBalance.selector); + + s_onRamp.setNops(getNopsAndWeights()); + } + + function test_NonOwnerOrAdmin_Revert() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = getNopsAndWeights(); + vm.startPrank(STRANGER); + vm.expectRevert(EVM2EVMOnRamp.OnlyCallableByOwnerOrAdmin.selector); + s_onRamp.setNops(nopsAndWeights); + } + + function test_LinkTokenCannotBeNop_Revert() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = getNopsAndWeights(); + nopsAndWeights[0].nop = address(s_sourceTokens[0]); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.InvalidNopAddress.selector, address(s_sourceTokens[0]))); + + s_onRamp.setNops(nopsAndWeights); + } + + function test_ZeroAddressCannotBeNop_Revert() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = getNopsAndWeights(); + nopsAndWeights[0].nop = address(0); + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.InvalidNopAddress.selector, address(0))); + + s_onRamp.setNops(nopsAndWeights); + } + + function test_TooManyNops_Revert() public { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = new EVM2EVMOnRamp.NopAndWeight[](257); + + vm.expectRevert(EVM2EVMOnRamp.TooManyNops.selector); + + s_onRamp.setNops(nopsAndWeights); + } +} + +contract EVM2EVMOnRamp_withdrawNonLinkFees is EVM2EVMOnRampSetup { + IERC20 internal s_token; + + function setUp() public virtual override { + EVM2EVMOnRampSetup.setUp(); + // Send some non-link tokens to the onRamp + s_token = IERC20(s_sourceTokens[1]); + deal(s_sourceTokens[1], address(s_onRamp), 100); + } + + function test_WithdrawNonLinkFees_Success() public { + s_onRamp.withdrawNonLinkFees(address(s_token), address(this)); + + assertEq(0, s_token.balanceOf(address(s_onRamp))); + assertEq(100, s_token.balanceOf(address(this))); + } + + function test_SettlingBalance_Success() public { + // Set Nop fee juels + uint96 nopFeesJuels = 10000000; + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), nopFeesJuels, OWNER); + vm.startPrank(OWNER); + + vm.expectRevert(EVM2EVMOnRamp.LinkBalanceNotSettled.selector); + s_onRamp.withdrawNonLinkFees(address(s_token), address(this)); + + // It doesnt matter how the link tokens get to the onRamp + // In this case we simply deal them to the ramp to show + // anyone can settle the balance + deal(s_sourceTokens[0], address(s_onRamp), nopFeesJuels); + + s_onRamp.withdrawNonLinkFees(address(s_token), address(this)); + } + + function test_Fuzz_FuzzWithdrawalOnlyLeftoverLink_Success(uint96 nopFeeJuels, uint64 extraJuels) public { + nopFeeJuels = uint96(bound(nopFeeJuels, 1, MAX_NOP_FEES_JUELS)); + + // Set Nop fee juels + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), nopFeeJuels, OWNER); + vm.startPrank(OWNER); + + vm.expectRevert(EVM2EVMOnRamp.LinkBalanceNotSettled.selector); + s_onRamp.withdrawNonLinkFees(address(s_token), address(this)); + + address linkToken = s_sourceTokens[0]; + // It doesnt matter how the link tokens get to the onRamp + // In this case we simply deal them to the ramp to show + // anyone can settle the balance + deal(linkToken, address(s_onRamp), nopFeeJuels + uint96(extraJuels)); + + // Now that we've sent nopFeesJuels + extraJuels, we should be able to withdraw extraJuels + address linkRecipient = address(0x123456789); + assertEq(0, IERC20(linkToken).balanceOf(linkRecipient)); + + s_onRamp.withdrawNonLinkFees(linkToken, linkRecipient); + + assertEq(extraJuels, IERC20(linkToken).balanceOf(linkRecipient)); + } + + // Reverts + + function test_LinkBalanceNotSettled_Revert() public { + // Set Nop fee juels + uint96 nopFeesJuels = 10000000; + vm.startPrank(address(s_sourceRouter)); + s_onRamp.forwardFromRouter(DEST_CHAIN_SELECTOR, _generateEmptyMessage(), nopFeesJuels, OWNER); + vm.startPrank(OWNER); + + vm.expectRevert(EVM2EVMOnRamp.LinkBalanceNotSettled.selector); + + s_onRamp.withdrawNonLinkFees(address(s_token), address(this)); + } + + function test_NonOwnerOrAdmin_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert(EVM2EVMOnRamp.OnlyCallableByOwnerOrAdmin.selector); + s_onRamp.withdrawNonLinkFees(address(s_token), address(this)); + } + + function test_WithdrawToZeroAddress_Revert() public { + vm.expectRevert(EVM2EVMOnRamp.InvalidWithdrawParams.selector); + s_onRamp.withdrawNonLinkFees(address(s_token), address(0)); + } +} + +contract EVM2EVMOnRamp_setFeeTokenConfig is EVM2EVMOnRampSetup { + function test_SetFeeTokenConfig_Success() public { + EVM2EVMOnRamp.FeeTokenConfigArgs[] memory feeConfig; + + vm.expectEmit(); + emit EVM2EVMOnRamp.FeeConfigSet(feeConfig); + + s_onRamp.setFeeTokenConfig(feeConfig); + } + + function test_SetFeeTokenConfigByAdmin_Success() public { + EVM2EVMOnRamp.FeeTokenConfigArgs[] memory feeConfig; + + vm.startPrank(ADMIN); + + vm.expectEmit(); + emit EVM2EVMOnRamp.FeeConfigSet(feeConfig); + + s_onRamp.setFeeTokenConfig(feeConfig); + } + + // Reverts + + function test_OnlyCallableByOwnerOrAdmin_Revert() public { + EVM2EVMOnRamp.FeeTokenConfigArgs[] memory feeConfig; + vm.startPrank(STRANGER); + + vm.expectRevert(EVM2EVMOnRamp.OnlyCallableByOwnerOrAdmin.selector); + + s_onRamp.setFeeTokenConfig(feeConfig); + } +} + +contract EVM2EVMOnRamp_setTokenTransferFeeConfig is EVM2EVMOnRampSetup { + function test__setTokenTransferFeeConfig_Success() public { + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] memory tokenTransferFeeArgs = + new EVM2EVMOnRamp.TokenTransferFeeConfigArgs[](2); + tokenTransferFeeArgs[0] = EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: address(5), + minFeeUSDCents: 6, + maxFeeUSDCents: 7, + deciBps: 8, + destGasOverhead: 9, + destBytesOverhead: 312, + aggregateRateLimitEnabled: true + }); + tokenTransferFeeArgs[1] = EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: address(11), + minFeeUSDCents: 12, + maxFeeUSDCents: 13, + deciBps: 14, + destGasOverhead: 15, + destBytesOverhead: 394, + aggregateRateLimitEnabled: false + }); + + vm.expectEmit(); + emit EVM2EVMOnRamp.TokenTransferFeeConfigSet(tokenTransferFeeArgs); + + s_onRamp.setTokenTransferFeeConfig(tokenTransferFeeArgs, new address[](0)); + + EVM2EVMOnRamp.TokenTransferFeeConfig memory config0 = + s_onRamp.getTokenTransferFeeConfig(tokenTransferFeeArgs[0].token); + + assertEq(tokenTransferFeeArgs[0].minFeeUSDCents, config0.minFeeUSDCents); + assertEq(tokenTransferFeeArgs[0].maxFeeUSDCents, config0.maxFeeUSDCents); + assertEq(tokenTransferFeeArgs[0].deciBps, config0.deciBps); + assertEq(tokenTransferFeeArgs[0].destGasOverhead, config0.destGasOverhead); + assertEq(tokenTransferFeeArgs[0].destBytesOverhead, config0.destBytesOverhead); + assertEq(tokenTransferFeeArgs[0].aggregateRateLimitEnabled, config0.aggregateRateLimitEnabled); + assertTrue(config0.isEnabled); + + EVM2EVMOnRamp.TokenTransferFeeConfig memory config1 = + s_onRamp.getTokenTransferFeeConfig(tokenTransferFeeArgs[1].token); + + assertEq(tokenTransferFeeArgs[1].minFeeUSDCents, config1.minFeeUSDCents); + assertEq(tokenTransferFeeArgs[1].maxFeeUSDCents, config1.maxFeeUSDCents); + assertEq(tokenTransferFeeArgs[1].deciBps, config1.deciBps); + assertEq(tokenTransferFeeArgs[1].destGasOverhead, config1.destGasOverhead); + assertEq(tokenTransferFeeArgs[1].destBytesOverhead, config1.destBytesOverhead); + assertEq(tokenTransferFeeArgs[1].aggregateRateLimitEnabled, config1.aggregateRateLimitEnabled); + assertTrue(config0.isEnabled); + + // Remove only the first token and validate only the first token is removed + address[] memory tokensToRemove = new address[](1); + tokensToRemove[0] = tokenTransferFeeArgs[0].token; + + vm.expectEmit(); + emit EVM2EVMOnRamp.TokenTransferFeeConfigDeleted(tokensToRemove); + + s_onRamp.setTokenTransferFeeConfig(new EVM2EVMOnRamp.TokenTransferFeeConfigArgs[](0), tokensToRemove); + + config0 = s_onRamp.getTokenTransferFeeConfig(tokenTransferFeeArgs[0].token); + + assertEq(0, config0.minFeeUSDCents); + assertEq(0, config0.maxFeeUSDCents); + assertEq(0, config0.deciBps); + assertEq(0, config0.destGasOverhead); + assertEq(0, config0.destBytesOverhead); + assertFalse(config0.aggregateRateLimitEnabled); + assertFalse(config0.isEnabled); + + config1 = s_onRamp.getTokenTransferFeeConfig(tokenTransferFeeArgs[1].token); + + assertEq(tokenTransferFeeArgs[1].minFeeUSDCents, config1.minFeeUSDCents); + assertEq(tokenTransferFeeArgs[1].maxFeeUSDCents, config1.maxFeeUSDCents); + assertEq(tokenTransferFeeArgs[1].deciBps, config1.deciBps); + assertEq(tokenTransferFeeArgs[1].destGasOverhead, config1.destGasOverhead); + assertEq(tokenTransferFeeArgs[1].destBytesOverhead, config1.destBytesOverhead); + assertEq(tokenTransferFeeArgs[1].aggregateRateLimitEnabled, config1.aggregateRateLimitEnabled); + assertTrue(config1.isEnabled); + } + + function test__setTokenTransferFeeConfig_byAdmin_Success() public { + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] memory transferFeeConfig; + vm.startPrank(ADMIN); + + vm.expectEmit(); + emit EVM2EVMOnRamp.TokenTransferFeeConfigSet(transferFeeConfig); + + s_onRamp.setTokenTransferFeeConfig(transferFeeConfig, new address[](0)); + } + + // Reverts + + function test__setTokenTransferFeeConfig_InvalidDestBytesOverhead_Revert() public { + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] memory transferFeeConfig = + new EVM2EVMOnRamp.TokenTransferFeeConfigArgs[](1); + transferFeeConfig[0].destBytesOverhead = uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) - 1; + vm.expectRevert( + abi.encodeWithSelector( + EVM2EVMOnRamp.InvalidDestBytesOverhead.selector, + transferFeeConfig[0].token, + transferFeeConfig[0].destBytesOverhead + ) + ); + s_onRamp.setTokenTransferFeeConfig(transferFeeConfig, new address[](0)); + } + + function test__setTokenTransferFeeConfig_OnlyCallableByOwnerOrAdmin_Revert() public { + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] memory transferFeeConfig; + vm.startPrank(STRANGER); + + vm.expectRevert(EVM2EVMOnRamp.OnlyCallableByOwnerOrAdmin.selector); + + s_onRamp.setTokenTransferFeeConfig(transferFeeConfig, new address[](0)); + } +} + +contract EVM2EVMOnRamp_getTokenPool is EVM2EVMOnRampSetup { + function test_GetTokenPool_Success() public view { + assertEq( + s_sourcePoolByToken[s_sourceTokens[0]], + address(s_onRamp.getPoolBySourceToken(DEST_CHAIN_SELECTOR, IERC20(s_sourceTokens[0]))) + ); + assertEq( + s_sourcePoolByToken[s_sourceTokens[1]], + address(s_onRamp.getPoolBySourceToken(DEST_CHAIN_SELECTOR, IERC20(s_sourceTokens[1]))) + ); + + address wrongToken = address(123); + address nonExistentPool = address(s_onRamp.getPoolBySourceToken(DEST_CHAIN_SELECTOR, IERC20(wrongToken))); + + assertEq(address(0), nonExistentPool); + } +} + +contract EVM2EVMOnRamp_setDynamicConfig is EVM2EVMOnRampSetup { + function test_SetDynamicConfig_Success() public { + EVM2EVMOnRamp.StaticConfig memory staticConfig = s_onRamp.getStaticConfig(); + EVM2EVMOnRamp.DynamicConfig memory newConfig = EVM2EVMOnRamp.DynamicConfig({ + router: address(2134), + maxNumberOfTokensPerMsg: 14, + destGasOverhead: DEST_GAS_OVERHEAD / 2, + destGasPerPayloadByte: DEST_GAS_PER_PAYLOAD_BYTE / 2, + destDataAvailabilityOverheadGas: DEST_DATA_AVAILABILITY_OVERHEAD_GAS, + destGasPerDataAvailabilityByte: DEST_GAS_PER_DATA_AVAILABILITY_BYTE, + destDataAvailabilityMultiplierBps: DEST_GAS_DATA_AVAILABILITY_MULTIPLIER_BPS, + priceRegistry: address(23423), + maxDataBytes: 400, + maxPerMsgGasLimit: MAX_GAS_LIMIT / 2, + defaultTokenFeeUSDCents: DEFAULT_TOKEN_FEE_USD_CENTS, + defaultTokenDestGasOverhead: DEFAULT_TOKEN_DEST_GAS_OVERHEAD, + defaultTokenDestBytesOverhead: DEFAULT_TOKEN_BYTES_OVERHEAD, + enforceOutOfOrder: false + }); + + vm.expectEmit(); + emit EVM2EVMOnRamp.ConfigSet(staticConfig, newConfig); + + s_onRamp.setDynamicConfig(newConfig); + + EVM2EVMOnRamp.DynamicConfig memory gotDynamicConfig = s_onRamp.getDynamicConfig(); + assertEq(newConfig.router, gotDynamicConfig.router); + assertEq(newConfig.maxNumberOfTokensPerMsg, gotDynamicConfig.maxNumberOfTokensPerMsg); + assertEq(newConfig.destGasOverhead, gotDynamicConfig.destGasOverhead); + assertEq(newConfig.destGasPerPayloadByte, gotDynamicConfig.destGasPerPayloadByte); + assertEq(newConfig.priceRegistry, gotDynamicConfig.priceRegistry); + assertEq(newConfig.maxDataBytes, gotDynamicConfig.maxDataBytes); + assertEq(newConfig.maxPerMsgGasLimit, gotDynamicConfig.maxPerMsgGasLimit); + } + + // Reverts + + function test_SetConfigInvalidConfig_Revert() public { + EVM2EVMOnRamp.DynamicConfig memory newConfig = EVM2EVMOnRamp.DynamicConfig({ + router: address(1), + maxNumberOfTokensPerMsg: 14, + destGasOverhead: DEST_GAS_OVERHEAD / 2, + destGasPerPayloadByte: DEST_GAS_PER_PAYLOAD_BYTE / 2, + destDataAvailabilityOverheadGas: DEST_DATA_AVAILABILITY_OVERHEAD_GAS, + destGasPerDataAvailabilityByte: DEST_GAS_PER_DATA_AVAILABILITY_BYTE, + destDataAvailabilityMultiplierBps: DEST_GAS_DATA_AVAILABILITY_MULTIPLIER_BPS, + priceRegistry: address(23423), + maxDataBytes: 400, + maxPerMsgGasLimit: MAX_GAS_LIMIT / 2, + defaultTokenFeeUSDCents: DEFAULT_TOKEN_FEE_USD_CENTS, + defaultTokenDestGasOverhead: DEFAULT_TOKEN_DEST_GAS_OVERHEAD, + defaultTokenDestBytesOverhead: DEFAULT_TOKEN_BYTES_OVERHEAD, + enforceOutOfOrder: false + }); + + // Invalid price reg reverts. + newConfig.priceRegistry = address(0); + vm.expectRevert(EVM2EVMOnRamp.InvalidConfig.selector); + s_onRamp.setDynamicConfig(newConfig); + + // Succeeds if valid + newConfig.priceRegistry = address(23423); + s_onRamp.setDynamicConfig(newConfig); + } + + function test_SetConfigOnlyOwner_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + s_onRamp.setDynamicConfig(generateDynamicOnRampConfig(address(1), address(2))); + vm.startPrank(ADMIN); + vm.expectRevert("Only callable by owner"); + s_onRamp.setDynamicConfig(generateDynamicOnRampConfig(address(1), address(2))); + } +} diff --git a/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMOnRampSetup.t.sol b/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMOnRampSetup.t.sol new file mode 100644 index 00000000000..6659b1217fd --- /dev/null +++ b/contracts/src/v0.8/ccip/test/onRamp/EVM2EVMOnRampSetup.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPoolV1} from "../../interfaces/IPool.sol"; + +import {PriceRegistry} from "../../PriceRegistry.sol"; +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {EVM2EVMOnRamp} from "../../onRamp/EVM2EVMOnRamp.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {TokenSetup} from "../TokenSetup.t.sol"; +import {EVM2EVMOnRampHelper} from "../helpers/EVM2EVMOnRampHelper.sol"; +import {PriceRegistrySetup} from "../priceRegistry/PriceRegistry.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract EVM2EVMOnRampSetup is TokenSetup, PriceRegistrySetup { + uint256 internal immutable i_tokenAmount0 = 9; + uint256 internal immutable i_tokenAmount1 = 7; + + bytes32 internal s_metadataHash; + + EVM2EVMOnRampHelper internal s_onRamp; + address[] internal s_offRamps; + + address internal s_destTokenPool = makeAddr("destTokenPool"); + address internal s_destToken = makeAddr("destToken"); + + EVM2EVMOnRamp.FeeTokenConfigArgs[] internal s_feeTokenConfigArgs; + EVM2EVMOnRamp.TokenTransferFeeConfigArgs[] internal s_tokenTransferFeeConfigArgs; + + function setUp() public virtual override(TokenSetup, PriceRegistrySetup) { + TokenSetup.setUp(); + PriceRegistrySetup.setUp(); + + s_priceRegistry.updatePrices(getSingleTokenPriceUpdateStruct(CUSTOM_TOKEN, CUSTOM_TOKEN_PRICE)); + + address WETH = s_sourceRouter.getWrappedNative(); + + s_feeTokenConfigArgs.push( + EVM2EVMOnRamp.FeeTokenConfigArgs({ + token: s_sourceFeeToken, + networkFeeUSDCents: 1_00, // 1 USD + gasMultiplierWeiPerEth: 1e18, // 1x + premiumMultiplierWeiPerEth: 5e17, // 0.5x + enabled: true + }) + ); + s_feeTokenConfigArgs.push( + EVM2EVMOnRamp.FeeTokenConfigArgs({ + token: WETH, + networkFeeUSDCents: 5_00, // 5 USD + gasMultiplierWeiPerEth: 2e18, // 2x + premiumMultiplierWeiPerEth: 2e18, // 2x + enabled: true + }) + ); + + s_tokenTransferFeeConfigArgs.push( + EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: s_sourceFeeToken, + minFeeUSDCents: 1_00, // 1 USD + maxFeeUSDCents: 1000_00, // 1,000 USD + deciBps: 2_5, // 2.5 bps, or 0.025% + destGasOverhead: 40_000, + destBytesOverhead: uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES), + aggregateRateLimitEnabled: true + }) + ); + s_tokenTransferFeeConfigArgs.push( + EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: s_sourceRouter.getWrappedNative(), + minFeeUSDCents: 50, // 0.5 USD + maxFeeUSDCents: 500_00, // 500 USD + deciBps: 5_0, // 5 bps, or 0.05% + destGasOverhead: 10_000, + destBytesOverhead: 100, + aggregateRateLimitEnabled: true + }) + ); + s_tokenTransferFeeConfigArgs.push( + EVM2EVMOnRamp.TokenTransferFeeConfigArgs({ + token: CUSTOM_TOKEN, + minFeeUSDCents: 2_00, // 1 USD + maxFeeUSDCents: 2000_00, // 1,000 USD + deciBps: 10_0, // 10 bps, or 0.1% + destGasOverhead: 1, + destBytesOverhead: 200, + aggregateRateLimitEnabled: true + }) + ); + + s_onRamp = new EVM2EVMOnRampHelper( + EVM2EVMOnRamp.StaticConfig({ + linkToken: s_sourceTokens[0], + chainSelector: SOURCE_CHAIN_SELECTOR, + destChainSelector: DEST_CHAIN_SELECTOR, + defaultTxGasLimit: GAS_LIMIT, + maxNopFeesJuels: MAX_NOP_FEES_JUELS, + prevOnRamp: address(0), + rmnProxy: address(s_mockRMN), + tokenAdminRegistry: address(s_tokenAdminRegistry) + }), + generateDynamicOnRampConfig(address(s_sourceRouter), address(s_priceRegistry)), + getOutboundRateLimiterConfig(), + s_feeTokenConfigArgs, + s_tokenTransferFeeConfigArgs, + getNopsAndWeights() + ); + s_onRamp.setAdmin(ADMIN); + + s_metadataHash = keccak256( + abi.encode(Internal.EVM_2_EVM_MESSAGE_HASH, SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, address(s_onRamp)) + ); + + s_offRamps = new address[](2); + s_offRamps[0] = address(10); + s_offRamps[1] = address(11); + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](2); + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: address(s_onRamp)}); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: s_offRamps[0]}); + offRampUpdates[1] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: s_offRamps[1]}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + + // Pre approve the first token so the gas estimates of the tests + // only cover actual gas usage from the ramps + IERC20(s_sourceTokens[0]).approve(address(s_sourceRouter), 2 ** 128); + IERC20(s_sourceTokens[1]).approve(address(s_sourceRouter), 2 ** 128); + } + + function getNopsAndWeights() internal pure returns (EVM2EVMOnRamp.NopAndWeight[] memory) { + EVM2EVMOnRamp.NopAndWeight[] memory nopsAndWeights = new EVM2EVMOnRamp.NopAndWeight[](3); + nopsAndWeights[0] = EVM2EVMOnRamp.NopAndWeight({nop: USER_1, weight: 19284}); + nopsAndWeights[1] = EVM2EVMOnRamp.NopAndWeight({nop: USER_2, weight: 52935}); + nopsAndWeights[2] = EVM2EVMOnRamp.NopAndWeight({nop: USER_3, weight: 8}); + return nopsAndWeights; + } + + function generateDynamicOnRampConfig( + address router, + address priceRegistry + ) internal pure returns (EVM2EVMOnRamp.DynamicConfig memory) { + return EVM2EVMOnRamp.DynamicConfig({ + router: router, + maxNumberOfTokensPerMsg: MAX_TOKENS_LENGTH, + destGasOverhead: DEST_GAS_OVERHEAD, + destGasPerPayloadByte: DEST_GAS_PER_PAYLOAD_BYTE, + destDataAvailabilityOverheadGas: DEST_DATA_AVAILABILITY_OVERHEAD_GAS, + destGasPerDataAvailabilityByte: DEST_GAS_PER_DATA_AVAILABILITY_BYTE, + destDataAvailabilityMultiplierBps: DEST_GAS_DATA_AVAILABILITY_MULTIPLIER_BPS, + priceRegistry: priceRegistry, + maxDataBytes: MAX_DATA_SIZE, + maxPerMsgGasLimit: MAX_GAS_LIMIT, + defaultTokenFeeUSDCents: DEFAULT_TOKEN_FEE_USD_CENTS, + defaultTokenDestGasOverhead: DEFAULT_TOKEN_DEST_GAS_OVERHEAD, + defaultTokenDestBytesOverhead: DEFAULT_TOKEN_BYTES_OVERHEAD, + enforceOutOfOrder: false + }); + } + + function _generateTokenMessage() public view returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = getCastedSourceEVMTokenAmountsWithZeroAmounts(); + tokenAmounts[0].amount = i_tokenAmount0; + tokenAmounts[1].amount = i_tokenAmount1; + return Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + } + + function _generateSingleTokenMessage( + address token, + uint256 amount + ) public view returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount}); + + return Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + } + + function _generateEmptyMessage() public view returns (Client.EVM2AnyMessage memory) { + return Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](0), + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + } + + function _messageToEvent( + Client.EVM2AnyMessage memory message, + uint64 seqNum, + uint64 nonce, + uint256 feeTokenAmount, + address originalSender + ) public view returns (Internal.EVM2EVMMessage memory) { + // Slicing is only available for calldata. So we have to build a new bytes array. + bytes memory args = new bytes(message.extraArgs.length - 4); + for (uint256 i = 4; i < message.extraArgs.length; ++i) { + args[i - 4] = message.extraArgs[i]; + } + uint256 numberOfTokens = message.tokenAmounts.length; + Client.EVMExtraArgsV2 memory extraArgs = _extraArgsFromBytes(bytes4(message.extraArgs), args); + Internal.EVM2EVMMessage memory messageEvent = Internal.EVM2EVMMessage({ + sequenceNumber: seqNum, + feeTokenAmount: feeTokenAmount, + sender: originalSender, + nonce: extraArgs.allowOutOfOrderExecution ? 0 : nonce, + gasLimit: extraArgs.gasLimit, + strict: false, + sourceChainSelector: SOURCE_CHAIN_SELECTOR, + receiver: abi.decode(message.receiver, (address)), + data: message.data, + tokenAmounts: message.tokenAmounts, + sourceTokenData: new bytes[](numberOfTokens), + feeToken: message.feeToken, + messageId: "" + }); + + for (uint256 i = 0; i < numberOfTokens; ++i) { + messageEvent.sourceTokenData[i] = abi.encode( + Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(s_sourcePoolByToken[message.tokenAmounts[i].token]), + destTokenAddress: abi.encode(s_destTokenBySourceToken[message.tokenAmounts[i].token]), + extraData: "" + }) + ); + } + + messageEvent.messageId = Internal._hash(messageEvent, s_metadataHash); + return messageEvent; + } + + function _extraArgsFromBytes( + bytes4 sig, + bytes memory extraArgData + ) public pure returns (Client.EVMExtraArgsV2 memory) { + if (sig == Client.EVM_EXTRA_ARGS_V1_TAG) { + Client.EVMExtraArgsV1 memory extraArgsV1 = abi.decode(extraArgData, (Client.EVMExtraArgsV1)); + return Client.EVMExtraArgsV2({gasLimit: extraArgsV1.gasLimit, allowOutOfOrderExecution: false}); + } else if (sig == Client.EVM_EXTRA_ARGS_V2_TAG) { + return abi.decode(extraArgData, (Client.EVMExtraArgsV2)); + } else { + revert("Invalid extraArgs tag"); + } + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/BurnFromMintTokenPool.t.sol b/contracts/src/v0.8/ccip/test/pools/BurnFromMintTokenPool.t.sol new file mode 100644 index 00000000000..290c4ae1537 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/BurnFromMintTokenPool.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {BurnFromMintTokenPool} from "../../pools/BurnFromMintTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {BaseTest} from "../BaseTest.t.sol"; +import {BurnMintSetup} from "./BurnMintSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract BurnFromMintTokenPoolSetup is BurnMintSetup { + BurnFromMintTokenPool internal s_pool; + + function setUp() public virtual override { + BurnMintSetup.setUp(); + + s_pool = new BurnFromMintTokenPool(s_burnMintERC677, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + s_burnMintERC677.grantMintAndBurnRoles(address(s_pool)); + + _applyChainUpdates(address(s_pool)); + } +} + +contract BurnFromMintTokenPool_lockOrBurn is BurnFromMintTokenPoolSetup { + function test_Setup_Success() public view { + assertEq(address(s_burnMintERC677), address(s_pool.getToken())); + assertEq(address(s_mockRMN), s_pool.getRmnProxy()); + assertEq(false, s_pool.getAllowListEnabled()); + assertEq(type(uint256).max, s_burnMintERC677.allowance(address(s_pool), address(s_pool))); + assertEq("BurnFromMintTokenPool 1.5.0-dev", s_pool.typeAndVersion()); + } + + function test_PoolBurn_Success() public { + uint256 burnAmount = 20_000e18; + + deal(address(s_burnMintERC677), address(s_pool), burnAmount); + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), burnAmount); + + vm.startPrank(s_burnMintOnRamp); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(burnAmount); + + vm.expectEmit(); + emit IERC20.Transfer(address(s_pool), address(0), burnAmount); + + vm.expectEmit(); + emit TokenPool.Burned(address(s_burnMintOnRamp), burnAmount); + + bytes4 expectedSignature = bytes4(keccak256("burnFrom(address,uint256)")); + vm.expectCall(address(s_burnMintERC677), abi.encodeWithSelector(expectedSignature, address(s_pool), burnAmount)); + + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: burnAmount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_burnMintERC677) + }) + ); + + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), 0); + } + + // Should not burn tokens if cursed. + function test_PoolBurnRevertNotHealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + uint256 before = s_burnMintERC677.balanceOf(address(s_pool)); + vm.startPrank(s_burnMintOnRamp); + + vm.expectRevert(TokenPool.CursedByRMN.selector); + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: 1e5, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_burnMintERC677) + }) + ); + + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), before); + } + + function test_ChainNotAllowed_Revert() public { + uint64 wrongChainSelector = 8838833; + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, wrongChainSelector)); + s_pool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: 1, + localToken: address(s_burnMintERC677), + remoteChainSelector: wrongChainSelector, + sourcePoolAddress: generateSourceTokenData().sourcePoolAddress, + sourcePoolData: generateSourceTokenData().extraData, + offchainTokenData: "" + }) + ); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/BurnMintSetup.t.sol b/contracts/src/v0.8/ccip/test/pools/BurnMintSetup.t.sol new file mode 100644 index 00000000000..a39fd1bb9fa --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/BurnMintSetup.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {Router} from "../../Router.sol"; +import {BurnMintTokenPool} from "../../pools/BurnMintTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {RouterSetup} from "../router/RouterSetup.t.sol"; + +contract BurnMintSetup is RouterSetup { + BurnMintERC677 internal s_burnMintERC677; + address internal s_burnMintOffRamp = makeAddr("burn_mint_offRamp"); + address internal s_burnMintOnRamp = makeAddr("burn_mint_onRamp"); + + address internal s_remoteBurnMintPool = makeAddr("remote_burn_mint_pool"); + address internal s_remoteToken = makeAddr("remote_token"); + + function setUp() public virtual override { + RouterSetup.setUp(); + + s_burnMintERC677 = new BurnMintERC677("Chainlink Token", "LINK", 18, 0); + } + + function _applyChainUpdates(address pool) internal { + TokenPool.ChainUpdate[] memory chains = new TokenPool.ChainUpdate[](1); + chains[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_remoteBurnMintPool), + remoteTokenAddress: abi.encode(s_remoteToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + + BurnMintTokenPool(pool).applyChainUpdates(chains); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: s_burnMintOnRamp}); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: DEST_CHAIN_SELECTOR, offRamp: s_burnMintOffRamp}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/BurnMintTokenPool.t.sol b/contracts/src/v0.8/ccip/test/pools/BurnMintTokenPool.t.sol new file mode 100644 index 00000000000..c628c510d43 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/BurnMintTokenPool.t.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPoolV1} from "../../interfaces/IPool.sol"; + +import {Internal} from "../../libraries/Internal.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {EVM2EVMOffRamp} from "../../offRamp/EVM2EVMOffRamp.sol"; +import {BurnMintTokenPool} from "../../pools/BurnMintTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {BaseTest} from "../BaseTest.t.sol"; +import {BurnMintSetup} from "./BurnMintSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract BurnMintTokenPoolSetup is BurnMintSetup { + BurnMintTokenPool internal s_pool; + + function setUp() public virtual override { + BurnMintSetup.setUp(); + + s_pool = new BurnMintTokenPool(s_burnMintERC677, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + s_burnMintERC677.grantMintAndBurnRoles(address(s_pool)); + + _applyChainUpdates(address(s_pool)); + } +} + +contract BurnMintTokenPool_lockOrBurn is BurnMintTokenPoolSetup { + function test_Setup_Success() public view { + assertEq(address(s_burnMintERC677), address(s_pool.getToken())); + assertEq(address(s_mockRMN), s_pool.getRmnProxy()); + assertEq(false, s_pool.getAllowListEnabled()); + assertEq("BurnMintTokenPool 1.5.0-dev", s_pool.typeAndVersion()); + } + + function test_PoolBurn_Success() public { + uint256 burnAmount = 20_000e18; + + deal(address(s_burnMintERC677), address(s_pool), burnAmount); + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), burnAmount); + + vm.startPrank(s_burnMintOnRamp); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(burnAmount); + + vm.expectEmit(); + emit IERC20.Transfer(address(s_pool), address(0), burnAmount); + + vm.expectEmit(); + emit TokenPool.Burned(address(s_burnMintOnRamp), burnAmount); + + bytes4 expectedSignature = bytes4(keccak256("burn(uint256)")); + vm.expectCall(address(s_burnMintERC677), abi.encodeWithSelector(expectedSignature, burnAmount)); + + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: burnAmount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_burnMintERC677) + }) + ); + + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), 0); + } + + // Should not burn tokens if cursed. + function test_PoolBurnRevertNotHealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + uint256 before = s_burnMintERC677.balanceOf(address(s_pool)); + vm.startPrank(s_burnMintOnRamp); + + vm.expectRevert(TokenPool.CursedByRMN.selector); + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: 1e5, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_burnMintERC677) + }) + ); + + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), before); + } + + function test_ChainNotAllowed_Revert() public { + uint64 wrongChainSelector = 8838833; + + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, wrongChainSelector)); + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: 1, + remoteChainSelector: wrongChainSelector, + localToken: address(s_burnMintERC677) + }) + ); + } +} + +contract BurnMintTokenPool_releaseOrMint is BurnMintTokenPoolSetup { + function test_PoolMint_Success() public { + uint256 amount = 1e19; + + vm.startPrank(s_burnMintOffRamp); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), address(s_burnMintOffRamp), amount); + s_pool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: amount, + localToken: address(s_burnMintERC677), + remoteChainSelector: DEST_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_remoteBurnMintPool), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + + assertEq(s_burnMintERC677.balanceOf(s_burnMintOffRamp), amount); + } + + function test_PoolMintNotHealthy_Revert() public { + // Should not mint tokens if cursed. + s_mockRMN.setGlobalCursed(true); + uint256 before = s_burnMintERC677.balanceOf(OWNER); + vm.startPrank(s_burnMintOffRamp); + + vm.expectRevert(TokenPool.CursedByRMN.selector); + s_pool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: 1e5, + localToken: address(s_burnMintERC677), + remoteChainSelector: DEST_CHAIN_SELECTOR, + sourcePoolAddress: generateSourceTokenData().sourcePoolAddress, + sourcePoolData: generateSourceTokenData().extraData, + offchainTokenData: "" + }) + ); + + assertEq(s_burnMintERC677.balanceOf(OWNER), before); + } + + function test_ChainNotAllowed_Revert() public { + uint64 wrongChainSelector = 8838833; + + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, wrongChainSelector)); + s_pool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: 1, + localToken: address(s_burnMintERC677), + remoteChainSelector: wrongChainSelector, + sourcePoolAddress: generateSourceTokenData().sourcePoolAddress, + sourcePoolData: generateSourceTokenData().extraData, + offchainTokenData: "" + }) + ); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/BurnWithFromMintTokenPool.t.sol b/contracts/src/v0.8/ccip/test/pools/BurnWithFromMintTokenPool.t.sol new file mode 100644 index 00000000000..22362ee4a55 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/BurnWithFromMintTokenPool.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {BurnWithFromMintTokenPool} from "../../pools/BurnWithFromMintTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {BaseTest} from "../BaseTest.t.sol"; +import {BurnMintSetup} from "./BurnMintSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract BurnWithFromMintTokenPoolSetup is BurnMintSetup { + BurnWithFromMintTokenPool internal s_pool; + + function setUp() public virtual override { + BurnMintSetup.setUp(); + + s_pool = + new BurnWithFromMintTokenPool(s_burnMintERC677, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + s_burnMintERC677.grantMintAndBurnRoles(address(s_pool)); + + _applyChainUpdates(address(s_pool)); + } +} + +contract BurnWithFromMintTokenPool_lockOrBurn is BurnWithFromMintTokenPoolSetup { + function test_Setup_Success() public view { + assertEq(address(s_burnMintERC677), address(s_pool.getToken())); + assertEq(address(s_mockRMN), s_pool.getRmnProxy()); + assertEq(false, s_pool.getAllowListEnabled()); + assertEq(type(uint256).max, s_burnMintERC677.allowance(address(s_pool), address(s_pool))); + assertEq("BurnWithFromMintTokenPool 1.5.0-dev", s_pool.typeAndVersion()); + } + + function test_PoolBurn_Success() public { + uint256 burnAmount = 20_000e18; + + deal(address(s_burnMintERC677), address(s_pool), burnAmount); + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), burnAmount); + + vm.startPrank(s_burnMintOnRamp); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(burnAmount); + + vm.expectEmit(); + emit IERC20.Transfer(address(s_pool), address(0), burnAmount); + + vm.expectEmit(); + emit TokenPool.Burned(address(s_burnMintOnRamp), burnAmount); + + bytes4 expectedSignature = bytes4(keccak256("burn(address,uint256)")); + vm.expectCall(address(s_burnMintERC677), abi.encodeWithSelector(expectedSignature, address(s_pool), burnAmount)); + + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: burnAmount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_burnMintERC677) + }) + ); + + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), 0); + } + + // Should not burn tokens if cursed. + function test_PoolBurnRevertNotHealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + uint256 before = s_burnMintERC677.balanceOf(address(s_pool)); + vm.startPrank(s_burnMintOnRamp); + + vm.expectRevert(TokenPool.CursedByRMN.selector); + s_pool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: bytes(""), + amount: 1e5, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_burnMintERC677) + }) + ); + + assertEq(s_burnMintERC677.balanceOf(address(s_pool)), before); + } + + function test_ChainNotAllowed_Revert() public { + uint64 wrongChainSelector = 8838833; + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, wrongChainSelector)); + s_pool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: 1, + localToken: address(s_burnMintERC677), + remoteChainSelector: wrongChainSelector, + sourcePoolAddress: generateSourceTokenData().sourcePoolAddress, + sourcePoolData: generateSourceTokenData().extraData, + offchainTokenData: "" + }) + ); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/LockReleaseTokenPool.t.sol b/contracts/src/v0.8/ccip/test/pools/LockReleaseTokenPool.t.sol new file mode 100644 index 00000000000..97d0d4e8947 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/LockReleaseTokenPool.t.sol @@ -0,0 +1,512 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPoolV1} from "../../interfaces/IPool.sol"; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {Router} from "../../Router.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {EVM2EVMOffRamp} from "../../offRamp/EVM2EVMOffRamp.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {BaseTest} from "../BaseTest.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; +import {RouterSetup} from "../router/RouterSetup.t.sol"; + +contract LockReleaseTokenPoolSetup is RouterSetup { + IERC20 internal s_token; + LockReleaseTokenPool internal s_lockReleaseTokenPool; + LockReleaseTokenPool internal s_lockReleaseTokenPoolWithAllowList; + address[] internal s_allowedList; + + address internal s_allowedOnRamp = address(123); + address internal s_allowedOffRamp = address(234); + + address internal s_destPoolAddress = address(2736782345); + address internal s_sourcePoolAddress = address(53852352095); + + function setUp() public virtual override { + RouterSetup.setUp(); + s_token = new BurnMintERC677("LINK", "LNK", 18, 0); + deal(address(s_token), OWNER, type(uint256).max); + s_lockReleaseTokenPool = + new LockReleaseTokenPool(s_token, new address[](0), address(s_mockRMN), true, address(s_sourceRouter)); + + s_allowedList.push(USER_1); + s_allowedList.push(DUMMY_CONTRACT_ADDRESS); + s_lockReleaseTokenPoolWithAllowList = + new LockReleaseTokenPool(s_token, s_allowedList, address(s_mockRMN), true, address(s_sourceRouter)); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_destPoolAddress), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + + s_lockReleaseTokenPool.applyChainUpdates(chainUpdate); + s_lockReleaseTokenPoolWithAllowList.applyChainUpdates(chainUpdate); + s_lockReleaseTokenPool.setRebalancer(OWNER); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: s_allowedOnRamp}); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: s_allowedOffRamp}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } +} + +contract LockReleaseTokenPool_setRebalancer is LockReleaseTokenPoolSetup { + function test_SetRebalancer_Success() public { + assertEq(address(s_lockReleaseTokenPool.getRebalancer()), OWNER); + s_lockReleaseTokenPool.setRebalancer(STRANGER); + assertEq(address(s_lockReleaseTokenPool.getRebalancer()), STRANGER); + } + + function test_SetRebalancer_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert("Only callable by owner"); + s_lockReleaseTokenPool.setRebalancer(STRANGER); + } +} + +contract LockReleaseTokenPool_lockOrBurn is LockReleaseTokenPoolSetup { + function test_Fuzz_LockOrBurnNoAllowList_Success(uint256 amount) public { + amount = bound(amount, 1, getOutboundRateLimiterConfig().capacity); + vm.startPrank(s_allowedOnRamp); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(amount); + vm.expectEmit(); + emit TokenPool.Locked(s_allowedOnRamp, amount); + + s_lockReleaseTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: STRANGER, + receiver: bytes(""), + amount: amount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + } + + function test_LockOrBurnWithAllowList_Success() public { + uint256 amount = 100; + vm.startPrank(s_allowedOnRamp); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(amount); + vm.expectEmit(); + emit TokenPool.Locked(s_allowedOnRamp, amount); + + s_lockReleaseTokenPoolWithAllowList.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: s_allowedList[0], + receiver: bytes(""), + amount: amount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + + vm.expectEmit(); + emit TokenPool.Locked(s_allowedOnRamp, amount); + + s_lockReleaseTokenPoolWithAllowList.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: s_allowedList[1], + receiver: bytes(""), + amount: amount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + } + + function test_LockOrBurnWithAllowList_Revert() public { + vm.startPrank(s_allowedOnRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.SenderNotAllowed.selector, STRANGER)); + + s_lockReleaseTokenPoolWithAllowList.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: STRANGER, + receiver: bytes(""), + amount: 100, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + } + + function test_PoolBurnRevertNotHealthy_Revert() public { + // Should not burn tokens if cursed. + s_mockRMN.setGlobalCursed(true); + uint256 before = s_token.balanceOf(address(s_lockReleaseTokenPoolWithAllowList)); + + vm.startPrank(s_allowedOnRamp); + vm.expectRevert(TokenPool.CursedByRMN.selector); + + s_lockReleaseTokenPoolWithAllowList.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: s_allowedList[0], + receiver: bytes(""), + amount: 1e5, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + + assertEq(s_token.balanceOf(address(s_lockReleaseTokenPoolWithAllowList)), before); + } +} + +contract LockReleaseTokenPool_releaseOrMint is LockReleaseTokenPoolSetup { + function setUp() public virtual override { + LockReleaseTokenPoolSetup.setUp(); + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(s_sourcePoolAddress), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + + s_lockReleaseTokenPool.applyChainUpdates(chainUpdate); + s_lockReleaseTokenPoolWithAllowList.applyChainUpdates(chainUpdate); + } + + function test_ReleaseOrMint_Success() public { + vm.startPrank(s_allowedOffRamp); + + uint256 amount = 100; + deal(address(s_token), address(s_lockReleaseTokenPool), amount); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(amount); + vm.expectEmit(); + emit TokenPool.Released(s_allowedOffRamp, OWNER, amount); + + s_lockReleaseTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: amount, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_sourcePoolAddress), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + } + + function test_Fuzz_ReleaseOrMint_Success(address recipient, uint256 amount) public { + // Since the owner already has tokens this would break the checks + vm.assume(recipient != OWNER); + vm.assume(recipient != address(0)); + vm.assume(recipient != address(s_token)); + + // Makes sure the pool always has enough funds + deal(address(s_token), address(s_lockReleaseTokenPool), amount); + vm.startPrank(s_allowedOffRamp); + + uint256 capacity = getInboundRateLimiterConfig().capacity; + // Determine if we hit the rate limit or the txs should succeed. + if (amount > capacity) { + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.TokenMaxCapacityExceeded.selector, capacity, amount, address(s_token)) + ); + } else { + // Only rate limit if the amount is >0 + if (amount > 0) { + vm.expectEmit(); + emit RateLimiter.TokensConsumed(amount); + } + + vm.expectEmit(); + emit TokenPool.Released(s_allowedOffRamp, recipient, amount); + } + + s_lockReleaseTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: recipient, + amount: amount, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_sourcePoolAddress), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + } + + function test_ChainNotAllowed_Revert() public { + address notAllowedRemotePoolAddress = address(1); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(notAllowedRemotePoolAddress), + remoteTokenAddress: abi.encode(address(2)), + allowed: false, + outboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), + inboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + + s_lockReleaseTokenPool.applyChainUpdates(chainUpdate); + + vm.startPrank(s_allowedOffRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, SOURCE_CHAIN_SELECTOR)); + s_lockReleaseTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: 1e5, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: abi.encode(s_sourcePoolAddress), + sourcePoolData: "", + offchainTokenData: "" + }) + ); + } + + function test_PoolMintNotHealthy_Revert() public { + // Should not mint tokens if cursed. + s_mockRMN.setGlobalCursed(true); + uint256 before = s_token.balanceOf(OWNER); + vm.startPrank(s_allowedOffRamp); + vm.expectRevert(TokenPool.CursedByRMN.selector); + s_lockReleaseTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: bytes(""), + receiver: OWNER, + amount: 1e5, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: generateSourceTokenData().sourcePoolAddress, + sourcePoolData: generateSourceTokenData().extraData, + offchainTokenData: "" + }) + ); + + assertEq(s_token.balanceOf(OWNER), before); + } +} + +contract LockReleaseTokenPool_canAcceptLiquidity is LockReleaseTokenPoolSetup { + function test_CanAcceptLiquidity_Success() public { + assertEq(true, s_lockReleaseTokenPool.canAcceptLiquidity()); + + s_lockReleaseTokenPool = + new LockReleaseTokenPool(s_token, new address[](0), address(s_mockRMN), false, address(s_sourceRouter)); + assertEq(false, s_lockReleaseTokenPool.canAcceptLiquidity()); + } +} + +contract LockReleaseTokenPool_provideLiquidity is LockReleaseTokenPoolSetup { + function test_Fuzz_ProvideLiquidity_Success(uint256 amount) public { + uint256 balancePre = s_token.balanceOf(OWNER); + s_token.approve(address(s_lockReleaseTokenPool), amount); + + s_lockReleaseTokenPool.provideLiquidity(amount); + + assertEq(s_token.balanceOf(OWNER), balancePre - amount); + assertEq(s_token.balanceOf(address(s_lockReleaseTokenPool)), amount); + } + + // Reverts + + function test_Unauthorized_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(LockReleaseTokenPool.Unauthorized.selector, STRANGER)); + + s_lockReleaseTokenPool.provideLiquidity(1); + } + + function test_Fuzz_ExceedsAllowance(uint256 amount) public { + vm.assume(amount > 0); + vm.expectRevert("ERC20: insufficient allowance"); + s_lockReleaseTokenPool.provideLiquidity(amount); + } + + function test_LiquidityNotAccepted_Revert() public { + s_lockReleaseTokenPool = + new LockReleaseTokenPool(s_token, new address[](0), address(s_mockRMN), false, address(s_sourceRouter)); + + vm.expectRevert(LockReleaseTokenPool.LiquidityNotAccepted.selector); + s_lockReleaseTokenPool.provideLiquidity(1); + } +} + +contract LockReleaseTokenPool_withdrawalLiquidity is LockReleaseTokenPoolSetup { + function test_Fuzz_WithdrawalLiquidity_Success(uint256 amount) public { + uint256 balancePre = s_token.balanceOf(OWNER); + s_token.approve(address(s_lockReleaseTokenPool), amount); + s_lockReleaseTokenPool.provideLiquidity(amount); + + s_lockReleaseTokenPool.withdrawLiquidity(amount); + + assertEq(s_token.balanceOf(OWNER), balancePre); + } + + // Reverts + + function test_Unauthorized_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(LockReleaseTokenPool.Unauthorized.selector, STRANGER)); + + s_lockReleaseTokenPool.withdrawLiquidity(1); + } + + function test_InsufficientLiquidity_Revert() public { + uint256 maxUint256 = 2 ** 256 - 1; + s_token.approve(address(s_lockReleaseTokenPool), maxUint256); + s_lockReleaseTokenPool.provideLiquidity(maxUint256); + + vm.startPrank(address(s_lockReleaseTokenPool)); + s_token.transfer(OWNER, maxUint256); + vm.startPrank(OWNER); + + vm.expectRevert(LockReleaseTokenPool.InsufficientLiquidity.selector); + s_lockReleaseTokenPool.withdrawLiquidity(1); + } +} + +contract LockReleaseTokenPool_supportsInterface is LockReleaseTokenPoolSetup { + function test_SupportsInterface_Success() public view { + assertTrue(s_lockReleaseTokenPool.supportsInterface(type(IPoolV1).interfaceId)); + assertTrue(s_lockReleaseTokenPool.supportsInterface(type(IERC165).interfaceId)); + } +} + +contract LockReleaseTokenPool_setChainRateLimiterConfig is LockReleaseTokenPoolSetup { + uint64 internal s_remoteChainSelector; + + function setUp() public virtual override { + LockReleaseTokenPoolSetup.setUp(); + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + s_remoteChainSelector = 123124; + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: s_remoteChainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_lockReleaseTokenPool.applyChainUpdates(chainUpdates); + } + + function test_Fuzz_SetChainRateLimiterConfig_Success(uint128 capacity, uint128 rate, uint32 newTime) public { + // Cap the lower bound to 4 so 4/2 is still >= 2 + vm.assume(capacity >= 4); + // Cap the lower bound to 2 so 2/2 is still >= 1 + rate = uint128(bound(rate, 2, capacity - 2)); + // Bucket updates only work on increasing time + newTime = uint32(bound(newTime, block.timestamp + 1, type(uint32).max)); + vm.warp(newTime); + + uint256 oldOutboundTokens = s_lockReleaseTokenPool.getCurrentOutboundRateLimiterState(s_remoteChainSelector).tokens; + uint256 oldInboundTokens = s_lockReleaseTokenPool.getCurrentInboundRateLimiterState(s_remoteChainSelector).tokens; + + RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({isEnabled: true, capacity: capacity, rate: rate}); + RateLimiter.Config memory newInboundConfig = + RateLimiter.Config({isEnabled: true, capacity: capacity / 2, rate: rate / 2}); + + vm.expectEmit(); + emit RateLimiter.ConfigChanged(newOutboundConfig); + vm.expectEmit(); + emit RateLimiter.ConfigChanged(newInboundConfig); + vm.expectEmit(); + emit TokenPool.ChainConfigured(s_remoteChainSelector, newOutboundConfig, newInboundConfig); + + s_lockReleaseTokenPool.setChainRateLimiterConfig(s_remoteChainSelector, newOutboundConfig, newInboundConfig); + + uint256 expectedTokens = RateLimiter._min(newOutboundConfig.capacity, oldOutboundTokens); + + RateLimiter.TokenBucket memory bucket = + s_lockReleaseTokenPool.getCurrentOutboundRateLimiterState(s_remoteChainSelector); + assertEq(bucket.capacity, newOutboundConfig.capacity); + assertEq(bucket.rate, newOutboundConfig.rate); + assertEq(bucket.tokens, expectedTokens); + assertEq(bucket.lastUpdated, newTime); + + expectedTokens = RateLimiter._min(newInboundConfig.capacity, oldInboundTokens); + + bucket = s_lockReleaseTokenPool.getCurrentInboundRateLimiterState(s_remoteChainSelector); + assertEq(bucket.capacity, newInboundConfig.capacity); + assertEq(bucket.rate, newInboundConfig.rate); + assertEq(bucket.tokens, expectedTokens); + assertEq(bucket.lastUpdated, newTime); + } + + function test_OnlyOwnerOrRateLimitAdmin_Revert() public { + address rateLimiterAdmin = address(28973509103597907); + + s_lockReleaseTokenPool.setRateLimitAdmin(rateLimiterAdmin); + + vm.startPrank(rateLimiterAdmin); + + s_lockReleaseTokenPool.setChainRateLimiterConfig( + s_remoteChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + + vm.startPrank(OWNER); + + s_lockReleaseTokenPool.setChainRateLimiterConfig( + s_remoteChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + } + + // Reverts + + function test_OnlyOwner_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert(abi.encodeWithSelector(LockReleaseTokenPool.Unauthorized.selector, STRANGER)); + s_lockReleaseTokenPool.setChainRateLimiterConfig( + s_remoteChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + } + + function test_NonExistentChain_Revert() public { + uint64 wrongChainSelector = 9084102894; + + vm.expectRevert(abi.encodeWithSelector(TokenPool.NonExistentChain.selector, wrongChainSelector)); + s_lockReleaseTokenPool.setChainRateLimiterConfig( + wrongChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + } +} + +contract LockReleaseTokenPool_setRateLimitAdmin is LockReleaseTokenPoolSetup { + function test_SetRateLimitAdmin_Success() public { + assertEq(address(0), s_lockReleaseTokenPool.getRateLimitAdmin()); + s_lockReleaseTokenPool.setRateLimitAdmin(OWNER); + assertEq(OWNER, s_lockReleaseTokenPool.getRateLimitAdmin()); + } + + // Reverts + + function test_SetRateLimitAdmin_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert("Only callable by owner"); + s_lockReleaseTokenPool.setRateLimitAdmin(STRANGER); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/TokenPool.t.sol b/contracts/src/v0.8/ccip/test/pools/TokenPool.t.sol new file mode 100644 index 00000000000..e5eb04b7413 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/TokenPool.t.sol @@ -0,0 +1,767 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {Router} from "../../Router.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {BaseTest} from "../BaseTest.t.sol"; +import {TokenPoolHelper} from "../helpers/TokenPoolHelper.sol"; +import {RouterSetup} from "../router/RouterSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract TokenPoolSetup is RouterSetup { + IERC20 internal s_token; + TokenPoolHelper internal s_tokenPool; + + function setUp() public virtual override { + RouterSetup.setUp(); + s_token = new BurnMintERC677("LINK", "LNK", 18, 0); + deal(address(s_token), OWNER, type(uint256).max); + + s_tokenPool = new TokenPoolHelper(s_token, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + } +} + +contract TokenPool_constructor is TokenPoolSetup { + function test_immutableFields_Success() public view { + assertEq(address(s_token), address(s_tokenPool.getToken())); + assertEq(address(s_mockRMN), s_tokenPool.getRmnProxy()); + assertEq(false, s_tokenPool.getAllowListEnabled()); + assertEq(address(s_sourceRouter), s_tokenPool.getRouter()); + } + + // Reverts + function test_ZeroAddressNotAllowed_Revert() public { + vm.expectRevert(TokenPool.ZeroAddressNotAllowed.selector); + + s_tokenPool = new TokenPoolHelper(IERC20(address(0)), new address[](0), address(s_mockRMN), address(s_sourceRouter)); + } +} + +contract TokenPool_getRemotePool is TokenPoolSetup { + function test_getRemotePool_Success() public { + uint64 chainSelector = 123124; + address remotePool = makeAddr("remotePool"); + address remoteToken = makeAddr("remoteToken"); + + // Zero indicates nothing is set + assertEq(0, s_tokenPool.getRemotePool(chainSelector).length); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(remotePool), + remoteTokenAddress: abi.encode(remoteToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdates); + + assertEq(remotePool, abi.decode(s_tokenPool.getRemotePool(chainSelector), (address))); + } +} + +contract TokenPool_setRemotePool is TokenPoolSetup { + function test_setRemotePool_Success() public { + uint64 chainSelector = DEST_CHAIN_SELECTOR; + address initialPool = makeAddr("remotePool"); + address remoteToken = makeAddr("remoteToken"); + // The new pool is a non-evm pool, as it doesn't fit in the normal 160 bits + bytes memory newPool = abi.encode(type(uint256).max); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(initialPool), + remoteTokenAddress: abi.encode(remoteToken), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdates); + + vm.expectEmit(); + emit TokenPool.RemotePoolSet(chainSelector, abi.encode(initialPool), newPool); + + s_tokenPool.setRemotePool(chainSelector, newPool); + + assertEq(keccak256(newPool), keccak256(s_tokenPool.getRemotePool(chainSelector))); + } + + // Reverts + + function test_setRemotePool_NonExistentChain_Reverts() public { + uint64 chainSelector = 123124; + bytes memory remotePool = abi.encode(makeAddr("remotePool")); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.NonExistentChain.selector, chainSelector)); + s_tokenPool.setRemotePool(chainSelector, remotePool); + } + + function test_setRemotePool_OnlyOwner_Reverts() public { + vm.startPrank(STRANGER); + + vm.expectRevert("Only callable by owner"); + s_tokenPool.setRemotePool(123124, abi.encode(makeAddr("remotePool"))); + } +} + +contract TokenPool_applyChainUpdates is TokenPoolSetup { + function assertState(TokenPool.ChainUpdate[] memory chainUpdates) public view { + uint64[] memory chainSelectors = s_tokenPool.getSupportedChains(); + for (uint256 i = 0; i < chainUpdates.length; i++) { + assertEq(chainUpdates[i].remoteChainSelector, chainSelectors[i]); + } + + for (uint256 i = 0; i < chainUpdates.length; ++i) { + assertTrue(s_tokenPool.isSupportedChain(chainUpdates[i].remoteChainSelector)); + RateLimiter.TokenBucket memory bkt = + s_tokenPool.getCurrentOutboundRateLimiterState(chainUpdates[i].remoteChainSelector); + assertEq(bkt.capacity, chainUpdates[i].outboundRateLimiterConfig.capacity); + assertEq(bkt.rate, chainUpdates[i].outboundRateLimiterConfig.rate); + assertEq(bkt.isEnabled, chainUpdates[i].outboundRateLimiterConfig.isEnabled); + + bkt = s_tokenPool.getCurrentInboundRateLimiterState(chainUpdates[i].remoteChainSelector); + assertEq(bkt.capacity, chainUpdates[i].inboundRateLimiterConfig.capacity); + assertEq(bkt.rate, chainUpdates[i].inboundRateLimiterConfig.rate); + assertEq(bkt.isEnabled, chainUpdates[i].inboundRateLimiterConfig.isEnabled); + } + } + + function test_applyChainUpdates_Success() public { + RateLimiter.Config memory outboundRateLimit1 = RateLimiter.Config({isEnabled: true, capacity: 100e28, rate: 1e18}); + RateLimiter.Config memory inboundRateLimit1 = RateLimiter.Config({isEnabled: true, capacity: 100e29, rate: 1e19}); + RateLimiter.Config memory outboundRateLimit2 = RateLimiter.Config({isEnabled: true, capacity: 100e26, rate: 1e16}); + RateLimiter.Config memory inboundRateLimit2 = RateLimiter.Config({isEnabled: true, capacity: 100e27, rate: 1e17}); + + // EVM chain, which uses the 160 bit evm address space + uint64 evmChainSelector = 1; + bytes memory evmRemotePool = abi.encode(makeAddr("evm_remote_pool")); + bytes memory evmRemoteToken = abi.encode(makeAddr("evm_remote_token")); + + // Non EVM chain, which uses the full 256 bits + uint64 nonEvmChainSelector = type(uint64).max; + bytes memory nonEvmRemotePool = abi.encode(keccak256("non_evm_remote_pool")); + bytes memory nonEvmRemoteToken = abi.encode(keccak256("non_evm_remote_token")); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](2); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: evmChainSelector, + remotePoolAddress: evmRemotePool, + remoteTokenAddress: evmRemoteToken, + allowed: true, + outboundRateLimiterConfig: outboundRateLimit1, + inboundRateLimiterConfig: inboundRateLimit1 + }); + chainUpdates[1] = TokenPool.ChainUpdate({ + remoteChainSelector: nonEvmChainSelector, + remotePoolAddress: nonEvmRemotePool, + remoteTokenAddress: nonEvmRemoteToken, + allowed: true, + outboundRateLimiterConfig: outboundRateLimit2, + inboundRateLimiterConfig: inboundRateLimit2 + }); + + // Assert configuration is applied + vm.expectEmit(); + emit TokenPool.ChainAdded( + chainUpdates[0].remoteChainSelector, + chainUpdates[0].remoteTokenAddress, + chainUpdates[0].outboundRateLimiterConfig, + chainUpdates[0].inboundRateLimiterConfig + ); + vm.expectEmit(); + emit TokenPool.ChainAdded( + chainUpdates[1].remoteChainSelector, + chainUpdates[1].remoteTokenAddress, + chainUpdates[1].outboundRateLimiterConfig, + chainUpdates[1].inboundRateLimiterConfig + ); + s_tokenPool.applyChainUpdates(chainUpdates); + // on1: rateLimit1, on2: rateLimit2, off1: rateLimit1, off2: rateLimit3 + assertState(chainUpdates); + + // Removing an non-existent chain should revert + TokenPool.ChainUpdate[] memory chainRemoves = new TokenPool.ChainUpdate[](1); + uint64 strangerChainSelector = 120938; + chainRemoves[0] = TokenPool.ChainUpdate({ + remoteChainSelector: strangerChainSelector, + remotePoolAddress: evmRemotePool, + remoteTokenAddress: evmRemoteToken, + allowed: false, + outboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), + inboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + vm.expectRevert(abi.encodeWithSelector(TokenPool.NonExistentChain.selector, strangerChainSelector)); + s_tokenPool.applyChainUpdates(chainRemoves); + // State remains + assertState(chainUpdates); + + // Can remove a chain + chainRemoves[0].remoteChainSelector = evmChainSelector; + + vm.expectEmit(); + emit TokenPool.ChainRemoved(chainRemoves[0].remoteChainSelector); + + s_tokenPool.applyChainUpdates(chainRemoves); + + // State updated, only chain 2 remains + TokenPool.ChainUpdate[] memory singleChainConfigured = new TokenPool.ChainUpdate[](1); + singleChainConfigured[0] = chainUpdates[1]; + assertState(singleChainConfigured); + + // Cannot reset already configured ramp + vm.expectRevert( + abi.encodeWithSelector(TokenPool.ChainAlreadyExists.selector, singleChainConfigured[0].remoteChainSelector) + ); + s_tokenPool.applyChainUpdates(singleChainConfigured); + } + + // Reverts + + function test_applyChainUpdates_OnlyCallableByOwner_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + s_tokenPool.applyChainUpdates(new TokenPool.ChainUpdate[](0)); + } + + function test_applyChainUpdates_ZeroAddressNotAllowed_Revert() public { + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: 1, + remotePoolAddress: "", + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100e28, rate: 1e18}), + inboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100e28, rate: 1e18}) + }); + + vm.expectRevert(TokenPool.ZeroAddressNotAllowed.selector); + s_tokenPool.applyChainUpdates(chainUpdates); + + chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: 1, + remotePoolAddress: abi.encode(address(2)), + remoteTokenAddress: "", + allowed: true, + outboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100e28, rate: 1e18}), + inboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100e28, rate: 1e18}) + }); + + vm.expectRevert(TokenPool.ZeroAddressNotAllowed.selector); + s_tokenPool.applyChainUpdates(chainUpdates); + } + + function test_applyChainUpdates_DisabledNonZeroRateLimit_Revert() public { + RateLimiter.Config memory outboundRateLimit = RateLimiter.Config({isEnabled: true, capacity: 100e28, rate: 1e18}); + RateLimiter.Config memory inboundRateLimit = RateLimiter.Config({isEnabled: true, capacity: 100e22, rate: 1e12}); + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: 1, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: outboundRateLimit, + inboundRateLimiterConfig: inboundRateLimit + }); + + s_tokenPool.applyChainUpdates(chainUpdates); + + chainUpdates[0].allowed = false; + chainUpdates[0].outboundRateLimiterConfig = RateLimiter.Config({isEnabled: false, capacity: 10, rate: 1}); + chainUpdates[0].inboundRateLimiterConfig = RateLimiter.Config({isEnabled: false, capacity: 10, rate: 1}); + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.DisabledNonZeroRateLimit.selector, chainUpdates[0].outboundRateLimiterConfig) + ); + s_tokenPool.applyChainUpdates(chainUpdates); + } + + function test_applyChainUpdates_NonExistentChain_Revert() public { + RateLimiter.Config memory outboundRateLimit = RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + RateLimiter.Config memory inboundRateLimit = RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: 1, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: false, + outboundRateLimiterConfig: outboundRateLimit, + inboundRateLimiterConfig: inboundRateLimit + }); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.NonExistentChain.selector, chainUpdates[0].remoteChainSelector)); + s_tokenPool.applyChainUpdates(chainUpdates); + } + + function test_applyChainUpdates_InvalidRateLimitRate_Revert() public { + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: 1, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 0, rate: 0}), + inboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100e22, rate: 1e12}) + }); + + // Outbound + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.InvalidRateLimitRate.selector, chainUpdates[0].outboundRateLimiterConfig) + ); + s_tokenPool.applyChainUpdates(chainUpdates); + + chainUpdates[0].outboundRateLimiterConfig.rate = 100; + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.InvalidRateLimitRate.selector, chainUpdates[0].outboundRateLimiterConfig) + ); + s_tokenPool.applyChainUpdates(chainUpdates); + + chainUpdates[0].outboundRateLimiterConfig.capacity = 100; + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.InvalidRateLimitRate.selector, chainUpdates[0].outboundRateLimiterConfig) + ); + s_tokenPool.applyChainUpdates(chainUpdates); + + chainUpdates[0].outboundRateLimiterConfig.capacity = 101; + + s_tokenPool.applyChainUpdates(chainUpdates); + + // Change the chain selector as adding the same one would revert + chainUpdates[0].remoteChainSelector = 2; + + // Inbound + + chainUpdates[0].inboundRateLimiterConfig.capacity = 0; + chainUpdates[0].inboundRateLimiterConfig.rate = 0; + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.InvalidRateLimitRate.selector, chainUpdates[0].inboundRateLimiterConfig) + ); + s_tokenPool.applyChainUpdates(chainUpdates); + + chainUpdates[0].inboundRateLimiterConfig.rate = 100; + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.InvalidRateLimitRate.selector, chainUpdates[0].inboundRateLimiterConfig) + ); + s_tokenPool.applyChainUpdates(chainUpdates); + + chainUpdates[0].inboundRateLimiterConfig.capacity = 100; + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.InvalidRateLimitRate.selector, chainUpdates[0].inboundRateLimiterConfig) + ); + s_tokenPool.applyChainUpdates(chainUpdates); + + chainUpdates[0].inboundRateLimiterConfig.capacity = 101; + + s_tokenPool.applyChainUpdates(chainUpdates); + } +} + +contract TokenPool_setChainRateLimiterConfig is TokenPoolSetup { + uint64 internal s_remoteChainSelector; + + function setUp() public virtual override { + TokenPoolSetup.setUp(); + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + s_remoteChainSelector = 123124; + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: s_remoteChainSelector, + remotePoolAddress: abi.encode(address(2)), + remoteTokenAddress: abi.encode(address(3)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdates); + } + + function test_Fuzz_SetChainRateLimiterConfig_Success(uint128 capacity, uint128 rate, uint32 newTime) public { + // Cap the lower bound to 4 so 4/2 is still >= 2 + vm.assume(capacity >= 4); + // Cap the lower bound to 2 so 2/2 is still >= 1 + rate = uint128(bound(rate, 2, capacity - 2)); + // Bucket updates only work on increasing time + newTime = uint32(bound(newTime, block.timestamp + 1, type(uint32).max)); + vm.warp(newTime); + + uint256 oldOutboundTokens = s_tokenPool.getCurrentOutboundRateLimiterState(s_remoteChainSelector).tokens; + uint256 oldInboundTokens = s_tokenPool.getCurrentInboundRateLimiterState(s_remoteChainSelector).tokens; + + RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({isEnabled: true, capacity: capacity, rate: rate}); + RateLimiter.Config memory newInboundConfig = + RateLimiter.Config({isEnabled: true, capacity: capacity / 2, rate: rate / 2}); + + vm.expectEmit(); + emit RateLimiter.ConfigChanged(newOutboundConfig); + vm.expectEmit(); + emit RateLimiter.ConfigChanged(newInboundConfig); + vm.expectEmit(); + emit TokenPool.ChainConfigured(s_remoteChainSelector, newOutboundConfig, newInboundConfig); + + s_tokenPool.setChainRateLimiterConfig(s_remoteChainSelector, newOutboundConfig, newInboundConfig); + + uint256 expectedTokens = RateLimiter._min(newOutboundConfig.capacity, oldOutboundTokens); + + RateLimiter.TokenBucket memory bucket = s_tokenPool.getCurrentOutboundRateLimiterState(s_remoteChainSelector); + assertEq(bucket.capacity, newOutboundConfig.capacity); + assertEq(bucket.rate, newOutboundConfig.rate); + assertEq(bucket.tokens, expectedTokens); + assertEq(bucket.lastUpdated, newTime); + + expectedTokens = RateLimiter._min(newInboundConfig.capacity, oldInboundTokens); + + bucket = s_tokenPool.getCurrentInboundRateLimiterState(s_remoteChainSelector); + assertEq(bucket.capacity, newInboundConfig.capacity); + assertEq(bucket.rate, newInboundConfig.rate); + assertEq(bucket.tokens, expectedTokens); + assertEq(bucket.lastUpdated, newTime); + } + + // Reverts + + function test_OnlyOwner_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert("Only callable by owner"); + s_tokenPool.setChainRateLimiterConfig( + s_remoteChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + } + + function test_NonExistentChain_Revert() public { + uint64 wrongChainSelector = 9084102894; + + vm.expectRevert(abi.encodeWithSelector(TokenPool.NonExistentChain.selector, wrongChainSelector)); + s_tokenPool.setChainRateLimiterConfig( + wrongChainSelector, getOutboundRateLimiterConfig(), getInboundRateLimiterConfig() + ); + } +} + +contract TokenPool_onlyOnRamp is TokenPoolSetup { + function test_onlyOnRamp_Success() public { + uint64 chainSelector = 13377; + address onRamp = makeAddr("onRamp"); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdate); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: chainSelector, onRamp: onRamp}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), new Router.OffRamp[](0)); + + vm.startPrank(onRamp); + + s_tokenPool.onlyOnRampModifier(chainSelector); + } + + function test_ChainNotAllowed_Revert() public { + uint64 chainSelector = 13377; + address onRamp = makeAddr("onRamp"); + + vm.startPrank(onRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, chainSelector)); + s_tokenPool.onlyOnRampModifier(chainSelector); + + vm.startPrank(OWNER); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdate); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: chainSelector, onRamp: onRamp}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), new Router.OffRamp[](0)); + + vm.startPrank(onRamp); + // Should succeed now that we've added the chain + s_tokenPool.onlyOnRampModifier(chainSelector); + + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: false, + outboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), + inboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + + vm.startPrank(OWNER); + s_tokenPool.applyChainUpdates(chainUpdate); + + vm.startPrank(onRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, chainSelector)); + s_tokenPool.onlyOffRampModifier(chainSelector); + } + + function test_CallerIsNotARampOnRouter_Revert() public { + uint64 chainSelector = 13377; + address onRamp = makeAddr("onRamp"); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdate); + + vm.startPrank(onRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.CallerIsNotARampOnRouter.selector, onRamp)); + + s_tokenPool.onlyOnRampModifier(chainSelector); + } +} + +contract TokenPool_onlyOffRamp is TokenPoolSetup { + function test_onlyOffRamp_Success() public { + uint64 chainSelector = 13377; + address offRamp = makeAddr("onRamp"); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdate); + + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: chainSelector, offRamp: offRamp}); + s_sourceRouter.applyRampUpdates(new Router.OnRamp[](0), new Router.OffRamp[](0), offRampUpdates); + + vm.startPrank(offRamp); + + s_tokenPool.onlyOffRampModifier(chainSelector); + } + + function test_ChainNotAllowed_Revert() public { + uint64 chainSelector = 13377; + address offRamp = makeAddr("onRamp"); + + vm.startPrank(offRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, chainSelector)); + s_tokenPool.onlyOffRampModifier(chainSelector); + + vm.startPrank(OWNER); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdate); + + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: chainSelector, offRamp: offRamp}); + s_sourceRouter.applyRampUpdates(new Router.OnRamp[](0), new Router.OffRamp[](0), offRampUpdates); + + vm.startPrank(offRamp); + // Should succeed now that we've added the chain + s_tokenPool.onlyOffRampModifier(chainSelector); + + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: false, + outboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), + inboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + + vm.startPrank(OWNER); + s_tokenPool.applyChainUpdates(chainUpdate); + + vm.startPrank(offRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.ChainNotAllowed.selector, chainSelector)); + s_tokenPool.onlyOffRampModifier(chainSelector); + } + + function test_CallerIsNotARampOnRouter_Revert() public { + uint64 chainSelector = 13377; + address offRamp = makeAddr("offRamp"); + + TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); + chainUpdate[0] = TokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + s_tokenPool.applyChainUpdates(chainUpdate); + + vm.startPrank(offRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.CallerIsNotARampOnRouter.selector, offRamp)); + + s_tokenPool.onlyOffRampModifier(chainSelector); + } +} + +contract TokenPoolWithAllowListSetup is TokenPoolSetup { + address[] internal s_allowedSenders; + + function setUp() public virtual override { + TokenPoolSetup.setUp(); + + s_allowedSenders.push(STRANGER); + s_allowedSenders.push(DUMMY_CONTRACT_ADDRESS); + + s_tokenPool = new TokenPoolHelper(s_token, s_allowedSenders, address(s_mockRMN), address(s_sourceRouter)); + } +} + +contract TokenPoolWithAllowList_getAllowListEnabled is TokenPoolWithAllowListSetup { + function test_GetAllowListEnabled_Success() public view { + assertTrue(s_tokenPool.getAllowListEnabled()); + } +} + +contract TokenPoolWithAllowList_setRouter is TokenPoolWithAllowListSetup { + function test_SetRouter_Success() public { + assertEq(address(s_sourceRouter), s_tokenPool.getRouter()); + + address newRouter = makeAddr("newRouter"); + + vm.expectEmit(); + emit TokenPool.RouterUpdated(address(s_sourceRouter), newRouter); + + s_tokenPool.setRouter(newRouter); + + assertEq(newRouter, s_tokenPool.getRouter()); + } +} + +contract TokenPoolWithAllowList_getAllowList is TokenPoolWithAllowListSetup { + function test_GetAllowList_Success() public view { + address[] memory setAddresses = s_tokenPool.getAllowList(); + assertEq(2, setAddresses.length); + assertEq(s_allowedSenders[0], setAddresses[0]); + assertEq(s_allowedSenders[1], setAddresses[1]); + } +} + +contract TokenPoolWithAllowList_applyAllowListUpdates is TokenPoolWithAllowListSetup { + function test_SetAllowList_Success() public { + address[] memory newAddresses = new address[](2); + newAddresses[0] = address(1); + newAddresses[1] = address(2); + + for (uint256 i = 0; i < 2; ++i) { + vm.expectEmit(); + emit TokenPool.AllowListAdd(newAddresses[i]); + } + + s_tokenPool.applyAllowListUpdates(new address[](0), newAddresses); + address[] memory setAddresses = s_tokenPool.getAllowList(); + + assertEq(s_allowedSenders[0], setAddresses[0]); + assertEq(s_allowedSenders[1], setAddresses[1]); + assertEq(address(1), setAddresses[2]); + assertEq(address(2), setAddresses[3]); + + // address(2) exists noop, add address(3), remove address(1) + newAddresses = new address[](2); + newAddresses[0] = address(2); + newAddresses[1] = address(3); + + address[] memory removeAddresses = new address[](1); + removeAddresses[0] = address(1); + + vm.expectEmit(); + emit TokenPool.AllowListRemove(address(1)); + + vm.expectEmit(); + emit TokenPool.AllowListAdd(address(3)); + + s_tokenPool.applyAllowListUpdates(removeAddresses, newAddresses); + setAddresses = s_tokenPool.getAllowList(); + + assertEq(s_allowedSenders[0], setAddresses[0]); + assertEq(s_allowedSenders[1], setAddresses[1]); + assertEq(address(2), setAddresses[2]); + assertEq(address(3), setAddresses[3]); + + // remove all from allowList + for (uint256 i = 0; i < setAddresses.length; ++i) { + vm.expectEmit(); + emit TokenPool.AllowListRemove(setAddresses[i]); + } + + s_tokenPool.applyAllowListUpdates(setAddresses, new address[](0)); + setAddresses = s_tokenPool.getAllowList(); + + assertEq(0, setAddresses.length); + } + + function test_SetAllowListSkipsZero_Success() public { + uint256 setAddressesLength = s_tokenPool.getAllowList().length; + + address[] memory newAddresses = new address[](1); + newAddresses[0] = address(0); + + s_tokenPool.applyAllowListUpdates(new address[](0), newAddresses); + address[] memory setAddresses = s_tokenPool.getAllowList(); + + assertEq(setAddresses.length, setAddressesLength); + } + + // Reverts + + function test_OnlyOwner_Revert() public { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + address[] memory newAddresses = new address[](2); + s_tokenPool.applyAllowListUpdates(new address[](0), newAddresses); + } + + function test_AllowListNotEnabled_Revert() public { + s_tokenPool = new TokenPoolHelper(s_token, new address[](0), address(s_mockRMN), address(s_sourceRouter)); + + vm.expectRevert(TokenPool.AllowListNotEnabled.selector); + + s_tokenPool.applyAllowListUpdates(new address[](0), new address[](2)); + } +} diff --git a/contracts/src/v0.8/ccip/test/pools/USDCTokenPool.t.sol b/contracts/src/v0.8/ccip/test/pools/USDCTokenPool.t.sol new file mode 100644 index 00000000000..200ffb4f6d6 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/pools/USDCTokenPool.t.sol @@ -0,0 +1,690 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBurnMintERC20} from "../../../shared/token/ERC20/IBurnMintERC20.sol"; +import {IPoolV1} from "../../interfaces/IPool.sol"; +import {ITokenMessenger} from "../../pools/USDC/ITokenMessenger.sol"; + +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {Router} from "../../Router.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {USDCTokenPool} from "../../pools/USDC/USDCTokenPool.sol"; +import {BaseTest} from "../BaseTest.t.sol"; +import {USDCTokenPoolHelper} from "../helpers/USDCTokenPoolHelper.sol"; +import {MockE2EUSDCTransmitter} from "../mocks/MockE2EUSDCTransmitter.sol"; +import {MockUSDCTokenMessenger} from "../mocks/MockUSDCTokenMessenger.sol"; + +import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; + +contract USDCTokenPoolSetup is BaseTest { + IBurnMintERC20 internal s_token; + MockUSDCTokenMessenger internal s_mockUSDC; + MockE2EUSDCTransmitter internal s_mockUSDCTransmitter; + + struct USDCMessage { + uint32 version; + uint32 sourceDomain; + uint32 destinationDomain; + uint64 nonce; + bytes32 sender; + bytes32 recipient; + bytes32 destinationCaller; + bytes messageBody; + } + + uint32 internal constant SOURCE_DOMAIN_IDENTIFIER = 0x02020202; + uint32 internal constant DEST_DOMAIN_IDENTIFIER = 0; + + bytes32 internal constant SOURCE_CHAIN_TOKEN_SENDER = bytes32(uint256(uint160(0x01111111221))); + address internal constant SOURCE_CHAIN_USDC_POOL = address(0x23789765456789); + address internal constant DEST_CHAIN_USDC_POOL = address(0x987384873458734); + address internal constant DEST_CHAIN_USDC_TOKEN = address(0x23598918358198766); + + address internal s_routerAllowedOnRamp = address(3456); + address internal s_routerAllowedOffRamp = address(234); + Router internal s_router; + + USDCTokenPoolHelper internal s_usdcTokenPool; + USDCTokenPoolHelper internal s_usdcTokenPoolWithAllowList; + address[] internal s_allowedList; + + function setUp() public virtual override { + BaseTest.setUp(); + BurnMintERC677 usdcToken = new BurnMintERC677("LINK", "LNK", 18, 0); + s_token = usdcToken; + deal(address(s_token), OWNER, type(uint256).max); + setUpRamps(); + + s_mockUSDCTransmitter = new MockE2EUSDCTransmitter(0, DEST_DOMAIN_IDENTIFIER, address(s_token)); + s_mockUSDC = new MockUSDCTokenMessenger(0, address(s_mockUSDCTransmitter)); + + usdcToken.grantMintAndBurnRoles(address(s_mockUSDCTransmitter)); + + s_usdcTokenPool = + new USDCTokenPoolHelper(s_mockUSDC, s_token, new address[](0), address(s_mockRMN), address(s_router)); + usdcToken.grantMintAndBurnRoles(address(s_mockUSDC)); + + s_allowedList.push(USER_1); + s_usdcTokenPoolWithAllowList = + new USDCTokenPoolHelper(s_mockUSDC, s_token, s_allowedList, address(s_mockRMN), address(s_router)); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](2); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(SOURCE_CHAIN_USDC_POOL), + remoteTokenAddress: abi.encode(address(s_token)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + chainUpdates[1] = TokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(DEST_CHAIN_USDC_POOL), + remoteTokenAddress: abi.encode(DEST_CHAIN_USDC_TOKEN), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + + s_usdcTokenPool.applyChainUpdates(chainUpdates); + s_usdcTokenPoolWithAllowList.applyChainUpdates(chainUpdates); + + USDCTokenPool.DomainUpdate[] memory domains = new USDCTokenPool.DomainUpdate[](1); + domains[0] = USDCTokenPool.DomainUpdate({ + destChainSelector: DEST_CHAIN_SELECTOR, + domainIdentifier: 9999, + allowedCaller: keccak256("allowedCaller"), + enabled: true + }); + + s_usdcTokenPool.setDomains(domains); + s_usdcTokenPoolWithAllowList.setDomains(domains); + } + + function setUpRamps() internal { + s_router = new Router(address(s_token), address(s_mockRMN)); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: s_routerAllowedOnRamp}); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + address[] memory offRamps = new address[](1); + offRamps[0] = s_routerAllowedOffRamp; + offRampUpdates[0] = Router.OffRamp({sourceChainSelector: SOURCE_CHAIN_SELECTOR, offRamp: offRamps[0]}); + + s_router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + function _generateUSDCMessage(USDCMessage memory usdcMessage) internal pure returns (bytes memory) { + return abi.encodePacked( + usdcMessage.version, + usdcMessage.sourceDomain, + usdcMessage.destinationDomain, + usdcMessage.nonce, + usdcMessage.sender, + usdcMessage.recipient, + usdcMessage.destinationCaller, + usdcMessage.messageBody + ); + } +} + +contract USDCTokenPool_lockOrBurn is USDCTokenPoolSetup { + // Base test case, included for PR gas comparisons as fuzz tests are excluded from forge snapshot due to being flaky. + function test_LockOrBurn_Success() public { + bytes32 receiver = bytes32(uint256(uint160(STRANGER))); + uint256 amount = 1; + s_token.transfer(address(s_usdcTokenPool), amount); + vm.startPrank(s_routerAllowedOnRamp); + + USDCTokenPool.Domain memory expectedDomain = s_usdcTokenPool.getDomain(DEST_CHAIN_SELECTOR); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(amount); + + vm.expectEmit(); + emit ITokenMessenger.DepositForBurn( + s_mockUSDC.s_nonce(), + address(s_token), + amount, + address(s_usdcTokenPool), + expectedDomain.allowedCaller, + expectedDomain.domainIdentifier, + s_mockUSDC.DESTINATION_TOKEN_MESSENGER(), + expectedDomain.allowedCaller + ); + + vm.expectEmit(); + emit TokenPool.Burned(s_routerAllowedOnRamp, amount); + + Pool.LockOrBurnOutV1 memory poolReturnDataV1 = s_usdcTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: abi.encodePacked(receiver), + amount: amount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + + uint64 nonce = abi.decode(poolReturnDataV1.destPoolData, (uint64)); + assertEq(s_mockUSDC.s_nonce() - 1, nonce); + } + + function test_Fuzz_LockOrBurn_Success(bytes32 destinationReceiver, uint256 amount) public { + vm.assume(destinationReceiver != bytes32(0)); + amount = bound(amount, 1, getOutboundRateLimiterConfig().capacity); + s_token.transfer(address(s_usdcTokenPool), amount); + vm.startPrank(s_routerAllowedOnRamp); + + USDCTokenPool.Domain memory expectedDomain = s_usdcTokenPool.getDomain(DEST_CHAIN_SELECTOR); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(amount); + + vm.expectEmit(); + emit ITokenMessenger.DepositForBurn( + s_mockUSDC.s_nonce(), + address(s_token), + amount, + address(s_usdcTokenPool), + expectedDomain.allowedCaller, + expectedDomain.domainIdentifier, + s_mockUSDC.DESTINATION_TOKEN_MESSENGER(), + expectedDomain.allowedCaller + ); + + vm.expectEmit(); + emit TokenPool.Burned(s_routerAllowedOnRamp, amount); + + Pool.LockOrBurnOutV1 memory poolReturnDataV1 = s_usdcTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: abi.encodePacked(destinationReceiver), + amount: amount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + + uint64 nonce = abi.decode(poolReturnDataV1.destPoolData, (uint64)); + assertEq(s_mockUSDC.s_nonce() - 1, nonce); + assertEq(poolReturnDataV1.destTokenAddress, abi.encode(DEST_CHAIN_USDC_TOKEN)); + } + + function test_Fuzz_LockOrBurnWithAllowList_Success(bytes32 destinationReceiver, uint256 amount) public { + vm.assume(destinationReceiver != bytes32(0)); + amount = bound(amount, 1, getOutboundRateLimiterConfig().capacity); + s_token.transfer(address(s_usdcTokenPoolWithAllowList), amount); + vm.startPrank(s_routerAllowedOnRamp); + + USDCTokenPool.Domain memory expectedDomain = s_usdcTokenPoolWithAllowList.getDomain(DEST_CHAIN_SELECTOR); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(amount); + vm.expectEmit(); + emit ITokenMessenger.DepositForBurn( + s_mockUSDC.s_nonce(), + address(s_token), + amount, + address(s_usdcTokenPoolWithAllowList), + expectedDomain.allowedCaller, + expectedDomain.domainIdentifier, + s_mockUSDC.DESTINATION_TOKEN_MESSENGER(), + expectedDomain.allowedCaller + ); + vm.expectEmit(); + emit TokenPool.Burned(s_routerAllowedOnRamp, amount); + + Pool.LockOrBurnOutV1 memory poolReturnDataV1 = s_usdcTokenPoolWithAllowList.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: s_allowedList[0], + receiver: abi.encodePacked(destinationReceiver), + amount: amount, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + uint64 nonce = abi.decode(poolReturnDataV1.destPoolData, (uint64)); + assertEq(s_mockUSDC.s_nonce() - 1, nonce); + assertEq(poolReturnDataV1.destTokenAddress, abi.encode(DEST_CHAIN_USDC_TOKEN)); + } + + // Reverts + function test_UnknownDomain_Revert() public { + uint64 wrongDomain = DEST_CHAIN_SELECTOR + 1; + // We need to setup the wrong chainSelector so it reaches the domain check + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + onRampUpdates[0] = Router.OnRamp({destChainSelector: wrongDomain, onRamp: s_routerAllowedOnRamp}); + s_router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), new Router.OffRamp[](0)); + + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: wrongDomain, + remotePoolAddress: abi.encode(address(1)), + remoteTokenAddress: abi.encode(address(2)), + allowed: true, + outboundRateLimiterConfig: getOutboundRateLimiterConfig(), + inboundRateLimiterConfig: getInboundRateLimiterConfig() + }); + + s_usdcTokenPool.applyChainUpdates(chainUpdates); + + uint256 amount = 1000; + vm.startPrank(s_routerAllowedOnRamp); + deal(address(s_token), s_routerAllowedOnRamp, amount); + s_token.approve(address(s_usdcTokenPool), amount); + + vm.expectRevert(abi.encodeWithSelector(USDCTokenPool.UnknownDomain.selector, wrongDomain)); + + s_usdcTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: abi.encodePacked(address(0)), + amount: amount, + remoteChainSelector: wrongDomain, + localToken: address(s_token) + }) + ); + } + + function test_CallerIsNotARampOnRouter_Revert() public { + vm.expectRevert(abi.encodeWithSelector(TokenPool.CallerIsNotARampOnRouter.selector, OWNER)); + + s_usdcTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: abi.encodePacked(address(0)), + amount: 0, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + } + + function test_LockOrBurnWithAllowList_Revert() public { + vm.startPrank(s_routerAllowedOnRamp); + + vm.expectRevert(abi.encodeWithSelector(TokenPool.SenderNotAllowed.selector, STRANGER)); + + s_usdcTokenPoolWithAllowList.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: STRANGER, + receiver: abi.encodePacked(address(0)), + amount: 1000, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + } + + function test_lockOrBurn_InvalidReceiver_Revert() public { + vm.startPrank(s_routerAllowedOnRamp); + + bytes memory receiver = abi.encodePacked(address(0), address(1)); + + vm.expectRevert(abi.encodeWithSelector(USDCTokenPool.InvalidReceiver.selector, receiver)); + + s_usdcTokenPool.lockOrBurn( + Pool.LockOrBurnInV1({ + originalSender: OWNER, + receiver: receiver, + amount: 1, + remoteChainSelector: DEST_CHAIN_SELECTOR, + localToken: address(s_token) + }) + ); + } +} + +contract USDCTokenPool_releaseOrMint is USDCTokenPoolSetup { + function test_Fuzz_ReleaseOrMint_Success(address recipient, uint256 amount) public { + vm.assume(recipient != address(0) && recipient != address(s_token)); + amount = bound(amount, 0, getInboundRateLimiterConfig().capacity); + + USDCMessage memory usdcMessage = USDCMessage({ + version: 0, + sourceDomain: SOURCE_DOMAIN_IDENTIFIER, + destinationDomain: DEST_DOMAIN_IDENTIFIER, + nonce: 0x060606060606, + sender: SOURCE_CHAIN_TOKEN_SENDER, + recipient: bytes32(uint256(uint160(recipient))), + destinationCaller: bytes32(uint256(uint160(address(s_usdcTokenPool)))), + messageBody: bytes("") + }); + + bytes memory message = _generateUSDCMessage(usdcMessage); + bytes memory attestation = bytes("attestation bytes"); + + Internal.SourceTokenData memory sourceTokenData = Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(SOURCE_CHAIN_USDC_POOL), + destTokenAddress: abi.encode(address(s_usdcTokenPool)), + extraData: abi.encode( + USDCTokenPool.SourceTokenDataPayload({nonce: usdcMessage.nonce, sourceDomain: SOURCE_DOMAIN_IDENTIFIER}) + ) + }); + + bytes memory offchainTokenData = + abi.encode(USDCTokenPool.MessageAndAttestation({message: message, attestation: attestation})); + + // The mocked receiver does not release the token to the pool, so we manually do it here + deal(address(s_token), address(s_usdcTokenPool), amount); + + vm.expectEmit(); + emit TokenPool.Minted(s_routerAllowedOffRamp, recipient, amount); + + vm.expectCall( + address(s_mockUSDCTransmitter), + abi.encodeWithSelector(MockE2EUSDCTransmitter.receiveMessage.selector, message, attestation) + ); + + vm.startPrank(s_routerAllowedOffRamp); + s_usdcTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + receiver: recipient, + amount: amount, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData + }) + ); + } + + // https://etherscan.io/tx/0xac9f501fe0b76df1f07a22e1db30929fd12524bc7068d74012dff948632f0883 + function test_ReleaseOrMintRealTx_Success() public { + bytes memory encodedUsdcMessage = + hex"000000000000000300000000000000000000127a00000000000000000000000019330d10d9cc8751218eaf51e8885d058642e08a000000000000000000000000bd3fa81b58ba92a82136038b25adec7066af3155000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000004af08f56978be7dce2d1be3c65c005b41e79401c000000000000000000000000000000000000000000000000000000002057ff7a0000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000000000000000000000000000000000000000000000000000000000008274119237535fd659626b090f87e365ff89ebc7096bb32e8b0e85f155626b73ae7c4bb2485c184b7cc3cf7909045487890b104efb62ae74a73e32901bdcec91df1bb9ee08ccb014fcbcfe77b74d1263fd4e0b0e8de05d6c9a5913554364abfd5ea768b222f50c715908183905d74044bb2b97527c7e70ae7983c443a603557cac3b1c000000000000000000000000000000000000000000000000000000000000"; + bytes memory attestation = bytes("attestation bytes"); + + uint32 nonce = 4730; + uint32 sourceDomain = 3; + uint256 amount = 100; + + Internal.SourceTokenData memory sourceTokenData = Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(SOURCE_CHAIN_USDC_POOL), + destTokenAddress: abi.encode(address(s_usdcTokenPool)), + extraData: abi.encode(USDCTokenPool.SourceTokenDataPayload({nonce: nonce, sourceDomain: sourceDomain})) + }); + + // The mocked receiver does not release the token to the pool, so we manually do it here + deal(address(s_token), address(s_usdcTokenPool), amount); + + bytes memory offchainTokenData = + abi.encode(USDCTokenPool.MessageAndAttestation({message: encodedUsdcMessage, attestation: attestation})); + + vm.expectCall( + address(s_mockUSDCTransmitter), + abi.encodeWithSelector(MockE2EUSDCTransmitter.receiveMessage.selector, encodedUsdcMessage, attestation) + ); + + vm.startPrank(s_routerAllowedOffRamp); + s_usdcTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + receiver: OWNER, + amount: amount, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData + }) + ); + } + + // Reverts + function test_UnlockingUSDCFailed_Revert() public { + vm.startPrank(s_routerAllowedOffRamp); + s_mockUSDCTransmitter.setShouldSucceed(false); + + uint256 amount = 13255235235; + + USDCMessage memory usdcMessage = USDCMessage({ + version: 0, + sourceDomain: SOURCE_DOMAIN_IDENTIFIER, + destinationDomain: DEST_DOMAIN_IDENTIFIER, + nonce: 0x060606060606, + sender: SOURCE_CHAIN_TOKEN_SENDER, + recipient: bytes32(uint256(uint160(address(s_mockUSDC)))), + destinationCaller: bytes32(uint256(uint160(address(s_usdcTokenPool)))), + messageBody: bytes("") + }); + + Internal.SourceTokenData memory sourceTokenData = Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(SOURCE_CHAIN_USDC_POOL), + destTokenAddress: abi.encode(address(s_usdcTokenPool)), + extraData: abi.encode( + USDCTokenPool.SourceTokenDataPayload({nonce: usdcMessage.nonce, sourceDomain: SOURCE_DOMAIN_IDENTIFIER}) + ) + }); + + bytes memory offchainTokenData = abi.encode( + USDCTokenPool.MessageAndAttestation({message: _generateUSDCMessage(usdcMessage), attestation: bytes("")}) + ); + + vm.expectRevert(USDCTokenPool.UnlockingUSDCFailed.selector); + + s_usdcTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + receiver: OWNER, + amount: amount, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData + }) + ); + } + + function test_TokenMaxCapacityExceeded_Revert() public { + uint256 capacity = getInboundRateLimiterConfig().capacity; + uint256 amount = 10 * capacity; + address recipient = address(1); + vm.startPrank(s_routerAllowedOffRamp); + + Internal.SourceTokenData memory sourceTokenData = Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(SOURCE_CHAIN_USDC_POOL), + destTokenAddress: abi.encode(address(s_usdcTokenPool)), + extraData: abi.encode(USDCTokenPool.SourceTokenDataPayload({nonce: 1, sourceDomain: SOURCE_DOMAIN_IDENTIFIER})) + }); + + bytes memory offchainTokenData = + abi.encode(USDCTokenPool.MessageAndAttestation({message: bytes(""), attestation: bytes("")})); + + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.TokenMaxCapacityExceeded.selector, capacity, amount, address(s_token)) + ); + + s_usdcTokenPool.releaseOrMint( + Pool.ReleaseOrMintInV1({ + originalSender: abi.encode(OWNER), + receiver: recipient, + amount: amount, + localToken: address(s_token), + remoteChainSelector: SOURCE_CHAIN_SELECTOR, + sourcePoolAddress: sourceTokenData.sourcePoolAddress, + sourcePoolData: sourceTokenData.extraData, + offchainTokenData: offchainTokenData + }) + ); + } +} + +contract USDCTokenPool_supportsInterface is USDCTokenPoolSetup { + function test_SupportsInterface_Success() public view { + assertTrue(s_usdcTokenPool.supportsInterface(type(IPoolV1).interfaceId)); + assertTrue(s_usdcTokenPool.supportsInterface(type(IERC165).interfaceId)); + } +} + +contract USDCTokenPool_setDomains is USDCTokenPoolSetup { + mapping(uint64 destChainSelector => USDCTokenPool.Domain domain) private s_chainToDomain; + + // Setting lower fuzz run as 256 runs was causing differing gas results in snapshot. + /// forge-config: default.fuzz.runs = 32 + /// forge-config: ccip.fuzz.runs = 32 + function test_Fuzz_SetDomains_Success( + bytes32[5] calldata allowedCallers, + uint32[5] calldata domainIdentifiers, + uint64[5] calldata destChainSelectors + ) public { + uint256 numberOfDomains = allowedCallers.length; + USDCTokenPool.DomainUpdate[] memory domainUpdates = new USDCTokenPool.DomainUpdate[](numberOfDomains); + for (uint256 i = 0; i < numberOfDomains; ++i) { + vm.assume(allowedCallers[i] != bytes32(0) && domainIdentifiers[i] != 0 && destChainSelectors[i] != 0); + + domainUpdates[i] = USDCTokenPool.DomainUpdate({ + allowedCaller: allowedCallers[i], + domainIdentifier: domainIdentifiers[i], + destChainSelector: destChainSelectors[i], + enabled: true + }); + + s_chainToDomain[destChainSelectors[i]] = + USDCTokenPool.Domain({domainIdentifier: domainIdentifiers[i], allowedCaller: allowedCallers[i], enabled: true}); + } + + vm.expectEmit(); + emit USDCTokenPool.DomainsSet(domainUpdates); + + s_usdcTokenPool.setDomains(domainUpdates); + + for (uint256 i = 0; i < numberOfDomains; ++i) { + USDCTokenPool.Domain memory expected = s_chainToDomain[destChainSelectors[i]]; + USDCTokenPool.Domain memory got = s_usdcTokenPool.getDomain(destChainSelectors[i]); + assertEq(got.allowedCaller, expected.allowedCaller); + assertEq(got.domainIdentifier, expected.domainIdentifier); + } + } + + // Reverts + + function test_OnlyOwner_Revert() public { + USDCTokenPool.DomainUpdate[] memory domainUpdates = new USDCTokenPool.DomainUpdate[](0); + + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + + s_usdcTokenPool.setDomains(domainUpdates); + } + + function test_InvalidDomain_Revert() public { + bytes32 validCaller = bytes32(uint256(25)); + // Ensure valid domain works + USDCTokenPool.DomainUpdate[] memory domainUpdates = new USDCTokenPool.DomainUpdate[](1); + domainUpdates[0] = USDCTokenPool.DomainUpdate({ + allowedCaller: validCaller, + domainIdentifier: 0, // ensures 0 is valid, as this is eth mainnet + destChainSelector: 45690, + enabled: true + }); + + s_usdcTokenPool.setDomains(domainUpdates); + + // Make update invalid on allowedCaller + domainUpdates[0].allowedCaller = bytes32(0); + vm.expectRevert(abi.encodeWithSelector(USDCTokenPool.InvalidDomain.selector, domainUpdates[0])); + + s_usdcTokenPool.setDomains(domainUpdates); + + // Make valid again + domainUpdates[0].allowedCaller = validCaller; + + // Make invalid on destChainSelector + domainUpdates[0].destChainSelector = 0; + vm.expectRevert(abi.encodeWithSelector(USDCTokenPool.InvalidDomain.selector, domainUpdates[0])); + + s_usdcTokenPool.setDomains(domainUpdates); + } +} + +contract USDCTokenPool__validateMessage is USDCTokenPoolSetup { + function test_Fuzz_ValidateMessage_Success(uint32 sourceDomain, uint64 nonce) public { + vm.pauseGasMetering(); + USDCMessage memory usdcMessage = USDCMessage({ + version: 0, + sourceDomain: sourceDomain, + destinationDomain: DEST_DOMAIN_IDENTIFIER, + nonce: nonce, + sender: SOURCE_CHAIN_TOKEN_SENDER, + recipient: bytes32(uint256(299999)), + destinationCaller: bytes32(uint256(uint160(address(s_usdcTokenPool)))), + messageBody: bytes("") + }); + + bytes memory encodedUsdcMessage = _generateUSDCMessage(usdcMessage); + + vm.resumeGasMetering(); + s_usdcTokenPool.validateMessage( + encodedUsdcMessage, USDCTokenPool.SourceTokenDataPayload({nonce: nonce, sourceDomain: sourceDomain}) + ); + } + + // Reverts + + function test_ValidateInvalidMessage_Revert() public { + USDCMessage memory usdcMessage = USDCMessage({ + version: 0, + sourceDomain: 1553252, + destinationDomain: DEST_DOMAIN_IDENTIFIER, + nonce: 387289284924, + sender: SOURCE_CHAIN_TOKEN_SENDER, + recipient: bytes32(uint256(92398429395823)), + destinationCaller: bytes32(uint256(uint160(address(s_usdcTokenPool)))), + messageBody: bytes("") + }); + + USDCTokenPool.SourceTokenDataPayload memory sourceTokenData = + USDCTokenPool.SourceTokenDataPayload({nonce: usdcMessage.nonce, sourceDomain: usdcMessage.sourceDomain}); + + bytes memory encodedUsdcMessage = _generateUSDCMessage(usdcMessage); + + s_usdcTokenPool.validateMessage(encodedUsdcMessage, sourceTokenData); + + uint32 expectedSourceDomain = usdcMessage.sourceDomain + 1; + + vm.expectRevert( + abi.encodeWithSelector(USDCTokenPool.InvalidSourceDomain.selector, expectedSourceDomain, usdcMessage.sourceDomain) + ); + s_usdcTokenPool.validateMessage( + encodedUsdcMessage, + USDCTokenPool.SourceTokenDataPayload({nonce: usdcMessage.nonce, sourceDomain: expectedSourceDomain}) + ); + + uint64 expectedNonce = usdcMessage.nonce + 1; + + vm.expectRevert(abi.encodeWithSelector(USDCTokenPool.InvalidNonce.selector, expectedNonce, usdcMessage.nonce)); + s_usdcTokenPool.validateMessage( + encodedUsdcMessage, + USDCTokenPool.SourceTokenDataPayload({nonce: expectedNonce, sourceDomain: usdcMessage.sourceDomain}) + ); + + usdcMessage.destinationDomain = DEST_DOMAIN_IDENTIFIER + 1; + vm.expectRevert( + abi.encodeWithSelector( + USDCTokenPool.InvalidDestinationDomain.selector, DEST_DOMAIN_IDENTIFIER, usdcMessage.destinationDomain + ) + ); + + s_usdcTokenPool.validateMessage( + _generateUSDCMessage(usdcMessage), + USDCTokenPool.SourceTokenDataPayload({nonce: usdcMessage.nonce, sourceDomain: usdcMessage.sourceDomain}) + ); + usdcMessage.destinationDomain = DEST_DOMAIN_IDENTIFIER; + + uint32 wrongVersion = usdcMessage.version + 1; + + usdcMessage.version = wrongVersion; + encodedUsdcMessage = _generateUSDCMessage(usdcMessage); + + vm.expectRevert(abi.encodeWithSelector(USDCTokenPool.InvalidMessageVersion.selector, wrongVersion)); + s_usdcTokenPool.validateMessage(encodedUsdcMessage, sourceTokenData); + } +} diff --git a/contracts/src/v0.8/ccip/test/priceRegistry/PriceRegistry.t.sol b/contracts/src/v0.8/ccip/test/priceRegistry/PriceRegistry.t.sol new file mode 100644 index 00000000000..c3c22ef2909 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/priceRegistry/PriceRegistry.t.sol @@ -0,0 +1,2542 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IPriceRegistry} from "../../interfaces/IPriceRegistry.sol"; +import {ITokenAdminRegistry} from "../../interfaces/ITokenAdminRegistry.sol"; + +import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol"; +import {BurnMintERC677} from "../../../shared/token/ERC677/BurnMintERC677.sol"; +import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol"; +import {PriceRegistry} from "../../PriceRegistry.sol"; + +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {Pool} from "../../libraries/Pool.sol"; +import {USDPriceWith18Decimals} from "../../libraries/USDPriceWith18Decimals.sol"; +import {LockReleaseTokenPool} from "../../pools/LockReleaseTokenPool.sol"; +import {TokenPool} from "../../pools/TokenPool.sol"; +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; + +import {TokenSetup} from "../TokenSetup.t.sol"; +import {MaybeRevertingBurnMintTokenPool} from "../helpers/MaybeRevertingBurnMintTokenPool.sol"; +import {PriceRegistryHelper} from "../helpers/PriceRegistryHelper.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +import {Vm} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; + +contract PriceRegistrySetup is TokenSetup { + uint112 internal constant USD_PER_GAS = 1e6; // 0.001 gwei + uint112 internal constant USD_PER_DATA_AVAILABILITY_GAS = 1e9; // 1 gwei + + address internal constant CUSTOM_TOKEN = address(12345); + uint224 internal constant CUSTOM_TOKEN_PRICE = 1e17; // $0.1 CUSTOM + + // Encode L1 gas price and L2 gas price into a packed price. + // L1 gas price is left-shifted to the higher-order bits. + uint224 internal constant PACKED_USD_PER_GAS = + (uint224(USD_PER_DATA_AVAILABILITY_GAS) << Internal.GAS_PRICE_BITS) + USD_PER_GAS; + + PriceRegistryHelper internal s_priceRegistry; + // Cheat to store the price updates in storage since struct arrays aren't supported. + bytes internal s_encodedInitialPriceUpdates; + address internal s_weth; + + address[] internal s_sourceFeeTokens; + uint224[] internal s_sourceTokenPrices; + address[] internal s_destFeeTokens; + uint224[] internal s_destTokenPrices; + + PriceRegistry.PremiumMultiplierWeiPerEthArgs[] internal s_priceRegistryPremiumMultiplierWeiPerEthArgs; + PriceRegistry.TokenTransferFeeConfigArgs[] internal s_priceRegistryTokenTransferFeeConfigArgs; + + mapping(address token => address dataFeedAddress) internal s_dataFeedByToken; + + function setUp() public virtual override { + TokenSetup.setUp(); + + _deployTokenPriceDataFeed(s_sourceFeeToken, 8, 1e8); + + s_weth = s_sourceRouter.getWrappedNative(); + _deployTokenPriceDataFeed(s_weth, 8, 1e11); + + address[] memory sourceFeeTokens = new address[](3); + sourceFeeTokens[0] = s_sourceTokens[0]; + sourceFeeTokens[1] = s_sourceTokens[1]; + sourceFeeTokens[2] = s_sourceRouter.getWrappedNative(); + s_sourceFeeTokens = sourceFeeTokens; + + uint224[] memory sourceTokenPrices = new uint224[](3); + sourceTokenPrices[0] = 5e18; + sourceTokenPrices[1] = 2000e18; + sourceTokenPrices[2] = 2000e18; + s_sourceTokenPrices = sourceTokenPrices; + + address[] memory destFeeTokens = new address[](3); + destFeeTokens[0] = s_destTokens[0]; + destFeeTokens[1] = s_destTokens[1]; + destFeeTokens[2] = s_destRouter.getWrappedNative(); + s_destFeeTokens = destFeeTokens; + + uint224[] memory destTokenPrices = new uint224[](3); + destTokenPrices[0] = 5e18; + destTokenPrices[1] = 2000e18; + destTokenPrices[2] = 2000e18; + s_destTokenPrices = destTokenPrices; + + uint256 sourceTokenCount = sourceFeeTokens.length; + uint256 destTokenCount = destFeeTokens.length; + address[] memory pricedTokens = new address[](sourceTokenCount + destTokenCount); + uint224[] memory tokenPrices = new uint224[](sourceTokenCount + destTokenCount); + for (uint256 i = 0; i < sourceTokenCount; ++i) { + pricedTokens[i] = sourceFeeTokens[i]; + tokenPrices[i] = sourceTokenPrices[i]; + } + for (uint256 i = 0; i < destTokenCount; ++i) { + pricedTokens[i + sourceTokenCount] = destFeeTokens[i]; + tokenPrices[i + sourceTokenCount] = destTokenPrices[i]; + } + + Internal.PriceUpdates memory priceUpdates = getPriceUpdatesStruct(pricedTokens, tokenPrices); + priceUpdates.gasPriceUpdates = + getSingleGasPriceUpdateStruct(DEST_CHAIN_SELECTOR, PACKED_USD_PER_GAS).gasPriceUpdates; + + s_encodedInitialPriceUpdates = abi.encode(priceUpdates); + + address[] memory priceUpdaters = new address[](1); + priceUpdaters[0] = OWNER; + address[] memory feeTokens = new address[](2); + feeTokens[0] = s_sourceTokens[0]; + feeTokens[1] = s_weth; + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](0); + + s_priceRegistryPremiumMultiplierWeiPerEthArgs.push( + PriceRegistry.PremiumMultiplierWeiPerEthArgs({ + token: s_sourceFeeToken, + premiumMultiplierWeiPerEth: 5e17 // 0.5x + }) + ); + s_priceRegistryPremiumMultiplierWeiPerEthArgs.push( + PriceRegistry.PremiumMultiplierWeiPerEthArgs({ + token: s_sourceRouter.getWrappedNative(), + premiumMultiplierWeiPerEth: 2e18 // 2x + }) + ); + + s_priceRegistryTokenTransferFeeConfigArgs.push(); + s_priceRegistryTokenTransferFeeConfigArgs[0].destChainSelector = DEST_CHAIN_SELECTOR; + s_priceRegistryTokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs.push( + PriceRegistry.TokenTransferFeeConfigSingleTokenArgs({ + token: s_sourceFeeToken, + tokenTransferFeeConfig: PriceRegistry.TokenTransferFeeConfig({ + minFeeUSDCents: 1_00, // 1 USD + maxFeeUSDCents: 1000_00, // 1,000 USD + deciBps: 2_5, // 2.5 bps, or 0.025% + destGasOverhead: 40_000, + destBytesOverhead: 32, + isEnabled: true + }) + }) + ); + s_priceRegistryTokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs.push( + PriceRegistry.TokenTransferFeeConfigSingleTokenArgs({ + token: s_sourceRouter.getWrappedNative(), + tokenTransferFeeConfig: PriceRegistry.TokenTransferFeeConfig({ + minFeeUSDCents: 50, // 0.5 USD + maxFeeUSDCents: 500_00, // 500 USD + deciBps: 5_0, // 5 bps, or 0.05% + destGasOverhead: 10_000, + destBytesOverhead: 100, + isEnabled: true + }) + }) + ); + s_priceRegistryTokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs.push( + PriceRegistry.TokenTransferFeeConfigSingleTokenArgs({ + token: CUSTOM_TOKEN, + tokenTransferFeeConfig: PriceRegistry.TokenTransferFeeConfig({ + minFeeUSDCents: 2_00, // 1 USD + maxFeeUSDCents: 2000_00, // 1,000 USD + deciBps: 10_0, // 10 bps, or 0.1% + destGasOverhead: 1, + destBytesOverhead: 200, + isEnabled: true + }) + }) + ); + + s_priceRegistry = new PriceRegistryHelper( + PriceRegistry.StaticConfig({ + linkToken: s_sourceTokens[0], + maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS, + stalenessThreshold: uint32(TWELVE_HOURS) + }), + priceUpdaters, + feeTokens, + tokenPriceFeedUpdates, + s_priceRegistryTokenTransferFeeConfigArgs, + s_priceRegistryPremiumMultiplierWeiPerEthArgs, + _generatePriceRegistryDestChainConfigArgs() + ); + s_priceRegistry.updatePrices(priceUpdates); + } + + function _deployTokenPriceDataFeed(address token, uint8 decimals, int256 initialAnswer) internal returns (address) { + MockV3Aggregator dataFeed = new MockV3Aggregator(decimals, initialAnswer); + s_dataFeedByToken[token] = address(dataFeed); + return address(dataFeed); + } + + function getPriceUpdatesStruct( + address[] memory tokens, + uint224[] memory prices + ) internal pure returns (Internal.PriceUpdates memory) { + uint256 length = tokens.length; + + Internal.TokenPriceUpdate[] memory tokenPriceUpdates = new Internal.TokenPriceUpdate[](length); + for (uint256 i = 0; i < length; ++i) { + tokenPriceUpdates[i] = Internal.TokenPriceUpdate({sourceToken: tokens[i], usdPerToken: prices[i]}); + } + Internal.PriceUpdates memory priceUpdates = + Internal.PriceUpdates({tokenPriceUpdates: tokenPriceUpdates, gasPriceUpdates: new Internal.GasPriceUpdate[](0)}); + + return priceUpdates; + } + + function getEmptyPriceUpdates() internal pure returns (Internal.PriceUpdates memory priceUpdates) { + return Internal.PriceUpdates({ + tokenPriceUpdates: new Internal.TokenPriceUpdate[](0), + gasPriceUpdates: new Internal.GasPriceUpdate[](0) + }); + } + + function getSingleTokenPriceFeedUpdateStruct( + address sourceToken, + address dataFeedAddress, + uint8 tokenDecimals + ) internal pure returns (PriceRegistry.TokenPriceFeedUpdate memory) { + return PriceRegistry.TokenPriceFeedUpdate({ + sourceToken: sourceToken, + feedConfig: IPriceRegistry.TokenPriceFeedConfig({dataFeedAddress: dataFeedAddress, tokenDecimals: tokenDecimals}) + }); + } + + function _initialiseSingleTokenPriceFeed() internal returns (address) { + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = + getSingleTokenPriceFeedUpdateStruct(s_sourceTokens[0], s_dataFeedByToken[s_sourceTokens[0]], 18); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + return s_sourceTokens[0]; + } + + function _generateTokenTransferFeeConfigArgs( + uint256 destChainSelectorLength, + uint256 tokenLength + ) internal pure returns (PriceRegistry.TokenTransferFeeConfigArgs[] memory) { + PriceRegistry.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + new PriceRegistry.TokenTransferFeeConfigArgs[](destChainSelectorLength); + for (uint256 i = 0; i < destChainSelectorLength; ++i) { + tokenTransferFeeConfigArgs[i].tokenTransferFeeConfigs = + new PriceRegistry.TokenTransferFeeConfigSingleTokenArgs[](tokenLength); + } + return tokenTransferFeeConfigArgs; + } + + function _generatePriceRegistryDestChainConfigArgs() + internal + pure + returns (PriceRegistry.DestChainConfigArgs[] memory) + { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigs = new PriceRegistry.DestChainConfigArgs[](1); + destChainConfigs[0] = PriceRegistry.DestChainConfigArgs({ + destChainSelector: DEST_CHAIN_SELECTOR, + destChainConfig: PriceRegistry.DestChainConfig({ + isEnabled: true, + maxNumberOfTokensPerMsg: MAX_TOKENS_LENGTH, + destGasOverhead: DEST_GAS_OVERHEAD, + destGasPerPayloadByte: DEST_GAS_PER_PAYLOAD_BYTE, + destDataAvailabilityOverheadGas: DEST_DATA_AVAILABILITY_OVERHEAD_GAS, + destGasPerDataAvailabilityByte: DEST_GAS_PER_DATA_AVAILABILITY_BYTE, + destDataAvailabilityMultiplierBps: DEST_GAS_DATA_AVAILABILITY_MULTIPLIER_BPS, + maxDataBytes: MAX_DATA_SIZE, + maxPerMsgGasLimit: MAX_GAS_LIMIT, + defaultTokenFeeUSDCents: DEFAULT_TOKEN_FEE_USD_CENTS, + defaultTokenDestGasOverhead: DEFAULT_TOKEN_DEST_GAS_OVERHEAD, + defaultTokenDestBytesOverhead: DEFAULT_TOKEN_BYTES_OVERHEAD, + defaultTxGasLimit: GAS_LIMIT, + gasMultiplierWeiPerEth: 5e17, + networkFeeUSDCents: 1_00, + enforceOutOfOrder: false, + chainFamilySelector: Internal.CHAIN_FAMILY_SELECTOR_EVM + }) + }); + return destChainConfigs; + } + + function _assertTokenPriceFeedConfigEquality( + IPriceRegistry.TokenPriceFeedConfig memory config1, + IPriceRegistry.TokenPriceFeedConfig memory config2 + ) internal pure virtual { + assertEq(config1.dataFeedAddress, config2.dataFeedAddress); + assertEq(config1.tokenDecimals, config2.tokenDecimals); + } + + function _assertTokenPriceFeedConfigUnconfigured(IPriceRegistry.TokenPriceFeedConfig memory config) + internal + pure + virtual + { + _assertTokenPriceFeedConfigEquality( + config, IPriceRegistry.TokenPriceFeedConfig({dataFeedAddress: address(0), tokenDecimals: 0}) + ); + } + + function _assertTokenTransferFeeConfigEqual( + PriceRegistry.TokenTransferFeeConfig memory a, + PriceRegistry.TokenTransferFeeConfig memory b + ) internal pure { + assertEq(a.minFeeUSDCents, b.minFeeUSDCents); + assertEq(a.maxFeeUSDCents, b.maxFeeUSDCents); + assertEq(a.deciBps, b.deciBps); + assertEq(a.destGasOverhead, b.destGasOverhead); + assertEq(a.destBytesOverhead, b.destBytesOverhead); + assertEq(a.isEnabled, b.isEnabled); + } + + function _assertPriceRegistryStaticConfigsEqual( + PriceRegistry.StaticConfig memory a, + PriceRegistry.StaticConfig memory b + ) internal pure { + assertEq(a.linkToken, b.linkToken); + assertEq(a.maxFeeJuelsPerMsg, b.maxFeeJuelsPerMsg); + } + + function _assertPriceRegistryDestChainConfigsEqual( + PriceRegistry.DestChainConfig memory a, + PriceRegistry.DestChainConfig memory b + ) internal pure { + assertEq(a.isEnabled, b.isEnabled); + assertEq(a.maxNumberOfTokensPerMsg, b.maxNumberOfTokensPerMsg); + assertEq(a.maxDataBytes, b.maxDataBytes); + assertEq(a.maxPerMsgGasLimit, b.maxPerMsgGasLimit); + assertEq(a.destGasOverhead, b.destGasOverhead); + assertEq(a.destGasPerPayloadByte, b.destGasPerPayloadByte); + assertEq(a.destDataAvailabilityOverheadGas, b.destDataAvailabilityOverheadGas); + assertEq(a.destGasPerDataAvailabilityByte, b.destGasPerDataAvailabilityByte); + assertEq(a.destDataAvailabilityMultiplierBps, b.destDataAvailabilityMultiplierBps); + assertEq(a.defaultTokenFeeUSDCents, b.defaultTokenFeeUSDCents); + assertEq(a.defaultTokenDestGasOverhead, b.defaultTokenDestGasOverhead); + assertEq(a.defaultTokenDestBytesOverhead, b.defaultTokenDestBytesOverhead); + assertEq(a.defaultTxGasLimit, b.defaultTxGasLimit); + } +} + +contract PriceRegistryFeeSetup is PriceRegistrySetup { + uint224 internal s_feeTokenPrice; + uint224 internal s_wrappedTokenPrice; + uint224 internal s_customTokenPrice; + + address internal s_selfServeTokenDefaultPricing = makeAddr("self-serve-token-default-pricing"); + + address internal s_destTokenPool = makeAddr("destTokenPool"); + address internal s_destToken = makeAddr("destToken"); + + function setUp() public virtual override { + super.setUp(); + + s_feeTokenPrice = s_sourceTokenPrices[0]; + s_wrappedTokenPrice = s_sourceTokenPrices[2]; + s_customTokenPrice = CUSTOM_TOKEN_PRICE; + + s_priceRegistry.updatePrices(getSingleTokenPriceUpdateStruct(CUSTOM_TOKEN, CUSTOM_TOKEN_PRICE)); + } + + function _generateEmptyMessage() public view returns (Client.EVM2AnyMessage memory) { + return Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](0), + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + } + + function _generateSingleTokenMessage( + address token, + uint256 amount + ) public view returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount}); + + return Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + } + + function _messageToEvent( + Client.EVM2AnyMessage memory message, + uint64 sourceChainSelector, + uint64 destChainSelector, + uint64 seqNum, + uint64 nonce, + uint256 feeTokenAmount, + address originalSender, + bytes32 metadataHash, + TokenAdminRegistry tokenAdminRegistry + ) internal view returns (Internal.EVM2AnyRampMessage memory) { + Client.EVMExtraArgsV2 memory extraArgs = + s_priceRegistry.parseEVMExtraArgsFromBytes(message.extraArgs, destChainSelector); + + Internal.EVM2AnyRampMessage memory messageEvent = Internal.EVM2AnyRampMessage({ + header: Internal.RampMessageHeader({ + messageId: "", + sourceChainSelector: sourceChainSelector, + destChainSelector: destChainSelector, + sequenceNumber: seqNum, + nonce: extraArgs.allowOutOfOrderExecution ? 0 : nonce + }), + sender: originalSender, + data: message.data, + receiver: message.receiver, + extraArgs: Client._argsToBytes(extraArgs), + feeToken: message.feeToken, + feeTokenAmount: feeTokenAmount, + tokenAmounts: new Internal.RampTokenAmount[](message.tokenAmounts.length) + }); + + for (uint256 i = 0; i < message.tokenAmounts.length; ++i) { + messageEvent.tokenAmounts[i] = _getSourceTokenData(message.tokenAmounts[i], tokenAdminRegistry); + } + + messageEvent.header.messageId = Internal._hash(messageEvent, metadataHash); + return messageEvent; + } + + function _getSourceTokenData( + Client.EVMTokenAmount memory tokenAmount, + TokenAdminRegistry tokenAdminRegistry + ) internal view returns (Internal.RampTokenAmount memory) { + address destToken = s_destTokenBySourceToken[tokenAmount.token]; + + return Internal.RampTokenAmount({ + sourcePoolAddress: abi.encode(tokenAdminRegistry.getTokenConfig(tokenAmount.token).tokenPool), + destTokenAddress: abi.encode(destToken), + extraData: "", + amount: tokenAmount.amount + }); + } + + function calcUSDValueFromTokenAmount(uint224 tokenPrice, uint256 tokenAmount) internal pure returns (uint256) { + return (tokenPrice * tokenAmount) / 1e18; + } + + function applyBpsRatio(uint256 tokenAmount, uint16 ratio) internal pure returns (uint256) { + return (tokenAmount * ratio) / 1e5; + } + + function configUSDCentToWei(uint256 usdCent) internal pure returns (uint256) { + return usdCent * 1e16; + } +} + +contract PriceRegistry_constructor is PriceRegistrySetup { + function test_Setup_Success() public virtual { + address[] memory priceUpdaters = new address[](2); + priceUpdaters[0] = STRANGER; + priceUpdaters[1] = OWNER; + address[] memory feeTokens = new address[](2); + feeTokens[0] = s_sourceTokens[0]; + feeTokens[1] = s_sourceTokens[1]; + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](2); + tokenPriceFeedUpdates[0] = + getSingleTokenPriceFeedUpdateStruct(s_sourceTokens[0], s_dataFeedByToken[s_sourceTokens[0]], 18); + tokenPriceFeedUpdates[1] = + getSingleTokenPriceFeedUpdateStruct(s_sourceTokens[1], s_dataFeedByToken[s_sourceTokens[1]], 6); + + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + + PriceRegistry.StaticConfig memory staticConfig = PriceRegistry.StaticConfig({ + linkToken: s_sourceTokens[0], + maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS, + stalenessThreshold: uint32(TWELVE_HOURS) + }); + s_priceRegistry = new PriceRegistryHelper( + staticConfig, + priceUpdaters, + feeTokens, + tokenPriceFeedUpdates, + s_priceRegistryTokenTransferFeeConfigArgs, + s_priceRegistryPremiumMultiplierWeiPerEthArgs, + destChainConfigArgs + ); + + _assertPriceRegistryStaticConfigsEqual(s_priceRegistry.getStaticConfig(), staticConfig); + assertEq(feeTokens, s_priceRegistry.getFeeTokens()); + assertEq(priceUpdaters, s_priceRegistry.getAllAuthorizedCallers()); + assertEq(s_priceRegistry.typeAndVersion(), "PriceRegistry 1.6.0-dev"); + + _assertTokenPriceFeedConfigEquality( + tokenPriceFeedUpdates[0].feedConfig, s_priceRegistry.getTokenPriceFeedConfig(s_sourceTokens[0]) + ); + + _assertTokenPriceFeedConfigEquality( + tokenPriceFeedUpdates[1].feedConfig, s_priceRegistry.getTokenPriceFeedConfig(s_sourceTokens[1]) + ); + + assertEq( + s_priceRegistryPremiumMultiplierWeiPerEthArgs[0].premiumMultiplierWeiPerEth, + s_priceRegistry.getPremiumMultiplierWeiPerEth(s_priceRegistryPremiumMultiplierWeiPerEthArgs[0].token) + ); + + assertEq( + s_priceRegistryPremiumMultiplierWeiPerEthArgs[1].premiumMultiplierWeiPerEth, + s_priceRegistry.getPremiumMultiplierWeiPerEth(s_priceRegistryPremiumMultiplierWeiPerEthArgs[1].token) + ); + + PriceRegistry.TokenTransferFeeConfigArgs memory tokenTransferFeeConfigArg = + s_priceRegistryTokenTransferFeeConfigArgs[0]; + for (uint256 i = 0; i < tokenTransferFeeConfigArg.tokenTransferFeeConfigs.length; ++i) { + PriceRegistry.TokenTransferFeeConfigSingleTokenArgs memory tokenFeeArgs = + s_priceRegistryTokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[i]; + + _assertTokenTransferFeeConfigEqual( + tokenFeeArgs.tokenTransferFeeConfig, + s_priceRegistry.getTokenTransferFeeConfig(tokenTransferFeeConfigArg.destChainSelector, tokenFeeArgs.token) + ); + } + + for (uint256 i = 0; i < destChainConfigArgs.length; ++i) { + PriceRegistry.DestChainConfig memory expectedConfig = destChainConfigArgs[i].destChainConfig; + uint64 destChainSelector = destChainConfigArgs[i].destChainSelector; + + _assertPriceRegistryDestChainConfigsEqual(expectedConfig, s_priceRegistry.getDestChainConfig(destChainSelector)); + } + } + + function test_InvalidStalenessThreshold_Revert() public { + PriceRegistry.StaticConfig memory staticConfig = PriceRegistry.StaticConfig({ + linkToken: s_sourceTokens[0], + maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS, + stalenessThreshold: 0 + }); + + vm.expectRevert(PriceRegistry.InvalidStaticConfig.selector); + + s_priceRegistry = new PriceRegistryHelper( + staticConfig, + new address[](0), + new address[](0), + new PriceRegistry.TokenPriceFeedUpdate[](0), + s_priceRegistryTokenTransferFeeConfigArgs, + s_priceRegistryPremiumMultiplierWeiPerEthArgs, + new PriceRegistry.DestChainConfigArgs[](0) + ); + } + + function test_InvalidLinkTokenEqZeroAddress_Revert() public { + PriceRegistry.StaticConfig memory staticConfig = PriceRegistry.StaticConfig({ + linkToken: address(0), + maxFeeJuelsPerMsg: MAX_MSG_FEES_JUELS, + stalenessThreshold: uint32(TWELVE_HOURS) + }); + + vm.expectRevert(PriceRegistry.InvalidStaticConfig.selector); + + s_priceRegistry = new PriceRegistryHelper( + staticConfig, + new address[](0), + new address[](0), + new PriceRegistry.TokenPriceFeedUpdate[](0), + s_priceRegistryTokenTransferFeeConfigArgs, + s_priceRegistryPremiumMultiplierWeiPerEthArgs, + new PriceRegistry.DestChainConfigArgs[](0) + ); + } + + function test_InvalidMaxFeeJuelsPerMsg_Revert() public { + PriceRegistry.StaticConfig memory staticConfig = PriceRegistry.StaticConfig({ + linkToken: s_sourceTokens[0], + maxFeeJuelsPerMsg: 0, + stalenessThreshold: uint32(TWELVE_HOURS) + }); + + vm.expectRevert(PriceRegistry.InvalidStaticConfig.selector); + + s_priceRegistry = new PriceRegistryHelper( + staticConfig, + new address[](0), + new address[](0), + new PriceRegistry.TokenPriceFeedUpdate[](0), + s_priceRegistryTokenTransferFeeConfigArgs, + s_priceRegistryPremiumMultiplierWeiPerEthArgs, + new PriceRegistry.DestChainConfigArgs[](0) + ); + } +} + +contract PriceRegistry_getTokenPrices is PriceRegistrySetup { + function test_GetTokenPrices_Success() public view { + Internal.PriceUpdates memory priceUpdates = abi.decode(s_encodedInitialPriceUpdates, (Internal.PriceUpdates)); + + address[] memory tokens = new address[](3); + tokens[0] = s_sourceTokens[0]; + tokens[1] = s_sourceTokens[1]; + tokens[2] = s_weth; + + Internal.TimestampedPackedUint224[] memory tokenPrices = s_priceRegistry.getTokenPrices(tokens); + + assertEq(tokenPrices.length, 3); + assertEq(tokenPrices[0].value, priceUpdates.tokenPriceUpdates[0].usdPerToken); + assertEq(tokenPrices[1].value, priceUpdates.tokenPriceUpdates[1].usdPerToken); + assertEq(tokenPrices[2].value, priceUpdates.tokenPriceUpdates[2].usdPerToken); + } +} + +contract PriceRegistry_getTokenPrice is PriceRegistrySetup { + function test_GetTokenPriceFromFeed_Success() public { + uint256 originalTimestampValue = block.timestamp; + + // Below staleness threshold + vm.warp(originalTimestampValue + 1 hours); + + address sourceToken = _initialiseSingleTokenPriceFeed(); + Internal.TimestampedPackedUint224 memory tokenPriceAnswer = s_priceRegistry.getTokenPrice(sourceToken); + + // Price answer is 1e8 (18 decimal token) - unit is (1e18 * 1e18 / 1e18) -> expected 1e18 + assertEq(tokenPriceAnswer.value, uint224(1e18)); + assertEq(tokenPriceAnswer.timestamp, uint32(block.timestamp)); + } +} + +contract PriceRegistry_getValidatedTokenPrice is PriceRegistrySetup { + function test_GetValidatedTokenPrice_Success() public view { + Internal.PriceUpdates memory priceUpdates = abi.decode(s_encodedInitialPriceUpdates, (Internal.PriceUpdates)); + address token = priceUpdates.tokenPriceUpdates[0].sourceToken; + + uint224 tokenPrice = s_priceRegistry.getValidatedTokenPrice(token); + + assertEq(priceUpdates.tokenPriceUpdates[0].usdPerToken, tokenPrice); + } + + function test_GetValidatedTokenPriceFromFeed_Success() public { + uint256 originalTimestampValue = block.timestamp; + + // Right below staleness threshold + vm.warp(originalTimestampValue + TWELVE_HOURS); + + address sourceToken = _initialiseSingleTokenPriceFeed(); + uint224 tokenPriceAnswer = s_priceRegistry.getValidatedTokenPrice(sourceToken); + + // Price answer is 1e8 (18 decimal token) - unit is (1e18 * 1e18 / 1e18) -> expected 1e18 + assertEq(tokenPriceAnswer, uint224(1e18)); + } + + function test_GetValidatedTokenPriceFromFeedOverStalenessPeriod_Success() public { + uint256 originalTimestampValue = block.timestamp; + + // Right above staleness threshold + vm.warp(originalTimestampValue + TWELVE_HOURS + 1); + + address sourceToken = _initialiseSingleTokenPriceFeed(); + uint224 tokenPriceAnswer = s_priceRegistry.getValidatedTokenPrice(sourceToken); + + // Price answer is 1e8 (18 decimal token) - unit is (1e18 * 1e18 / 1e18) -> expected 1e18 + assertEq(tokenPriceAnswer, uint224(1e18)); + } + + function test_GetValidatedTokenPriceFromFeedMaxInt224Value_Success() public { + address tokenAddress = _deploySourceToken("testToken", 0, 18); + address feedAddress = _deployTokenPriceDataFeed(tokenAddress, 18, int256(uint256(type(uint224).max))); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = getSingleTokenPriceFeedUpdateStruct(tokenAddress, feedAddress, 18); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + uint224 tokenPriceAnswer = s_priceRegistry.getValidatedTokenPrice(tokenAddress); + + // Price answer is: uint224.MAX_VALUE * (10 ** (36 - 18 - 18)) + assertEq(tokenPriceAnswer, uint224(type(uint224).max)); + } + + function test_GetValidatedTokenPriceFromFeedErc20Below18Decimals_Success() public { + address tokenAddress = _deploySourceToken("testToken", 0, 6); + address feedAddress = _deployTokenPriceDataFeed(tokenAddress, 8, 1e8); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = getSingleTokenPriceFeedUpdateStruct(tokenAddress, feedAddress, 6); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + uint224 tokenPriceAnswer = s_priceRegistry.getValidatedTokenPrice(tokenAddress); + + // Price answer is 1e8 (6 decimal token) - unit is (1e18 * 1e18 / 1e6) -> expected 1e30 + assertEq(tokenPriceAnswer, uint224(1e30)); + } + + function test_GetValidatedTokenPriceFromFeedErc20Above18Decimals_Success() public { + address tokenAddress = _deploySourceToken("testToken", 0, 24); + address feedAddress = _deployTokenPriceDataFeed(tokenAddress, 8, 1e8); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = getSingleTokenPriceFeedUpdateStruct(tokenAddress, feedAddress, 24); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + uint224 tokenPriceAnswer = s_priceRegistry.getValidatedTokenPrice(tokenAddress); + + // Price answer is 1e8 (6 decimal token) - unit is (1e18 * 1e18 / 1e24) -> expected 1e12 + assertEq(tokenPriceAnswer, uint224(1e12)); + } + + function test_GetValidatedTokenPriceFromFeedFeedAt18Decimals_Success() public { + address tokenAddress = _deploySourceToken("testToken", 0, 18); + address feedAddress = _deployTokenPriceDataFeed(tokenAddress, 18, 1e18); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = getSingleTokenPriceFeedUpdateStruct(tokenAddress, feedAddress, 18); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + uint224 tokenPriceAnswer = s_priceRegistry.getValidatedTokenPrice(tokenAddress); + + // Price answer is 1e8 (6 decimal token) - unit is (1e18 * 1e18 / 1e18) -> expected 1e18 + assertEq(tokenPriceAnswer, uint224(1e18)); + } + + function test_GetValidatedTokenPriceFromFeedFeedAt0Decimals_Success() public { + address tokenAddress = _deploySourceToken("testToken", 0, 0); + address feedAddress = _deployTokenPriceDataFeed(tokenAddress, 0, 1e31); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = getSingleTokenPriceFeedUpdateStruct(tokenAddress, feedAddress, 0); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + uint224 tokenPriceAnswer = s_priceRegistry.getValidatedTokenPrice(tokenAddress); + + // Price answer is 1e31 (0 decimal token) - unit is (1e18 * 1e18 / 1e0) -> expected 1e36 + assertEq(tokenPriceAnswer, uint224(1e67)); + } + + function test_GetValidatedTokenPriceFromFeedFlippedDecimals_Success() public { + address tokenAddress = _deploySourceToken("testToken", 0, 20); + address feedAddress = _deployTokenPriceDataFeed(tokenAddress, 20, 1e18); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = getSingleTokenPriceFeedUpdateStruct(tokenAddress, feedAddress, 20); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + uint224 tokenPriceAnswer = s_priceRegistry.getValidatedTokenPrice(tokenAddress); + + // Price answer is 1e8 (6 decimal token) - unit is (1e18 * 1e18 / 1e20) -> expected 1e14 + assertEq(tokenPriceAnswer, uint224(1e14)); + } + + function test_StaleFeeToken_Success() public { + vm.warp(block.timestamp + TWELVE_HOURS + 1); + + Internal.PriceUpdates memory priceUpdates = abi.decode(s_encodedInitialPriceUpdates, (Internal.PriceUpdates)); + address token = priceUpdates.tokenPriceUpdates[0].sourceToken; + + uint224 tokenPrice = s_priceRegistry.getValidatedTokenPrice(token); + + assertEq(priceUpdates.tokenPriceUpdates[0].usdPerToken, tokenPrice); + } + + // Reverts + + function test_OverflowFeedPrice_Revert() public { + address tokenAddress = _deploySourceToken("testToken", 0, 18); + address feedAddress = _deployTokenPriceDataFeed(tokenAddress, 18, int256(uint256(type(uint224).max) + 1)); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = getSingleTokenPriceFeedUpdateStruct(tokenAddress, feedAddress, 18); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + vm.expectRevert(PriceRegistry.DataFeedValueOutOfUint224Range.selector); + s_priceRegistry.getValidatedTokenPrice(tokenAddress); + } + + function test_UnderflowFeedPrice_Revert() public { + address tokenAddress = _deploySourceToken("testToken", 0, 18); + address feedAddress = _deployTokenPriceDataFeed(tokenAddress, 18, -1); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = getSingleTokenPriceFeedUpdateStruct(tokenAddress, feedAddress, 18); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + vm.expectRevert(PriceRegistry.DataFeedValueOutOfUint224Range.selector); + s_priceRegistry.getValidatedTokenPrice(tokenAddress); + } + + function test_TokenNotSupported_Revert() public { + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.TokenNotSupported.selector, DUMMY_CONTRACT_ADDRESS)); + s_priceRegistry.getValidatedTokenPrice(DUMMY_CONTRACT_ADDRESS); + } + + function test_TokenNotSupportedFeed_Revert() public { + address sourceToken = _initialiseSingleTokenPriceFeed(); + MockV3Aggregator(s_dataFeedByToken[sourceToken]).updateAnswer(0); + + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.TokenNotSupported.selector, sourceToken)); + s_priceRegistry.getValidatedTokenPrice(sourceToken); + } +} + +contract PriceRegistry_applyFeeTokensUpdates is PriceRegistrySetup { + function test_ApplyFeeTokensUpdates_Success() public { + address[] memory feeTokens = new address[](1); + feeTokens[0] = s_sourceTokens[1]; + + vm.expectEmit(); + emit PriceRegistry.FeeTokenAdded(feeTokens[0]); + + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + assertEq(s_priceRegistry.getFeeTokens().length, 3); + assertEq(s_priceRegistry.getFeeTokens()[2], feeTokens[0]); + + // add same feeToken is no-op + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + assertEq(s_priceRegistry.getFeeTokens().length, 3); + assertEq(s_priceRegistry.getFeeTokens()[2], feeTokens[0]); + + vm.expectEmit(); + emit PriceRegistry.FeeTokenRemoved(feeTokens[0]); + + s_priceRegistry.applyFeeTokensUpdates(new address[](0), feeTokens); + assertEq(s_priceRegistry.getFeeTokens().length, 2); + + // removing already removed feeToken is no-op + s_priceRegistry.applyFeeTokensUpdates(new address[](0), feeTokens); + assertEq(s_priceRegistry.getFeeTokens().length, 2); + } + + function test_OnlyCallableByOwner_Revert() public { + address[] memory feeTokens = new address[](1); + feeTokens[0] = STRANGER; + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + } +} + +contract PriceRegistry_updatePrices is PriceRegistrySetup { + function test_OnlyTokenPrice_Success() public { + Internal.PriceUpdates memory update = Internal.PriceUpdates({ + tokenPriceUpdates: new Internal.TokenPriceUpdate[](1), + gasPriceUpdates: new Internal.GasPriceUpdate[](0) + }); + update.tokenPriceUpdates[0] = Internal.TokenPriceUpdate({sourceToken: s_sourceTokens[0], usdPerToken: 4e18}); + + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated( + update.tokenPriceUpdates[0].sourceToken, update.tokenPriceUpdates[0].usdPerToken, block.timestamp + ); + + s_priceRegistry.updatePrices(update); + + assertEq(s_priceRegistry.getTokenPrice(s_sourceTokens[0]).value, update.tokenPriceUpdates[0].usdPerToken); + } + + function test_OnlyGasPrice_Success() public { + Internal.PriceUpdates memory update = Internal.PriceUpdates({ + tokenPriceUpdates: new Internal.TokenPriceUpdate[](0), + gasPriceUpdates: new Internal.GasPriceUpdate[](1) + }); + update.gasPriceUpdates[0] = + Internal.GasPriceUpdate({destChainSelector: DEST_CHAIN_SELECTOR, usdPerUnitGas: 2000e18}); + + vm.expectEmit(); + emit PriceRegistry.UsdPerUnitGasUpdated( + update.gasPriceUpdates[0].destChainSelector, update.gasPriceUpdates[0].usdPerUnitGas, block.timestamp + ); + + s_priceRegistry.updatePrices(update); + + assertEq( + s_priceRegistry.getDestinationChainGasPrice(DEST_CHAIN_SELECTOR).value, update.gasPriceUpdates[0].usdPerUnitGas + ); + } + + function test_UpdateMultiplePrices_Success() public { + Internal.TokenPriceUpdate[] memory tokenPriceUpdates = new Internal.TokenPriceUpdate[](3); + tokenPriceUpdates[0] = Internal.TokenPriceUpdate({sourceToken: s_sourceTokens[0], usdPerToken: 4e18}); + tokenPriceUpdates[1] = Internal.TokenPriceUpdate({sourceToken: s_sourceTokens[1], usdPerToken: 1800e18}); + tokenPriceUpdates[2] = Internal.TokenPriceUpdate({sourceToken: address(12345), usdPerToken: 1e18}); + + Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](3); + gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: DEST_CHAIN_SELECTOR, usdPerUnitGas: 2e6}); + gasPriceUpdates[1] = Internal.GasPriceUpdate({destChainSelector: SOURCE_CHAIN_SELECTOR, usdPerUnitGas: 2000e18}); + gasPriceUpdates[2] = Internal.GasPriceUpdate({destChainSelector: 12345, usdPerUnitGas: 1e18}); + + Internal.PriceUpdates memory update = + Internal.PriceUpdates({tokenPriceUpdates: tokenPriceUpdates, gasPriceUpdates: gasPriceUpdates}); + + for (uint256 i = 0; i < tokenPriceUpdates.length; ++i) { + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated( + update.tokenPriceUpdates[i].sourceToken, update.tokenPriceUpdates[i].usdPerToken, block.timestamp + ); + } + for (uint256 i = 0; i < gasPriceUpdates.length; ++i) { + vm.expectEmit(); + emit PriceRegistry.UsdPerUnitGasUpdated( + update.gasPriceUpdates[i].destChainSelector, update.gasPriceUpdates[i].usdPerUnitGas, block.timestamp + ); + } + + s_priceRegistry.updatePrices(update); + + for (uint256 i = 0; i < tokenPriceUpdates.length; ++i) { + assertEq( + s_priceRegistry.getTokenPrice(update.tokenPriceUpdates[i].sourceToken).value, tokenPriceUpdates[i].usdPerToken + ); + } + for (uint256 i = 0; i < gasPriceUpdates.length; ++i) { + assertEq( + s_priceRegistry.getDestinationChainGasPrice(update.gasPriceUpdates[i].destChainSelector).value, + gasPriceUpdates[i].usdPerUnitGas + ); + } + } + + function test_UpdatableByAuthorizedCaller_Success() public { + Internal.PriceUpdates memory priceUpdates = Internal.PriceUpdates({ + tokenPriceUpdates: new Internal.TokenPriceUpdate[](1), + gasPriceUpdates: new Internal.GasPriceUpdate[](0) + }); + priceUpdates.tokenPriceUpdates[0] = Internal.TokenPriceUpdate({sourceToken: s_sourceTokens[0], usdPerToken: 4e18}); + + // Revert when caller is not authorized + vm.startPrank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(AuthorizedCallers.UnauthorizedCaller.selector, STRANGER)); + s_priceRegistry.updatePrices(priceUpdates); + + address[] memory priceUpdaters = new address[](1); + priceUpdaters[0] = STRANGER; + vm.startPrank(OWNER); + s_priceRegistry.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: priceUpdaters, removedCallers: new address[](0)}) + ); + + // Stranger is now an authorized caller to update prices + vm.expectEmit(); + emit PriceRegistry.UsdPerTokenUpdated( + priceUpdates.tokenPriceUpdates[0].sourceToken, priceUpdates.tokenPriceUpdates[0].usdPerToken, block.timestamp + ); + s_priceRegistry.updatePrices(priceUpdates); + + assertEq(s_priceRegistry.getTokenPrice(s_sourceTokens[0]).value, priceUpdates.tokenPriceUpdates[0].usdPerToken); + + vm.startPrank(OWNER); + s_priceRegistry.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: new address[](0), removedCallers: priceUpdaters}) + ); + + // Revert when authorized caller is removed + vm.startPrank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(AuthorizedCallers.UnauthorizedCaller.selector, STRANGER)); + s_priceRegistry.updatePrices(priceUpdates); + } + + // Reverts + + function test_OnlyCallableByUpdater_Revert() public { + Internal.PriceUpdates memory priceUpdates = Internal.PriceUpdates({ + tokenPriceUpdates: new Internal.TokenPriceUpdate[](0), + gasPriceUpdates: new Internal.GasPriceUpdate[](0) + }); + + vm.startPrank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(AuthorizedCallers.UnauthorizedCaller.selector, STRANGER)); + s_priceRegistry.updatePrices(priceUpdates); + } +} + +contract PriceRegistry_convertTokenAmount is PriceRegistrySetup { + function test_ConvertTokenAmount_Success() public view { + Internal.PriceUpdates memory initialPriceUpdates = abi.decode(s_encodedInitialPriceUpdates, (Internal.PriceUpdates)); + uint256 amount = 3e16; + uint256 conversionRate = (uint256(initialPriceUpdates.tokenPriceUpdates[2].usdPerToken) * 1e18) + / uint256(initialPriceUpdates.tokenPriceUpdates[0].usdPerToken); + uint256 expected = (amount * conversionRate) / 1e18; + assertEq(s_priceRegistry.convertTokenAmount(s_weth, amount, s_sourceTokens[0]), expected); + } + + function test_Fuzz_ConvertTokenAmount_Success( + uint256 feeTokenAmount, + uint224 usdPerFeeToken, + uint160 usdPerLinkToken, + uint224 usdPerUnitGas + ) public { + vm.assume(usdPerFeeToken > 0); + vm.assume(usdPerLinkToken > 0); + // We bound the max fees to be at most uint96.max link. + feeTokenAmount = bound(feeTokenAmount, 0, (uint256(type(uint96).max) * usdPerLinkToken) / usdPerFeeToken); + + address feeToken = address(1); + address linkToken = address(2); + address[] memory feeTokens = new address[](1); + feeTokens[0] = feeToken; + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + + Internal.TokenPriceUpdate[] memory tokenPriceUpdates = new Internal.TokenPriceUpdate[](2); + tokenPriceUpdates[0] = Internal.TokenPriceUpdate({sourceToken: feeToken, usdPerToken: usdPerFeeToken}); + tokenPriceUpdates[1] = Internal.TokenPriceUpdate({sourceToken: linkToken, usdPerToken: usdPerLinkToken}); + + Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](1); + gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: DEST_CHAIN_SELECTOR, usdPerUnitGas: usdPerUnitGas}); + + Internal.PriceUpdates memory priceUpdates = + Internal.PriceUpdates({tokenPriceUpdates: tokenPriceUpdates, gasPriceUpdates: gasPriceUpdates}); + + s_priceRegistry.updatePrices(priceUpdates); + + uint256 linkFee = s_priceRegistry.convertTokenAmount(feeToken, feeTokenAmount, linkToken); + assertEq(linkFee, (feeTokenAmount * usdPerFeeToken) / usdPerLinkToken); + } + + // Reverts + + function test_LinkTokenNotSupported_Revert() public { + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.TokenNotSupported.selector, DUMMY_CONTRACT_ADDRESS)); + s_priceRegistry.convertTokenAmount(DUMMY_CONTRACT_ADDRESS, 3e16, s_sourceTokens[0]); + + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.TokenNotSupported.selector, DUMMY_CONTRACT_ADDRESS)); + s_priceRegistry.convertTokenAmount(s_sourceTokens[0], 3e16, DUMMY_CONTRACT_ADDRESS); + } +} + +contract PriceRegistry_getTokenAndGasPrices is PriceRegistrySetup { + function test_GetFeeTokenAndGasPrices_Success() public view { + (uint224 feeTokenPrice, uint224 gasPrice) = + s_priceRegistry.getTokenAndGasPrices(s_sourceFeeToken, DEST_CHAIN_SELECTOR); + + Internal.PriceUpdates memory priceUpdates = abi.decode(s_encodedInitialPriceUpdates, (Internal.PriceUpdates)); + + assertEq(feeTokenPrice, s_sourceTokenPrices[0]); + assertEq(gasPrice, priceUpdates.gasPriceUpdates[0].usdPerUnitGas); + } + + function test_ZeroGasPrice_Success() public { + uint64 zeroGasDestChainSelector = 345678; + Internal.GasPriceUpdate[] memory gasPriceUpdates = new Internal.GasPriceUpdate[](1); + gasPriceUpdates[0] = Internal.GasPriceUpdate({destChainSelector: zeroGasDestChainSelector, usdPerUnitGas: 0}); + + Internal.PriceUpdates memory priceUpdates = + Internal.PriceUpdates({tokenPriceUpdates: new Internal.TokenPriceUpdate[](0), gasPriceUpdates: gasPriceUpdates}); + s_priceRegistry.updatePrices(priceUpdates); + + (, uint224 gasPrice) = s_priceRegistry.getTokenAndGasPrices(s_sourceFeeToken, zeroGasDestChainSelector); + + assertEq(gasPrice, priceUpdates.gasPriceUpdates[0].usdPerUnitGas); + } + + function test_UnsupportedChain_Revert() public { + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.ChainNotSupported.selector, DEST_CHAIN_SELECTOR + 1)); + s_priceRegistry.getTokenAndGasPrices(s_sourceTokens[0], DEST_CHAIN_SELECTOR + 1); + } + + function test_StaleGasPrice_Revert() public { + uint256 diff = TWELVE_HOURS + 1; + vm.warp(block.timestamp + diff); + vm.expectRevert( + abi.encodeWithSelector(PriceRegistry.StaleGasPrice.selector, DEST_CHAIN_SELECTOR, TWELVE_HOURS, diff) + ); + s_priceRegistry.getTokenAndGasPrices(s_sourceTokens[0], DEST_CHAIN_SELECTOR); + } +} + +contract PriceRegistry_updateTokenPriceFeeds is PriceRegistrySetup { + function test_ZeroFeeds_Success() public { + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](0); + vm.recordLogs(); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + // Verify no log emissions + assertEq(logEntries.length, 0); + } + + function test_SingleFeedUpdate_Success() public { + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = + getSingleTokenPriceFeedUpdateStruct(s_sourceTokens[0], s_dataFeedByToken[s_sourceTokens[0]], 18); + + _assertTokenPriceFeedConfigUnconfigured( + s_priceRegistry.getTokenPriceFeedConfig(tokenPriceFeedUpdates[0].sourceToken) + ); + + vm.expectEmit(); + emit PriceRegistry.PriceFeedPerTokenUpdated( + tokenPriceFeedUpdates[0].sourceToken, tokenPriceFeedUpdates[0].feedConfig + ); + + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + _assertTokenPriceFeedConfigEquality( + s_priceRegistry.getTokenPriceFeedConfig(tokenPriceFeedUpdates[0].sourceToken), tokenPriceFeedUpdates[0].feedConfig + ); + } + + function test_MultipleFeedUpdate_Success() public { + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](2); + + for (uint256 i = 0; i < 2; ++i) { + tokenPriceFeedUpdates[i] = + getSingleTokenPriceFeedUpdateStruct(s_sourceTokens[i], s_dataFeedByToken[s_sourceTokens[i]], 18); + + _assertTokenPriceFeedConfigUnconfigured( + s_priceRegistry.getTokenPriceFeedConfig(tokenPriceFeedUpdates[i].sourceToken) + ); + + vm.expectEmit(); + emit PriceRegistry.PriceFeedPerTokenUpdated( + tokenPriceFeedUpdates[i].sourceToken, tokenPriceFeedUpdates[i].feedConfig + ); + } + + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + _assertTokenPriceFeedConfigEquality( + s_priceRegistry.getTokenPriceFeedConfig(tokenPriceFeedUpdates[0].sourceToken), tokenPriceFeedUpdates[0].feedConfig + ); + _assertTokenPriceFeedConfigEquality( + s_priceRegistry.getTokenPriceFeedConfig(tokenPriceFeedUpdates[1].sourceToken), tokenPriceFeedUpdates[1].feedConfig + ); + } + + function test_FeedUnset_Success() public { + Internal.TimestampedPackedUint224 memory priceQueryInitial = s_priceRegistry.getTokenPrice(s_sourceTokens[0]); + assertFalse(priceQueryInitial.value == 0); + assertFalse(priceQueryInitial.timestamp == 0); + + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = + getSingleTokenPriceFeedUpdateStruct(s_sourceTokens[0], s_dataFeedByToken[s_sourceTokens[0]], 18); + + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + _assertTokenPriceFeedConfigEquality( + s_priceRegistry.getTokenPriceFeedConfig(tokenPriceFeedUpdates[0].sourceToken), tokenPriceFeedUpdates[0].feedConfig + ); + + tokenPriceFeedUpdates[0].feedConfig.dataFeedAddress = address(0); + vm.expectEmit(); + emit PriceRegistry.PriceFeedPerTokenUpdated( + tokenPriceFeedUpdates[0].sourceToken, tokenPriceFeedUpdates[0].feedConfig + ); + + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + _assertTokenPriceFeedConfigEquality( + s_priceRegistry.getTokenPriceFeedConfig(tokenPriceFeedUpdates[0].sourceToken), tokenPriceFeedUpdates[0].feedConfig + ); + + // Price data should remain after a feed has been set->unset + Internal.TimestampedPackedUint224 memory priceQueryPostUnsetFeed = s_priceRegistry.getTokenPrice(s_sourceTokens[0]); + assertEq(priceQueryPostUnsetFeed.value, priceQueryInitial.value); + assertEq(priceQueryPostUnsetFeed.timestamp, priceQueryInitial.timestamp); + } + + function test_FeedNotUpdated() public { + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = + getSingleTokenPriceFeedUpdateStruct(s_sourceTokens[0], s_dataFeedByToken[s_sourceTokens[0]], 18); + + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + + _assertTokenPriceFeedConfigEquality( + s_priceRegistry.getTokenPriceFeedConfig(tokenPriceFeedUpdates[0].sourceToken), tokenPriceFeedUpdates[0].feedConfig + ); + } + + // Reverts + + function test_FeedUpdatedByNonOwner_Revert() public { + PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeedUpdates = new PriceRegistry.TokenPriceFeedUpdate[](1); + tokenPriceFeedUpdates[0] = + getSingleTokenPriceFeedUpdateStruct(s_sourceTokens[0], s_dataFeedByToken[s_sourceTokens[0]], 18); + + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + + s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeedUpdates); + } +} + +contract PriceRegistry_applyDestChainConfigUpdates is PriceRegistrySetup { + function test_Fuzz_applyDestChainConfigUpdates_Success(PriceRegistry.DestChainConfigArgs memory destChainConfigArgs) + public + { + vm.assume(destChainConfigArgs.destChainSelector != 0); + vm.assume(destChainConfigArgs.destChainConfig.maxPerMsgGasLimit != 0); + destChainConfigArgs.destChainConfig.defaultTxGasLimit = uint32( + bound( + destChainConfigArgs.destChainConfig.defaultTxGasLimit, 1, destChainConfigArgs.destChainConfig.maxPerMsgGasLimit + ) + ); + destChainConfigArgs.destChainConfig.defaultTokenDestBytesOverhead = uint32( + bound( + destChainConfigArgs.destChainConfig.defaultTokenDestBytesOverhead, + Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, + type(uint32).max + ) + ); + destChainConfigArgs.destChainConfig.chainFamilySelector = Internal.CHAIN_FAMILY_SELECTOR_EVM; + + bool isNewChain = destChainConfigArgs.destChainSelector != DEST_CHAIN_SELECTOR; + + PriceRegistry.DestChainConfigArgs[] memory newDestChainConfigArgs = new PriceRegistry.DestChainConfigArgs[](1); + newDestChainConfigArgs[0] = destChainConfigArgs; + + if (isNewChain) { + vm.expectEmit(); + emit PriceRegistry.DestChainAdded(destChainConfigArgs.destChainSelector, destChainConfigArgs.destChainConfig); + } else { + vm.expectEmit(); + emit PriceRegistry.DestChainConfigUpdated( + destChainConfigArgs.destChainSelector, destChainConfigArgs.destChainConfig + ); + } + + s_priceRegistry.applyDestChainConfigUpdates(newDestChainConfigArgs); + + _assertPriceRegistryDestChainConfigsEqual( + destChainConfigArgs.destChainConfig, s_priceRegistry.getDestChainConfig(destChainConfigArgs.destChainSelector) + ); + } + + function test_applyDestChainConfigUpdates_Success() public { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = new PriceRegistry.DestChainConfigArgs[](2); + destChainConfigArgs[0] = _generatePriceRegistryDestChainConfigArgs()[0]; + destChainConfigArgs[0].destChainConfig.isEnabled = false; + destChainConfigArgs[1] = _generatePriceRegistryDestChainConfigArgs()[0]; + destChainConfigArgs[1].destChainSelector = DEST_CHAIN_SELECTOR + 1; + + vm.expectEmit(); + emit PriceRegistry.DestChainConfigUpdated(DEST_CHAIN_SELECTOR, destChainConfigArgs[0].destChainConfig); + vm.expectEmit(); + emit PriceRegistry.DestChainAdded(DEST_CHAIN_SELECTOR + 1, destChainConfigArgs[1].destChainConfig); + + vm.recordLogs(); + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + + PriceRegistry.DestChainConfig memory gotDestChainConfig0 = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + PriceRegistry.DestChainConfig memory gotDestChainConfig1 = + s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR + 1); + + assertEq(vm.getRecordedLogs().length, 2); + _assertPriceRegistryDestChainConfigsEqual(destChainConfigArgs[0].destChainConfig, gotDestChainConfig0); + _assertPriceRegistryDestChainConfigsEqual(destChainConfigArgs[1].destChainConfig, gotDestChainConfig1); + } + + function test_applyDestChainConfigUpdatesZeroIntput_Success() public { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = new PriceRegistry.DestChainConfigArgs[](0); + + vm.recordLogs(); + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + + assertEq(vm.getRecordedLogs().length, 0); + } + + // Reverts + + function test_applyDestChainConfigUpdatesDefaultTxGasLimitEqZero_Revert() public { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + PriceRegistry.DestChainConfigArgs memory destChainConfigArg = destChainConfigArgs[0]; + + destChainConfigArg.destChainConfig.defaultTxGasLimit = 0; + vm.expectRevert( + abi.encodeWithSelector(PriceRegistry.InvalidDestChainConfig.selector, destChainConfigArg.destChainSelector) + ); + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + } + + function test_applyDestChainConfigUpdatesDefaultTxGasLimitGtMaxPerMessageGasLimit_Revert() public { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + PriceRegistry.DestChainConfigArgs memory destChainConfigArg = destChainConfigArgs[0]; + + // Allow setting to the max value + destChainConfigArg.destChainConfig.defaultTxGasLimit = destChainConfigArg.destChainConfig.maxPerMsgGasLimit; + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + + // Revert when exceeding max value + destChainConfigArg.destChainConfig.defaultTxGasLimit = destChainConfigArg.destChainConfig.maxPerMsgGasLimit + 1; + vm.expectRevert( + abi.encodeWithSelector(PriceRegistry.InvalidDestChainConfig.selector, destChainConfigArg.destChainSelector) + ); + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + } + + function test_InvalidDestChainConfigDestChainSelectorEqZero_Revert() public { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + PriceRegistry.DestChainConfigArgs memory destChainConfigArg = destChainConfigArgs[0]; + + destChainConfigArg.destChainSelector = 0; + vm.expectRevert( + abi.encodeWithSelector(PriceRegistry.InvalidDestChainConfig.selector, destChainConfigArg.destChainSelector) + ); + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + } + + function test_InvalidDestBytesOverhead_Revert() public { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + PriceRegistry.DestChainConfigArgs memory destChainConfigArg = destChainConfigArgs[0]; + + destChainConfigArg.destChainConfig.defaultTokenDestBytesOverhead = uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES - 1); + + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.InvalidDestChainConfig.selector, DEST_CHAIN_SELECTOR)); + + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + } + + function test_InvalidChainFamilySelector_Revert() public { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + PriceRegistry.DestChainConfigArgs memory destChainConfigArg = destChainConfigArgs[0]; + + destChainConfigArg.destChainConfig.chainFamilySelector = bytes4(uint32(1)); + + vm.expectRevert( + abi.encodeWithSelector(PriceRegistry.InvalidDestChainConfig.selector, destChainConfigArg.destChainSelector) + ); + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + } +} + +contract PriceRegistry_getDataAvailabilityCost is PriceRegistrySetup { + function test_EmptyMessageCalculatesDataAvailabilityCost_Success() public { + uint256 dataAvailabilityCostUSD = + s_priceRegistry.getDataAvailabilityCost(DEST_CHAIN_SELECTOR, USD_PER_DATA_AVAILABILITY_GAS, 0, 0, 0); + + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + + uint256 dataAvailabilityGas = destChainConfig.destDataAvailabilityOverheadGas + + destChainConfig.destGasPerDataAvailabilityByte * Internal.ANY_2_EVM_MESSAGE_FIXED_BYTES; + uint256 expectedDataAvailabilityCostUSD = + USD_PER_DATA_AVAILABILITY_GAS * dataAvailabilityGas * destChainConfig.destDataAvailabilityMultiplierBps * 1e14; + + assertEq(expectedDataAvailabilityCostUSD, dataAvailabilityCostUSD); + + // Test that the cost is destnation chain specific + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + destChainConfigArgs[0].destChainSelector = DEST_CHAIN_SELECTOR + 1; + destChainConfigArgs[0].destChainConfig.destDataAvailabilityOverheadGas = + destChainConfig.destDataAvailabilityOverheadGas * 2; + destChainConfigArgs[0].destChainConfig.destGasPerDataAvailabilityByte = + destChainConfig.destGasPerDataAvailabilityByte * 2; + destChainConfigArgs[0].destChainConfig.destDataAvailabilityMultiplierBps = + destChainConfig.destDataAvailabilityMultiplierBps * 2; + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + + destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR + 1); + uint256 dataAvailabilityCostUSD2 = + s_priceRegistry.getDataAvailabilityCost(DEST_CHAIN_SELECTOR + 1, USD_PER_DATA_AVAILABILITY_GAS, 0, 0, 0); + dataAvailabilityGas = destChainConfig.destDataAvailabilityOverheadGas + + destChainConfig.destGasPerDataAvailabilityByte * Internal.ANY_2_EVM_MESSAGE_FIXED_BYTES; + expectedDataAvailabilityCostUSD = + USD_PER_DATA_AVAILABILITY_GAS * dataAvailabilityGas * destChainConfig.destDataAvailabilityMultiplierBps * 1e14; + + assertEq(expectedDataAvailabilityCostUSD, dataAvailabilityCostUSD2); + assertFalse(dataAvailabilityCostUSD == dataAvailabilityCostUSD2); + } + + function test_SimpleMessageCalculatesDataAvailabilityCost_Success() public view { + uint256 dataAvailabilityCostUSD = + s_priceRegistry.getDataAvailabilityCost(DEST_CHAIN_SELECTOR, USD_PER_DATA_AVAILABILITY_GAS, 100, 5, 50); + + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + + uint256 dataAvailabilityLengthBytes = + Internal.ANY_2_EVM_MESSAGE_FIXED_BYTES + 100 + (5 * Internal.ANY_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN) + 50; + uint256 dataAvailabilityGas = destChainConfig.destDataAvailabilityOverheadGas + + destChainConfig.destGasPerDataAvailabilityByte * dataAvailabilityLengthBytes; + uint256 expectedDataAvailabilityCostUSD = + USD_PER_DATA_AVAILABILITY_GAS * dataAvailabilityGas * destChainConfig.destDataAvailabilityMultiplierBps * 1e14; + + assertEq(expectedDataAvailabilityCostUSD, dataAvailabilityCostUSD); + } + + function test_SimpleMessageCalculatesDataAvailabilityCostUnsupportedDestChainSelector_Success() public view { + uint256 dataAvailabilityCostUSD = + s_priceRegistry.getDataAvailabilityCost(0, USD_PER_DATA_AVAILABILITY_GAS, 100, 5, 50); + + assertEq(dataAvailabilityCostUSD, 0); + } + + function test_Fuzz_ZeroDataAvailabilityGasPriceAlwaysCalculatesZeroDataAvailabilityCost_Success( + uint64 messageDataLength, + uint32 numberOfTokens, + uint32 tokenTransferBytesOverhead + ) public view { + uint256 dataAvailabilityCostUSD = s_priceRegistry.getDataAvailabilityCost( + DEST_CHAIN_SELECTOR, 0, messageDataLength, numberOfTokens, tokenTransferBytesOverhead + ); + + assertEq(0, dataAvailabilityCostUSD); + } + + function test_Fuzz_CalculateDataAvailabilityCost_Success( + uint64 destChainSelector, + uint32 destDataAvailabilityOverheadGas, + uint16 destGasPerDataAvailabilityByte, + uint16 destDataAvailabilityMultiplierBps, + uint112 dataAvailabilityGasPrice, + uint64 messageDataLength, + uint32 numberOfTokens, + uint32 tokenTransferBytesOverhead + ) public { + vm.assume(destChainSelector != 0); + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = new PriceRegistry.DestChainConfigArgs[](1); + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(destChainSelector); + destChainConfigArgs[0] = + PriceRegistry.DestChainConfigArgs({destChainSelector: destChainSelector, destChainConfig: destChainConfig}); + destChainConfigArgs[0].destChainConfig.destDataAvailabilityOverheadGas = destDataAvailabilityOverheadGas; + destChainConfigArgs[0].destChainConfig.destGasPerDataAvailabilityByte = destGasPerDataAvailabilityByte; + destChainConfigArgs[0].destChainConfig.destDataAvailabilityMultiplierBps = destDataAvailabilityMultiplierBps; + destChainConfigArgs[0].destChainConfig.defaultTxGasLimit = GAS_LIMIT; + destChainConfigArgs[0].destChainConfig.maxPerMsgGasLimit = GAS_LIMIT; + destChainConfigArgs[0].destChainConfig.chainFamilySelector = Internal.CHAIN_FAMILY_SELECTOR_EVM; + destChainConfigArgs[0].destChainConfig.defaultTokenDestBytesOverhead = DEFAULT_TOKEN_BYTES_OVERHEAD; + + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + + uint256 dataAvailabilityCostUSD = s_priceRegistry.getDataAvailabilityCost( + destChainConfigArgs[0].destChainSelector, + dataAvailabilityGasPrice, + messageDataLength, + numberOfTokens, + tokenTransferBytesOverhead + ); + + uint256 dataAvailabilityLengthBytes = Internal.ANY_2_EVM_MESSAGE_FIXED_BYTES + messageDataLength + + (numberOfTokens * Internal.ANY_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN) + tokenTransferBytesOverhead; + + uint256 dataAvailabilityGas = + destDataAvailabilityOverheadGas + destGasPerDataAvailabilityByte * dataAvailabilityLengthBytes; + uint256 expectedDataAvailabilityCostUSD = + dataAvailabilityGasPrice * dataAvailabilityGas * destDataAvailabilityMultiplierBps * 1e14; + + assertEq(expectedDataAvailabilityCostUSD, dataAvailabilityCostUSD); + } +} + +contract PriceRegistry_applyPremiumMultiplierWeiPerEthUpdates is PriceRegistrySetup { + function test_Fuzz_applyPremiumMultiplierWeiPerEthUpdates_Success( + PriceRegistry.PremiumMultiplierWeiPerEthArgs memory premiumMultiplierWeiPerEthArg + ) public { + PriceRegistry.PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs = + new PriceRegistry.PremiumMultiplierWeiPerEthArgs[](1); + premiumMultiplierWeiPerEthArgs[0] = premiumMultiplierWeiPerEthArg; + + vm.expectEmit(); + emit PriceRegistry.PremiumMultiplierWeiPerEthUpdated( + premiumMultiplierWeiPerEthArg.token, premiumMultiplierWeiPerEthArg.premiumMultiplierWeiPerEth + ); + + s_priceRegistry.applyPremiumMultiplierWeiPerEthUpdates(premiumMultiplierWeiPerEthArgs); + + assertEq( + premiumMultiplierWeiPerEthArg.premiumMultiplierWeiPerEth, + s_priceRegistry.getPremiumMultiplierWeiPerEth(premiumMultiplierWeiPerEthArg.token) + ); + } + + function test_applyPremiumMultiplierWeiPerEthUpdatesSingleToken_Success() public { + PriceRegistry.PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs = + new PriceRegistry.PremiumMultiplierWeiPerEthArgs[](1); + premiumMultiplierWeiPerEthArgs[0] = s_priceRegistryPremiumMultiplierWeiPerEthArgs[0]; + premiumMultiplierWeiPerEthArgs[0].token = vm.addr(1); + + vm.expectEmit(); + emit PriceRegistry.PremiumMultiplierWeiPerEthUpdated( + vm.addr(1), premiumMultiplierWeiPerEthArgs[0].premiumMultiplierWeiPerEth + ); + + s_priceRegistry.applyPremiumMultiplierWeiPerEthUpdates(premiumMultiplierWeiPerEthArgs); + + assertEq( + s_priceRegistryPremiumMultiplierWeiPerEthArgs[0].premiumMultiplierWeiPerEth, + s_priceRegistry.getPremiumMultiplierWeiPerEth(vm.addr(1)) + ); + } + + function test_applyPremiumMultiplierWeiPerEthUpdatesMultipleTokens_Success() public { + PriceRegistry.PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs = + new PriceRegistry.PremiumMultiplierWeiPerEthArgs[](2); + premiumMultiplierWeiPerEthArgs[0] = s_priceRegistryPremiumMultiplierWeiPerEthArgs[0]; + premiumMultiplierWeiPerEthArgs[0].token = vm.addr(1); + premiumMultiplierWeiPerEthArgs[1].token = vm.addr(2); + + vm.expectEmit(); + emit PriceRegistry.PremiumMultiplierWeiPerEthUpdated( + vm.addr(1), premiumMultiplierWeiPerEthArgs[0].premiumMultiplierWeiPerEth + ); + vm.expectEmit(); + emit PriceRegistry.PremiumMultiplierWeiPerEthUpdated( + vm.addr(2), premiumMultiplierWeiPerEthArgs[1].premiumMultiplierWeiPerEth + ); + + s_priceRegistry.applyPremiumMultiplierWeiPerEthUpdates(premiumMultiplierWeiPerEthArgs); + + assertEq( + premiumMultiplierWeiPerEthArgs[0].premiumMultiplierWeiPerEth, + s_priceRegistry.getPremiumMultiplierWeiPerEth(vm.addr(1)) + ); + assertEq( + premiumMultiplierWeiPerEthArgs[1].premiumMultiplierWeiPerEth, + s_priceRegistry.getPremiumMultiplierWeiPerEth(vm.addr(2)) + ); + } + + function test_applyPremiumMultiplierWeiPerEthUpdatesZeroInput() public { + vm.recordLogs(); + s_priceRegistry.applyPremiumMultiplierWeiPerEthUpdates(new PriceRegistry.PremiumMultiplierWeiPerEthArgs[](0)); + + assertEq(vm.getRecordedLogs().length, 0); + } + + // Reverts + + function test_OnlyCallableByOwnerOrAdmin_Revert() public { + PriceRegistry.PremiumMultiplierWeiPerEthArgs[] memory premiumMultiplierWeiPerEthArgs; + vm.startPrank(STRANGER); + + vm.expectRevert("Only callable by owner"); + + s_priceRegistry.applyPremiumMultiplierWeiPerEthUpdates(premiumMultiplierWeiPerEthArgs); + } +} + +contract PriceRegistry_applyTokenTransferFeeConfigUpdates is PriceRegistrySetup { + function test_Fuzz_ApplyTokenTransferFeeConfig_Success( + PriceRegistry.TokenTransferFeeConfig[2] memory tokenTransferFeeConfigs + ) public { + PriceRegistry.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + _generateTokenTransferFeeConfigArgs(2, 2); + tokenTransferFeeConfigArgs[0].destChainSelector = DEST_CHAIN_SELECTOR; + tokenTransferFeeConfigArgs[1].destChainSelector = DEST_CHAIN_SELECTOR + 1; + + for (uint256 i = 0; i < tokenTransferFeeConfigArgs.length; ++i) { + for (uint256 j = 0; j < tokenTransferFeeConfigs.length; ++j) { + tokenTransferFeeConfigs[j].destBytesOverhead = uint32( + bound(tokenTransferFeeConfigs[j].destBytesOverhead, Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, type(uint32).max) + ); + address feeToken = s_sourceTokens[j]; + tokenTransferFeeConfigArgs[i].tokenTransferFeeConfigs[j].token = feeToken; + tokenTransferFeeConfigArgs[i].tokenTransferFeeConfigs[j].tokenTransferFeeConfig = tokenTransferFeeConfigs[j]; + + vm.expectEmit(); + emit PriceRegistry.TokenTransferFeeConfigUpdated( + tokenTransferFeeConfigArgs[i].destChainSelector, feeToken, tokenTransferFeeConfigs[j] + ); + } + } + + s_priceRegistry.applyTokenTransferFeeConfigUpdates( + tokenTransferFeeConfigArgs, new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](0) + ); + + for (uint256 i = 0; i < tokenTransferFeeConfigs.length; ++i) { + _assertTokenTransferFeeConfigEqual( + tokenTransferFeeConfigs[i], + s_priceRegistry.getTokenTransferFeeConfig( + tokenTransferFeeConfigArgs[0].destChainSelector, + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[i].token + ) + ); + } + } + + function test_ApplyTokenTransferFeeConfig_Success() public { + PriceRegistry.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + _generateTokenTransferFeeConfigArgs(1, 2); + tokenTransferFeeConfigArgs[0].destChainSelector = DEST_CHAIN_SELECTOR; + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token = address(5); + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig = PriceRegistry + .TokenTransferFeeConfig({ + minFeeUSDCents: 6, + maxFeeUSDCents: 7, + deciBps: 8, + destGasOverhead: 9, + destBytesOverhead: 312, + isEnabled: true + }); + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].token = address(11); + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].tokenTransferFeeConfig = PriceRegistry + .TokenTransferFeeConfig({ + minFeeUSDCents: 12, + maxFeeUSDCents: 13, + deciBps: 14, + destGasOverhead: 15, + destBytesOverhead: 394, + isEnabled: true + }); + + vm.expectEmit(); + emit PriceRegistry.TokenTransferFeeConfigUpdated( + tokenTransferFeeConfigArgs[0].destChainSelector, + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token, + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig + ); + vm.expectEmit(); + emit PriceRegistry.TokenTransferFeeConfigUpdated( + tokenTransferFeeConfigArgs[0].destChainSelector, + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].token, + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].tokenTransferFeeConfig + ); + + PriceRegistry.TokenTransferFeeConfigRemoveArgs[] memory tokensToRemove = + new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](0); + s_priceRegistry.applyTokenTransferFeeConfigUpdates(tokenTransferFeeConfigArgs, tokensToRemove); + + PriceRegistry.TokenTransferFeeConfig memory config0 = s_priceRegistry.getTokenTransferFeeConfig( + tokenTransferFeeConfigArgs[0].destChainSelector, tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token + ); + PriceRegistry.TokenTransferFeeConfig memory config1 = s_priceRegistry.getTokenTransferFeeConfig( + tokenTransferFeeConfigArgs[0].destChainSelector, tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].token + ); + + _assertTokenTransferFeeConfigEqual( + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig, config0 + ); + _assertTokenTransferFeeConfigEqual( + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].tokenTransferFeeConfig, config1 + ); + + // Remove only the first token and validate only the first token is removed + tokensToRemove = new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](1); + tokensToRemove[0] = PriceRegistry.TokenTransferFeeConfigRemoveArgs({ + destChainSelector: tokenTransferFeeConfigArgs[0].destChainSelector, + token: tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token + }); + + vm.expectEmit(); + emit PriceRegistry.TokenTransferFeeConfigDeleted( + tokenTransferFeeConfigArgs[0].destChainSelector, tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token + ); + + s_priceRegistry.applyTokenTransferFeeConfigUpdates( + new PriceRegistry.TokenTransferFeeConfigArgs[](0), tokensToRemove + ); + + config0 = s_priceRegistry.getTokenTransferFeeConfig( + tokenTransferFeeConfigArgs[0].destChainSelector, tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token + ); + config1 = s_priceRegistry.getTokenTransferFeeConfig( + tokenTransferFeeConfigArgs[0].destChainSelector, tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].token + ); + + PriceRegistry.TokenTransferFeeConfig memory emptyConfig; + + _assertTokenTransferFeeConfigEqual(emptyConfig, config0); + _assertTokenTransferFeeConfigEqual( + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].tokenTransferFeeConfig, config1 + ); + } + + function test_ApplyTokenTransferFeeZeroInput() public { + vm.recordLogs(); + s_priceRegistry.applyTokenTransferFeeConfigUpdates( + new PriceRegistry.TokenTransferFeeConfigArgs[](0), new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](0) + ); + + assertEq(vm.getRecordedLogs().length, 0); + } + + // Reverts + + function test_OnlyCallableByOwnerOrAdmin_Revert() public { + vm.startPrank(STRANGER); + PriceRegistry.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs; + + vm.expectRevert("Only callable by owner"); + + s_priceRegistry.applyTokenTransferFeeConfigUpdates( + tokenTransferFeeConfigArgs, new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](0) + ); + } + + function test_InvalidDestBytesOverhead_Revert() public { + PriceRegistry.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + _generateTokenTransferFeeConfigArgs(1, 1); + tokenTransferFeeConfigArgs[0].destChainSelector = DEST_CHAIN_SELECTOR; + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token = address(5); + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig = PriceRegistry + .TokenTransferFeeConfig({ + minFeeUSDCents: 6, + maxFeeUSDCents: 7, + deciBps: 8, + destGasOverhead: 9, + destBytesOverhead: uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES - 1), + isEnabled: true + }); + + vm.expectRevert( + abi.encodeWithSelector( + PriceRegistry.InvalidDestBytesOverhead.selector, + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token, + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig.destBytesOverhead + ) + ); + + s_priceRegistry.applyTokenTransferFeeConfigUpdates( + tokenTransferFeeConfigArgs, new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](0) + ); + } +} + +contract PriceRegistry_getTokenTransferCost is PriceRegistryFeeSetup { + using USDPriceWith18Decimals for uint224; + + function test_NoTokenTransferChargesZeroFee_Success() public view { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + assertEq(0, feeUSDWei); + assertEq(0, destGasOverhead); + assertEq(0, destBytesOverhead); + } + + function test_getTokenTransferCost_selfServeUsesDefaults_Success() public view { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_selfServeTokenDefaultPricing, 1000); + + // Get config to assert it isn't set + PriceRegistry.TokenTransferFeeConfig memory transferFeeConfig = + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token); + + assertFalse(transferFeeConfig.isEnabled); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + // Assert that the default values are used + assertEq(uint256(DEFAULT_TOKEN_FEE_USD_CENTS) * 1e16, feeUSDWei); + assertEq(DEFAULT_TOKEN_DEST_GAS_OVERHEAD, destGasOverhead); + assertEq(DEFAULT_TOKEN_BYTES_OVERHEAD, destBytesOverhead); + } + + function test_SmallTokenTransferChargesMinFeeAndGas_Success() public view { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, 1000); + PriceRegistry.TokenTransferFeeConfig memory transferFeeConfig = + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + assertEq(configUSDCentToWei(transferFeeConfig.minFeeUSDCents), feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(transferFeeConfig.destBytesOverhead, destBytesOverhead); + } + + function test_ZeroAmountTokenTransferChargesMinFeeAndGas_Success() public view { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, 0); + PriceRegistry.TokenTransferFeeConfig memory transferFeeConfig = + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + assertEq(configUSDCentToWei(transferFeeConfig.minFeeUSDCents), feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(transferFeeConfig.destBytesOverhead, destBytesOverhead); + } + + function test_LargeTokenTransferChargesMaxFeeAndGas_Success() public view { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, 1e36); + PriceRegistry.TokenTransferFeeConfig memory transferFeeConfig = + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + assertEq(configUSDCentToWei(transferFeeConfig.maxFeeUSDCents), feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(transferFeeConfig.destBytesOverhead, destBytesOverhead); + } + + function test_FeeTokenBpsFee_Success() public view { + uint256 tokenAmount = 10000e18; + + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, tokenAmount); + PriceRegistry.TokenTransferFeeConfig memory transferFeeConfig = + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + uint256 usdWei = calcUSDValueFromTokenAmount(s_feeTokenPrice, tokenAmount); + uint256 bpsUSDWei = applyBpsRatio( + usdWei, s_priceRegistryTokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig.deciBps + ); + + assertEq(bpsUSDWei, feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(transferFeeConfig.destBytesOverhead, destBytesOverhead); + } + + function test_WETHTokenBpsFee_Success() public view { + uint256 tokenAmount = 100e18; + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](1), + feeToken: s_sourceRouter.getWrappedNative(), + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceRouter.getWrappedNative(), amount: tokenAmount}); + + PriceRegistry.TokenTransferFeeConfig memory transferFeeConfig = + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = s_priceRegistry.getTokenTransferCost( + DEST_CHAIN_SELECTOR, message.feeToken, s_wrappedTokenPrice, message.tokenAmounts + ); + + uint256 usdWei = calcUSDValueFromTokenAmount(s_wrappedTokenPrice, tokenAmount); + uint256 bpsUSDWei = applyBpsRatio( + usdWei, s_priceRegistryTokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[1].tokenTransferFeeConfig.deciBps + ); + + assertEq(bpsUSDWei, feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(transferFeeConfig.destBytesOverhead, destBytesOverhead); + } + + function test_CustomTokenBpsFee_Success() public view { + uint256 tokenAmount = 200000e18; + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](1), + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + message.tokenAmounts[0] = Client.EVMTokenAmount({token: CUSTOM_TOKEN, amount: tokenAmount}); + + PriceRegistry.TokenTransferFeeConfig memory transferFeeConfig = + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token); + + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + uint256 usdWei = calcUSDValueFromTokenAmount(s_customTokenPrice, tokenAmount); + uint256 bpsUSDWei = applyBpsRatio( + usdWei, s_priceRegistryTokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[2].tokenTransferFeeConfig.deciBps + ); + + assertEq(bpsUSDWei, feeUSDWei); + assertEq(transferFeeConfig.destGasOverhead, destGasOverhead); + assertEq(transferFeeConfig.destBytesOverhead, destBytesOverhead); + } + + function test_ZeroFeeConfigChargesMinFee_Success() public { + PriceRegistry.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + _generateTokenTransferFeeConfigArgs(1, 1); + tokenTransferFeeConfigArgs[0].destChainSelector = DEST_CHAIN_SELECTOR; + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token = s_sourceFeeToken; + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig = PriceRegistry + .TokenTransferFeeConfig({ + minFeeUSDCents: 1, + maxFeeUSDCents: 0, + deciBps: 0, + destGasOverhead: 0, + destBytesOverhead: uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES), + isEnabled: true + }); + s_priceRegistry.applyTokenTransferFeeConfigUpdates( + tokenTransferFeeConfigArgs, new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](0) + ); + + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, 1e36); + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, message.feeToken, s_feeTokenPrice, message.tokenAmounts); + + // if token charges 0 bps, it should cost minFee to transfer + assertEq( + configUSDCentToWei(tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig.minFeeUSDCents), + feeUSDWei + ); + assertEq(0, destGasOverhead); + assertEq(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES, destBytesOverhead); + } + + function test_Fuzz_TokenTransferFeeDuplicateTokens_Success(uint256 transfers, uint256 amount) public view { + // It shouldn't be possible to pay materially lower fees by splitting up the transfers. + // Note it is possible to pay higher fees since the minimum fees are added. + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + transfers = bound(transfers, 1, destChainConfig.maxNumberOfTokensPerMsg); + // Cap amount to avoid overflow + amount = bound(amount, 0, 1e36); + Client.EVMTokenAmount[] memory multiple = new Client.EVMTokenAmount[](transfers); + for (uint256 i = 0; i < transfers; ++i) { + multiple[i] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: amount}); + } + Client.EVMTokenAmount[] memory single = new Client.EVMTokenAmount[](1); + single[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: amount * transfers}); + + address feeToken = s_sourceRouter.getWrappedNative(); + + (uint256 feeSingleUSDWei, uint32 gasOverheadSingle, uint32 bytesOverheadSingle) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, feeToken, s_wrappedTokenPrice, single); + (uint256 feeMultipleUSDWei, uint32 gasOverheadMultiple, uint32 bytesOverheadMultiple) = + s_priceRegistry.getTokenTransferCost(DEST_CHAIN_SELECTOR, feeToken, s_wrappedTokenPrice, multiple); + + // Note that there can be a rounding error once per split. + assertGe(feeMultipleUSDWei, (feeSingleUSDWei - destChainConfig.maxNumberOfTokensPerMsg)); + assertEq(gasOverheadMultiple, gasOverheadSingle * transfers); + assertEq(bytesOverheadMultiple, bytesOverheadSingle * transfers); + } + + function test_MixedTokenTransferFee_Success() public view { + address[3] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative(), CUSTOM_TOKEN]; + uint224[3] memory tokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice, s_customTokenPrice]; + PriceRegistry.TokenTransferFeeConfig[3] memory tokenTransferFeeConfigs = [ + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, testTokens[0]), + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, testTokens[1]), + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, testTokens[2]) + ]; + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](3), + feeToken: s_sourceRouter.getWrappedNative(), + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + uint256 expectedTotalGas = 0; + uint256 expectedTotalBytes = 0; + + // Start with small token transfers, total bps fee is lower than min token transfer fee + for (uint256 i = 0; i < testTokens.length; ++i) { + message.tokenAmounts[i] = Client.EVMTokenAmount({token: testTokens[i], amount: 1e14}); + expectedTotalGas += s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, testTokens[i]).destGasOverhead; + expectedTotalBytes += + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, testTokens[i]).destBytesOverhead; + } + (uint256 feeUSDWei, uint32 destGasOverhead, uint32 destBytesOverhead) = s_priceRegistry.getTokenTransferCost( + DEST_CHAIN_SELECTOR, message.feeToken, s_wrappedTokenPrice, message.tokenAmounts + ); + + uint256 expectedFeeUSDWei = 0; + for (uint256 i = 0; i < testTokens.length; ++i) { + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[i].minFeeUSDCents); + } + + assertEq(expectedFeeUSDWei, feeUSDWei); + assertEq(expectedTotalGas, destGasOverhead); + assertEq(expectedTotalBytes, destBytesOverhead); + + // Set 1st token transfer to a meaningful amount so its bps fee is now between min and max fee + message.tokenAmounts[0] = Client.EVMTokenAmount({token: testTokens[0], amount: 10000e18}); + + (feeUSDWei, destGasOverhead, destBytesOverhead) = s_priceRegistry.getTokenTransferCost( + DEST_CHAIN_SELECTOR, message.feeToken, s_wrappedTokenPrice, message.tokenAmounts + ); + expectedFeeUSDWei = applyBpsRatio( + calcUSDValueFromTokenAmount(tokenPrices[0], message.tokenAmounts[0].amount), tokenTransferFeeConfigs[0].deciBps + ); + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[1].minFeeUSDCents); + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[2].minFeeUSDCents); + + assertEq(expectedFeeUSDWei, feeUSDWei); + assertEq(expectedTotalGas, destGasOverhead); + assertEq(expectedTotalBytes, destBytesOverhead); + + // Set 2nd token transfer to a large amount that is higher than maxFeeUSD + message.tokenAmounts[1] = Client.EVMTokenAmount({token: testTokens[1], amount: 1e36}); + + (feeUSDWei, destGasOverhead, destBytesOverhead) = s_priceRegistry.getTokenTransferCost( + DEST_CHAIN_SELECTOR, message.feeToken, s_wrappedTokenPrice, message.tokenAmounts + ); + expectedFeeUSDWei = applyBpsRatio( + calcUSDValueFromTokenAmount(tokenPrices[0], message.tokenAmounts[0].amount), tokenTransferFeeConfigs[0].deciBps + ); + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[1].maxFeeUSDCents); + expectedFeeUSDWei += configUSDCentToWei(tokenTransferFeeConfigs[2].minFeeUSDCents); + + assertEq(expectedFeeUSDWei, feeUSDWei); + assertEq(expectedTotalGas, destGasOverhead); + assertEq(expectedTotalBytes, destBytesOverhead); + } +} + +contract PriceRegistry_getValidatedFee is PriceRegistryFeeSetup { + using USDPriceWith18Decimals for uint224; + + function test_EmptyMessage_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = testTokens[i]; + uint64 premiumMultiplierWeiPerEth = s_priceRegistry.getPremiumMultiplierWeiPerEth(message.feeToken); + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + + uint256 feeAmount = s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + + uint256 gasUsed = GAS_LIMIT + DEST_GAS_OVERHEAD; + uint256 gasFeeUSD = (gasUsed * destChainConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + uint256 messageFeeUSD = (configUSDCentToWei(destChainConfig.networkFeeUSDCents) * premiumMultiplierWeiPerEth); + uint256 dataAvailabilityFeeUSD = s_priceRegistry.getDataAvailabilityCost( + DEST_CHAIN_SELECTOR, USD_PER_DATA_AVAILABILITY_GAS, message.data.length, message.tokenAmounts.length, 0 + ); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD + dataAvailabilityFeeUSD) / feeTokenPrices[i]; + assertEq(totalPriceInFeeToken, feeAmount); + } + } + + function test_ZeroDataAvailabilityMultiplier_Success() public { + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = new PriceRegistry.DestChainConfigArgs[](1); + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + destChainConfigArgs[0] = + PriceRegistry.DestChainConfigArgs({destChainSelector: DEST_CHAIN_SELECTOR, destChainConfig: destChainConfig}); + destChainConfigArgs[0].destChainConfig.destDataAvailabilityMultiplierBps = 0; + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + uint64 premiumMultiplierWeiPerEth = s_priceRegistry.getPremiumMultiplierWeiPerEth(message.feeToken); + + uint256 feeAmount = s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + + uint256 gasUsed = GAS_LIMIT + DEST_GAS_OVERHEAD; + uint256 gasFeeUSD = (gasUsed * destChainConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + uint256 messageFeeUSD = (configUSDCentToWei(destChainConfig.networkFeeUSDCents) * premiumMultiplierWeiPerEth); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD) / s_feeTokenPrice; + assertEq(totalPriceInFeeToken, feeAmount); + } + + function test_HighGasMessage_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + uint256 customGasLimit = MAX_GAS_LIMIT; + uint256 customDataSize = MAX_DATA_SIZE; + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: new bytes(customDataSize), + tokenAmounts: new Client.EVMTokenAmount[](0), + feeToken: testTokens[i], + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: customGasLimit})) + }); + + uint64 premiumMultiplierWeiPerEth = s_priceRegistry.getPremiumMultiplierWeiPerEth(message.feeToken); + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + + uint256 feeAmount = s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + uint256 gasUsed = customGasLimit + DEST_GAS_OVERHEAD + customDataSize * DEST_GAS_PER_PAYLOAD_BYTE; + uint256 gasFeeUSD = (gasUsed * destChainConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + uint256 messageFeeUSD = (configUSDCentToWei(destChainConfig.networkFeeUSDCents) * premiumMultiplierWeiPerEth); + uint256 dataAvailabilityFeeUSD = s_priceRegistry.getDataAvailabilityCost( + DEST_CHAIN_SELECTOR, USD_PER_DATA_AVAILABILITY_GAS, message.data.length, message.tokenAmounts.length, 0 + ); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD + dataAvailabilityFeeUSD) / feeTokenPrices[i]; + assertEq(totalPriceInFeeToken, feeAmount); + } + } + + function test_SingleTokenMessage_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + uint256 tokenAmount = 10000e18; + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(s_sourceFeeToken, tokenAmount); + message.feeToken = testTokens[i]; + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + uint32 destBytesOverhead = + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token).destBytesOverhead; + uint32 tokenBytesOverhead = + destBytesOverhead == 0 ? uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) : destBytesOverhead; + + uint256 feeAmount = s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + + uint256 gasUsed = GAS_LIMIT + DEST_GAS_OVERHEAD + + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[0].token).destGasOverhead; + uint256 gasFeeUSD = (gasUsed * destChainConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + (uint256 transferFeeUSD,,) = s_priceRegistry.getTokenTransferCost( + DEST_CHAIN_SELECTOR, message.feeToken, feeTokenPrices[i], message.tokenAmounts + ); + uint256 messageFeeUSD = (transferFeeUSD * s_priceRegistry.getPremiumMultiplierWeiPerEth(message.feeToken)); + uint256 dataAvailabilityFeeUSD = s_priceRegistry.getDataAvailabilityCost( + DEST_CHAIN_SELECTOR, + USD_PER_DATA_AVAILABILITY_GAS, + message.data.length, + message.tokenAmounts.length, + tokenBytesOverhead + ); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD + dataAvailabilityFeeUSD) / feeTokenPrices[i]; + assertEq(totalPriceInFeeToken, feeAmount); + } + } + + function test_MessageWithDataAndTokenTransfer_Success() public view { + address[2] memory testTokens = [s_sourceFeeToken, s_sourceRouter.getWrappedNative()]; + uint224[2] memory feeTokenPrices = [s_feeTokenPrice, s_wrappedTokenPrice]; + + uint256 customGasLimit = 1_000_000; + for (uint256 i = 0; i < feeTokenPrices.length; ++i) { + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: new Client.EVMTokenAmount[](2), + feeToken: testTokens[i], + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: customGasLimit})) + }); + uint64 premiumMultiplierWeiPerEth = s_priceRegistry.getPremiumMultiplierWeiPerEth(message.feeToken); + PriceRegistry.DestChainConfig memory destChainConfig = s_priceRegistry.getDestChainConfig(DEST_CHAIN_SELECTOR); + + message.tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceFeeToken, amount: 10000e18}); // feeTokenAmount + message.tokenAmounts[1] = Client.EVMTokenAmount({token: CUSTOM_TOKEN, amount: 200000e18}); // customTokenAmount + message.data = "random bits and bytes that should be factored into the cost of the message"; + + uint32 tokenGasOverhead = 0; + uint32 tokenBytesOverhead = 0; + for (uint256 j = 0; j < message.tokenAmounts.length; ++j) { + tokenGasOverhead += + s_priceRegistry.getTokenTransferFeeConfig(DEST_CHAIN_SELECTOR, message.tokenAmounts[j].token).destGasOverhead; + uint32 destBytesOverhead = s_priceRegistry.getTokenTransferFeeConfig( + DEST_CHAIN_SELECTOR, message.tokenAmounts[j].token + ).destBytesOverhead; + tokenBytesOverhead += destBytesOverhead == 0 ? uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) : destBytesOverhead; + } + + uint256 gasUsed = + customGasLimit + DEST_GAS_OVERHEAD + message.data.length * DEST_GAS_PER_PAYLOAD_BYTE + tokenGasOverhead; + uint256 gasFeeUSD = (gasUsed * destChainConfig.gasMultiplierWeiPerEth * USD_PER_GAS); + (uint256 transferFeeUSD,,) = s_priceRegistry.getTokenTransferCost( + DEST_CHAIN_SELECTOR, message.feeToken, feeTokenPrices[i], message.tokenAmounts + ); + uint256 messageFeeUSD = (transferFeeUSD * premiumMultiplierWeiPerEth); + uint256 dataAvailabilityFeeUSD = s_priceRegistry.getDataAvailabilityCost( + DEST_CHAIN_SELECTOR, + USD_PER_DATA_AVAILABILITY_GAS, + message.data.length, + message.tokenAmounts.length, + tokenBytesOverhead + ); + + uint256 totalPriceInFeeToken = (gasFeeUSD + messageFeeUSD + dataAvailabilityFeeUSD) / feeTokenPrices[i]; + assertEq(totalPriceInFeeToken, s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message)); + } + } + + function test_Fuzz_EnforceOutOfOrder(bool enforce, bool allowOutOfOrderExecution) public { + // Update config to enforce allowOutOfOrderExecution = defaultVal. + vm.stopPrank(); + vm.startPrank(OWNER); + + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + destChainConfigArgs[0].destChainConfig.enforceOutOfOrder = enforce; + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = abi.encodeWithSelector( + Client.EVM_EXTRA_ARGS_V2_TAG, + Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT * 2, allowOutOfOrderExecution: allowOutOfOrderExecution}) + ); + + // If enforcement is on, only true should be allowed. + if (enforce && !allowOutOfOrderExecution) { + vm.expectRevert(PriceRegistry.ExtraArgOutOfOrderExecutionMustBeTrue.selector); + } + s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + } + + // Reverts + + function test_DestinationChainNotEnabled_Revert() public { + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.DestinationChainNotEnabled.selector, DEST_CHAIN_SELECTOR + 1)); + s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR + 1, _generateEmptyMessage()); + } + + function test_EnforceOutOfOrder_Revert() public { + // Update config to enforce allowOutOfOrderExecution = true. + vm.stopPrank(); + vm.startPrank(OWNER); + + PriceRegistry.DestChainConfigArgs[] memory destChainConfigArgs = _generatePriceRegistryDestChainConfigArgs(); + destChainConfigArgs[0].destChainConfig.enforceOutOfOrder = true; + s_priceRegistry.applyDestChainConfigUpdates(destChainConfigArgs); + vm.stopPrank(); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + // Empty extraArgs to should revert since it enforceOutOfOrder is true. + message.extraArgs = ""; + + vm.expectRevert(PriceRegistry.ExtraArgOutOfOrderExecutionMustBeTrue.selector); + s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + } + + function test_MessageTooLarge_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.data = new bytes(MAX_DATA_SIZE + 1); + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.MessageTooLarge.selector, MAX_DATA_SIZE, message.data.length)); + + s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + } + + function test_TooManyTokens_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + uint256 tooMany = MAX_TOKENS_LENGTH + 1; + message.tokenAmounts = new Client.EVMTokenAmount[](tooMany); + vm.expectRevert(PriceRegistry.UnsupportedNumberOfTokens.selector); + s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + } + + // Asserts gasLimit must be <=maxGasLimit + function test_MessageGasLimitTooHigh_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: MAX_GAS_LIMIT + 1})); + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.MessageGasLimitTooHigh.selector)); + s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + } + + function test_NotAFeeToken_Revert() public { + address notAFeeToken = address(0x111111); + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage(notAFeeToken, 1); + message.feeToken = notAFeeToken; + + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.TokenNotSupported.selector, notAFeeToken)); + + s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + } + + function test_InvalidEVMAddress_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.receiver = abi.encode(type(uint208).max); + + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, message.receiver)); + + s_priceRegistry.getValidatedFee(DEST_CHAIN_SELECTOR, message); + } +} + +contract PriceRegistry_processMessageArgs is PriceRegistryFeeSetup { + using USDPriceWith18Decimals for uint224; + + function setUp() public virtual override { + super.setUp(); + } + + function test_WithLinkTokenAmount_Success() public view { + ( + uint256 msgFeeJuels, + /* bool isOutOfOrderExecution */ + , + /* bytes memory convertedExtraArgs */ + ) = s_priceRegistry.processMessageArgs( + DEST_CHAIN_SELECTOR, + // LINK + s_sourceTokens[0], + MAX_MSG_FEES_JUELS, + "" + ); + + assertEq(msgFeeJuels, MAX_MSG_FEES_JUELS); + } + + function test_WithConvertedTokenAmount_Success() public view { + address feeToken = s_sourceTokens[1]; + uint256 feeTokenAmount = 10_000 gwei; + uint256 expectedConvertedAmount = s_priceRegistry.convertTokenAmount(feeToken, feeTokenAmount, s_sourceTokens[0]); + + ( + uint256 msgFeeJuels, + /* bool isOutOfOrderExecution */ + , + /* bytes memory convertedExtraArgs */ + ) = s_priceRegistry.processMessageArgs(DEST_CHAIN_SELECTOR, feeToken, feeTokenAmount, ""); + + assertEq(msgFeeJuels, expectedConvertedAmount); + } + + function test_WithEmptyEVMExtraArgs_Success() public view { + ( + /* uint256 msgFeeJuels */ + , + bool isOutOfOrderExecution, + bytes memory convertedExtraArgs + ) = s_priceRegistry.processMessageArgs(DEST_CHAIN_SELECTOR, s_sourceTokens[0], 0, ""); + + assertEq(isOutOfOrderExecution, false); + assertEq( + convertedExtraArgs, Client._argsToBytes(s_priceRegistry.parseEVMExtraArgsFromBytes("", DEST_CHAIN_SELECTOR)) + ); + } + + function test_WithEVMExtraArgsV1_Success() public view { + bytes memory extraArgs = Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 1000})); + + ( + /* uint256 msgFeeJuels */ + , + bool isOutOfOrderExecution, + bytes memory convertedExtraArgs + ) = s_priceRegistry.processMessageArgs(DEST_CHAIN_SELECTOR, s_sourceTokens[0], 0, extraArgs); + + assertEq(isOutOfOrderExecution, false); + assertEq( + convertedExtraArgs, + Client._argsToBytes(s_priceRegistry.parseEVMExtraArgsFromBytes(extraArgs, DEST_CHAIN_SELECTOR)) + ); + } + + function test_WitEVMExtraArgsV2_Success() public view { + bytes memory extraArgs = Client._argsToBytes(Client.EVMExtraArgsV2({gasLimit: 0, allowOutOfOrderExecution: true})); + + ( + /* uint256 msgFeeJuels */ + , + bool isOutOfOrderExecution, + bytes memory convertedExtraArgs + ) = s_priceRegistry.processMessageArgs(DEST_CHAIN_SELECTOR, s_sourceTokens[0], 0, extraArgs); + + assertEq(isOutOfOrderExecution, true); + assertEq( + convertedExtraArgs, + Client._argsToBytes(s_priceRegistry.parseEVMExtraArgsFromBytes(extraArgs, DEST_CHAIN_SELECTOR)) + ); + } + + // Reverts + + function test_MessageFeeTooHigh_Revert() public { + vm.expectRevert( + abi.encodeWithSelector(PriceRegistry.MessageFeeTooHigh.selector, MAX_MSG_FEES_JUELS + 1, MAX_MSG_FEES_JUELS) + ); + + s_priceRegistry.processMessageArgs(DEST_CHAIN_SELECTOR, s_sourceTokens[0], MAX_MSG_FEES_JUELS + 1, ""); + } + + function test_InvalidExtraArgs_Revert() public { + vm.expectRevert(PriceRegistry.InvalidExtraArgsTag.selector); + + s_priceRegistry.processMessageArgs(DEST_CHAIN_SELECTOR, s_sourceTokens[0], 0, "abcde"); + } + + function test_MalformedEVMExtraArgs_Revert() public { + // abi.decode error + vm.expectRevert(); + + s_priceRegistry.processMessageArgs( + DEST_CHAIN_SELECTOR, + s_sourceTokens[0], + 0, + abi.encodeWithSelector(Client.EVM_EXTRA_ARGS_V2_TAG, Client.EVMExtraArgsV1({gasLimit: 100})) + ); + } +} + +contract PriceRegistry_validatePoolReturnData is PriceRegistryFeeSetup { + function test_WithSingleToken_Success() public view { + Client.EVMTokenAmount[] memory sourceTokenAmounts = new Client.EVMTokenAmount[](1); + sourceTokenAmounts[0].amount = 1e18; + sourceTokenAmounts[0].token = s_sourceTokens[0]; + + Internal.RampTokenAmount[] memory rampTokenAmounts = new Internal.RampTokenAmount[](1); + rampTokenAmounts[0] = _getSourceTokenData(sourceTokenAmounts[0], s_tokenAdminRegistry); + + // No revert - successful + s_priceRegistry.validatePoolReturnData(DEST_CHAIN_SELECTOR, rampTokenAmounts, sourceTokenAmounts); + } + + function test_TokenAmountArraysMismatching_Revert() public { + Client.EVMTokenAmount[] memory sourceTokenAmounts = new Client.EVMTokenAmount[](1); + sourceTokenAmounts[0].amount = 1e18; + sourceTokenAmounts[0].token = s_sourceTokens[0]; + + Internal.RampTokenAmount[] memory rampTokenAmounts = new Internal.RampTokenAmount[](1); + rampTokenAmounts[0] = _getSourceTokenData(sourceTokenAmounts[0], s_tokenAdminRegistry); + + // Revert due to index out of bounds access + vm.expectRevert(); + + s_priceRegistry.validatePoolReturnData( + DEST_CHAIN_SELECTOR, new Internal.RampTokenAmount[](1), new Client.EVMTokenAmount[](0) + ); + } + + function test_SourceTokenDataTooLarge_Revert() public { + address sourceETH = s_sourceTokens[1]; + + Client.EVMTokenAmount[] memory sourceTokenAmounts = new Client.EVMTokenAmount[](1); + sourceTokenAmounts[0].amount = 1000; + sourceTokenAmounts[0].token = sourceETH; + + Internal.RampTokenAmount[] memory rampTokenAmounts = new Internal.RampTokenAmount[](1); + rampTokenAmounts[0] = _getSourceTokenData(sourceTokenAmounts[0], s_tokenAdminRegistry); + + // No data set, should succeed + s_priceRegistry.validatePoolReturnData(DEST_CHAIN_SELECTOR, rampTokenAmounts, sourceTokenAmounts); + + // Set max data length, should succeed + rampTokenAmounts[0].extraData = new bytes(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES); + s_priceRegistry.validatePoolReturnData(DEST_CHAIN_SELECTOR, rampTokenAmounts, sourceTokenAmounts); + + // Set data to max length +1, should revert + rampTokenAmounts[0].extraData = new bytes(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + 1); + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.SourceTokenDataTooLarge.selector, sourceETH)); + s_priceRegistry.validatePoolReturnData(DEST_CHAIN_SELECTOR, rampTokenAmounts, sourceTokenAmounts); + + // Set token config to allow larger data + PriceRegistry.TokenTransferFeeConfigArgs[] memory tokenTransferFeeConfigArgs = + _generateTokenTransferFeeConfigArgs(1, 1); + tokenTransferFeeConfigArgs[0].destChainSelector = DEST_CHAIN_SELECTOR; + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].token = sourceETH; + tokenTransferFeeConfigArgs[0].tokenTransferFeeConfigs[0].tokenTransferFeeConfig = PriceRegistry + .TokenTransferFeeConfig({ + minFeeUSDCents: 1, + maxFeeUSDCents: 0, + deciBps: 0, + destGasOverhead: 0, + destBytesOverhead: uint32(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES) + 32, + isEnabled: true + }); + s_priceRegistry.applyTokenTransferFeeConfigUpdates( + tokenTransferFeeConfigArgs, new PriceRegistry.TokenTransferFeeConfigRemoveArgs[](0) + ); + + s_priceRegistry.validatePoolReturnData(DEST_CHAIN_SELECTOR, rampTokenAmounts, sourceTokenAmounts); + + // Set the token data larger than the configured token data, should revert + rampTokenAmounts[0].extraData = new bytes(Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + 32 + 1); + + vm.expectRevert(abi.encodeWithSelector(PriceRegistry.SourceTokenDataTooLarge.selector, sourceETH)); + s_priceRegistry.validatePoolReturnData(DEST_CHAIN_SELECTOR, rampTokenAmounts, sourceTokenAmounts); + } + + function test_InvalidEVMAddressDestToken_Revert() public { + bytes memory nonEvmAddress = abi.encode(type(uint208).max); + + Client.EVMTokenAmount[] memory sourceTokenAmounts = new Client.EVMTokenAmount[](1); + sourceTokenAmounts[0].amount = 1e18; + sourceTokenAmounts[0].token = s_sourceTokens[0]; + + Internal.RampTokenAmount[] memory rampTokenAmounts = new Internal.RampTokenAmount[](1); + rampTokenAmounts[0] = _getSourceTokenData(sourceTokenAmounts[0], s_tokenAdminRegistry); + rampTokenAmounts[0].destTokenAddress = nonEvmAddress; + + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, nonEvmAddress)); + s_priceRegistry.validatePoolReturnData(DEST_CHAIN_SELECTOR, rampTokenAmounts, sourceTokenAmounts); + } +} + +contract PriceRegistry_validateDestFamilyAddress is PriceRegistrySetup { + function test_ValidEVMAddress_Success() public view { + bytes memory encodedAddress = abi.encode(address(10000)); + s_priceRegistry.validateDestFamilyAddress(Internal.CHAIN_FAMILY_SELECTOR_EVM, encodedAddress); + } + + function test_ValidNonEVMAddress_Success() public view { + s_priceRegistry.validateDestFamilyAddress(bytes4(uint32(1)), abi.encode(type(uint208).max)); + } + + // Reverts + + function test_InvalidEVMAddress_Revert() public { + bytes memory invalidAddress = abi.encode(type(uint208).max); + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, invalidAddress)); + s_priceRegistry.validateDestFamilyAddress(Internal.CHAIN_FAMILY_SELECTOR_EVM, invalidAddress); + } + + function test_InvalidEVMAddressEncodePacked_Revert() public { + bytes memory invalidAddress = abi.encodePacked(address(234)); + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, invalidAddress)); + s_priceRegistry.validateDestFamilyAddress(Internal.CHAIN_FAMILY_SELECTOR_EVM, invalidAddress); + } + + function test_InvalidEVMAddressPrecompiles_Revert() public { + for (uint160 i = 0; i < Internal.PRECOMPILE_SPACE; ++i) { + bytes memory invalidAddress = abi.encode(address(i)); + vm.expectRevert(abi.encodeWithSelector(Internal.InvalidEVMAddress.selector, invalidAddress)); + s_priceRegistry.validateDestFamilyAddress(Internal.CHAIN_FAMILY_SELECTOR_EVM, invalidAddress); + } + + s_priceRegistry.validateDestFamilyAddress( + Internal.CHAIN_FAMILY_SELECTOR_EVM, abi.encode(address(uint160(Internal.PRECOMPILE_SPACE))) + ); + } +} + +contract PriceRegistry_parseEVMExtraArgsFromBytes is PriceRegistrySetup { + PriceRegistry.DestChainConfig private s_destChainConfig; + + function setUp() public virtual override { + super.setUp(); + s_destChainConfig = _generatePriceRegistryDestChainConfigArgs()[0].destChainConfig; + } + + function test_EVMExtraArgsV1_Success() public view { + Client.EVMExtraArgsV1 memory inputArgs = Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT}); + bytes memory inputExtraArgs = Client._argsToBytes(inputArgs); + Client.EVMExtraArgsV2 memory expectedOutputArgs = + Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT, allowOutOfOrderExecution: false}); + + vm.assertEq( + abi.encode(s_priceRegistry.parseEVMExtraArgsFromBytes(inputExtraArgs, s_destChainConfig)), + abi.encode(expectedOutputArgs) + ); + } + + function test_EVMExtraArgsV2_Success() public view { + Client.EVMExtraArgsV2 memory inputArgs = + Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT, allowOutOfOrderExecution: true}); + bytes memory inputExtraArgs = Client._argsToBytes(inputArgs); + + vm.assertEq( + abi.encode(s_priceRegistry.parseEVMExtraArgsFromBytes(inputExtraArgs, s_destChainConfig)), abi.encode(inputArgs) + ); + } + + function test_EVMExtraArgsDefault_Success() public view { + Client.EVMExtraArgsV2 memory expectedOutputArgs = + Client.EVMExtraArgsV2({gasLimit: s_destChainConfig.defaultTxGasLimit, allowOutOfOrderExecution: false}); + + vm.assertEq( + abi.encode(s_priceRegistry.parseEVMExtraArgsFromBytes("", s_destChainConfig)), abi.encode(expectedOutputArgs) + ); + } + + // Reverts + + function test_EVMExtraArgsInvalidExtraArgsTag_Revert() public { + Client.EVMExtraArgsV2 memory inputArgs = + Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT, allowOutOfOrderExecution: true}); + bytes memory inputExtraArgs = Client._argsToBytes(inputArgs); + // Invalidate selector + inputExtraArgs[0] = bytes1(uint8(0)); + + vm.expectRevert(PriceRegistry.InvalidExtraArgsTag.selector); + s_priceRegistry.parseEVMExtraArgsFromBytes(inputExtraArgs, s_destChainConfig); + } + + function test_EVMExtraArgsEnforceOutOfOrder_Revert() public { + Client.EVMExtraArgsV2 memory inputArgs = + Client.EVMExtraArgsV2({gasLimit: GAS_LIMIT, allowOutOfOrderExecution: false}); + bytes memory inputExtraArgs = Client._argsToBytes(inputArgs); + s_destChainConfig.enforceOutOfOrder = true; + + vm.expectRevert(PriceRegistry.ExtraArgOutOfOrderExecutionMustBeTrue.selector); + s_priceRegistry.parseEVMExtraArgsFromBytes(inputExtraArgs, s_destChainConfig); + } + + function test_EVMExtraArgsGasLimitTooHigh_Revert() public { + Client.EVMExtraArgsV2 memory inputArgs = + Client.EVMExtraArgsV2({gasLimit: s_destChainConfig.maxPerMsgGasLimit + 1, allowOutOfOrderExecution: true}); + bytes memory inputExtraArgs = Client._argsToBytes(inputArgs); + + vm.expectRevert(PriceRegistry.MessageGasLimitTooHigh.selector); + s_priceRegistry.parseEVMExtraArgsFromBytes(inputExtraArgs, s_destChainConfig); + } +} diff --git a/contracts/src/v0.8/ccip/test/rateLimiter/AggregateRateLimiter.t.sol b/contracts/src/v0.8/ccip/test/rateLimiter/AggregateRateLimiter.t.sol new file mode 100644 index 00000000000..d3a07ef11e9 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/rateLimiter/AggregateRateLimiter.t.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {AggregateRateLimiter} from "../../AggregateRateLimiter.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {AggregateRateLimiterHelper} from "../helpers/AggregateRateLimiterHelper.sol"; +import {PriceRegistrySetup} from "../priceRegistry/PriceRegistry.t.sol"; + +import {stdError} from "forge-std/Test.sol"; + +contract AggregateTokenLimiterSetup is PriceRegistrySetup { + AggregateRateLimiterHelper internal s_rateLimiter; + RateLimiter.Config internal s_config; + + address internal immutable TOKEN = 0x21118E64E1fB0c487F25Dd6d3601FF6af8D32E4e; + uint224 internal constant TOKEN_PRICE = 4e18; + + function setUp() public virtual override { + PriceRegistrySetup.setUp(); + + Internal.PriceUpdates memory priceUpdates = getSingleTokenPriceUpdateStruct(TOKEN, TOKEN_PRICE); + s_priceRegistry.updatePrices(priceUpdates); + + s_config = RateLimiter.Config({isEnabled: true, rate: 5, capacity: 100}); + s_rateLimiter = new AggregateRateLimiterHelper(s_config); + s_rateLimiter.setAdmin(ADMIN); + } +} + +contract AggregateTokenLimiter_constructor is AggregateTokenLimiterSetup { + function test_Constructor_Success() public view { + assertEq(ADMIN, s_rateLimiter.getTokenLimitAdmin()); + assertEq(OWNER, s_rateLimiter.owner()); + + RateLimiter.TokenBucket memory bucket = s_rateLimiter.currentRateLimiterState(); + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.capacity, bucket.tokens); + assertEq(s_config.isEnabled, bucket.isEnabled); + assertEq(BLOCK_TIME, bucket.lastUpdated); + } +} + +contract AggregateTokenLimiter_getTokenLimitAdmin is AggregateTokenLimiterSetup { + function test_GetTokenLimitAdmin_Success() public view { + assertEq(ADMIN, s_rateLimiter.getTokenLimitAdmin()); + } +} + +contract AggregateTokenLimiter_setAdmin is AggregateTokenLimiterSetup { + function test_Owner_Success() public { + vm.expectEmit(); + emit AggregateRateLimiter.AdminSet(STRANGER); + + s_rateLimiter.setAdmin(STRANGER); + assertEq(STRANGER, s_rateLimiter.getTokenLimitAdmin()); + } + + // Reverts + + function test_OnlyOwnerOrAdmin_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert(RateLimiter.OnlyCallableByAdminOrOwner.selector); + + s_rateLimiter.setAdmin(STRANGER); + } +} + +contract AggregateTokenLimiter_getTokenBucket is AggregateTokenLimiterSetup { + function test_GetTokenBucket_Success() public view { + RateLimiter.TokenBucket memory bucket = s_rateLimiter.currentRateLimiterState(); + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.capacity, bucket.tokens); + assertEq(BLOCK_TIME, bucket.lastUpdated); + } + + function test_Refill_Success() public { + s_config.capacity = s_config.capacity * 2; + s_rateLimiter.setRateLimiterConfig(s_config); + + RateLimiter.TokenBucket memory bucket = s_rateLimiter.currentRateLimiterState(); + + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.capacity / 2, bucket.tokens); + assertEq(BLOCK_TIME, bucket.lastUpdated); + + uint256 warpTime = 4; + vm.warp(BLOCK_TIME + warpTime); + + bucket = s_rateLimiter.currentRateLimiterState(); + + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.capacity / 2 + warpTime * s_config.rate, bucket.tokens); + assertEq(BLOCK_TIME + warpTime, bucket.lastUpdated); + + vm.warp(BLOCK_TIME + warpTime * 100); + + // Bucket overflow + bucket = s_rateLimiter.currentRateLimiterState(); + assertEq(s_config.capacity, bucket.tokens); + } + + // Reverts + + function test_TimeUnderflow_Revert() public { + vm.warp(BLOCK_TIME - 1); + + vm.expectRevert(stdError.arithmeticError); + s_rateLimiter.currentRateLimiterState(); + } +} + +contract AggregateTokenLimiter_setRateLimiterConfig is AggregateTokenLimiterSetup { + function test_Owner_Success() public { + setConfig(); + } + + function test_TokenLimitAdmin_Success() public { + vm.startPrank(ADMIN); + setConfig(); + } + + function setConfig() private { + RateLimiter.TokenBucket memory bucket = s_rateLimiter.currentRateLimiterState(); + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + + if (bucket.isEnabled) { + s_config = RateLimiter.Config({isEnabled: false, rate: 0, capacity: 0}); + } else { + s_config = RateLimiter.Config({isEnabled: true, rate: 100, capacity: 200}); + } + + vm.expectEmit(); + emit RateLimiter.ConfigChanged(s_config); + + s_rateLimiter.setRateLimiterConfig(s_config); + + bucket = s_rateLimiter.currentRateLimiterState(); + assertEq(s_config.rate, bucket.rate); + assertEq(s_config.capacity, bucket.capacity); + assertEq(s_config.isEnabled, bucket.isEnabled); + } + + // Reverts + + function test_OnlyOnlyCallableByAdminOrOwner_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert(RateLimiter.OnlyCallableByAdminOrOwner.selector); + + s_rateLimiter.setRateLimiterConfig(s_config); + } +} + +contract AggregateTokenLimiter_rateLimitValue is AggregateTokenLimiterSetup { + function test_RateLimitValueSuccess_gas() public { + vm.pauseGasMetering(); + // start from blocktime that does not equal rate limiter init timestamp + vm.warp(BLOCK_TIME + 1); + + // 15 (tokens) * 4 (price) * 2 (number of times) > 100 (capacity) + uint256 numberOfTokens = 15; + uint256 value = (numberOfTokens * TOKEN_PRICE) / 1e18; + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(value); + + vm.resumeGasMetering(); + s_rateLimiter.rateLimitValue(value); + vm.pauseGasMetering(); + + // Get the updated bucket status + RateLimiter.TokenBucket memory bucket = s_rateLimiter.currentRateLimiterState(); + // Assert the proper value has been taken out of the bucket + assertEq(bucket.capacity - value, bucket.tokens); + + // Since value * 2 > bucket.capacity we cannot take it out twice. + // Expect a revert when we try, with a wait time. + uint256 waitTime = 4; + vm.expectRevert( + abi.encodeWithSelector(RateLimiter.AggregateValueRateLimitReached.selector, waitTime, bucket.tokens) + ); + s_rateLimiter.rateLimitValue(value); + + // Move the block time forward by 10 so the bucket refills by 10 * rate + vm.warp(BLOCK_TIME + 1 + waitTime); + + // The bucket has filled up enough so we can take out more tokens + s_rateLimiter.rateLimitValue(value); + bucket = s_rateLimiter.currentRateLimiterState(); + assertEq(bucket.capacity - value + waitTime * s_config.rate - value, bucket.tokens); + vm.resumeGasMetering(); + } + + // Reverts + + function test_AggregateValueMaxCapacityExceeded_Revert() public { + RateLimiter.TokenBucket memory bucket = s_rateLimiter.currentRateLimiterState(); + + uint256 numberOfTokens = 100; + uint256 value = (numberOfTokens * TOKEN_PRICE) / 1e18; + + vm.expectRevert( + abi.encodeWithSelector( + RateLimiter.AggregateValueMaxCapacityExceeded.selector, bucket.capacity, (numberOfTokens * TOKEN_PRICE) / 1e18 + ) + ); + s_rateLimiter.rateLimitValue(value); + } +} + +contract AggregateTokenLimiter_getTokenValue is AggregateTokenLimiterSetup { + function test_GetTokenValue_Success() public view { + uint256 numberOfTokens = 10; + Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: TOKEN, amount: 10}); + uint256 value = s_rateLimiter.getTokenValue(tokenAmount, s_priceRegistry); + assertEq(value, (numberOfTokens * TOKEN_PRICE) / 1e18); + } + + // Reverts + function test_NoTokenPrice_Reverts() public { + address tokenWithNoPrice = makeAddr("Token with no price"); + Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: tokenWithNoPrice, amount: 10}); + + vm.expectRevert(abi.encodeWithSelector(AggregateRateLimiter.PriceNotFoundForToken.selector, tokenWithNoPrice)); + s_rateLimiter.getTokenValue(tokenAmount, s_priceRegistry); + } +} diff --git a/contracts/src/v0.8/ccip/test/rateLimiter/MultiAggregateRateLimiter.t.sol b/contracts/src/v0.8/ccip/test/rateLimiter/MultiAggregateRateLimiter.t.sol new file mode 100644 index 00000000000..2bd31452f00 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/rateLimiter/MultiAggregateRateLimiter.t.sol @@ -0,0 +1,1201 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol"; +import {MultiAggregateRateLimiter} from "../../MultiAggregateRateLimiter.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {RateLimiter} from "../../libraries/RateLimiter.sol"; +import {BaseTest} from "../BaseTest.t.sol"; +import {MultiAggregateRateLimiterHelper} from "../helpers/MultiAggregateRateLimiterHelper.sol"; +import {PriceRegistrySetup} from "../priceRegistry/PriceRegistry.t.sol"; +import {stdError} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +contract MultiAggregateRateLimiterSetup is BaseTest, PriceRegistrySetup { + MultiAggregateRateLimiterHelper internal s_rateLimiter; + + address internal immutable TOKEN = 0x21118E64E1fB0c487F25Dd6d3601FF6af8D32E4e; + uint224 internal constant TOKEN_PRICE = 4e18; + + uint64 internal constant CHAIN_SELECTOR_1 = 5009297550715157269; + uint64 internal constant CHAIN_SELECTOR_2 = 4949039107694359620; + + RateLimiter.Config internal RATE_LIMITER_CONFIG_1 = RateLimiter.Config({isEnabled: true, rate: 5, capacity: 100}); + RateLimiter.Config internal RATE_LIMITER_CONFIG_2 = RateLimiter.Config({isEnabled: true, rate: 10, capacity: 200}); + + address internal immutable MOCK_OFFRAMP = address(1111); + address internal immutable MOCK_ONRAMP = address(1112); + + address[] internal s_authorizedCallers; + + function setUp() public virtual override(BaseTest, PriceRegistrySetup) { + BaseTest.setUp(); + PriceRegistrySetup.setUp(); + + Internal.PriceUpdates memory priceUpdates = getSingleTokenPriceUpdateStruct(TOKEN, TOKEN_PRICE); + s_priceRegistry.updatePrices(priceUpdates); + + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](4); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + configUpdates[1] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_2, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_2 + }); + configUpdates[2] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1, + isOutboundLane: true, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + configUpdates[3] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_2, + isOutboundLane: true, + rateLimiterConfig: RATE_LIMITER_CONFIG_2 + }); + + s_authorizedCallers = new address[](2); + s_authorizedCallers[0] = MOCK_OFFRAMP; + s_authorizedCallers[1] = MOCK_ONRAMP; + + s_rateLimiter = new MultiAggregateRateLimiterHelper(address(s_priceRegistry), s_authorizedCallers); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + } + + function _assertConfigWithTokenBucketEquality( + RateLimiter.Config memory config, + RateLimiter.TokenBucket memory tokenBucket + ) internal pure { + assertEq(config.rate, tokenBucket.rate); + assertEq(config.capacity, tokenBucket.capacity); + assertEq(config.capacity, tokenBucket.tokens); + assertEq(config.isEnabled, tokenBucket.isEnabled); + } + + function _assertTokenBucketEquality( + RateLimiter.TokenBucket memory tokenBucketA, + RateLimiter.TokenBucket memory tokenBucketB + ) internal pure { + assertEq(tokenBucketA.rate, tokenBucketB.rate); + assertEq(tokenBucketA.capacity, tokenBucketB.capacity); + assertEq(tokenBucketA.tokens, tokenBucketB.tokens); + assertEq(tokenBucketA.isEnabled, tokenBucketB.isEnabled); + } + + function _generateAny2EVMMessage( + uint64 sourceChainSelector, + Client.EVMTokenAmount[] memory tokenAmounts + ) internal pure returns (Client.Any2EVMMessage memory) { + return Client.Any2EVMMessage({ + messageId: keccak256(bytes("messageId")), + sourceChainSelector: sourceChainSelector, + sender: abi.encode(OWNER), + data: abi.encode(0), + destTokenAmounts: tokenAmounts + }); + } + + function _generateAny2EVMMessageNoTokens(uint64 sourceChainSelector) + internal + pure + returns (Client.Any2EVMMessage memory) + { + return _generateAny2EVMMessage(sourceChainSelector, new Client.EVMTokenAmount[](0)); + } +} + +contract MultiAggregateRateLimiter_constructor is MultiAggregateRateLimiterSetup { + function test_ConstructorNoAuthorizedCallers_Success() public { + address[] memory authorizedCallers = new address[](0); + + vm.recordLogs(); + s_rateLimiter = new MultiAggregateRateLimiterHelper(address(s_priceRegistry), authorizedCallers); + + // PriceRegistrySet + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 1); + + assertEq(OWNER, s_rateLimiter.owner()); + assertEq(address(s_priceRegistry), s_rateLimiter.getPriceRegistry()); + } + + function test_Constructor_Success() public { + address[] memory authorizedCallers = new address[](2); + authorizedCallers[0] = MOCK_OFFRAMP; + authorizedCallers[1] = MOCK_ONRAMP; + + vm.expectEmit(); + emit MultiAggregateRateLimiter.PriceRegistrySet(address(s_priceRegistry)); + + s_rateLimiter = new MultiAggregateRateLimiterHelper(address(s_priceRegistry), authorizedCallers); + + assertEq(OWNER, s_rateLimiter.owner()); + assertEq(address(s_priceRegistry), s_rateLimiter.getPriceRegistry()); + } +} + +contract MultiAggregateRateLimiter_setPriceRegistry is MultiAggregateRateLimiterSetup { + function test_Owner_Success() public { + address newAddress = address(42); + + vm.expectEmit(); + emit MultiAggregateRateLimiter.PriceRegistrySet(newAddress); + + s_rateLimiter.setPriceRegistry(newAddress); + assertEq(newAddress, s_rateLimiter.getPriceRegistry()); + } + + // Reverts + + function test_OnlyOwner_Revert() public { + vm.startPrank(STRANGER); + vm.expectRevert(bytes("Only callable by owner")); + + s_rateLimiter.setPriceRegistry(STRANGER); + } + + function test_ZeroAddress_Revert() public { + vm.expectRevert(AuthorizedCallers.ZeroAddressNotAllowed.selector); + s_rateLimiter.setPriceRegistry(address(0)); + } +} + +contract MultiAggregateRateLimiter_getTokenBucket is MultiAggregateRateLimiterSetup { + function test_GetTokenBucket_Success() public view { + RateLimiter.TokenBucket memory bucketInbound = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + _assertConfigWithTokenBucketEquality(RATE_LIMITER_CONFIG_1, bucketInbound); + assertEq(BLOCK_TIME, bucketInbound.lastUpdated); + + RateLimiter.TokenBucket memory bucketOutbound = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, true); + _assertConfigWithTokenBucketEquality(RATE_LIMITER_CONFIG_1, bucketOutbound); + assertEq(BLOCK_TIME, bucketOutbound.lastUpdated); + } + + function test_Refill_Success() public { + RATE_LIMITER_CONFIG_1.capacity = RATE_LIMITER_CONFIG_1.capacity * 2; + + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + RateLimiter.TokenBucket memory bucket = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + + assertEq(RATE_LIMITER_CONFIG_1.rate, bucket.rate); + assertEq(RATE_LIMITER_CONFIG_1.capacity, bucket.capacity); + assertEq(RATE_LIMITER_CONFIG_1.capacity / 2, bucket.tokens); + assertEq(BLOCK_TIME, bucket.lastUpdated); + + uint256 warpTime = 4; + vm.warp(BLOCK_TIME + warpTime); + + bucket = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + + assertEq(RATE_LIMITER_CONFIG_1.rate, bucket.rate); + assertEq(RATE_LIMITER_CONFIG_1.capacity, bucket.capacity); + assertEq(RATE_LIMITER_CONFIG_1.capacity / 2 + warpTime * RATE_LIMITER_CONFIG_1.rate, bucket.tokens); + assertEq(BLOCK_TIME + warpTime, bucket.lastUpdated); + + vm.warp(BLOCK_TIME + warpTime * 100); + + // Bucket overflow + bucket = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + assertEq(RATE_LIMITER_CONFIG_1.capacity, bucket.tokens); + } + + // Reverts + + function test_TimeUnderflow_Revert() public { + vm.warp(BLOCK_TIME - 1); + + vm.expectRevert(stdError.arithmeticError); + s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + } +} + +contract MultiAggregateRateLimiter_applyRateLimiterConfigUpdates is MultiAggregateRateLimiterSetup { + function test_ZeroConfigs_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](0); + + vm.recordLogs(); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 0); + } + + function test_SingleConfig_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1 + 1, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + + vm.expectEmit(); + emit MultiAggregateRateLimiter.RateLimiterConfigUpdated( + configUpdates[0].remoteChainSelector, false, configUpdates[0].rateLimiterConfig + ); + + vm.recordLogs(); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 1); + + RateLimiter.TokenBucket memory bucket1 = + s_rateLimiter.currentRateLimiterState(configUpdates[0].remoteChainSelector, false); + _assertConfigWithTokenBucketEquality(configUpdates[0].rateLimiterConfig, bucket1); + assertEq(BLOCK_TIME, bucket1.lastUpdated); + } + + function test_SingleConfigOutbound_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1 + 1, + isOutboundLane: true, + rateLimiterConfig: RATE_LIMITER_CONFIG_2 + }); + + vm.expectEmit(); + emit MultiAggregateRateLimiter.RateLimiterConfigUpdated( + configUpdates[0].remoteChainSelector, true, configUpdates[0].rateLimiterConfig + ); + + vm.recordLogs(); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 1); + + RateLimiter.TokenBucket memory bucket1 = + s_rateLimiter.currentRateLimiterState(configUpdates[0].remoteChainSelector, true); + _assertConfigWithTokenBucketEquality(configUpdates[0].rateLimiterConfig, bucket1); + assertEq(BLOCK_TIME, bucket1.lastUpdated); + } + + function test_MultipleConfigs_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](5); + + for (uint64 i; i < configUpdates.length; ++i) { + configUpdates[i] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1 + i + 1, + isOutboundLane: i % 2 == 0 ? false : true, + rateLimiterConfig: RateLimiter.Config({isEnabled: true, rate: 5 + i, capacity: 100 + i}) + }); + + vm.expectEmit(); + emit MultiAggregateRateLimiter.RateLimiterConfigUpdated( + configUpdates[i].remoteChainSelector, configUpdates[i].isOutboundLane, configUpdates[i].rateLimiterConfig + ); + } + + vm.recordLogs(); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, configUpdates.length); + + for (uint256 i; i < configUpdates.length; ++i) { + RateLimiter.TokenBucket memory bucket = + s_rateLimiter.currentRateLimiterState(configUpdates[i].remoteChainSelector, configUpdates[i].isOutboundLane); + _assertConfigWithTokenBucketEquality(configUpdates[i].rateLimiterConfig, bucket); + assertEq(BLOCK_TIME, bucket.lastUpdated); + } + } + + function test_MultipleConfigsBothLanes_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](2); + + for (uint64 i; i < configUpdates.length; ++i) { + configUpdates[i] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1 + 1, + isOutboundLane: i % 2 == 0 ? false : true, + rateLimiterConfig: RateLimiter.Config({isEnabled: true, rate: 5 + i, capacity: 100 + i}) + }); + + vm.expectEmit(); + emit MultiAggregateRateLimiter.RateLimiterConfigUpdated( + configUpdates[i].remoteChainSelector, configUpdates[i].isOutboundLane, configUpdates[i].rateLimiterConfig + ); + } + + vm.recordLogs(); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, configUpdates.length); + + for (uint256 i; i < configUpdates.length; ++i) { + RateLimiter.TokenBucket memory bucket = + s_rateLimiter.currentRateLimiterState(configUpdates[i].remoteChainSelector, configUpdates[i].isOutboundLane); + _assertConfigWithTokenBucketEquality(configUpdates[i].rateLimiterConfig, bucket); + assertEq(BLOCK_TIME, bucket.lastUpdated); + } + } + + function test_UpdateExistingConfig_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_2 + }); + + RateLimiter.TokenBucket memory bucket1 = + s_rateLimiter.currentRateLimiterState(configUpdates[0].remoteChainSelector, false); + + // Capacity equals tokens + assertEq(bucket1.capacity, bucket1.tokens); + + vm.expectEmit(); + emit MultiAggregateRateLimiter.RateLimiterConfigUpdated( + configUpdates[0].remoteChainSelector, false, configUpdates[0].rateLimiterConfig + ); + + vm.recordLogs(); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + vm.warp(BLOCK_TIME + 1); + bucket1 = s_rateLimiter.currentRateLimiterState(configUpdates[0].remoteChainSelector, false); + assertEq(BLOCK_TIME + 1, bucket1.lastUpdated); + + // Tokens < capacity since capacity doubled + assertTrue(bucket1.capacity != bucket1.tokens); + + // Outbound lane config remains unchanged + _assertConfigWithTokenBucketEquality( + RATE_LIMITER_CONFIG_1, s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, true) + ); + } + + function test_UpdateExistingConfigWithNoDifference_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + + RateLimiter.TokenBucket memory bucketPreUpdate = + s_rateLimiter.currentRateLimiterState(configUpdates[0].remoteChainSelector, false); + + vm.expectEmit(); + emit MultiAggregateRateLimiter.RateLimiterConfigUpdated( + configUpdates[0].remoteChainSelector, false, configUpdates[0].rateLimiterConfig + ); + + vm.recordLogs(); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + vm.warp(BLOCK_TIME + 1); + RateLimiter.TokenBucket memory bucketPostUpdate = + s_rateLimiter.currentRateLimiterState(configUpdates[0].remoteChainSelector, false); + _assertTokenBucketEquality(bucketPreUpdate, bucketPostUpdate); + assertEq(BLOCK_TIME + 1, bucketPostUpdate.lastUpdated); + } + + // Reverts + function test_ZeroChainSelector_Revert() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: 0, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + + vm.expectRevert(MultiAggregateRateLimiter.ZeroChainSelectorNotAllowed.selector); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + } + + function test_OnlyCallableByOwner_Revert() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1 + 1, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + vm.startPrank(STRANGER); + + vm.expectRevert(bytes("Only callable by owner")); + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + } +} + +contract MultiAggregateRateLimiter_getTokenValue is MultiAggregateRateLimiterSetup { + function test_GetTokenValue_Success() public view { + uint256 numberOfTokens = 10; + Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: TOKEN, amount: 10}); + uint256 value = s_rateLimiter.getTokenValue(tokenAmount); + assertEq(value, (numberOfTokens * TOKEN_PRICE) / 1e18); + } + + // Reverts + function test_NoTokenPrice_Reverts() public { + address tokenWithNoPrice = makeAddr("Token with no price"); + Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: tokenWithNoPrice, amount: 10}); + + vm.expectRevert(abi.encodeWithSelector(MultiAggregateRateLimiter.PriceNotFoundForToken.selector, tokenWithNoPrice)); + s_rateLimiter.getTokenValue(tokenAmount); + } +} + +contract MultiAggregateRateLimiter_updateRateLimitTokens is MultiAggregateRateLimiterSetup { + function setUp() public virtual override { + super.setUp(); + + // Clear rate limit tokens state + MultiAggregateRateLimiter.LocalRateLimitToken[] memory removes = + new MultiAggregateRateLimiter.LocalRateLimitToken[](s_sourceTokens.length); + for (uint256 i = 0; i < s_sourceTokens.length; ++i) { + removes[i] = MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[i] + }); + } + s_rateLimiter.updateRateLimitTokens(removes, new MultiAggregateRateLimiter.RateLimitTokenArgs[](0)); + } + + function test_UpdateRateLimitTokensSingleChain_Success() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory adds = new MultiAggregateRateLimiter.RateLimitTokenArgs[](2); + adds[0] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[0] + }), + remoteToken: bytes32(bytes20(s_sourceTokens[0])) + }); + adds[1] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[1] + }), + remoteToken: bytes32(bytes20(s_sourceTokens[1])) + }); + + for (uint256 i = 0; i < adds.length; ++i) { + vm.expectEmit(); + emit MultiAggregateRateLimiter.TokenAggregateRateLimitAdded( + CHAIN_SELECTOR_1, adds[i].remoteToken, adds[i].localTokenArgs.localToken + ); + } + + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), adds); + + (address[] memory localTokens, bytes32[] memory remoteTokens) = + s_rateLimiter.getAllRateLimitTokens(CHAIN_SELECTOR_1); + + assertEq(localTokens.length, adds.length); + assertEq(localTokens.length, remoteTokens.length); + + for (uint256 i = 0; i < adds.length; ++i) { + assertEq(adds[i].remoteToken, remoteTokens[i]); + assertEq(adds[i].localTokenArgs.localToken, localTokens[i]); + } + } + + function test_UpdateRateLimitTokensMultipleChains_Success() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory adds = new MultiAggregateRateLimiter.RateLimitTokenArgs[](2); + adds[0] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[0] + }), + remoteToken: bytes32(bytes20(s_sourceTokens[0])) + }); + adds[1] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_2, + localToken: s_destTokens[1] + }), + remoteToken: bytes32(bytes20(s_sourceTokens[1])) + }); + + for (uint256 i = 0; i < adds.length; ++i) { + vm.expectEmit(); + emit MultiAggregateRateLimiter.TokenAggregateRateLimitAdded( + adds[i].localTokenArgs.remoteChainSelector, adds[i].remoteToken, adds[i].localTokenArgs.localToken + ); + } + + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), adds); + + (address[] memory localTokensChain1, bytes32[] memory remoteTokensChain1) = + s_rateLimiter.getAllRateLimitTokens(CHAIN_SELECTOR_1); + + assertEq(localTokensChain1.length, 1); + assertEq(localTokensChain1.length, remoteTokensChain1.length); + assertEq(localTokensChain1[0], adds[0].localTokenArgs.localToken); + assertEq(remoteTokensChain1[0], adds[0].remoteToken); + + (address[] memory localTokensChain2, bytes32[] memory remoteTokensChain2) = + s_rateLimiter.getAllRateLimitTokens(CHAIN_SELECTOR_2); + + assertEq(localTokensChain2.length, 1); + assertEq(localTokensChain2.length, remoteTokensChain2.length); + assertEq(localTokensChain2[0], adds[1].localTokenArgs.localToken); + assertEq(remoteTokensChain2[0], adds[1].remoteToken); + } + + function test_UpdateRateLimitTokens_AddsAndRemoves_Success() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory adds = new MultiAggregateRateLimiter.RateLimitTokenArgs[](2); + adds[0] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[0] + }), + remoteToken: bytes32(bytes20(s_sourceTokens[0])) + }); + adds[1] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[1] + }), + remoteToken: bytes32(bytes20(s_sourceTokens[1])) + }); + + MultiAggregateRateLimiter.LocalRateLimitToken[] memory removes = + new MultiAggregateRateLimiter.LocalRateLimitToken[](1); + removes[0] = adds[0].localTokenArgs; + + for (uint256 i = 0; i < adds.length; ++i) { + vm.expectEmit(); + emit MultiAggregateRateLimiter.TokenAggregateRateLimitAdded( + CHAIN_SELECTOR_1, adds[i].remoteToken, adds[i].localTokenArgs.localToken + ); + } + + s_rateLimiter.updateRateLimitTokens(removes, adds); + + for (uint256 i = 0; i < removes.length; ++i) { + vm.expectEmit(); + emit MultiAggregateRateLimiter.TokenAggregateRateLimitRemoved(CHAIN_SELECTOR_1, removes[i].localToken); + } + + s_rateLimiter.updateRateLimitTokens(removes, new MultiAggregateRateLimiter.RateLimitTokenArgs[](0)); + + (address[] memory localTokens, bytes32[] memory remoteTokens) = + s_rateLimiter.getAllRateLimitTokens(CHAIN_SELECTOR_1); + + assertEq(1, remoteTokens.length); + assertEq(adds[1].remoteToken, remoteTokens[0]); + + assertEq(1, localTokens.length); + assertEq(adds[1].localTokenArgs.localToken, localTokens[0]); + } + + function test_UpdateRateLimitTokens_RemoveNonExistentToken_Success() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory adds = new MultiAggregateRateLimiter.RateLimitTokenArgs[](0); + + MultiAggregateRateLimiter.LocalRateLimitToken[] memory removes = + new MultiAggregateRateLimiter.LocalRateLimitToken[](1); + removes[0] = MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[0] + }); + + vm.recordLogs(); + s_rateLimiter.updateRateLimitTokens(removes, adds); + + // No event since no remove occurred + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 0); + + (address[] memory localTokens, bytes32[] memory remoteTokens) = + s_rateLimiter.getAllRateLimitTokens(CHAIN_SELECTOR_1); + + assertEq(localTokens.length, 0); + assertEq(localTokens.length, remoteTokens.length); + } + + // Reverts + + function test_ZeroSourceToken_Revert() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory adds = new MultiAggregateRateLimiter.RateLimitTokenArgs[](1); + adds[0] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[0] + }), + remoteToken: bytes32(bytes20(address(0))) + }); + + vm.expectRevert(AuthorizedCallers.ZeroAddressNotAllowed.selector); + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), adds); + } + + function test_ZeroDestToken_Revert() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory adds = new MultiAggregateRateLimiter.RateLimitTokenArgs[](1); + adds[0] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: address(0) + }), + remoteToken: bytes32(bytes20(s_destTokens[0])) + }); + + vm.expectRevert(AuthorizedCallers.ZeroAddressNotAllowed.selector); + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), adds); + } + + function test_NonOwner_Revert() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory adds = new MultiAggregateRateLimiter.RateLimitTokenArgs[](4); + + vm.startPrank(STRANGER); + + vm.expectRevert(bytes("Only callable by owner")); + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), adds); + } +} + +contract MultiAggregateRateLimiter_onInboundMessage is MultiAggregateRateLimiterSetup { + address internal immutable MOCK_RECEIVER = address(1113); + + function setUp() public virtual override { + super.setUp(); + + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory tokensToAdd = + new MultiAggregateRateLimiter.RateLimitTokenArgs[](s_sourceTokens.length); + for (uint224 i = 0; i < s_sourceTokens.length; ++i) { + tokensToAdd[i] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[i] + }), + remoteToken: bytes32(bytes20(s_sourceTokens[i])) + }); + + Internal.PriceUpdates memory priceUpdates = + getSingleTokenPriceUpdateStruct(s_destTokens[i], TOKEN_PRICE * (i + 1)); + s_priceRegistry.updatePrices(priceUpdates); + } + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), tokensToAdd); + } + + function test_ValidateMessageWithNoTokens_Success() public { + vm.startPrank(MOCK_OFFRAMP); + + vm.recordLogs(); + s_rateLimiter.onInboundMessage(_generateAny2EVMMessageNoTokens(CHAIN_SELECTOR_1)); + + // No consumed rate limit events + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 0); + } + + function test_ValidateMessageWithTokens_Success() public { + vm.startPrank(MOCK_OFFRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_destTokens[0], amount: 3}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_destTokens[1], amount: 1}); + + // 3 tokens * TOKEN_PRICE + 1 token * (2 * TOKEN_PRICE) + vm.expectEmit(); + emit RateLimiter.TokensConsumed((5 * TOKEN_PRICE) / 1e18); + + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + } + + function test_ValidateMessageWithDisabledRateLimitToken_Success() public { + MultiAggregateRateLimiter.LocalRateLimitToken[] memory removes = + new MultiAggregateRateLimiter.LocalRateLimitToken[](1); + removes[0] = MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_destTokens[1] + }); + s_rateLimiter.updateRateLimitTokens(removes, new MultiAggregateRateLimiter.RateLimitTokenArgs[](0)); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_destTokens[0], amount: 5}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_destTokens[1], amount: 1}); + + vm.startPrank(MOCK_OFFRAMP); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed((5 * TOKEN_PRICE) / 1e18); + + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + } + + function test_ValidateMessageWithRateLimitDisabled_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1, + isOutboundLane: false, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + configUpdates[0].rateLimiterConfig.isEnabled = false; + + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_destTokens[0], amount: 1000}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_destTokens[1], amount: 50}); + + vm.startPrank(MOCK_OFFRAMP); + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + + // No consumed rate limit events + Vm.Log[] memory logEntries = vm.getRecordedLogs(); + assertEq(logEntries.length, 0); + } + + function test_ValidateMessageWithTokensOnDifferentChains_Success() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory tokensToAdd = + new MultiAggregateRateLimiter.RateLimitTokenArgs[](s_sourceTokens.length); + for (uint224 i = 0; i < s_sourceTokens.length; ++i) { + tokensToAdd[i] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_2, + localToken: s_destTokens[i] + }), + // Create a remote token address that is different from CHAIN_SELECTOR_1 + remoteToken: bytes32(uint256(uint160(s_sourceTokens[i])) + type(uint160).max + 1) + }); + } + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), tokensToAdd); + + vm.startPrank(MOCK_OFFRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_destTokens[0], amount: 2}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_destTokens[1], amount: 1}); + + // 2 tokens * (TOKEN_PRICE) + 1 token * (2 * TOKEN_PRICE) + uint256 totalValue = (4 * TOKEN_PRICE) / 1e18; + + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + + // Chain 1 changed + RateLimiter.TokenBucket memory bucketChain1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + assertEq(bucketChain1.capacity - totalValue, bucketChain1.tokens); + + // Chain 2 unchanged + RateLimiter.TokenBucket memory bucketChain2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_2, false); + assertEq(bucketChain2.capacity, bucketChain2.tokens); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(totalValue); + + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_2, tokenAmounts)); + + // Chain 1 unchanged + bucketChain1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + assertEq(bucketChain1.capacity - totalValue, bucketChain1.tokens); + + // Chain 2 changed + bucketChain2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_2, false); + assertEq(bucketChain2.capacity - totalValue, bucketChain2.tokens); + } + + function test_ValidateMessageWithDifferentTokensOnDifferentChains_Success() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory tokensToAdd = + new MultiAggregateRateLimiter.RateLimitTokenArgs[](1); + + // Only 1 rate limited token on different chain + tokensToAdd[0] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_2, + localToken: s_destTokens[0] + }), + // Create a remote token address that is different from CHAIN_SELECTOR_1 + remoteToken: bytes32(uint256(uint160(s_sourceTokens[0])) + type(uint160).max + 1) + }); + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), tokensToAdd); + + vm.startPrank(MOCK_OFFRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_destTokens[0], amount: 3}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_destTokens[1], amount: 1}); + + // 3 tokens * (TOKEN_PRICE) + 1 token * (2 * TOKEN_PRICE) + uint256 totalValue = (5 * TOKEN_PRICE) / 1e18; + + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + + // Chain 1 changed + RateLimiter.TokenBucket memory bucketChain1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + assertEq(bucketChain1.capacity - totalValue, bucketChain1.tokens); + + // Chain 2 unchanged + RateLimiter.TokenBucket memory bucketChain2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_2, false); + assertEq(bucketChain2.capacity, bucketChain2.tokens); + + // 3 tokens * (TOKEN_PRICE) + uint256 totalValue2 = (3 * TOKEN_PRICE) / 1e18; + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(totalValue2); + + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_2, tokenAmounts)); + + // Chain 1 unchanged + bucketChain1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + assertEq(bucketChain1.capacity - totalValue, bucketChain1.tokens); + + // Chain 2 changed + bucketChain2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_2, false); + assertEq(bucketChain2.capacity - totalValue2, bucketChain2.tokens); + } + + function test_ValidateMessageWithRateLimitReset_Success() public { + vm.startPrank(MOCK_OFFRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_destTokens[0], amount: 20}); + + // Remaining capacity: 100 -> 20 + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + + // Cannot fit 80 rate limit value (need to wait at least 12 blocks, current capacity is 20) + vm.expectRevert(abi.encodeWithSelector(RateLimiter.AggregateValueRateLimitReached.selector, 12, 20)); + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + + // Remaining capacity: 20 -> 35 (need to wait 9 more blocks) + vm.warp(BLOCK_TIME + 3); + vm.expectRevert(abi.encodeWithSelector(RateLimiter.AggregateValueRateLimitReached.selector, 9, 35)); + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + + // Remaining capacity: 35 -> 80 (can fit exactly 80) + vm.warp(BLOCK_TIME + 12); + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + } + + // Reverts + + function test_ValidateMessageWithRateLimitExceeded_Revert() public { + vm.startPrank(MOCK_OFFRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_destTokens[0], amount: 80}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_destTokens[1], amount: 30}); + + uint256 totalValue = (80 * TOKEN_PRICE + 2 * (30 * TOKEN_PRICE)) / 1e18; + vm.expectRevert(abi.encodeWithSelector(RateLimiter.AggregateValueMaxCapacityExceeded.selector, 100, totalValue)); + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + } + + function test_ValidateMessageFromUnauthorizedCaller_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert(abi.encodeWithSelector(AuthorizedCallers.UnauthorizedCaller.selector, STRANGER)); + s_rateLimiter.onInboundMessage(_generateAny2EVMMessageNoTokens(CHAIN_SELECTOR_1)); + } +} + +contract MultiAggregateRateLimiter_onOutboundMessage is MultiAggregateRateLimiterSetup { + function setUp() public virtual override { + super.setUp(); + + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory tokensToAdd = + new MultiAggregateRateLimiter.RateLimitTokenArgs[](s_sourceTokens.length); + for (uint224 i = 0; i < s_sourceTokens.length; ++i) { + tokensToAdd[i] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_sourceTokens[i] + }), + remoteToken: bytes32(bytes20(s_destTokenBySourceToken[s_sourceTokens[i]])) + }); + + Internal.PriceUpdates memory priceUpdates = + getSingleTokenPriceUpdateStruct(s_sourceTokens[i], TOKEN_PRICE * (i + 1)); + s_priceRegistry.updatePrices(priceUpdates); + } + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), tokensToAdd); + } + + function test_ValidateMessageWithNoTokens_Success() public { + vm.startPrank(MOCK_ONRAMP); + + vm.recordLogs(); + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessageNoTokens()); + + // No consumed rate limit events + assertEq(vm.getRecordedLogs().length, 0); + } + + function test_onOutboundMessage_ValidateMessageWithTokens_Success() public { + vm.startPrank(MOCK_ONRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: 3}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_sourceTokens[1], amount: 1}); + + // 3 tokens * TOKEN_PRICE + 1 token * (2 * TOKEN_PRICE) + vm.expectEmit(); + emit RateLimiter.TokensConsumed((5 * TOKEN_PRICE) / 1e18); + + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + } + + function test_onOutboundMessage_ValidateMessageWithDisabledRateLimitToken_Success() public { + MultiAggregateRateLimiter.LocalRateLimitToken[] memory removes = + new MultiAggregateRateLimiter.LocalRateLimitToken[](1); + removes[0] = MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_1, + localToken: s_sourceTokens[1] + }); + s_rateLimiter.updateRateLimitTokens(removes, new MultiAggregateRateLimiter.RateLimitTokenArgs[](0)); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: 5}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_sourceTokens[1], amount: 1}); + + vm.startPrank(MOCK_ONRAMP); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed((5 * TOKEN_PRICE) / 1e18); + + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + } + + function test_onOutboundMessage_ValidateMessageWithRateLimitDisabled_Success() public { + MultiAggregateRateLimiter.RateLimiterConfigArgs[] memory configUpdates = + new MultiAggregateRateLimiter.RateLimiterConfigArgs[](1); + configUpdates[0] = MultiAggregateRateLimiter.RateLimiterConfigArgs({ + remoteChainSelector: CHAIN_SELECTOR_1, + isOutboundLane: true, + rateLimiterConfig: RATE_LIMITER_CONFIG_1 + }); + configUpdates[0].rateLimiterConfig.isEnabled = false; + + s_rateLimiter.applyRateLimiterConfigUpdates(configUpdates); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: 1000}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_sourceTokens[1], amount: 50}); + + vm.startPrank(MOCK_ONRAMP); + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + + // No consumed rate limit events + assertEq(vm.getRecordedLogs().length, 0); + } + + function test_onOutboundMessage_ValidateMessageWithTokensOnDifferentChains_Success() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory tokensToAdd = + new MultiAggregateRateLimiter.RateLimitTokenArgs[](s_sourceTokens.length); + for (uint224 i = 0; i < s_sourceTokens.length; ++i) { + tokensToAdd[i] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_2, + localToken: s_sourceTokens[i] + }), + // Create a remote token address that is different from CHAIN_SELECTOR_1 + remoteToken: bytes32(uint256(uint160(s_destTokenBySourceToken[s_sourceTokens[i]])) + type(uint160).max + 1) + }); + } + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), tokensToAdd); + + vm.startPrank(MOCK_ONRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: 2}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_sourceTokens[1], amount: 1}); + + // 2 tokens * (TOKEN_PRICE) + 1 token * (2 * TOKEN_PRICE) + uint256 totalValue = (4 * TOKEN_PRICE) / 1e18; + + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + + // Chain 1 changed + RateLimiter.TokenBucket memory bucketChain1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, true); + assertEq(bucketChain1.capacity - totalValue, bucketChain1.tokens); + + // Chain 2 unchanged + RateLimiter.TokenBucket memory bucketChain2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_2, true); + assertEq(bucketChain2.capacity, bucketChain2.tokens); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(totalValue); + + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_2, _generateEVM2AnyMessage(tokenAmounts)); + + // Chain 1 unchanged + bucketChain1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, true); + assertEq(bucketChain1.capacity - totalValue, bucketChain1.tokens); + + // Chain 2 changed + bucketChain2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_2, true); + assertEq(bucketChain2.capacity - totalValue, bucketChain2.tokens); + } + + function test_onOutboundMessage_ValidateMessageWithDifferentTokensOnDifferentChains_Success() public { + MultiAggregateRateLimiter.RateLimitTokenArgs[] memory tokensToAdd = + new MultiAggregateRateLimiter.RateLimitTokenArgs[](1); + + // Only 1 rate limited token on different chain + tokensToAdd[0] = MultiAggregateRateLimiter.RateLimitTokenArgs({ + localTokenArgs: MultiAggregateRateLimiter.LocalRateLimitToken({ + remoteChainSelector: CHAIN_SELECTOR_2, + localToken: s_sourceTokens[0] + }), + // Create a remote token address that is different from CHAIN_SELECTOR_1 + remoteToken: bytes32(uint256(uint160(s_destTokenBySourceToken[s_sourceTokens[0]])) + type(uint160).max + 1) + }); + s_rateLimiter.updateRateLimitTokens(new MultiAggregateRateLimiter.LocalRateLimitToken[](0), tokensToAdd); + + vm.startPrank(MOCK_ONRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: 3}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_sourceTokens[1], amount: 1}); + + // 3 tokens * (TOKEN_PRICE) + 1 token * (2 * TOKEN_PRICE) + uint256 totalValue = (5 * TOKEN_PRICE) / 1e18; + + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + + // Chain 1 changed + RateLimiter.TokenBucket memory bucketChain1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, true); + assertEq(bucketChain1.capacity - totalValue, bucketChain1.tokens); + + // Chain 2 unchanged + RateLimiter.TokenBucket memory bucketChain2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_2, true); + assertEq(bucketChain2.capacity, bucketChain2.tokens); + + // 3 tokens * (TOKEN_PRICE) + uint256 totalValue2 = (3 * TOKEN_PRICE) / 1e18; + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(totalValue2); + + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_2, _generateEVM2AnyMessage(tokenAmounts)); + + // Chain 1 unchanged + bucketChain1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, true); + assertEq(bucketChain1.capacity - totalValue, bucketChain1.tokens); + + // Chain 2 changed + bucketChain2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_2, true); + assertEq(bucketChain2.capacity - totalValue2, bucketChain2.tokens); + } + + function test_onOutboundMessage_ValidateMessageWithRateLimitReset_Success() public { + vm.startPrank(MOCK_ONRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: 20}); + + // Remaining capacity: 100 -> 20 + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + + // Cannot fit 80 rate limit value (need to wait at least 12 blocks, current capacity is 20) + vm.expectRevert(abi.encodeWithSelector(RateLimiter.AggregateValueRateLimitReached.selector, 12, 20)); + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + + // Remaining capacity: 20 -> 35 (need to wait 9 more blocks) + vm.warp(BLOCK_TIME + 3); + vm.expectRevert(abi.encodeWithSelector(RateLimiter.AggregateValueRateLimitReached.selector, 9, 35)); + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + + // Remaining capacity: 35 -> 80 (can fit exactly 80) + vm.warp(BLOCK_TIME + 12); + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + } + + function test_RateLimitValueDifferentLanes_Success() public { + vm.pauseGasMetering(); + // start from blocktime that does not equal rate limiter init timestamp + vm.warp(BLOCK_TIME + 1); + + // 10 (tokens) * 4 (price) * 2 (number of times) = 80 < 100 (capacity) + uint256 numberOfTokens = 10; + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: numberOfTokens}); + uint256 value = (numberOfTokens * TOKEN_PRICE) / 1e18; + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(value); + + vm.resumeGasMetering(); + vm.startPrank(MOCK_ONRAMP); + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + vm.pauseGasMetering(); + + // Get the updated bucket status + RateLimiter.TokenBucket memory bucket1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, true); + RateLimiter.TokenBucket memory bucket2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + + // Assert the proper value has been taken out of the bucket + assertEq(bucket1.capacity - value, bucket1.tokens); + // Inbound lane should remain unchanged + assertEq(bucket2.capacity, bucket2.tokens); + + vm.expectEmit(); + emit RateLimiter.TokensConsumed(value); + + vm.resumeGasMetering(); + s_rateLimiter.onInboundMessage(_generateAny2EVMMessage(CHAIN_SELECTOR_1, tokenAmounts)); + vm.pauseGasMetering(); + + bucket1 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, true); + bucket2 = s_rateLimiter.currentRateLimiterState(CHAIN_SELECTOR_1, false); + + // Inbound lane should remain unchanged + assertEq(bucket1.capacity - value, bucket1.tokens); + assertEq(bucket2.capacity - value, bucket2.tokens); + } + + // Reverts + + function test_onOutboundMessage_ValidateMessageWithRateLimitExceeded_Revert() public { + vm.startPrank(MOCK_OFFRAMP); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](2); + tokenAmounts[0] = Client.EVMTokenAmount({token: s_sourceTokens[0], amount: 80}); + tokenAmounts[1] = Client.EVMTokenAmount({token: s_sourceTokens[1], amount: 30}); + + uint256 totalValue = (80 * TOKEN_PRICE + 2 * (30 * TOKEN_PRICE)) / 1e18; + vm.expectRevert(abi.encodeWithSelector(RateLimiter.AggregateValueMaxCapacityExceeded.selector, 100, totalValue)); + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessage(tokenAmounts)); + } + + function test_onOutboundMessage_ValidateMessageFromUnauthorizedCaller_Revert() public { + vm.startPrank(STRANGER); + + vm.expectRevert(abi.encodeWithSelector(AuthorizedCallers.UnauthorizedCaller.selector, STRANGER)); + s_rateLimiter.onOutboundMessage(CHAIN_SELECTOR_1, _generateEVM2AnyMessageNoTokens()); + } + + function _generateEVM2AnyMessage(Client.EVMTokenAmount[] memory tokenAmounts) + public + view + returns (Client.EVM2AnyMessage memory) + { + return Client.EVM2AnyMessage({ + receiver: abi.encode(OWNER), + data: "", + tokenAmounts: tokenAmounts, + feeToken: s_sourceFeeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: GAS_LIMIT})) + }); + } + + function _generateEVM2AnyMessageNoTokens() internal view returns (Client.EVM2AnyMessage memory) { + return _generateEVM2AnyMessage(new Client.EVMTokenAmount[](0)); + } +} diff --git a/contracts/src/v0.8/ccip/test/router/Router.t.sol b/contracts/src/v0.8/ccip/test/router/Router.t.sol new file mode 100644 index 00000000000..cfe01e3c417 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/router/Router.t.sol @@ -0,0 +1,889 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IAny2EVMMessageReceiver} from "../../interfaces/IAny2EVMMessageReceiver.sol"; +import {IRouter} from "../../interfaces/IRouter.sol"; +import {IRouterClient} from "../../interfaces/IRouterClient.sol"; +import {IWrappedNative} from "../../interfaces/IWrappedNative.sol"; + +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {EVM2EVMOnRamp} from "../../onRamp/EVM2EVMOnRamp.sol"; +import {MaybeRevertMessageReceiver} from "../helpers/receivers/MaybeRevertMessageReceiver.sol"; +import {EVM2EVMOffRampSetup} from "../offRamp/EVM2EVMOffRampSetup.t.sol"; +import {EVM2EVMOnRampSetup} from "../onRamp/EVM2EVMOnRampSetup.t.sol"; +import {RouterSetup} from "../router/RouterSetup.t.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract Router_constructor is EVM2EVMOnRampSetup { + function test_Constructor_Success() public view { + assertEq("Router 1.2.0", s_sourceRouter.typeAndVersion()); + assertEq(OWNER, s_sourceRouter.owner()); + } +} + +contract Router_recoverTokens is EVM2EVMOnRampSetup { + function test_RecoverTokens_Success() public { + // Assert we can recover sourceToken + IERC20 token = IERC20(s_sourceTokens[0]); + uint256 balanceBefore = token.balanceOf(OWNER); + token.transfer(address(s_sourceRouter), 1); + assertEq(token.balanceOf(address(s_sourceRouter)), 1); + s_sourceRouter.recoverTokens(address(token), OWNER, 1); + assertEq(token.balanceOf(address(s_sourceRouter)), 0); + assertEq(token.balanceOf(OWNER), balanceBefore); + + // Assert we can recover native + balanceBefore = OWNER.balance; + deal(address(s_sourceRouter), 10); + assertEq(address(s_sourceRouter).balance, 10); + s_sourceRouter.recoverTokens(address(0), OWNER, 10); + assertEq(OWNER.balance, balanceBefore + 10); + assertEq(address(s_sourceRouter).balance, 0); + } + + function test_RecoverTokensNonOwner_Revert() public { + // Reverts if not owner + vm.startPrank(STRANGER); + vm.expectRevert("Only callable by owner"); + s_sourceRouter.recoverTokens(address(0), STRANGER, 1); + } + + function test_RecoverTokensInvalidRecipient_Revert() public { + vm.expectRevert(abi.encodeWithSelector(Router.InvalidRecipientAddress.selector, address(0))); + s_sourceRouter.recoverTokens(address(0), address(0), 1); + } + + function test_RecoverTokensNoFunds_Revert() public { + // Reverts if no funds present + vm.expectRevert(); + s_sourceRouter.recoverTokens(address(0), OWNER, 10); + } + + function test_RecoverTokensValueReceiver_Revert() public { + MaybeRevertMessageReceiver revertingValueReceiver = new MaybeRevertMessageReceiver(true); + deal(address(s_sourceRouter), 10); + + // Value receiver reverts + vm.expectRevert(Router.FailedToSendValue.selector); + s_sourceRouter.recoverTokens(address(0), address(revertingValueReceiver), 10); + } +} + +contract Router_ccipSend is EVM2EVMOnRampSetup { + event Burned(address indexed sender, uint256 amount); + + function test_CCIPSendLinkFeeOneTokenSuccess_gas() public { + vm.pauseGasMetering(); + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + IERC20 sourceToken1 = IERC20(s_sourceTokens[1]); + sourceToken1.approve(address(s_sourceRouter), 2 ** 64); + + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 2 ** 64; + message.tokenAmounts[0].token = s_sourceTokens[1]; + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + assertGt(expectedFee, 0); + + uint256 balanceBefore = sourceToken1.balanceOf(OWNER); + + // Assert that the tokens are burned + vm.expectEmit(); + emit Burned(address(s_onRamp), message.tokenAmounts[0].amount); + + Internal.EVM2EVMMessage memory msgEvent = _messageToEvent(message, 1, 1, expectedFee, OWNER); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent); + + vm.resumeGasMetering(); + bytes32 messageId = s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + vm.pauseGasMetering(); + + assertEq(msgEvent.messageId, messageId); + // Assert the user balance is lowered by the tokenAmounts sent and the fee amount + uint256 expectedBalance = balanceBefore - (message.tokenAmounts[0].amount); + assertEq(expectedBalance, sourceToken1.balanceOf(OWNER)); + vm.resumeGasMetering(); + } + + function test_CCIPSendLinkFeeNoTokenSuccess_gas() public { + vm.pauseGasMetering(); + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + assertGt(expectedFee, 0); + + Internal.EVM2EVMMessage memory msgEvent = _messageToEvent(message, 1, 1, expectedFee, OWNER); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent); + + vm.resumeGasMetering(); + bytes32 messageId = s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + vm.pauseGasMetering(); + + assertEq(msgEvent.messageId, messageId); + vm.resumeGasMetering(); + } + + function test_CCIPSendNativeFeeOneTokenSuccess_gas() public { + vm.pauseGasMetering(); + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + IERC20 sourceToken1 = IERC20(s_sourceTokens[1]); + sourceToken1.approve(address(s_sourceRouter), 2 ** 64); + + message.tokenAmounts = new Client.EVMTokenAmount[](1); + message.tokenAmounts[0].amount = 2 ** 64; + message.tokenAmounts[0].token = s_sourceTokens[1]; + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + assertGt(expectedFee, 0); + + uint256 balanceBefore = sourceToken1.balanceOf(OWNER); + + // Assert that the tokens are burned + vm.expectEmit(); + emit Burned(address(s_onRamp), message.tokenAmounts[0].amount); + + // Native fees will be wrapped so we need to calculate the event with + // the wrapped native feeCoin address. + message.feeToken = s_sourceRouter.getWrappedNative(); + Internal.EVM2EVMMessage memory msgEvent = _messageToEvent(message, 1, 1, expectedFee, OWNER); + // Set it to address(0) to indicate native + message.feeToken = address(0); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent); + + vm.resumeGasMetering(); + bytes32 messageId = s_sourceRouter.ccipSend{value: expectedFee}(DEST_CHAIN_SELECTOR, message); + vm.pauseGasMetering(); + + assertEq(msgEvent.messageId, messageId); + // Assert the user balance is lowered by the tokenAmounts sent and the fee amount + uint256 expectedBalance = balanceBefore - (message.tokenAmounts[0].amount); + assertEq(expectedBalance, sourceToken1.balanceOf(OWNER)); + vm.resumeGasMetering(); + } + + function test_CCIPSendNativeFeeNoTokenSuccess_gas() public { + vm.pauseGasMetering(); + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + assertGt(expectedFee, 0); + + // Native fees will be wrapped so we need to calculate the event with + // the wrapped native feeCoin address. + message.feeToken = s_sourceRouter.getWrappedNative(); + Internal.EVM2EVMMessage memory msgEvent = _messageToEvent(message, 1, 1, expectedFee, OWNER); + // Set it to address(0) to indicate native + message.feeToken = address(0); + + vm.expectEmit(); + emit EVM2EVMOnRamp.CCIPSendRequested(msgEvent); + + vm.resumeGasMetering(); + bytes32 messageId = s_sourceRouter.ccipSend{value: expectedFee}(DEST_CHAIN_SELECTOR, message); + vm.pauseGasMetering(); + + assertEq(msgEvent.messageId, messageId); + // Assert the user balance is lowered by the tokenAmounts sent and the fee amount + vm.resumeGasMetering(); + } + + function test_NonLinkFeeToken_Success() public { + EVM2EVMOnRamp.FeeTokenConfigArgs[] memory feeTokenConfigArgs = new EVM2EVMOnRamp.FeeTokenConfigArgs[](1); + feeTokenConfigArgs[0] = EVM2EVMOnRamp.FeeTokenConfigArgs({ + token: s_sourceTokens[1], + networkFeeUSDCents: 1, + gasMultiplierWeiPerEth: 108e16, + premiumMultiplierWeiPerEth: 1e18, + enabled: true + }); + s_onRamp.setFeeTokenConfig(feeTokenConfigArgs); + + address[] memory feeTokens = new address[](1); + feeTokens[0] = s_sourceTokens[1]; + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = s_sourceTokens[1]; + IERC20(s_sourceTokens[1]).approve(address(s_sourceRouter), 2 ** 64); + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_NativeFeeToken_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = address(0); // Raw native + uint256 nativeQuote = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + vm.stopPrank(); + hoax(address(1), 100 ether); + s_sourceRouter.ccipSend{value: nativeQuote}(DEST_CHAIN_SELECTOR, message); + } + + function test_NativeFeeTokenOverpay_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = address(0); // Raw native + uint256 nativeQuote = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + vm.stopPrank(); + hoax(address(1), 100 ether); + s_sourceRouter.ccipSend{value: nativeQuote + 1}(DEST_CHAIN_SELECTOR, message); + // We expect the overpayment to be taken in full. + assertEq(address(1).balance, 100 ether - (nativeQuote + 1)); + assertEq(address(s_sourceRouter).balance, 0); + } + + function test_WrappedNativeFeeToken_Success() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = s_sourceRouter.getWrappedNative(); + uint256 nativeQuote = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + vm.stopPrank(); + hoax(address(1), 100 ether); + // Now address(1) has nativeQuote wrapped. + IWrappedNative(s_sourceRouter.getWrappedNative()).deposit{value: nativeQuote}(); + IWrappedNative(s_sourceRouter.getWrappedNative()).approve(address(s_sourceRouter), nativeQuote); + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + // Since sending with zero fees is a legitimate use case for some destination + // chains, e.g. private chains, we want to make sure that we can still send even + // when the configured fee is 0. + function test_ZeroFeeAndGasPrice_Success() public { + // Configure a new fee token that has zero gas and zero fees but is still + // enabled and valid to pay with. + address feeTokenWithZeroFeeAndGas = s_sourceTokens[1]; + + // Set the new token as feeToken + address[] memory feeTokens = new address[](1); + feeTokens[0] = feeTokenWithZeroFeeAndGas; + s_priceRegistry.applyFeeTokensUpdates(feeTokens, new address[](0)); + + // Update the price of the newly set feeToken + Internal.PriceUpdates memory priceUpdates = getSingleTokenPriceUpdateStruct(feeTokenWithZeroFeeAndGas, 2_000 ether); + priceUpdates.gasPriceUpdates = getSingleGasPriceUpdateStruct(DEST_CHAIN_SELECTOR, 0).gasPriceUpdates; + s_priceRegistry.updatePrices(priceUpdates); + + // Set the feeToken args on the onRamp + EVM2EVMOnRamp.FeeTokenConfigArgs[] memory feeTokenConfigArgs = new EVM2EVMOnRamp.FeeTokenConfigArgs[](1); + feeTokenConfigArgs[0] = EVM2EVMOnRamp.FeeTokenConfigArgs({ + token: s_sourceTokens[1], + networkFeeUSDCents: 0, + gasMultiplierWeiPerEth: 108e16, + premiumMultiplierWeiPerEth: 1e18, + enabled: true + }); + + s_onRamp.setFeeTokenConfig(feeTokenConfigArgs); + + // Send a message with the new feeToken + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = feeTokenWithZeroFeeAndGas; + + // Fee should be 0 and sending should not revert + uint256 fee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + assertEq(fee, 0); + + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + // Reverts + + function test_WhenNotHealthy_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + s_mockRMN.setGlobalCursed(true); + vm.expectRevert(Router.BadARMSignal.selector); + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_UnsupportedDestinationChain_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + uint64 wrongChain = DEST_CHAIN_SELECTOR + 1; + + vm.expectRevert(abi.encodeWithSelector(IRouterClient.UnsupportedDestinationChain.selector, wrongChain)); + + s_sourceRouter.ccipSend(wrongChain, message); + } + + function test_Fuzz_UnsupportedFeeToken_Reverts(address wrongFeeToken) public { + // We have three fee tokens set, all others should revert. + vm.assume(address(s_sourceFeeToken) != wrongFeeToken); + vm.assume(address(s_sourceRouter.getWrappedNative()) != wrongFeeToken); + vm.assume(address(0) != wrongFeeToken); + + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = wrongFeeToken; + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.NotAFeeToken.selector, wrongFeeToken)); + + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_Fuzz_UnsupportedToken_Reverts(address wrongToken) public { + for (uint256 i = 0; i < s_sourceTokens.length; ++i) { + vm.assume(address(s_sourceTokens[i]) != wrongToken); + } + + for (uint256 i = 0; i < s_destTokens.length; ++i) { + vm.assume(address(s_destTokens[i]) != wrongToken); + } + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: wrongToken, amount: 1}); + message.tokenAmounts = tokenAmounts; + + vm.expectRevert(abi.encodeWithSelector(EVM2EVMOnRamp.UnsupportedToken.selector, wrongToken)); + + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_FeeTokenAmountTooLow_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + IERC20(s_sourceTokens[0]).approve(address(s_sourceRouter), 0); + + vm.expectRevert("ERC20: insufficient allowance"); + + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_InvalidMsgValue() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + // Non-empty feeToken but with msg.value should revert + vm.stopPrank(); + hoax(address(1), 1); + vm.expectRevert(IRouterClient.InvalidMsgValue.selector); + s_sourceRouter.ccipSend{value: 1}(DEST_CHAIN_SELECTOR, message); + } + + function test_NativeFeeTokenZeroValue() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = address(0); // Raw native + // Include no value, should revert + vm.expectRevert(); + s_sourceRouter.ccipSend(DEST_CHAIN_SELECTOR, message); + } + + function test_NativeFeeTokenInsufficientValue() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + message.feeToken = address(0); // Raw native + // Include insufficient, should also revert + vm.stopPrank(); + + s_onRamp.getFeeTokenConfig(s_sourceRouter.getWrappedNative()); + + hoax(address(1), 1); + vm.expectRevert(IRouterClient.InsufficientFeeTokenAmount.selector); + s_sourceRouter.ccipSend{value: 1}(DEST_CHAIN_SELECTOR, message); + } +} + +contract Router_getArmProxy is RouterSetup { + function test_getArmProxy() public view { + assertEq(s_sourceRouter.getArmProxy(), address(s_mockRMN)); + } +} + +contract Router_applyRampUpdates is RouterSetup { + MaybeRevertMessageReceiver internal s_receiver; + + function setUp() public virtual override(RouterSetup) { + RouterSetup.setUp(); + s_receiver = new MaybeRevertMessageReceiver(false); + } + + function assertOffRampRouteSucceeds(Router.OffRamp memory offRamp) internal { + vm.startPrank(offRamp.offRamp); + + Client.Any2EVMMessage memory message = generateReceiverMessage(offRamp.sourceChainSelector); + vm.expectCall(address(s_receiver), abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message)); + s_sourceRouter.routeMessage(message, GAS_FOR_CALL_EXACT_CHECK, 100_000, address(s_receiver)); + } + + function assertOffRampRouteReverts(Router.OffRamp memory offRamp) internal { + vm.startPrank(offRamp.offRamp); + + vm.expectRevert(IRouter.OnlyOffRamp.selector); + s_sourceRouter.routeMessage( + generateReceiverMessage(offRamp.sourceChainSelector), GAS_FOR_CALL_EXACT_CHECK, 100_000, address(s_receiver) + ); + } + + function test_Fuzz_OffRampUpdates(address[20] memory offRampsInput) public { + Router.OffRamp[] memory offRamps = new Router.OffRamp[](20); + + for (uint256 i = 0; i < offRampsInput.length; ++i) { + offRamps[i] = Router.OffRamp({sourceChainSelector: uint64(i), offRamp: offRampsInput[i]}); + } + + // Test adding offRamps + s_sourceRouter.applyRampUpdates(new Router.OnRamp[](0), new Router.OffRamp[](0), offRamps); + + // There is no uniqueness guarantee on fuzz input, offRamps will not emit in case of a duplicate, + // hence cannot assert on number of offRamps event emissions, we need to use isOffRa + for (uint256 i = 0; i < offRamps.length; ++i) { + assertTrue(s_sourceRouter.isOffRamp(offRamps[i].sourceChainSelector, offRamps[i].offRamp)); + } + + // Test removing offRamps + s_sourceRouter.applyRampUpdates(new Router.OnRamp[](0), s_sourceRouter.getOffRamps(), new Router.OffRamp[](0)); + + assertEq(0, s_sourceRouter.getOffRamps().length); + for (uint256 i = 0; i < offRamps.length; ++i) { + assertFalse(s_sourceRouter.isOffRamp(offRamps[i].sourceChainSelector, offRamps[i].offRamp)); + } + + // Testing removing and adding in same call + s_sourceRouter.applyRampUpdates(new Router.OnRamp[](0), new Router.OffRamp[](0), offRamps); + s_sourceRouter.applyRampUpdates(new Router.OnRamp[](0), offRamps, offRamps); + for (uint256 i = 0; i < offRamps.length; ++i) { + assertTrue(s_sourceRouter.isOffRamp(offRamps[i].sourceChainSelector, offRamps[i].offRamp)); + } + } + + function test_OffRampUpdatesWithRouting() public { + // Explicitly construct chain selectors and ramp addresses so we have ramp uniqueness for the various test scenarios. + uint256 numberOfSelectors = 10; + uint64[] memory sourceChainSelectors = new uint64[](numberOfSelectors); + for (uint256 i = 0; i < numberOfSelectors; ++i) { + sourceChainSelectors[i] = uint64(i); + } + + uint256 numberOfOffRamps = 5; + address[] memory offRamps = new address[](numberOfOffRamps); + for (uint256 i = 0; i < numberOfOffRamps; ++i) { + offRamps[i] = address(uint160(i * 10)); + } + + // 1st test scenario: add offramps. + // Check all the offramps are added correctly, and can route messages. + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](0); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](numberOfSelectors * numberOfOffRamps); + + // Ensure there are multi-offramp source and multi-source offramps + for (uint256 i = 0; i < numberOfSelectors; ++i) { + for (uint256 j = 0; j < numberOfOffRamps; ++j) { + offRampUpdates[(i * numberOfOffRamps) + j] = Router.OffRamp(sourceChainSelectors[i], offRamps[j]); + } + } + + for (uint256 i = 0; i < offRampUpdates.length; ++i) { + vm.expectEmit(); + emit Router.OffRampAdded(offRampUpdates[i].sourceChainSelector, offRampUpdates[i].offRamp); + } + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + + Router.OffRamp[] memory gotOffRamps = s_sourceRouter.getOffRamps(); + assertEq(offRampUpdates.length, gotOffRamps.length); + + for (uint256 i = 0; i < offRampUpdates.length; ++i) { + assertEq(offRampUpdates[i].offRamp, gotOffRamps[i].offRamp); + assertTrue(s_sourceRouter.isOffRamp(offRampUpdates[i].sourceChainSelector, offRampUpdates[i].offRamp)); + assertOffRampRouteSucceeds(offRampUpdates[i]); + } + + vm.startPrank(OWNER); + + // 2nd test scenario: partially remove existing offramps, add new offramps. + // Check offramps are removed correctly. Removed offramps cannot route messages. + // Check new offramps are added correctly. New offramps can route messages. + // Check unmodified offramps remain correct, and can still route messages. + uint256 numberOfPartialUpdates = offRampUpdates.length / 2; + Router.OffRamp[] memory partialOffRampRemoves = new Router.OffRamp[](numberOfPartialUpdates); + Router.OffRamp[] memory partialOffRampAdds = new Router.OffRamp[](numberOfPartialUpdates); + for (uint256 i = 0; i < numberOfPartialUpdates; ++i) { + partialOffRampRemoves[i] = offRampUpdates[i]; + partialOffRampAdds[i] = Router.OffRamp({ + sourceChainSelector: offRampUpdates[i].sourceChainSelector, + offRamp: address(uint160(offRampUpdates[i].offRamp) + 1e18) // Ensure unique new offRamps addresses + }); + } + + for (uint256 i = 0; i < numberOfPartialUpdates; ++i) { + vm.expectEmit(); + emit Router.OffRampRemoved(partialOffRampRemoves[i].sourceChainSelector, partialOffRampRemoves[i].offRamp); + } + for (uint256 i = 0; i < numberOfPartialUpdates; ++i) { + vm.expectEmit(); + emit Router.OffRampAdded(partialOffRampAdds[i].sourceChainSelector, partialOffRampAdds[i].offRamp); + } + s_sourceRouter.applyRampUpdates(onRampUpdates, partialOffRampRemoves, partialOffRampAdds); + + gotOffRamps = s_sourceRouter.getOffRamps(); + assertEq(offRampUpdates.length, gotOffRamps.length); + + for (uint256 i = 0; i < numberOfPartialUpdates; ++i) { + assertFalse( + s_sourceRouter.isOffRamp(partialOffRampRemoves[i].sourceChainSelector, partialOffRampRemoves[i].offRamp) + ); + assertOffRampRouteReverts(partialOffRampRemoves[i]); + + assertTrue(s_sourceRouter.isOffRamp(partialOffRampAdds[i].sourceChainSelector, partialOffRampAdds[i].offRamp)); + assertOffRampRouteSucceeds(partialOffRampAdds[i]); + } + for (uint256 i = numberOfPartialUpdates; i < offRampUpdates.length; ++i) { + assertTrue(s_sourceRouter.isOffRamp(offRampUpdates[i].sourceChainSelector, offRampUpdates[i].offRamp)); + assertOffRampRouteSucceeds(offRampUpdates[i]); + } + + vm.startPrank(OWNER); + + // 3rd test scenario: remove all offRamps. + // Check all offramps have been removed, no offramp is able to route messages. + for (uint256 i = 0; i < numberOfPartialUpdates; ++i) { + vm.expectEmit(); + emit Router.OffRampRemoved(partialOffRampAdds[i].sourceChainSelector, partialOffRampAdds[i].offRamp); + } + s_sourceRouter.applyRampUpdates(onRampUpdates, partialOffRampAdds, new Router.OffRamp[](0)); + + uint256 numberOfRemainingOfframps = offRampUpdates.length - numberOfPartialUpdates; + Router.OffRamp[] memory remainingOffRampRemoves = new Router.OffRamp[](numberOfRemainingOfframps); + for (uint256 i = 0; i < numberOfRemainingOfframps; ++i) { + remainingOffRampRemoves[i] = offRampUpdates[i + numberOfPartialUpdates]; + } + + for (uint256 i = 0; i < numberOfRemainingOfframps; ++i) { + vm.expectEmit(); + emit Router.OffRampRemoved(remainingOffRampRemoves[i].sourceChainSelector, remainingOffRampRemoves[i].offRamp); + } + s_sourceRouter.applyRampUpdates(onRampUpdates, remainingOffRampRemoves, new Router.OffRamp[](0)); + + // Check there are no offRamps. + assertEq(0, s_sourceRouter.getOffRamps().length); + + for (uint256 i = 0; i < numberOfPartialUpdates; ++i) { + assertFalse(s_sourceRouter.isOffRamp(partialOffRampAdds[i].sourceChainSelector, partialOffRampAdds[i].offRamp)); + assertOffRampRouteReverts(partialOffRampAdds[i]); + } + for (uint256 i = 0; i < offRampUpdates.length; ++i) { + assertFalse(s_sourceRouter.isOffRamp(offRampUpdates[i].sourceChainSelector, offRampUpdates[i].offRamp)); + assertOffRampRouteReverts(offRampUpdates[i]); + } + + vm.startPrank(OWNER); + + // 4th test scenario: add initial onRamps back. + // Check the offramps are added correctly, and can route messages. + // Check offramps that were not added back remain unset, and cannot route messages. + for (uint256 i = 0; i < offRampUpdates.length; ++i) { + vm.expectEmit(); + emit Router.OffRampAdded(offRampUpdates[i].sourceChainSelector, offRampUpdates[i].offRamp); + } + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + + // Check initial offRamps are added back and can route to receiver. + gotOffRamps = s_sourceRouter.getOffRamps(); + assertEq(offRampUpdates.length, gotOffRamps.length); + + for (uint256 i = 0; i < offRampUpdates.length; ++i) { + assertEq(offRampUpdates[i].offRamp, gotOffRamps[i].offRamp); + assertTrue(s_sourceRouter.isOffRamp(offRampUpdates[i].sourceChainSelector, offRampUpdates[i].offRamp)); + assertOffRampRouteSucceeds(offRampUpdates[i]); + } + + // Check offramps that were not added back remain unset. + for (uint256 i = 0; i < numberOfPartialUpdates; ++i) { + assertFalse(s_sourceRouter.isOffRamp(partialOffRampAdds[i].sourceChainSelector, partialOffRampAdds[i].offRamp)); + assertOffRampRouteReverts(partialOffRampAdds[i]); + } + } + + function test_Fuzz_OnRampUpdates(Router.OnRamp[] memory onRamps) public { + // Test adding onRamps + for (uint256 i = 0; i < onRamps.length; ++i) { + vm.expectEmit(); + emit Router.OnRampSet(onRamps[i].destChainSelector, onRamps[i].onRamp); + } + + s_sourceRouter.applyRampUpdates(onRamps, new Router.OffRamp[](0), new Router.OffRamp[](0)); + + // Test setting onRamps to unsupported + for (uint256 i = 0; i < onRamps.length; ++i) { + onRamps[i].onRamp = address(0); + + vm.expectEmit(); + emit Router.OnRampSet(onRamps[i].destChainSelector, onRamps[i].onRamp); + } + s_sourceRouter.applyRampUpdates(onRamps, new Router.OffRamp[](0), new Router.OffRamp[](0)); + for (uint256 i = 0; i < onRamps.length; ++i) { + assertEq(address(0), s_sourceRouter.getOnRamp(onRamps[i].destChainSelector)); + assertFalse(s_sourceRouter.isChainSupported(onRamps[i].destChainSelector)); + } + } + + function test_OnRampDisable() public { + // Add onRamp + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](0); + address onRamp = address(uint160(2)); + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: onRamp}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + assertEq(onRamp, s_sourceRouter.getOnRamp(DEST_CHAIN_SELECTOR)); + assertTrue(s_sourceRouter.isChainSupported(DEST_CHAIN_SELECTOR)); + + // Disable onRamp + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: address(0)}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), new Router.OffRamp[](0)); + assertEq(address(0), s_sourceRouter.getOnRamp(DEST_CHAIN_SELECTOR)); + assertFalse(s_sourceRouter.isChainSupported(DEST_CHAIN_SELECTOR)); + + // Re-enable onRamp + onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: onRamp}); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), new Router.OffRamp[](0)); + assertEq(onRamp, s_sourceRouter.getOnRamp(DEST_CHAIN_SELECTOR)); + assertTrue(s_sourceRouter.isChainSupported(DEST_CHAIN_SELECTOR)); + } + + function test_OnlyOwner_Revert() public { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](0); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](0); + s_sourceRouter.applyRampUpdates(onRampUpdates, offRampUpdates, offRampUpdates); + } + + function test_OffRampMismatch_Revert() public { + address offRamp = address(uint160(2)); + + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](0); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + offRampUpdates[0] = Router.OffRamp(DEST_CHAIN_SELECTOR, offRamp); + + vm.expectEmit(); + emit Router.OffRampAdded(DEST_CHAIN_SELECTOR, offRamp); + s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + + offRampUpdates[0] = Router.OffRamp(SOURCE_CHAIN_SELECTOR, offRamp); + + vm.expectRevert(abi.encodeWithSelector(Router.OffRampMismatch.selector, SOURCE_CHAIN_SELECTOR, offRamp)); + s_sourceRouter.applyRampUpdates(onRampUpdates, offRampUpdates, offRampUpdates); + } +} + +contract Router_setWrappedNative is EVM2EVMOnRampSetup { + function test_Fuzz_SetWrappedNative_Success(address wrappedNative) public { + s_sourceRouter.setWrappedNative(wrappedNative); + assertEq(wrappedNative, s_sourceRouter.getWrappedNative()); + } + + // Reverts + function test_OnlyOwner_Revert() public { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + s_sourceRouter.setWrappedNative(address(1)); + } +} + +contract Router_getSupportedTokens is EVM2EVMOnRampSetup { + function test_GetSupportedTokens_Revert() public { + vm.expectRevert(EVM2EVMOnRamp.GetSupportedTokensFunctionalityRemovedCheckAdminRegistry.selector); + s_onRamp.getSupportedTokens(DEST_CHAIN_SELECTOR); + } +} + +contract Router_routeMessage is EVM2EVMOffRampSetup { + function setUp() public virtual override { + EVM2EVMOffRampSetup.setUp(); + vm.startPrank(address(s_offRamp)); + } + + function generateManualGasLimit(uint256 callDataLength) internal view returns (uint256) { + return ((gasleft() - 2 * (16 * callDataLength + GAS_FOR_CALL_EXACT_CHECK)) * 62) / 64; + } + + function test_ManualExec_Success() public { + Client.Any2EVMMessage memory message = generateReceiverMessage(SOURCE_CHAIN_SELECTOR); + // Manuel execution cannot run out of gas + + (bool success, bytes memory retData, uint256 gasUsed) = s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), + GAS_FOR_CALL_EXACT_CHECK, + generateManualGasLimit(message.data.length), + address(s_receiver) + ); + assertTrue(success); + assertEq("", retData); + assertGt(gasUsed, 3_000); + } + + function test_ExecutionEvent_Success() public { + Client.Any2EVMMessage memory message = generateReceiverMessage(SOURCE_CHAIN_SELECTOR); + // Should revert with reason + bytes memory realError1 = new bytes(2); + realError1[0] = 0xbe; + realError1[1] = 0xef; + s_reverting_receiver.setErr(realError1); + + vm.expectEmit(); + emit Router.MessageExecuted( + message.messageId, + message.sourceChainSelector, + address(s_offRamp), + keccak256(abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message)) + ); + + (bool success, bytes memory retData, uint256 gasUsed) = s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), + GAS_FOR_CALL_EXACT_CHECK, + generateManualGasLimit(message.data.length), + address(s_reverting_receiver) + ); + + assertFalse(success); + assertEq(abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, realError1), retData); + assertGt(gasUsed, 3_000); + + // Reason is truncated + // Over the MAX_RET_BYTES limit (including offset and length word since we have a dynamic values), should be ignored + bytes memory realError2 = new bytes(32 * 2 + 1); + realError2[32 * 2 - 1] = 0xAA; + realError2[32 * 2] = 0xFF; + s_reverting_receiver.setErr(realError2); + + vm.expectEmit(); + emit Router.MessageExecuted( + message.messageId, + message.sourceChainSelector, + address(s_offRamp), + keccak256(abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message)) + ); + + (success, retData, gasUsed) = s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), + GAS_FOR_CALL_EXACT_CHECK, + generateManualGasLimit(message.data.length), + address(s_reverting_receiver) + ); + + assertFalse(success); + assertEq( + abi.encodeWithSelector( + MaybeRevertMessageReceiver.CustomError.selector, + uint256(32), + uint256(realError2.length), + uint256(0), + uint256(0xAA) + ), + retData + ); + assertGt(gasUsed, 3_000); + + // Should emit success + vm.expectEmit(); + emit Router.MessageExecuted( + message.messageId, + message.sourceChainSelector, + address(s_offRamp), + keccak256(abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message)) + ); + + (success, retData, gasUsed) = s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), + GAS_FOR_CALL_EXACT_CHECK, + generateManualGasLimit(message.data.length), + address(s_receiver) + ); + + assertTrue(success); + assertEq("", retData); + assertGt(gasUsed, 3_000); + } + + function test_Fuzz_ExecutionEvent_Success(bytes calldata error) public { + Client.Any2EVMMessage memory message = generateReceiverMessage(SOURCE_CHAIN_SELECTOR); + s_reverting_receiver.setErr(error); + + bytes memory expectedRetData; + + if (error.length >= 33) { + uint256 cutOff = error.length > 64 ? 64 : error.length; + vm.expectEmit(); + emit Router.MessageExecuted( + message.messageId, + message.sourceChainSelector, + address(s_offRamp), + keccak256(abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message)) + ); + expectedRetData = abi.encodeWithSelector( + MaybeRevertMessageReceiver.CustomError.selector, + uint256(32), + uint256(error.length), + bytes32(error[:32]), + bytes32(error[32:cutOff]) + ); + } else { + vm.expectEmit(); + emit Router.MessageExecuted( + message.messageId, + message.sourceChainSelector, + address(s_offRamp), + keccak256(abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message)) + ); + expectedRetData = abi.encodeWithSelector(MaybeRevertMessageReceiver.CustomError.selector, error); + } + + (bool success, bytes memory retData,) = s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), + GAS_FOR_CALL_EXACT_CHECK, + generateManualGasLimit(message.data.length), + address(s_reverting_receiver) + ); + + assertFalse(success); + assertEq(expectedRetData, retData); + } + + function test_AutoExec_Success() public { + (bool success,,) = s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), GAS_FOR_CALL_EXACT_CHECK, 100_000, address(s_receiver) + ); + + assertTrue(success); + + (success,,) = s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), GAS_FOR_CALL_EXACT_CHECK, 1, address(s_receiver) + ); + + // Can run out of gas, should return false + assertFalse(success); + } + + // Reverts + function test_OnlyOffRamp_Revert() public { + vm.stopPrank(); + vm.startPrank(STRANGER); + + vm.expectRevert(IRouter.OnlyOffRamp.selector); + s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), GAS_FOR_CALL_EXACT_CHECK, 100_000, address(s_receiver) + ); + } + + function test_WhenNotHealthy_Revert() public { + s_mockRMN.setGlobalCursed(true); + vm.expectRevert(Router.BadARMSignal.selector); + s_destRouter.routeMessage( + generateReceiverMessage(SOURCE_CHAIN_SELECTOR), GAS_FOR_CALL_EXACT_CHECK, 100_000, address(s_receiver) + ); + } +} + +contract Router_getFee is EVM2EVMOnRampSetup { + function test_GetFeeSupportedChain_Success() public view { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + uint256 expectedFee = s_sourceRouter.getFee(DEST_CHAIN_SELECTOR, message); + assertGt(expectedFee, 10e9); + } + + // Reverts + function test_UnsupportedDestinationChain_Revert() public { + Client.EVM2AnyMessage memory message = _generateEmptyMessage(); + + vm.expectRevert(abi.encodeWithSelector(IRouterClient.UnsupportedDestinationChain.selector, 999)); + s_sourceRouter.getFee(999, message); + } +} diff --git a/contracts/src/v0.8/ccip/test/router/RouterSetup.t.sol b/contracts/src/v0.8/ccip/test/router/RouterSetup.t.sol new file mode 100644 index 00000000000..de751617612 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/router/RouterSetup.t.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Router} from "../../Router.sol"; +import {Client} from "../../libraries/Client.sol"; +import {Internal} from "../../libraries/Internal.sol"; +import {BaseTest} from "../BaseTest.t.sol"; +import {WETH9} from "../WETH9.sol"; + +contract RouterSetup is BaseTest { + Router internal s_sourceRouter; + Router internal s_destRouter; + + function setUp() public virtual override { + BaseTest.setUp(); + + if (address(s_sourceRouter) == address(0)) { + WETH9 weth = new WETH9(); + s_sourceRouter = new Router(address(weth), address(s_mockRMN)); + vm.label(address(s_sourceRouter), "sourceRouter"); + } + if (address(s_destRouter) == address(0)) { + WETH9 weth = new WETH9(); + s_destRouter = new Router(address(weth), address(s_mockRMN)); + vm.label(address(s_destRouter), "destRouter"); + } + } + + function generateReceiverMessage(uint64 chainSelector) internal pure returns (Client.Any2EVMMessage memory) { + Client.EVMTokenAmount[] memory ta = new Client.EVMTokenAmount[](0); + return Client.Any2EVMMessage({ + messageId: bytes32("a"), + sourceChainSelector: chainSelector, + sender: bytes("a"), + data: bytes("a"), + destTokenAmounts: ta + }); + } + + function generateSourceTokenData() internal pure returns (Internal.SourceTokenData memory) { + return Internal.SourceTokenData({ + sourcePoolAddress: abi.encode(address(12312412312)), + destTokenAddress: abi.encode(address(9809808909)), + extraData: "" + }); + } +} diff --git a/contracts/src/v0.8/ccip/test/tokenAdminRegistry/RegistryModuleOwnerCustom.t.sol b/contracts/src/v0.8/ccip/test/tokenAdminRegistry/RegistryModuleOwnerCustom.t.sol new file mode 100644 index 00000000000..dfb599bd307 --- /dev/null +++ b/contracts/src/v0.8/ccip/test/tokenAdminRegistry/RegistryModuleOwnerCustom.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IGetCCIPAdmin} from "../../interfaces/IGetCCIPAdmin.sol"; +import {IOwner} from "../../interfaces/IOwner.sol"; + +import {RegistryModuleOwnerCustom} from "../../tokenAdminRegistry/RegistryModuleOwnerCustom.sol"; +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {BurnMintERC677Helper} from "../helpers/BurnMintERC677Helper.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract RegistryModuleOwnerCustomSetup is Test { + address internal constant OWNER = 0x00007e64E1fB0C487F25dd6D3601ff6aF8d32e4e; + + RegistryModuleOwnerCustom internal s_registryModuleOwnerCustom; + TokenAdminRegistry internal s_tokenAdminRegistry; + address internal s_token; + + function setUp() public virtual { + vm.startPrank(OWNER); + + s_tokenAdminRegistry = new TokenAdminRegistry(); + s_token = address(new BurnMintERC677Helper("Test", "TST")); + s_registryModuleOwnerCustom = new RegistryModuleOwnerCustom(address(s_tokenAdminRegistry)); + s_tokenAdminRegistry.addRegistryModule(address(s_registryModuleOwnerCustom)); + } +} + +contract RegistryModuleOwnerCustom_constructor is RegistryModuleOwnerCustomSetup { + function test_constructor_Revert() public { + vm.expectRevert(abi.encodeWithSelector(RegistryModuleOwnerCustom.AddressZero.selector)); + + new RegistryModuleOwnerCustom(address(0)); + } +} + +contract RegistryModuleOwnerCustom_registerAdminViaGetCCIPAdmin is RegistryModuleOwnerCustomSetup { + function test_registerAdminViaGetCCIPAdmin_Success() public { + assertEq(s_tokenAdminRegistry.getTokenConfig(s_token).administrator, address(0)); + + address expectedOwner = IGetCCIPAdmin(s_token).getCCIPAdmin(); + + vm.expectCall(s_token, abi.encodeWithSelector(IGetCCIPAdmin.getCCIPAdmin.selector), 1); + vm.expectCall( + address(s_tokenAdminRegistry), + abi.encodeWithSelector(TokenAdminRegistry.proposeAdministrator.selector, s_token, expectedOwner), + 1 + ); + + vm.expectEmit(); + emit RegistryModuleOwnerCustom.AdministratorRegistered(s_token, expectedOwner); + + s_registryModuleOwnerCustom.registerAdminViaGetCCIPAdmin(s_token); + + assertEq(s_tokenAdminRegistry.getTokenConfig(s_token).pendingAdministrator, OWNER); + } + + function test_registerAdminViaGetCCIPAdmin_Revert() public { + address expectedOwner = IGetCCIPAdmin(s_token).getCCIPAdmin(); + + vm.startPrank(makeAddr("Not_expected_owner")); + + vm.expectRevert( + abi.encodeWithSelector(RegistryModuleOwnerCustom.CanOnlySelfRegister.selector, expectedOwner, s_token) + ); + + s_registryModuleOwnerCustom.registerAdminViaGetCCIPAdmin(s_token); + } +} + +contract RegistryModuleOwnerCustom_registerAdminViaOwner is RegistryModuleOwnerCustomSetup { + function test_registerAdminViaOwner_Success() public { + assertEq(s_tokenAdminRegistry.getTokenConfig(s_token).administrator, address(0)); + + address expectedOwner = IOwner(s_token).owner(); + + vm.expectCall(s_token, abi.encodeWithSelector(IOwner.owner.selector), 1); + vm.expectCall( + address(s_tokenAdminRegistry), + abi.encodeWithSelector(TokenAdminRegistry.proposeAdministrator.selector, s_token, expectedOwner), + 1 + ); + + vm.expectEmit(); + emit RegistryModuleOwnerCustom.AdministratorRegistered(s_token, expectedOwner); + + s_registryModuleOwnerCustom.registerAdminViaOwner(s_token); + + assertEq(s_tokenAdminRegistry.getTokenConfig(s_token).pendingAdministrator, OWNER); + } + + function test_registerAdminViaOwner_Revert() public { + address expectedOwner = IOwner(s_token).owner(); + + vm.startPrank(makeAddr("Not_expected_owner")); + + vm.expectRevert( + abi.encodeWithSelector(RegistryModuleOwnerCustom.CanOnlySelfRegister.selector, expectedOwner, s_token) + ); + + s_registryModuleOwnerCustom.registerAdminViaOwner(s_token); + } +} diff --git a/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenAdminRegistry.t.sol b/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenAdminRegistry.t.sol new file mode 100644 index 00000000000..ada0369045c --- /dev/null +++ b/contracts/src/v0.8/ccip/test/tokenAdminRegistry/TokenAdminRegistry.t.sol @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IPoolV1} from "../../interfaces/IPool.sol"; + +import {TokenAdminRegistry} from "../../tokenAdminRegistry/TokenAdminRegistry.sol"; +import {TokenSetup} from "../TokenSetup.t.sol"; + +contract TokenAdminRegistrySetup is TokenSetup { + address internal s_registryModule = makeAddr("registryModule"); + + function setUp() public virtual override { + TokenSetup.setUp(); + + s_tokenAdminRegistry.addRegistryModule(s_registryModule); + } +} + +contract TokenAdminRegistry_getPools is TokenAdminRegistrySetup { + function test_getPools_Success() public { + address[] memory tokens = new address[](1); + tokens[0] = s_sourceTokens[0]; + + address[] memory got = s_tokenAdminRegistry.getPools(tokens); + assertEq(got.length, 1); + assertEq(got[0], s_sourcePoolByToken[tokens[0]]); + + got = s_tokenAdminRegistry.getPools(s_sourceTokens); + assertEq(got.length, s_sourceTokens.length); + for (uint256 i = 0; i < s_sourceTokens.length; i++) { + assertEq(got[i], s_sourcePoolByToken[s_sourceTokens[i]]); + } + + address doesNotExist = makeAddr("doesNotExist"); + tokens[0] = doesNotExist; + got = s_tokenAdminRegistry.getPools(tokens); + assertEq(got.length, 1); + assertEq(got[0], address(0)); + } +} + +contract TokenAdminRegistry_getPool is TokenAdminRegistrySetup { + function test_getPool_Success() public view { + address got = s_tokenAdminRegistry.getPool(s_sourceTokens[0]); + assertEq(got, s_sourcePoolByToken[s_sourceTokens[0]]); + } +} + +contract TokenAdminRegistry_setPool is TokenAdminRegistrySetup { + function test_setPool_Success() public { + address pool = makeAddr("pool"); + vm.mockCall(pool, abi.encodeWithSelector(IPoolV1.isSupportedToken.selector), abi.encode(true)); + + vm.expectEmit(); + emit TokenAdminRegistry.PoolSet(s_sourceTokens[0], s_sourcePoolByToken[s_sourceTokens[0]], pool); + + s_tokenAdminRegistry.setPool(s_sourceTokens[0], pool); + + assertEq(s_tokenAdminRegistry.getPool(s_sourceTokens[0]), pool); + + // Assert the event is not emitted if the pool is the same as the current pool. + vm.recordLogs(); + s_tokenAdminRegistry.setPool(s_sourceTokens[0], pool); + + vm.assertEq(vm.getRecordedLogs().length, 0); + } + + function test_setPool_ZeroAddressRemovesPool_Success() public { + address pool = makeAddr("pool"); + vm.mockCall(pool, abi.encodeWithSelector(IPoolV1.isSupportedToken.selector), abi.encode(true)); + s_tokenAdminRegistry.setPool(s_sourceTokens[0], pool); + + assertEq(s_tokenAdminRegistry.getPool(s_sourceTokens[0]), pool); + + vm.expectEmit(); + emit TokenAdminRegistry.PoolSet(s_sourceTokens[0], pool, address(0)); + + s_tokenAdminRegistry.setPool(s_sourceTokens[0], address(0)); + + assertEq(s_tokenAdminRegistry.getPool(s_sourceTokens[0]), address(0)); + } + + function test_setPool_InvalidTokenPoolToken_Revert() public { + address pool = makeAddr("pool"); + vm.mockCall(pool, abi.encodeWithSelector(IPoolV1.isSupportedToken.selector), abi.encode(false)); + + vm.expectRevert(abi.encodeWithSelector(TokenAdminRegistry.InvalidTokenPoolToken.selector, s_sourceTokens[0])); + s_tokenAdminRegistry.setPool(s_sourceTokens[0], pool); + } + + function test_setPool_OnlyAdministrator_Revert() public { + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector(TokenAdminRegistry.OnlyAdministrator.selector, address(this), s_sourceTokens[0]) + ); + s_tokenAdminRegistry.setPool(s_sourceTokens[0], makeAddr("pool")); + } +} + +contract TokenAdminRegistry_getAllConfiguredTokens is TokenAdminRegistrySetup { + function test_Fuzz_getAllConfiguredTokens_Success(uint8 numberOfTokens) public { + TokenAdminRegistry cleanTokenAdminRegistry = new TokenAdminRegistry(); + for (uint160 i = 0; i < numberOfTokens; ++i) { + cleanTokenAdminRegistry.proposeAdministrator(address(i), address(i + 1000)); + } + + uint160 count = 0; + for (uint160 start = 0; start < numberOfTokens; start += count++) { + address[] memory got = cleanTokenAdminRegistry.getAllConfiguredTokens(uint64(start), uint64(count)); + if (start + count > numberOfTokens) { + assertEq(got.length, numberOfTokens - start); + } else { + assertEq(got.length, count); + } + + for (uint160 j = 0; j < got.length; ++j) { + assertEq(got[j], address(j + start)); + } + } + } + + function test_getAllConfiguredTokens_outOfBounds_Success() public view { + address[] memory tokens = s_tokenAdminRegistry.getAllConfiguredTokens(type(uint64).max, 10); + assertEq(tokens.length, 0); + } +} + +contract TokenAdminRegistry_transferAdminRole is TokenAdminRegistrySetup { + function test_transferAdminRole_Success() public { + address token = s_sourceTokens[0]; + + address currentAdmin = s_tokenAdminRegistry.getTokenConfig(token).administrator; + address newAdmin = makeAddr("newAdmin"); + + vm.expectEmit(); + emit TokenAdminRegistry.AdministratorTransferRequested(token, currentAdmin, newAdmin); + + s_tokenAdminRegistry.transferAdminRole(token, newAdmin); + + TokenAdminRegistry.TokenConfig memory config = s_tokenAdminRegistry.getTokenConfig(token); + + // Assert only the pending admin updates, without affecting the pending admin. + assertEq(config.pendingAdministrator, newAdmin); + assertEq(config.administrator, currentAdmin); + } + + function test_transferAdminRole_OnlyAdministrator_Revert() public { + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector(TokenAdminRegistry.OnlyAdministrator.selector, address(this), s_sourceTokens[0]) + ); + s_tokenAdminRegistry.transferAdminRole(s_sourceTokens[0], makeAddr("newAdmin")); + } +} + +contract TokenAdminRegistry_acceptAdminRole is TokenAdminRegistrySetup { + function test_acceptAdminRole_Success() public { + address token = s_sourceTokens[0]; + + address currentAdmin = s_tokenAdminRegistry.getTokenConfig(token).administrator; + address newAdmin = makeAddr("newAdmin"); + + vm.expectEmit(); + emit TokenAdminRegistry.AdministratorTransferRequested(token, currentAdmin, newAdmin); + + s_tokenAdminRegistry.transferAdminRole(token, newAdmin); + + TokenAdminRegistry.TokenConfig memory config = s_tokenAdminRegistry.getTokenConfig(token); + + // Assert only the pending admin updates, without affecting the pending admin. + assertEq(config.pendingAdministrator, newAdmin); + assertEq(config.administrator, currentAdmin); + + vm.startPrank(newAdmin); + + vm.expectEmit(); + emit TokenAdminRegistry.AdministratorTransferred(token, newAdmin); + + s_tokenAdminRegistry.acceptAdminRole(token); + + config = s_tokenAdminRegistry.getTokenConfig(token); + + // Assert only the pending admin updates, without affecting the pending admin. + assertEq(config.pendingAdministrator, address(0)); + assertEq(config.administrator, newAdmin); + } + + function test_acceptAdminRole_OnlyPendingAdministrator_Revert() public { + address token = s_sourceTokens[0]; + address currentAdmin = s_tokenAdminRegistry.getTokenConfig(token).administrator; + address newAdmin = makeAddr("newAdmin"); + + s_tokenAdminRegistry.transferAdminRole(token, newAdmin); + + TokenAdminRegistry.TokenConfig memory config = s_tokenAdminRegistry.getTokenConfig(token); + + // Assert only the pending admin updates, without affecting the pending admin. + assertEq(config.pendingAdministrator, newAdmin); + assertEq(config.administrator, currentAdmin); + + address notNewAdmin = makeAddr("notNewAdmin"); + vm.startPrank(notNewAdmin); + + vm.expectRevert(abi.encodeWithSelector(TokenAdminRegistry.OnlyPendingAdministrator.selector, notNewAdmin, token)); + s_tokenAdminRegistry.acceptAdminRole(token); + } +} + +contract TokenAdminRegistry_isAdministrator is TokenAdminRegistrySetup { + function test_isAdministrator_Success() public { + address newAdmin = makeAddr("newAdmin"); + address newToken = makeAddr("newToken"); + assertFalse(s_tokenAdminRegistry.isAdministrator(newToken, newAdmin)); + assertFalse(s_tokenAdminRegistry.isAdministrator(newToken, OWNER)); + + s_tokenAdminRegistry.proposeAdministrator(newToken, newAdmin); + changePrank(newAdmin); + s_tokenAdminRegistry.acceptAdminRole(newToken); + + assertTrue(s_tokenAdminRegistry.isAdministrator(newToken, newAdmin)); + assertFalse(s_tokenAdminRegistry.isAdministrator(newToken, OWNER)); + } +} + +contract TokenAdminRegistry_proposeAdministrator is TokenAdminRegistrySetup { + function test_proposeAdministrator_module_Success() public { + vm.startPrank(s_registryModule); + address newAdmin = makeAddr("newAdmin"); + address newToken = makeAddr("newToken"); + + vm.expectEmit(); + emit TokenAdminRegistry.AdministratorTransferRequested(newToken, address(0), newAdmin); + + s_tokenAdminRegistry.proposeAdministrator(newToken, newAdmin); + + assertEq(s_tokenAdminRegistry.getTokenConfig(newToken).pendingAdministrator, newAdmin); + assertEq(s_tokenAdminRegistry.getTokenConfig(newToken).administrator, address(0)); + assertEq(s_tokenAdminRegistry.getTokenConfig(newToken).tokenPool, address(0)); + + changePrank(newAdmin); + s_tokenAdminRegistry.acceptAdminRole(newToken); + + assertTrue(s_tokenAdminRegistry.isAdministrator(newToken, newAdmin)); + } + + function test_proposeAdministrator_owner_Success() public { + address newAdmin = makeAddr("newAdmin"); + address newToken = makeAddr("newToken"); + + vm.expectEmit(); + emit TokenAdminRegistry.AdministratorTransferRequested(newToken, address(0), newAdmin); + + s_tokenAdminRegistry.proposeAdministrator(newToken, newAdmin); + + assertEq(s_tokenAdminRegistry.getTokenConfig(newToken).pendingAdministrator, newAdmin); + + changePrank(newAdmin); + s_tokenAdminRegistry.acceptAdminRole(newToken); + + assertTrue(s_tokenAdminRegistry.isAdministrator(newToken, newAdmin)); + } + + function test_proposeAdministrator_reRegisterWhileUnclaimed_Success() public { + address newAdmin = makeAddr("wrongAddress"); + address newToken = makeAddr("newToken"); + + vm.expectEmit(); + emit TokenAdminRegistry.AdministratorTransferRequested(newToken, address(0), newAdmin); + + s_tokenAdminRegistry.proposeAdministrator(newToken, newAdmin); + + assertEq(s_tokenAdminRegistry.getTokenConfig(newToken).pendingAdministrator, newAdmin); + + newAdmin = makeAddr("correctAddress"); + + vm.expectEmit(); + emit TokenAdminRegistry.AdministratorTransferRequested(newToken, address(0), newAdmin); + + // Ensure we can still register the correct admin while the previous admin is unclaimed. + s_tokenAdminRegistry.proposeAdministrator(newToken, newAdmin); + + changePrank(newAdmin); + s_tokenAdminRegistry.acceptAdminRole(newToken); + + assertTrue(s_tokenAdminRegistry.isAdministrator(newToken, newAdmin)); + } + + mapping(address token => address admin) internal s_AdminByToken; + + function test_Fuzz_proposeAdministrator_Success(address[50] memory tokens, address[50] memory admins) public { + TokenAdminRegistry cleanTokenAdminRegistry = new TokenAdminRegistry(); + for (uint256 i = 0; i < tokens.length; i++) { + if (admins[i] == address(0)) { + continue; + } + if (cleanTokenAdminRegistry.getTokenConfig(tokens[i]).administrator != address(0)) { + continue; + } + cleanTokenAdminRegistry.proposeAdministrator(tokens[i], admins[i]); + s_AdminByToken[tokens[i]] = admins[i]; + } + + for (uint256 i = 0; i < tokens.length; i++) { + assertEq(cleanTokenAdminRegistry.getTokenConfig(tokens[i]).pendingAdministrator, s_AdminByToken[tokens[i]]); + } + } + + function test_proposeAdministrator_OnlyRegistryModule_Revert() public { + address newToken = makeAddr("newToken"); + vm.stopPrank(); + + vm.expectRevert(abi.encodeWithSelector(TokenAdminRegistry.OnlyRegistryModuleOrOwner.selector, address(this))); + s_tokenAdminRegistry.proposeAdministrator(newToken, OWNER); + } + + function test_proposeAdministrator_ZeroAddress_Revert() public { + address newToken = makeAddr("newToken"); + + vm.expectRevert(abi.encodeWithSelector(TokenAdminRegistry.ZeroAddress.selector)); + s_tokenAdminRegistry.proposeAdministrator(newToken, address(0)); + } + + function test_proposeAdministrator_AlreadyRegistered_Revert() public { + address newAdmin = makeAddr("newAdmin"); + address newToken = makeAddr("newToken"); + + s_tokenAdminRegistry.proposeAdministrator(newToken, newAdmin); + changePrank(newAdmin); + s_tokenAdminRegistry.acceptAdminRole(newToken); + + changePrank(OWNER); + + vm.expectRevert(abi.encodeWithSelector(TokenAdminRegistry.AlreadyRegistered.selector, newToken)); + s_tokenAdminRegistry.proposeAdministrator(newToken, newAdmin); + } +} + +contract TokenAdminRegistry_addRegistryModule is TokenAdminRegistrySetup { + function test_addRegistryModule_Success() public { + address newModule = makeAddr("newModule"); + + s_tokenAdminRegistry.addRegistryModule(newModule); + + assertTrue(s_tokenAdminRegistry.isRegistryModule(newModule)); + + // Assert the event is not emitted if the module is already added. + vm.recordLogs(); + s_tokenAdminRegistry.addRegistryModule(newModule); + + vm.assertEq(vm.getRecordedLogs().length, 0); + } + + function test_addRegistryModule_OnlyOwner_Revert() public { + address newModule = makeAddr("newModule"); + vm.stopPrank(); + + vm.expectRevert("Only callable by owner"); + s_tokenAdminRegistry.addRegistryModule(newModule); + } +} + +contract TokenAdminRegistry_removeRegistryModule is TokenAdminRegistrySetup { + function test_removeRegistryModule_Success() public { + address newModule = makeAddr("newModule"); + + s_tokenAdminRegistry.addRegistryModule(newModule); + + assertTrue(s_tokenAdminRegistry.isRegistryModule(newModule)); + + vm.expectEmit(); + emit TokenAdminRegistry.RegistryModuleRemoved(newModule); + + s_tokenAdminRegistry.removeRegistryModule(newModule); + + assertFalse(s_tokenAdminRegistry.isRegistryModule(newModule)); + + // Assert the event is not emitted if the module is already removed. + vm.recordLogs(); + s_tokenAdminRegistry.removeRegistryModule(newModule); + + vm.assertEq(vm.getRecordedLogs().length, 0); + } + + function test_removeRegistryModule_OnlyOwner_Revert() public { + address newModule = makeAddr("newModule"); + vm.stopPrank(); + + vm.expectRevert("Only callable by owner"); + s_tokenAdminRegistry.removeRegistryModule(newModule); + } +} diff --git a/contracts/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol b/contracts/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol new file mode 100644 index 00000000000..3cd17df05f2 --- /dev/null +++ b/contracts/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IGetCCIPAdmin} from "../interfaces/IGetCCIPAdmin.sol"; +import {IOwner} from "../interfaces/IOwner.sol"; +import {ITokenAdminRegistry} from "../interfaces/ITokenAdminRegistry.sol"; + +contract RegistryModuleOwnerCustom is ITypeAndVersion { + error CanOnlySelfRegister(address admin, address token); + error AddressZero(); + + event AdministratorRegistered(address indexed token, address indexed administrator); + + string public constant override typeAndVersion = "RegistryModuleOwnerCustom 1.5.0-dev"; + + // The TokenAdminRegistry contract + ITokenAdminRegistry internal immutable i_tokenAdminRegistry; + + constructor(address tokenAdminRegistry) { + if (tokenAdminRegistry == address(0)) { + revert AddressZero(); + } + i_tokenAdminRegistry = ITokenAdminRegistry(tokenAdminRegistry); + } + + /// @notice Registers the admin of the token using the `getCCIPAdmin` method. + /// @param token The token to register the admin for. + /// @dev The caller must be the admin returned by the `getCCIPAdmin` method. + function registerAdminViaGetCCIPAdmin(address token) external { + _registerAdmin(token, IGetCCIPAdmin(token).getCCIPAdmin()); + } + + /// @notice Registers the admin of the token using the `owner` method. + /// @param token The token to register the admin for. + /// @dev The caller must be the admin returned by the `owner` method. + function registerAdminViaOwner(address token) external { + _registerAdmin(token, IOwner(token).owner()); + } + + /// @notice Registers the admin of the token to msg.sender given that the + /// admin is equal to msg.sender. + /// @param token The token to register the admin for. + /// @param admin The caller must be the admin. + function _registerAdmin(address token, address admin) internal { + if (admin != msg.sender) { + revert CanOnlySelfRegister(admin, token); + } + + i_tokenAdminRegistry.proposeAdministrator(token, admin); + + emit AdministratorRegistered(token, admin); + } +} diff --git a/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenAdminRegistry.sol b/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenAdminRegistry.sol new file mode 100644 index 00000000000..32394a396ec --- /dev/null +++ b/contracts/src/v0.8/ccip/tokenAdminRegistry/TokenAdminRegistry.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IPoolV1} from "../interfaces/IPool.sol"; +import {ITokenAdminRegistry} from "../interfaces/ITokenAdminRegistry.sol"; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; + +import {EnumerableSet} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice This contract stores the token pool configuration for all CCIP enabled tokens. It works +/// on a self-serve basis, where tokens can be registered without intervention from the CCIP owner. +/// @dev This contract is not considered upgradable, as it is a customer facing contract that will store +/// significant amounts of data. +contract TokenAdminRegistry is ITokenAdminRegistry, ITypeAndVersion, OwnerIsCreator { + using EnumerableSet for EnumerableSet.AddressSet; + + error OnlyRegistryModuleOrOwner(address sender); + error OnlyAdministrator(address sender, address token); + error OnlyPendingAdministrator(address sender, address token); + error AlreadyRegistered(address token); + error ZeroAddress(); + error InvalidTokenPoolToken(address token); + + event PoolSet(address indexed token, address indexed previousPool, address indexed newPool); + event AdministratorTransferRequested(address indexed token, address indexed currentAdmin, address indexed newAdmin); + event AdministratorTransferred(address indexed token, address indexed newAdmin); + event DisableReRegistrationSet(address indexed token, bool disabled); + event RemovedAdministrator(address token); + event RegistryModuleAdded(address module); + event RegistryModuleRemoved(address indexed module); + + // The struct is packed in a way that optimizes the attributes that are accessed together. + // solhint-disable-next-line gas-struct-packing + struct TokenConfig { + address administrator; // the current administrator of the token + address pendingAdministrator; // the address that is pending to become the new administrator + address tokenPool; // the token pool for this token. Can be address(0) if not deployed or not configured. + } + + string public constant override typeAndVersion = "TokenAdminRegistry 1.5.0-dev"; + + // Mapping of token address to token configuration + mapping(address token => TokenConfig) internal s_tokenConfig; + + // All tokens that have been configured + EnumerableSet.AddressSet internal s_tokens; + + // Registry modules are allowed to register administrators for tokens + EnumerableSet.AddressSet internal s_registryModules; + + /// @notice Returns all pools for the given tokens. + /// @dev Will return address(0) for tokens that do not have a pool. + function getPools(address[] calldata tokens) external view returns (address[] memory) { + address[] memory pools = new address[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + pools[i] = s_tokenConfig[tokens[i]].tokenPool; + } + return pools; + } + + /// @inheritdoc ITokenAdminRegistry + function getPool(address token) external view returns (address) { + return s_tokenConfig[token].tokenPool; + } + + /// @notice Returns the configuration for a token. + /// @param token The token to get the configuration for. + /// @return config The configuration for the token. + function getTokenConfig(address token) external view returns (TokenConfig memory) { + return s_tokenConfig[token]; + } + + /// @notice Returns a list of tokens that are configured in the token admin registry. + /// @param startIndex Starting index in list, can be 0 if you want to start from the beginning. + /// @param maxCount Maximum number of tokens to retrieve. Since the list can be large, + /// it is recommended to use a paging mechanism to retrieve all tokens. If querying for very + /// large lists, RPCs can time out. If you want all tokens, use type(uint64).max. + /// @return tokens List of configured tokens. + /// @dev The function is paginated to avoid RPC timeouts. + /// @dev The ordering is guaranteed to remain the same as it is not possible to remove tokens + /// from s_tokens. + function getAllConfiguredTokens(uint64 startIndex, uint64 maxCount) external view returns (address[] memory tokens) { + uint256 numberOfTokens = s_tokens.length(); + if (startIndex >= numberOfTokens) { + return tokens; + } + uint256 count = maxCount; + if (count + startIndex > numberOfTokens) { + count = numberOfTokens - startIndex; + } + tokens = new address[](count); + for (uint256 i = 0; i < count; ++i) { + tokens[i] = s_tokens.at(startIndex + i); + } + + return tokens; + } + + // ================================================================ + // │ Administrator functions │ + // ================================================================ + + /// @notice Sets the pool for a token. Setting the pool to address(0) effectively delists the token + /// from CCIP. Setting the pool to any other address enables the token on CCIP. + /// @param localToken The token to set the pool for. + /// @param pool The pool to set for the token. + function setPool(address localToken, address pool) external onlyTokenAdmin(localToken) { + // The pool has to support the token, but we want to allow removing the pool, so we only check + // if the pool supports the token if it is not address(0). + if (pool != address(0) && !IPoolV1(pool).isSupportedToken(localToken)) { + revert InvalidTokenPoolToken(localToken); + } + + TokenConfig storage config = s_tokenConfig[localToken]; + + address previousPool = config.tokenPool; + config.tokenPool = pool; + + if (previousPool != pool) { + emit PoolSet(localToken, previousPool, pool); + } + } + + /// @notice Transfers the administrator role for a token to a new address with a 2-step process. + /// @param localToken The token to transfer the administrator role for. + /// @param newAdmin The address to transfer the administrator role to. Can be address(0) to cancel + /// a pending transfer. + /// @dev The new admin must call `acceptAdminRole` to accept the role. + function transferAdminRole(address localToken, address newAdmin) external onlyTokenAdmin(localToken) { + TokenConfig storage config = s_tokenConfig[localToken]; + config.pendingAdministrator = newAdmin; + + emit AdministratorTransferRequested(localToken, msg.sender, newAdmin); + } + + /// @notice Accepts the administrator role for a token. + /// @param localToken The token to accept the administrator role for. + /// @dev This function can only be called by the pending administrator. + function acceptAdminRole(address localToken) external { + TokenConfig storage config = s_tokenConfig[localToken]; + if (config.pendingAdministrator != msg.sender) { + revert OnlyPendingAdministrator(msg.sender, localToken); + } + + config.administrator = msg.sender; + config.pendingAdministrator = address(0); + + emit AdministratorTransferred(localToken, msg.sender); + } + + // ================================================================ + // │ Administrator config │ + // ================================================================ + + /// @notice Public getter to check for permissions of an administrator + function isAdministrator(address localToken, address administrator) external view returns (bool) { + return s_tokenConfig[localToken].administrator == administrator; + } + + /// @inheritdoc ITokenAdminRegistry + /// @dev Can only be called by a registry module. + function proposeAdministrator(address localToken, address administrator) external { + if (!isRegistryModule(msg.sender) && msg.sender != owner()) { + revert OnlyRegistryModuleOrOwner(msg.sender); + } + if (administrator == address(0)) { + revert ZeroAddress(); + } + TokenConfig storage config = s_tokenConfig[localToken]; + + if (config.administrator != address(0)) { + revert AlreadyRegistered(localToken); + } + + config.pendingAdministrator = administrator; + + // We don't care if it's already in the set, as it's a no-op. + s_tokens.add(localToken); + + emit AdministratorTransferRequested(localToken, address(0), administrator); + } + + // ================================================================ + // │ Registry Modules │ + // ================================================================ + + /// @notice Checks if an address is a registry module. + /// @param module The address to check. + /// @return True if the address is a registry module, false otherwise. + function isRegistryModule(address module) public view returns (bool) { + return s_registryModules.contains(module); + } + + /// @notice Adds a new registry module to the list of allowed modules. + /// @param module The module to add. + function addRegistryModule(address module) external onlyOwner { + if (s_registryModules.add(module)) { + emit RegistryModuleAdded(module); + } + } + + /// @notice Removes a registry module from the list of allowed modules. + /// @param module The module to remove. + function removeRegistryModule(address module) external onlyOwner { + if (s_registryModules.remove(module)) { + emit RegistryModuleRemoved(module); + } + } + + // ================================================================ + // │ Access │ + // ================================================================ + + /// @notice Checks if an address is the administrator of the given token. + modifier onlyTokenAdmin(address token) { + if (s_tokenConfig[token].administrator != msg.sender) { + revert OnlyAdministrator(msg.sender, token); + } + _; + } +} diff --git a/contracts/src/v0.8/ccip/v1.4-CCIP-License-grants.md b/contracts/src/v0.8/ccip/v1.4-CCIP-License-grants.md new file mode 100644 index 00000000000..f206b8adcc1 --- /dev/null +++ b/contracts/src/v0.8/ccip/v1.4-CCIP-License-grants.md @@ -0,0 +1,5 @@ +v1.4-CCIP-License-grants + +Additional Use Grant(s): + +You may make use of the Cross-Chain Interoperability Protocol v1.4 (which is available subject to the license here the “Licensed Work ”) solely for purposes of importing client-side libraries or example clients to facilitate the integration of the Licensed Work into your application. \ No newline at end of file diff --git a/contracts/src/v0.8/liquiditymanager/LiquidityManager.sol b/contracts/src/v0.8/liquiditymanager/LiquidityManager.sol new file mode 100644 index 00000000000..070930b904a --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/LiquidityManager.sol @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBridgeAdapter} from "./interfaces/IBridge.sol"; +import {ILiquidityManager} from "./interfaces/ILiquidityManager.sol"; +import {ILiquidityContainer} from "./interfaces/ILiquidityContainer.sol"; +import {IWrappedNative} from "../ccip/interfaces/IWrappedNative.sol"; + +import {OCR3Base} from "./ocr/OCR3Base.sol"; + +import {IERC20} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice LiquidityManager for a single token over multiple chains. +/// @dev This contract is designed to be used with the LockReleaseTokenPool contract but +/// isn't constrained to it. It can be used with any contract that implements the ILiquidityContainer +/// interface. +/// @dev The OCR3 DON should only be able to transfer funds to other pre-approved contracts +/// on other chains. Under no circumstances should it be able to transfer funds to arbitrary +/// addresses. The owner is therefore in full control of the funds in this contract, not the DON. +/// This is a security feature. The worst that can happen is that the DON can lock up funds in +/// bridges, but it can't steal them. +/// @dev References to local mean logic on the same chain as this contract is deployed on. +/// References to remote mean logic on other chains. +contract LiquidityManager is ILiquidityManager, OCR3Base { + using SafeERC20 for IERC20; + + error ZeroAddress(); + error InvalidRemoteChain(uint64 chainSelector); + error ZeroChainSelector(); + error InsufficientLiquidity(uint256 requested, uint256 available, uint256 reserve); + error EmptyReport(); + error TransferFailed(); + error OnlyFinanceRole(); + + /// @notice Emitted when a finalization step is completed without funds being available. + /// @param ocrSeqNum The OCR sequence number of the report. + /// @param remoteChainSelector The chain selector of the remote chain funds are coming from. + /// @param bridgeSpecificData The bridge specific data that was used to finalize the transfer. + event FinalizationStepCompleted( + uint64 indexed ocrSeqNum, + uint64 indexed remoteChainSelector, + bytes bridgeSpecificData + ); + + /// @notice Emitted when the CLL finance role is set. + /// @param financeRole The address of the new finance role. + event FinanceRoleSet(address financeRole); + + /// @notice Emitted when liquidity is transferred to another chain, or received from another chain. + /// @param ocrSeqNum The OCR sequence number of the report. + /// @param fromChainSelector The chain selector of the chain the funds are coming from. + /// In the event fromChainSelector == i_localChainSelector, this is an outgoing transfer. + /// Otherwise, it is an incoming transfer. + /// @param toChainSelector The chain selector of the chain the funds are going to. + /// In the event toChainSelector == i_localChainSelector, this is an incoming transfer. + /// Otherwise, it is an outgoing transfer. + /// @param to The address the funds are going to. + /// If this is address(this), the funds are arriving in this contract. + /// @param amount The amount of tokens being transferred. + /// @param bridgeSpecificData The bridge specific data that was passed to the local bridge adapter + /// when transferring the funds. + /// @param bridgeReturnData The return data from the local bridge adapter when transferring the funds. + event LiquidityTransferred( + uint64 indexed ocrSeqNum, + uint64 indexed fromChainSelector, + uint64 indexed toChainSelector, + address to, + uint256 amount, + bytes bridgeSpecificData, + bytes bridgeReturnData + ); + + /// @notice Emitted when liquidity is added to the local liquidity container. + /// @param provider The address of the provider that added the liquidity. + /// @param amount The amount of liquidity that was added. + event LiquidityAddedToContainer(address indexed provider, uint256 indexed amount); + + /// @notice Emitted when liquidity is removed from the local liquidity container. + /// @param remover The address of the remover that removed the liquidity. + /// @param amount The amount of liquidity that was removed. + event LiquidityRemovedFromContainer(address indexed remover, uint256 indexed amount); + + /// @notice Emitted when the local liquidity container is set. + /// @param newLiquidityContainer The address of the new liquidity container. + event LiquidityContainerSet(address indexed newLiquidityContainer); + + /// @notice Emitted when the minimum liquidity is set. + /// @param oldBalance The old minimum liquidity. + /// @param newBalance The new minimum liquidity. + event MinimumLiquiditySet(uint256 oldBalance, uint256 newBalance); + + /// @notice Emitted when someone sends native to this contract + /// @param amount The amount of native deposited + /// @param depositor The address that deposited the native + event NativeDeposited(uint256 amount, address depositor); + + /// @notice Emitted when native balance is withdrawn by contract owner + /// @param amount The amount of native withdrawn + /// @param destination The address the native is sent to + event NativeWithdrawn(uint256 amount, address destination); + + /// @notice Emitted when a cross chain rebalancer is set. + /// @param remoteChainSelector The chain selector of the remote chain. + /// @param localBridge The local bridge adapter that will be used to transfer funds. + /// @param remoteToken The address of the token on the remote chain. + /// @param remoteRebalancer The address of the remote rebalancer contract. + /// @param enabled Whether the rebalancer is enabled. + event CrossChainRebalancerSet( + uint64 indexed remoteChainSelector, + IBridgeAdapter localBridge, + address remoteToken, + address remoteRebalancer, + bool enabled + ); + + /// @notice Emitted when a finalization step fails. + /// @param ocrSeqNum The OCR sequence number of the report. + /// @param remoteChainSelector The chain selector of the remote chain funds are coming from. + /// @param bridgeSpecificData The bridge specific data that was used to finalize the transfer. + /// @param reason The reason the finalization failed. + event FinalizationFailed( + uint64 indexed ocrSeqNum, + uint64 indexed remoteChainSelector, + bytes bridgeSpecificData, + bytes reason + ); + + struct CrossChainRebalancer { + address remoteRebalancer; + IBridgeAdapter localBridge; + address remoteToken; + bool enabled; + } + + string public constant override typeAndVersion = "LiquidityManager 1.0.0-dev"; + + /// @notice The token that this pool manages liquidity for. + IERC20 public immutable i_localToken; + + /// @notice The chain selector belonging to the chain this pool is deployed on. + uint64 internal immutable i_localChainSelector; + + /// @notice The target balance defines the expected amount of tokens for this network. + /// Setting the balance to 0 will disable any automated rebalancing operations. + uint256 internal s_minimumLiquidity; + + /// @notice Mapping of chain selector to liquidity container on other chains + mapping(uint64 chainSelector => CrossChainRebalancer) private s_crossChainRebalancer; + + uint64[] private s_supportedDestChains; + + /// @notice The liquidity container on the local chain + /// @dev In the case of CCIP, this would be the token pool. + ILiquidityContainer private s_localLiquidityContainer; + + /// @notice The CLL finance team multisig + address private s_finance; + + constructor( + IERC20 token, + uint64 localChainSelector, + ILiquidityContainer localLiquidityContainer, + uint256 minimumLiquidity, + address finance + ) OCR3Base() { + if (localChainSelector == 0) { + revert ZeroChainSelector(); + } + + if (address(token) == address(0) || address(localLiquidityContainer) == address(0)) { + revert ZeroAddress(); + } + i_localToken = token; + i_localChainSelector = localChainSelector; + s_localLiquidityContainer = localLiquidityContainer; + s_minimumLiquidity = minimumLiquidity; + s_finance = finance; + } + + // ================================================================ + // │ Native Management │ + // ================================================================ + + receive() external payable { + emit NativeDeposited(msg.value, msg.sender); + } + + /// @notice withdraw native balance + function withdrawNative(uint256 amount, address payable destination) external onlyFinance { + (bool success, ) = destination.call{value: amount}(""); + if (!success) revert TransferFailed(); + + emit NativeWithdrawn(amount, destination); + } + + // ================================================================ + // │ Liquidity Management │ + // ================================================================ + + /// @inheritdoc ILiquidityManager + function getLiquidity() public view returns (uint256 currentLiquidity) { + return i_localToken.balanceOf(address(s_localLiquidityContainer)); + } + + /// @notice Adds liquidity to the multi-chain system. + /// @dev Anyone can call this function, but anyone other than the owner should regard + /// adding liquidity as a donation to the system, as there is no way to get it out. + /// This function is open to anyone to be able to quickly add funds to the system + /// without having to go through potentially complicated multisig schemes to do it from + /// the owner address. + function addLiquidity(uint256 amount) external { + i_localToken.safeTransferFrom(msg.sender, address(this), amount); + + // Make sure this is tether compatible, as they have strange approval requirements + // Should be good since all approvals are always immediately used. + i_localToken.safeApprove(address(s_localLiquidityContainer), amount); + s_localLiquidityContainer.provideLiquidity(amount); + + emit LiquidityAddedToContainer(msg.sender, amount); + } + + /// @notice Removes liquidity from the system and sends it to the caller, so the owner. + /// @dev Only the owner can call this function. + function removeLiquidity(uint256 amount) external onlyFinance { + uint256 currentBalance = getLiquidity(); + if (currentBalance < amount) { + revert InsufficientLiquidity(amount, currentBalance, 0); + } + + s_localLiquidityContainer.withdrawLiquidity(amount); + i_localToken.safeTransfer(msg.sender, amount); + + emit LiquidityRemovedFromContainer(msg.sender, amount); + } + + /// @notice escape hatch to manually withdraw any ERC20 token from the LM contract + /// @param token The address of the token to withdraw + /// @param amount The amount of tokens to withdraw + /// @param destination The address to send the tokens to + function withdrawERC20(address token, uint256 amount, address destination) external onlyFinance { + IERC20(token).safeTransfer(destination, amount); + } + + /// @notice Transfers liquidity to another chain. + /// @dev This function is a public version of the internal _rebalanceLiquidity function. + /// to allow the owner to also initiate a rebalancing when needed. + function rebalanceLiquidity( + uint64 chainSelector, + uint256 amount, + uint256 nativeBridgeFee, + bytes calldata bridgeSpecificPayload + ) external onlyFinance { + _rebalanceLiquidity(chainSelector, amount, nativeBridgeFee, type(uint64).max, bridgeSpecificPayload); + } + + /// @notice Finalizes liquidity from another chain. + /// @dev This function is a public version of the internal _receiveLiquidity function. + /// to allow the owner to also initiate a finalization when needed. + function receiveLiquidity( + uint64 remoteChainSelector, + uint256 amount, + bool shouldWrapNative, + bytes calldata bridgeSpecificPayload + ) external onlyFinance { + _receiveLiquidity(remoteChainSelector, amount, bridgeSpecificPayload, shouldWrapNative, type(uint64).max); + } + + /// @notice Transfers liquidity to another chain. + /// @dev Called by both the owner and the DON. + /// @param chainSelector The chain selector of the chain to transfer liquidity to. + /// @param tokenAmount The amount of tokens to transfer. + /// @param nativeBridgeFee The fee to pay to the bridge. + /// @param ocrSeqNum The OCR sequence number of the report. + /// @param bridgeSpecificPayload The bridge specific data to pass to the bridge adapter. + function _rebalanceLiquidity( + uint64 chainSelector, + uint256 tokenAmount, + uint256 nativeBridgeFee, + uint64 ocrSeqNum, + bytes memory bridgeSpecificPayload + ) internal { + uint256 currentBalance = getLiquidity(); + uint256 minBalance = s_minimumLiquidity; + if (currentBalance < minBalance || currentBalance - minBalance < tokenAmount) { + revert InsufficientLiquidity(tokenAmount, currentBalance, minBalance); + } + + CrossChainRebalancer memory remoteLiqManager = s_crossChainRebalancer[chainSelector]; + + if (!remoteLiqManager.enabled) { + revert InvalidRemoteChain(chainSelector); + } + + // XXX: Could be optimized by withdrawing once and then sending to all destinations + s_localLiquidityContainer.withdrawLiquidity(tokenAmount); + i_localToken.safeApprove(address(remoteLiqManager.localBridge), tokenAmount); + + bytes memory bridgeReturnData = remoteLiqManager.localBridge.sendERC20{value: nativeBridgeFee}( + address(i_localToken), + remoteLiqManager.remoteToken, + remoteLiqManager.remoteRebalancer, + tokenAmount, + bridgeSpecificPayload + ); + + emit LiquidityTransferred( + ocrSeqNum, + i_localChainSelector, + chainSelector, + remoteLiqManager.remoteRebalancer, + tokenAmount, + bridgeSpecificPayload, + bridgeReturnData + ); + } + + /// @notice Receives liquidity from another chain. + /// @dev Called by both the owner and the DON. + /// @param remoteChainSelector The chain selector of the chain to receive liquidity from. + /// @param amount The amount of tokens to receive. + /// @param bridgeSpecificPayload The bridge specific data to pass to the bridge adapter finalizeWithdrawERC20 call. + /// @param shouldWrapNative Whether the token should be wrapped before injecting it into the liquidity container. + /// This only applies to native tokens wrapper contracts, e.g WETH. + /// @param ocrSeqNum The OCR sequence number of the report. + function _receiveLiquidity( + uint64 remoteChainSelector, + uint256 amount, + bytes memory bridgeSpecificPayload, + bool shouldWrapNative, + uint64 ocrSeqNum + ) internal { + // check if the remote chain is supported + CrossChainRebalancer memory remoteRebalancer = s_crossChainRebalancer[remoteChainSelector]; + if (!remoteRebalancer.enabled) { + revert InvalidRemoteChain(remoteChainSelector); + } + + // finalize the withdrawal through the bridge adapter + try + remoteRebalancer.localBridge.finalizeWithdrawERC20( + remoteRebalancer.remoteRebalancer, // remoteSender: the remote rebalancer + address(this), // localReceiver: this contract + bridgeSpecificPayload + ) + returns (bool fundsAvailable) { + if (fundsAvailable) { + // finalization was successful and we can inject the liquidity into the container. + // approve and liquidity container should transferFrom. + _injectLiquidity(amount, ocrSeqNum, remoteChainSelector, bridgeSpecificPayload, shouldWrapNative); + } else { + // a finalization step was completed, but funds are not available. + // hence, we cannot inject any liquidity yet. + emit FinalizationStepCompleted(ocrSeqNum, remoteChainSelector, bridgeSpecificPayload); + } + + // return here on the happy path. + // sad path is when finalizeWithdrawERC20 reverts, which is handled after the catch block. + return; + } catch (bytes memory lowLevelData) { + // failed to finalize the withdrawal. + // this could mean that the withdrawal was already finalized + // or that the withdrawal failed. + // we assume the former and continue + emit FinalizationFailed(ocrSeqNum, remoteChainSelector, bridgeSpecificPayload, lowLevelData); + } + + // if we reach this point, the finalization failed. + // since we don't have enough information to know why it failed, + // we assume that it failed because the withdrawal was already finalized, + // and that the funds are available. + _injectLiquidity(amount, ocrSeqNum, remoteChainSelector, bridgeSpecificPayload, shouldWrapNative); + } + + /// @notice Injects liquidity into the local liquidity container. + /// @param amount The amount of tokens to inject. + /// @param ocrSeqNum The OCR sequence number of the report. + /// @param remoteChainSelector The chain selector of the remote chain. + /// @param bridgeSpecificPayload The bridge specific data passed to the bridge adapter finalizeWithdrawERC20 call. + /// @param shouldWrapNative Whether the token should be wrapped before injecting it into the liquidity container. + function _injectLiquidity( + uint256 amount, + uint64 ocrSeqNum, + uint64 remoteChainSelector, + bytes memory bridgeSpecificPayload, + bool shouldWrapNative + ) private { + // We trust the DON or the owner (the only two actors who can end up calling this function) + // to correctly set the shouldWrapNative flag. + // Some bridges only bridge native and not wrapped native. + // In such a case we need to re-wrap the native in order to inject it into the liquidity container. + // TODO: escape hatch in case of bug? + if (shouldWrapNative) { + IWrappedNative(address(i_localToken)).deposit{value: amount}(); + } + + i_localToken.safeIncreaseAllowance(address(s_localLiquidityContainer), amount); + s_localLiquidityContainer.provideLiquidity(amount); + + emit LiquidityTransferred( + ocrSeqNum, + remoteChainSelector, + i_localChainSelector, + address(this), + amount, + bridgeSpecificPayload, + bytes("") // no bridge return data when receiving + ); + } + + /// @notice Process the OCR report. + /// @dev Called by OCR3Base's transmit() function. + function _report(bytes calldata report, uint64 ocrSeqNum) internal override { + ILiquidityManager.LiquidityInstructions memory instructions = abi.decode( + report, + (ILiquidityManager.LiquidityInstructions) + ); + + uint256 sendInstructions = instructions.sendLiquidityParams.length; + uint256 receiveInstructions = instructions.receiveLiquidityParams.length; + + // There should always be instructions to send or receive, if not, the report is invalid + // and we revert to save the gas of the signature validation of OCR. + if (sendInstructions == 0 && receiveInstructions == 0) { + revert EmptyReport(); + } + + for (uint256 i = 0; i < sendInstructions; ++i) { + _rebalanceLiquidity( + instructions.sendLiquidityParams[i].remoteChainSelector, + instructions.sendLiquidityParams[i].amount, + instructions.sendLiquidityParams[i].nativeBridgeFee, + ocrSeqNum, + instructions.sendLiquidityParams[i].bridgeData + ); + } + + for (uint256 i = 0; i < receiveInstructions; ++i) { + _receiveLiquidity( + instructions.receiveLiquidityParams[i].remoteChainSelector, + instructions.receiveLiquidityParams[i].amount, + instructions.receiveLiquidityParams[i].bridgeData, + instructions.receiveLiquidityParams[i].shouldWrapNative, + ocrSeqNum + ); + } + } + + // ================================================================ + // │ Config │ + // ================================================================ + + function getSupportedDestChains() external view returns (uint64[] memory) { + return s_supportedDestChains; + } + + /// @notice Gets the cross chain liquidity manager + function getCrossChainRebalancer(uint64 chainSelector) external view returns (CrossChainRebalancer memory) { + return s_crossChainRebalancer[chainSelector]; + } + + /// @notice Gets all cross chain liquidity managers + /// @dev We don't care too much about gas since this function is intended for offchain usage. + function getAllCrossChainRebalancers() external view returns (CrossChainRebalancerArgs[] memory) { + uint256 numChains = s_supportedDestChains.length; + CrossChainRebalancerArgs[] memory managers = new CrossChainRebalancerArgs[](numChains); + for (uint256 i = 0; i < numChains; ++i) { + uint64 chainSelector = s_supportedDestChains[i]; + CrossChainRebalancer memory currentManager = s_crossChainRebalancer[chainSelector]; + managers[i] = CrossChainRebalancerArgs({ + remoteRebalancer: currentManager.remoteRebalancer, + localBridge: currentManager.localBridge, + remoteToken: currentManager.remoteToken, + remoteChainSelector: chainSelector, + enabled: currentManager.enabled + }); + } + + return managers; + } + + /// @notice Sets a list of cross chain liquidity managers. + /// @dev Will update the list of supported dest chains if the chain is new. + function setCrossChainRebalancers(CrossChainRebalancerArgs[] calldata crossChainRebalancers) external onlyOwner { + for (uint256 i = 0; i < crossChainRebalancers.length; ++i) { + _setCrossChainRebalancer(crossChainRebalancers[i]); + } + } + + function setCrossChainRebalancer(CrossChainRebalancerArgs calldata crossChainLiqManager) external onlyOwner { + _setCrossChainRebalancer(crossChainLiqManager); + } + + /// @notice Sets a single cross chain liquidity manager. + /// @dev Will update the list of supported dest chains if the chain is new. + function _setCrossChainRebalancer(CrossChainRebalancerArgs calldata crossChainLiqManager) internal { + if (crossChainLiqManager.remoteChainSelector == 0) { + revert ZeroChainSelector(); + } + + if ( + crossChainLiqManager.remoteRebalancer == address(0) || + address(crossChainLiqManager.localBridge) == address(0) || + crossChainLiqManager.remoteToken == address(0) + ) { + revert ZeroAddress(); + } + + // If the destination chain is new, add it to the list of supported chains + if (s_crossChainRebalancer[crossChainLiqManager.remoteChainSelector].remoteToken == address(0)) { + s_supportedDestChains.push(crossChainLiqManager.remoteChainSelector); + } + + s_crossChainRebalancer[crossChainLiqManager.remoteChainSelector] = CrossChainRebalancer({ + remoteRebalancer: crossChainLiqManager.remoteRebalancer, + localBridge: crossChainLiqManager.localBridge, + remoteToken: crossChainLiqManager.remoteToken, + enabled: crossChainLiqManager.enabled + }); + + emit CrossChainRebalancerSet( + crossChainLiqManager.remoteChainSelector, + crossChainLiqManager.localBridge, + crossChainLiqManager.remoteToken, + crossChainLiqManager.remoteRebalancer, + crossChainLiqManager.enabled + ); + } + + /// @notice Gets the local liquidity container. + function getLocalLiquidityContainer() external view returns (address) { + return address(s_localLiquidityContainer); + } + + /// @notice Sets the local liquidity container. + /// @dev Only the owner can call this function. + function setLocalLiquidityContainer(ILiquidityContainer localLiquidityContainer) external onlyOwner { + if (address(localLiquidityContainer) == address(0)) { + revert ZeroAddress(); + } + s_localLiquidityContainer = localLiquidityContainer; + + emit LiquidityContainerSet(address(localLiquidityContainer)); + } + + /// @notice Gets the target tokens balance. + function getMinimumLiquidity() external view returns (uint256) { + return s_minimumLiquidity; + } + + /// @notice Sets the target tokens balance. + /// @dev Only the owner can call this function. + function setMinimumLiquidity(uint256 minimumLiquidity) external onlyOwner { + uint256 oldLiquidity = s_minimumLiquidity; + s_minimumLiquidity = minimumLiquidity; + emit MinimumLiquiditySet(oldLiquidity, s_minimumLiquidity); + } + + /// @notice Gets the CLL finance team multisig address + function getFinanceRole() external view returns (address) { + return s_finance; + } + + /// @notice Sets the finance team multisig address + /// @dev Only the owner can call this function. + function setFinanceRole(address finance) external onlyOwner { + s_finance = finance; + emit FinanceRoleSet(finance); + } + + modifier onlyFinance() { + if (msg.sender != s_finance) revert OnlyFinanceRole(); + _; + } +} diff --git a/contracts/src/v0.8/liquiditymanager/bridge-adapters/ArbitrumL1BridgeAdapter.sol b/contracts/src/v0.8/liquiditymanager/bridge-adapters/ArbitrumL1BridgeAdapter.sol new file mode 100644 index 00000000000..9ab7376c273 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/bridge-adapters/ArbitrumL1BridgeAdapter.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBridgeAdapter} from "../interfaces/IBridge.sol"; + +import {IL1GatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/gateway/IL1GatewayRouter.sol"; +import {IGatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/gateway/IGatewayRouter.sol"; +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +interface IOutbox { + /** + * @notice Executes a messages in an Outbox entry. + * @dev Reverts if dispute period hasn't expired, since the outbox entry + * is only created once the rollup confirms the respective assertion. + * @dev it is not possible to execute any L2-to-L1 transaction which contains data + * to a contract address without any code (as enforced by the Bridge contract). + * @param proof Merkle proof of message inclusion in send root + * @param index Merkle path to message + * @param l2Sender sender if original message (i.e., caller of ArbSys.sendTxToL1) + * @param to destination address for L1 contract call + * @param l2Block l2 block number at which sendTxToL1 call was made + * @param l1Block l1 block number at which sendTxToL1 call was made + * @param l2Timestamp l2 Timestamp at which sendTxToL1 call was made + * @param value wei in L1 message + * @param data abi-encoded L1 message data + */ + function executeTransaction( + bytes32[] calldata proof, + uint256 index, + address l2Sender, + address to, + uint256 l2Block, + uint256 l1Block, + uint256 l2Timestamp, + uint256 value, + bytes calldata data + ) external; +} + +/// @notice Arbitrum L1 Bridge adapter +/// @dev Auto unwraps and re-wraps wrapped eth in the bridge. +contract ArbitrumL1BridgeAdapter is IBridgeAdapter { + using SafeERC20 for IERC20; + + IL1GatewayRouter internal immutable i_l1GatewayRouter; + IOutbox internal immutable i_l1Outbox; + + error NoGatewayForToken(address token); + error Unimplemented(); + + constructor(IL1GatewayRouter l1GatewayRouter, IOutbox l1Outbox) { + if (address(l1GatewayRouter) == address(0) || address(l1Outbox) == address(0)) { + revert BridgeAddressCannotBeZero(); + } + i_l1GatewayRouter = l1GatewayRouter; + i_l1Outbox = l1Outbox; + } + + /// @dev these are parameters provided by the caller of the sendERC20 function + /// and must be determined offchain. + struct SendERC20Params { + uint256 gasLimit; + uint256 maxSubmissionCost; + uint256 maxFeePerGas; + } + + /// @inheritdoc IBridgeAdapter + function sendERC20( + address localToken, + address /* remoteToken */, + address recipient, + uint256 amount, + bytes calldata bridgeSpecificPayload + ) external payable override returns (bytes memory) { + // receive the token transfer from the msg.sender + IERC20(localToken).safeTransferFrom(msg.sender, address(this), amount); + + // Note: the gateway router could return 0x0 for the gateway address + // if that token is not yet registered + address gateway = IGatewayRouter(address(i_l1GatewayRouter)).getGateway(localToken); + if (gateway == address(0)) { + revert NoGatewayForToken(localToken); + } + + // approve the gateway to transfer the token amount sent to the adapter + IERC20(localToken).safeApprove(gateway, amount); + + SendERC20Params memory params = abi.decode(bridgeSpecificPayload, (SendERC20Params)); + + uint256 expectedMsgValue = (params.gasLimit * params.maxFeePerGas) + params.maxSubmissionCost; + if (msg.value < expectedMsgValue) { + revert MsgValueDoesNotMatchAmount(msg.value, expectedMsgValue); + } + + // The router will route the call to the gateway that we approved + // above. The gateway will then transfer the tokens to the L2. + // outboundTransferCustomRefund will return the abi encoded inbox sequence number + // which is 256 bits, so we can cap the return data to 256 bits. + bytes memory inboxSequenceNumber = i_l1GatewayRouter.outboundTransferCustomRefund{value: msg.value}( + localToken, + recipient, + recipient, + amount, + params.gasLimit, + params.maxFeePerGas, + abi.encode(params.maxSubmissionCost, bytes("")) + ); + + return inboxSequenceNumber; + } + + /// @dev This function is so that we can easily abi-encode the arbitrum-specific payload for the sendERC20 function. + function exposeSendERC20Params(SendERC20Params memory params) public pure {} + + /// @dev fees have to be determined offchain for arbitrum, therefore revert here to discourage usage. + function getBridgeFeeInNative() public pure override returns (uint256) { + revert Unimplemented(); + } + + /// @param proof Merkle proof of message inclusion in send root + /// @param index Merkle path to message + /// @param l2Sender sender if original message (i.e., caller of ArbSys.sendTxToL1) + /// @param to destination address for L1 contract call + /// @param l2Block l2 block number at which sendTxToL1 call was made + /// @param l1Block l1 block number at which sendTxToL1 call was made + /// @param l2Timestamp l2 Timestamp at which sendTxToL1 call was made + /// @param value wei in L1 message + /// @param data abi-encoded L1 message data + struct ArbitrumFinalizationPayload { + bytes32[] proof; + uint256 index; + address l2Sender; + address to; + uint256 l2Block; + uint256 l1Block; + uint256 l2Timestamp; + uint256 value; + bytes data; + } + + /// @dev This function is so that we can easily abi-encode the arbitrum-specific payload for the finalizeWithdrawERC20 function. + function exposeArbitrumFinalizationPayload(ArbitrumFinalizationPayload memory payload) public pure {} + + /// @notice Finalize an L2 -> L1 transfer. + /// Arbitrum finalizations are single-step, so we always return true. + /// Calls to this function will revert in two cases, 1) if the finalization payload is wrong, + /// i.e incorrect merkle proof, or index and 2) if the withdrawal was already finalized. + /// @return true iff the finalization does not revert. + function finalizeWithdrawERC20( + address /* remoteSender */, + address /* localReceiver */, + bytes calldata arbitrumFinalizationPayload + ) external override returns (bool) { + ArbitrumFinalizationPayload memory payload = abi.decode(arbitrumFinalizationPayload, (ArbitrumFinalizationPayload)); + i_l1Outbox.executeTransaction( + payload.proof, + payload.index, + payload.l2Sender, + payload.to, + payload.l2Block, + payload.l1Block, + payload.l2Timestamp, + payload.value, + payload.data + ); + return true; + } + + /// @notice Convenience function to get the L2 token address from the L1 token address. + /// @return The L2 token address for the given L1 token address. + function getL2Token(address l1Token) external view returns (address) { + return i_l1GatewayRouter.calculateL2TokenAddress(l1Token); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/bridge-adapters/ArbitrumL2BridgeAdapter.sol b/contracts/src/v0.8/liquiditymanager/bridge-adapters/ArbitrumL2BridgeAdapter.sol new file mode 100644 index 00000000000..6ee97163f65 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/bridge-adapters/ArbitrumL2BridgeAdapter.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBridgeAdapter} from "../interfaces/IBridge.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +interface IArbSys { + function withdrawEth(address destination) external payable returns (uint256); +} + +interface IL2GatewayRouter { + function outboundTransfer( + address l1Token, + address to, + uint256 amount, + bytes calldata data + ) external payable returns (bytes memory); +} + +/// @notice Arbitrum L2 Bridge adapter +/// @dev Auto unwraps and re-wraps wrapped eth in the bridge. +contract ArbitrumL2BridgeAdapter is IBridgeAdapter { + using SafeERC20 for IERC20; + + IL2GatewayRouter internal immutable i_l2GatewayRouter; + // address internal immutable i_l1ERC20Gateway; + IArbSys internal constant ARB_SYS = IArbSys(address(0x64)); + + constructor(IL2GatewayRouter l2GatewayRouter) { + if (address(l2GatewayRouter) == address(0)) { + revert BridgeAddressCannotBeZero(); + } + i_l2GatewayRouter = l2GatewayRouter; + } + + /// @inheritdoc IBridgeAdapter + function sendERC20( + address localToken, + address remoteToken, + address recipient, + uint256 amount, + bytes calldata /* bridgeSpecificPayload */ + ) external payable override returns (bytes memory) { + if (msg.value != 0) { + revert MsgShouldNotContainValue(msg.value); + } + + IERC20(localToken).safeTransferFrom(msg.sender, address(this), amount); + + // the data returned is the unique id of the L2 to L1 transfer + // see https://github.com/OffchainLabs/token-bridge-contracts/blob/bf9ad3d7f25c0eaf0a5f89eec7a0a370833cea16/contracts/tokenbridge/arbitrum/gateway/L2ArbitrumGateway.sol#L169-L191 + // No approval needed, the bridge will burn the tokens from this contract. + bytes memory l2ToL1TxId = i_l2GatewayRouter.outboundTransfer(remoteToken, recipient, amount, bytes("")); + + return l2ToL1TxId; + } + + /// @notice No-op since L1 -> L2 transfers do not need finalization. + /// @return true always. + function finalizeWithdrawERC20( + address /* remoteSender */, + address /* localReceiver */, + bytes calldata /* bridgeSpecificPayload */ + ) external pure override returns (bool) { + return true; + } + + /// @notice There are no fees to bridge back to L1 + function getBridgeFeeInNative() external pure returns (uint256) { + return 0; + } + + function depositNativeToL1(address recipient) external payable { + ARB_SYS.withdrawEth{value: msg.value}(recipient); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/bridge-adapters/OptimismL1BridgeAdapter.sol b/contracts/src/v0.8/liquiditymanager/bridge-adapters/OptimismL1BridgeAdapter.sol new file mode 100644 index 00000000000..6734c74bd8d --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/bridge-adapters/OptimismL1BridgeAdapter.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBridgeAdapter} from "../interfaces/IBridge.sol"; +import {IWrappedNative} from "../../ccip/interfaces/IWrappedNative.sol"; +import {Types} from "../interfaces/optimism/Types.sol"; +import {IOptimismPortal} from "../interfaces/optimism/IOptimismPortal.sol"; + +import {IL1StandardBridge} from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice OptimismL1BridgeAdapter implements IBridgeAdapter for the Optimism L1<=>L2 bridge. +/// @dev L1 -> L2 deposits are done via the depositERC20To and depositETHTo functions on the L1StandardBridge. +/// The amount of gas provided for the transaction must be buffered - the Optimism SDK recommends a 20% buffer. +/// The Optimism Bridge implements 2-step withdrawals from L2 to L1. Once a withdrawal transaction is included +/// in the L2 chain, it must be proven on L1 before it can be finalized. There is a buffer between the transaction +/// being posted on L2 before it can be proven, and similarly, there is a buffer in the time it takes to prove +/// the transaction before it can be finalized. +/// See https://blog.oplabs.co/two-step-withdrawals/ for more details on this mechanism. +/// @dev We have to unwrap WETH into ether before depositing it to L2. Therefore this bridge adapter bridges +/// WETH to ether. The receiver on L2 must wrap the ether back into WETH. +contract OptimismL1BridgeAdapter is IBridgeAdapter { + using SafeERC20 for IERC20; + + /// @notice used when the action in the payload is invalid. + error InvalidFinalizationAction(); + + /// @notice Payload for proving a withdrawal from L2 on L1 via finalizeWithdrawERC20. + /// @param withdrawalTransaction The withdrawal transaction, see its docstring for more details. + /// @param l2OutputIndex The index of the output in the L2 block, or the dispute game index post fault proof upgrade. + /// @param outputRootProof The inclusion proof of the L2ToL1MessagePasser contract's storage root. + /// @param withdrawalProof The Merkle proof of the withdrawal key presence in the L2ToL1MessagePasser contract's state trie. + struct OptimismProveWithdrawalPayload { + Types.WithdrawalTransaction withdrawalTransaction; + uint256 l2OutputIndex; + Types.OutputRootProof outputRootProof; + bytes[] withdrawalProof; + } + + /// @notice Payload for finalizing a withdrawal from L2 on L1. + /// Note that the withdrawal must be proven first before it can be finalized. + /// @param withdrawalTransaction The withdrawal transaction, see its docstring for more details. + struct OptimismFinalizationPayload { + Types.WithdrawalTransaction withdrawalTransaction; + } + + /// @notice The action to take when finalizing a withdrawal. + /// Optimism implements two-step withdrawals, so we need to specify the action to take + /// each time the finalizeWithdrawERC20 function is called. + enum FinalizationAction { + ProveWithdrawal, + FinalizeWithdrawal + } + + /// @notice Payload for interacting with the finalizeWithdrawERC20 function. + /// Since Optimism has 2-step withdrawals, we cannot finalize and get the funds on L1 in the same transaction. + /// @param action The action to take; either ProveWithdrawal or FinalizeWithdrawal. + /// @param data The payload for the action. If ProveWithdrawal, it must be an abi-encoded OptimismProveWithdrawalPayload. + /// If FinalizeWithdrawal, it must be an abi-encoded OptimismFinalizationPayload. + struct FinalizeWithdrawERC20Payload { + FinalizationAction action; + bytes data; + } + + /// @dev Reference to the L1StandardBridge contract. Deposits to L2 go through this contract. + IL1StandardBridge internal immutable i_L1Bridge; + + /// @dev Reference to the WrappedNative contract. Optimism bridges ether directly rather than WETH, + /// so we need to unwrap WETH into ether before depositing it to L2. + IWrappedNative internal immutable i_wrappedNative; + + /// @dev Reference to the OptimismPortal contract, which is used to prove and finalize withdrawals. + IOptimismPortal internal immutable i_optimismPortal; + + /// @dev Nonce to use for L2 deposits to allow for better tracking offchain. + uint64 private s_nonce = 0; + + constructor(IL1StandardBridge l1Bridge, IWrappedNative wrappedNative, IOptimismPortal optimismPortal) { + if ( + address(l1Bridge) == address(0) || address(wrappedNative) == address(0) || address(optimismPortal) == address(0) + ) { + revert BridgeAddressCannotBeZero(); + } + i_L1Bridge = l1Bridge; + i_wrappedNative = wrappedNative; + i_optimismPortal = optimismPortal; + } + + /// @notice The WETH withdraw requires this be present otherwise withdraws will fail. + receive() external payable {} + + /// @inheritdoc IBridgeAdapter + function sendERC20( + address localToken, + address remoteToken, + address recipient, + uint256 amount, + bytes calldata /* bridgeSpecificPayload */ + ) external payable override returns (bytes memory) { + IERC20(localToken).safeTransferFrom(msg.sender, address(this), amount); + + if (msg.value != 0) { + revert MsgShouldNotContainValue(msg.value); + } + + // Extra data for the L2 deposit. + // We encode the nonce in the extra data so that we can track the L2 deposit offchain. + bytes memory extraData = abi.encode(s_nonce++); + + // If the token is the wrapped native, we unwrap it and deposit native + if (localToken == address(i_wrappedNative)) { + i_wrappedNative.withdraw(amount); + i_L1Bridge.depositETHTo{value: amount}(recipient, 0, extraData); + return extraData; + } + + // Token is a normal ERC20. + IERC20(localToken).safeApprove(address(i_L1Bridge), amount); + i_L1Bridge.depositERC20To(localToken, remoteToken, recipient, amount, 0, extraData); + + return extraData; + } + + /// @notice Bridging to Optimism is paid for with gas + /// @dev Since the gas amount charged is dynamic, the gas burn can change from block to block. + /// You should always add a buffer of at least 20% to the gas limit for your L1 to L2 transaction + /// to avoid running out of gas. + function getBridgeFeeInNative() public pure returns (uint256) { + return 0; + } + + /// @notice Prove or finalize an ERC20 withdrawal from L2. + /// The action to take is specified in the payload. See the docstring of FinalizeWithdrawERC20Payload for more details. + /// @param data The payload for the action. This is an abi.encode'd FinalizeWithdrawERC20Payload with the appropriate data. + /// @return true iff finalization is successful, and false for proving a withdrawal. If either of these fail, + /// the call to this function will revert. + function finalizeWithdrawERC20( + address /* remoteSender */, + address /* localReceiver */, + bytes calldata data + ) external override returns (bool) { + // decode the data into FinalizeWithdrawERC20Payload first and extract the action. + FinalizeWithdrawERC20Payload memory payload = abi.decode(data, (FinalizeWithdrawERC20Payload)); + if (payload.action == FinalizationAction.ProveWithdrawal) { + // The action being ProveWithdrawal indicates that this is a withdrawal proof payload. + // Decode the data into OptimismProveWithdrawalPayload and call the proveWithdrawal function. + OptimismProveWithdrawalPayload memory provePayload = abi.decode(payload.data, (OptimismProveWithdrawalPayload)); + _proveWithdrawal(provePayload); + return false; + } else if (payload.action == FinalizationAction.FinalizeWithdrawal) { + // decode the data into OptimismFinalizationPayload and call the finalizeWithdrawal function. + OptimismFinalizationPayload memory finalizePayload = abi.decode(payload.data, (OptimismFinalizationPayload)); + // NOTE: finalizing ether withdrawals will currently send ether to the receiver address as indicated by the + // withdrawal tx. However, this is problematic because we need to re-wrap it into WETH. + // However, we can't do that from within this adapter because it doesn't actually have the ether. + // So its up to the caller to rectify this by re-wrapping the ether. + _finalizeWithdrawal(finalizePayload); + return true; + } else { + revert InvalidFinalizationAction(); + } + } + + function _proveWithdrawal(OptimismProveWithdrawalPayload memory payload) internal { + // will revert if the proof is invalid or the output index is not yet included on L1. + i_optimismPortal.proveWithdrawalTransaction( + payload.withdrawalTransaction, + payload.l2OutputIndex, + payload.outputRootProof, + payload.withdrawalProof + ); + } + + function _finalizeWithdrawal(OptimismFinalizationPayload memory payload) internal { + i_optimismPortal.finalizeWithdrawalTransaction(payload.withdrawalTransaction); + } + + /// @notice returns the address of the WETH token used by this adapter. + /// @return the address of the WETH token used by this adapter. + function getWrappedNative() external view returns (address) { + return address(i_wrappedNative); + } + + /// @notice returns the address of the Optimism portal contract. + /// @return the address of the Optimism portal contract. + function getOptimismPortal() external view returns (address) { + return address(i_optimismPortal); + } + + /// @notice returns the address of the Optimism L1StandardBridge bridge contract. + /// @return the address of the Optimism L1StandardBridge bridge contract. + function getL1Bridge() external view returns (address) { + return address(i_L1Bridge); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/bridge-adapters/OptimismL2BridgeAdapter.sol b/contracts/src/v0.8/liquiditymanager/bridge-adapters/OptimismL2BridgeAdapter.sol new file mode 100644 index 00000000000..fd1218f6704 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/bridge-adapters/OptimismL2BridgeAdapter.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IBridgeAdapter} from "../interfaces/IBridge.sol"; +import {IWrappedNative} from "../../ccip/interfaces/IWrappedNative.sol"; + +import {Lib_PredeployAddresses} from "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @dev copy/pasted from https://github.com/ethereum-optimism/optimism/blob/f707883038d527cbf1e9f8ea513fe33255deadbc/packages/contracts-bedrock/src/L2/L2StandardBridge.sol#L114-L122. +/// We can't import it because of hard pin solidity version in the pragma (0.8.15). +interface IL2StandardBridge { + /// @custom:legacy + /// @notice Initiates a withdrawal from L2 to L1 to a target account on L1. + /// Note that if ETH is sent to a contract on L1 and the call fails, then that ETH will + /// be locked in the L1StandardBridge. ETH may be recoverable if the call can be + /// successfully replayed by increasing the amount of gas supplied to the call. If the + /// call will fail for any amount of gas, then the ETH will be locked permanently. + /// This function only works with OptimismMintableERC20 tokens or ether. Use the + /// `bridgeERC20To` function to bridge native L2 tokens to L1. + /// @param _l2Token Address of the L2 token to withdraw. + /// @param _to Recipient account on L1. + /// @param _amount Amount of the L2 token to withdraw. + /// @param _minGasLimit Minimum gas limit to use for the transaction. + /// @param _extraData Extra data attached to the withdrawal. + function withdrawTo( + address _l2Token, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) external payable; +} + +/// @notice OptimismL2BridgeAdapter implements IBridgeAdapter for the Optimism L2<=>L1 bridge. +/// @dev We have to unwrap WETH into ether before withdrawing it to L1. Therefore this bridge adapter bridges +/// WETH to ether. The receiver on L1 must wrap the ether back into WETH. +contract OptimismL2BridgeAdapter is IBridgeAdapter { + using SafeERC20 for IERC20; + + IL2StandardBridge internal immutable i_L2Bridge = IL2StandardBridge(Lib_PredeployAddresses.L2_STANDARD_BRIDGE); + IWrappedNative internal immutable i_wrappedNative; + + // Nonce to use for L1 withdrawals to allow for better tracking offchain. + uint64 private s_nonce = 0; + + constructor(IWrappedNative wrappedNative) { + // Wrapped native can be address zero, this means that auto-wrapping is disabled. + i_wrappedNative = wrappedNative; + } + + /// @notice The WETH withdraw requires this be present otherwise withdraws will fail. + receive() external payable {} + + /// @inheritdoc IBridgeAdapter + function sendERC20( + address localToken, + address /* remoteToken */, + address recipient, + uint256 amount, + bytes calldata /* bridgeSpecificPayload */ + ) external payable override returns (bytes memory) { + if (msg.value != 0) { + revert MsgShouldNotContainValue(msg.value); + } + + IERC20(localToken).safeTransferFrom(msg.sender, address(this), amount); + + // Extra data for the L2 withdraw. + // We encode the nonce in the extra data so that we can track the L2 withdraw offchain. + bytes memory extraData = abi.encode(s_nonce++); + + // If the token is the wrapped native, we unwrap it and withdraw native + if (localToken == address(i_wrappedNative)) { + i_wrappedNative.withdraw(amount); + // XXX: Lib_PredeployAddresses.OVM_ETH is actually 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000. + // This code path still works because the L2 bridge is hardcoded to handle this specific address. + // The better approach might be to use the bridgeEthTo function, which is on the StandardBridge + // abstract contract, inherited by both L1StandardBridge and L2StandardBridge. + // This is also marked as legacy, so it might mean that this will be deprecated soon. + i_L2Bridge.withdrawTo{value: amount}(Lib_PredeployAddresses.OVM_ETH, recipient, amount, 0, extraData); + return extraData; + } + + // Token is normal ERC20 + IERC20(localToken).approve(address(i_L2Bridge), amount); + i_L2Bridge.withdrawTo(localToken, recipient, amount, 0, extraData); + return extraData; + } + + /// @notice No-op since L1 -> L2 transfers do not need finalization. + /// @return true always. + function finalizeWithdrawERC20( + address /* remoteSender */, + address /* localReceiver */, + bytes calldata /* bridgeSpecificPayload */ + ) external pure override returns (bool) { + return true; + } + + /// @notice There are no fees to bridge back to L1 + function getBridgeFeeInNative() external pure returns (uint256) { + return 0; + } + + /// @notice returns the address of the WETH token used by this adapter. + /// @return the address of the WETH token used by this adapter. + function getWrappedNative() external view returns (address) { + return address(i_wrappedNative); + } + + /// @notice returns the address of the L2 bridge used by this adapter. + /// @return the address of the L2 bridge used by this adapter. + function getL2Bridge() external view returns (address) { + return address(i_L2Bridge); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/encoders/OptimismL1BridgeAdapterEncoder.sol b/contracts/src/v0.8/liquiditymanager/encoders/OptimismL1BridgeAdapterEncoder.sol new file mode 100644 index 00000000000..888b48732d7 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/encoders/OptimismL1BridgeAdapterEncoder.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {OptimismL1BridgeAdapter} from "../bridge-adapters/OptimismL1BridgeAdapter.sol"; + +/// @dev to generate abi's for the OptimismL1BridgeAdapter's various payload types. +/// @dev for usage examples see core/scripts/ccip/liquiditymanager/opstack/prove_withdrawal.go +/// @dev or core/scripts/ccip/liquiditymanager/opstack/finalize.go. +abstract contract OptimismL1BridgeAdapterEncoder { + function encodeFinalizeWithdrawalERC20Payload( + OptimismL1BridgeAdapter.FinalizeWithdrawERC20Payload memory payload + ) public pure {} + + function encodeOptimismProveWithdrawalPayload( + OptimismL1BridgeAdapter.OptimismProveWithdrawalPayload memory payload + ) public pure {} + + function encodeOptimismFinalizationPayload( + OptimismL1BridgeAdapter.OptimismFinalizationPayload memory payload + ) public pure {} +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/IBridge.sol b/contracts/src/v0.8/liquiditymanager/interfaces/IBridge.sol new file mode 100644 index 00000000000..83e64edce48 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/IBridge.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/// @dev IBridgeAdapter provides a common interface to interact with the native bridge. +interface IBridgeAdapter { + error BridgeAddressCannotBeZero(); + error MsgValueDoesNotMatchAmount(uint256 msgValue, uint256 amount); + error InsufficientEthValue(uint256 wanted, uint256 got); + error MsgShouldNotContainValue(uint256 value); + + /// @notice Send the specified amount of the local token cross-chain to the remote chain. + /// The tokens on the remote chain will then be sourced from the remoteToken address. + /// The amount to be sent must be approved by the caller beforehand on the localToken contract. + /// The caller must provide the bridging fee in native currency, i.e msg.value. + /// @param localToken The address of the local ERC-20 token. + /// @param remoteToken The address of the remote ERC-20 token. + /// @param recipient The address of the recipient on the remote chain. + /// @param amount The amount of the local token to send. + /// @param bridgeSpecificPayload The payload of the cross-chain transfer. Bridge-specific. + function sendERC20( + address localToken, + address remoteToken, + address recipient, + uint256 amount, + bytes calldata bridgeSpecificPayload + ) external payable returns (bytes memory); + + /// @notice Get the bridging fee in native currency. This fee must be provided upon sending tokens via + /// the sendERC20 function. + /// @return The bridging fee in native currency. + function getBridgeFeeInNative() external view returns (uint256); + + /// @notice Finalize the withdrawal of a cross-chain transfer. + /// Not all implementations will finalize a transfer in a single call to this function. + /// Optimism, for example, requires a two-step process to finalize a transfer. The first + /// step requires proving the withdrawal that occurred on L2 on L1. The second step is then + /// the finalization, whereby funds become available to the recipient. So, in that particular + /// scenario, `false` is returned from `finalizeWithdrawERC20` when the first step is completed, + /// and `true` is returned when the second step is completed. + /// @param remoteSender The address of the sender on the remote chain. + /// @param localReceiver The address of the receiver on the local chain. + /// @param bridgeSpecificPayload The payload of the cross-chain transfer, bridge-specific, i.e a proof of some kind. + /// @return true iff the funds are available, false otherwise. + function finalizeWithdrawERC20( + address remoteSender, + address localReceiver, + bytes calldata bridgeSpecificPayload + ) external returns (bool); +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/ILiquidityContainer.sol b/contracts/src/v0.8/liquiditymanager/interfaces/ILiquidityContainer.sol new file mode 100644 index 00000000000..062325d9531 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/ILiquidityContainer.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/// @notice Interface for a liquidity container, this can be a CCIP token pool. +interface ILiquidityContainer { + event LiquidityAdded(address indexed provider, uint256 indexed amount); + event LiquidityRemoved(address indexed provider, uint256 indexed amount); + + /// @notice Provide additional liquidity to the container. + /// @dev Should emit LiquidityAdded + function provideLiquidity(uint256 amount) external; + + /// @notice Withdraws liquidity from the container to the msg sender + /// @dev Should emit LiquidityRemoved + function withdrawLiquidity(uint256 amount) external; +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/ILiquidityManager.sol b/contracts/src/v0.8/liquiditymanager/interfaces/ILiquidityManager.sol new file mode 100644 index 00000000000..19fd1014a4d --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/ILiquidityManager.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IBridgeAdapter} from "./IBridge.sol"; + +interface ILiquidityManager { + /// @notice Parameters for sending liquidity to a remote chain. + /// @param amount The amount of tokens to be sent to the remote chain. + /// @param nativeBridgeFee The amount of native that should be sent by the liquiditymanager in the sendERC20 call. + /// Used to pay for the bridge fees. + /// @param remoteChainSelector The selector of the remote chain. + /// @param bridgeData The bridge data that should be passed to the sendERC20 call. + struct SendLiquidityParams { + uint256 amount; + uint256 nativeBridgeFee; + uint64 remoteChainSelector; + bytes bridgeData; + } + + /// @notice Parameters for receiving liquidity from a remote chain. + /// @param amount The amount of tokens to be received from the remote chain. + /// @param remoteChainSelector The selector of the remote chain. + /// @param bridgeData The bridge data that should be passed to the finalizeWithdrawERC20 call. + /// @param shouldWrapNative Whether the received native token should be wrapped into wrapped native. + /// This is needed for when the bridge being used doesn't bridge wrapped native but native directly. + struct ReceiveLiquidityParams { + uint256 amount; + uint64 remoteChainSelector; + bool shouldWrapNative; + bytes bridgeData; + } + + /// @notice Instructions for the rebalancer on what to do with the available liquidity. + /// @param sendLiquidityParams The parameters for sending liquidity to a remote chain. + /// @param receiveLiquidityParams The parameters for receiving liquidity from a remote chain. + struct LiquidityInstructions { + SendLiquidityParams[] sendLiquidityParams; + ReceiveLiquidityParams[] receiveLiquidityParams; + } + + /// @notice Parameters for adding a cross-chain rebalancer. + /// @param remoteRebalancer The address of the remote rebalancer. + /// @param localBridge The local bridge adapter address. + /// @param remoteToken The address of the remote token. + /// @param remoteChainSelector The selector of the remote chain. + /// @param enabled Whether the rebalancer is enabled. + struct CrossChainRebalancerArgs { + address remoteRebalancer; + IBridgeAdapter localBridge; + address remoteToken; + uint64 remoteChainSelector; + bool enabled; + } + + /// @notice Returns the current liquidity in the liquidity container. + /// @return currentLiquidity The current liquidity in the liquidity container. + function getLiquidity() external view returns (uint256 currentLiquidity); + + /// @notice Returns all the cross-chain rebalancers. + /// @return All the cross-chain rebalancers. + function getAllCrossChainRebalancers() external view returns (CrossChainRebalancerArgs[] memory); +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IAbstractArbitrumTokenGateway.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IAbstractArbitrumTokenGateway.sol new file mode 100644 index 00000000000..c695729fa93 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IAbstractArbitrumTokenGateway.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {TokenGateway} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/gateway/TokenGateway.sol"; + +/// @dev to generate gethwrappers +abstract contract IAbstractArbitrumTokenGateway is TokenGateway {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbRollupCore.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbRollupCore.sol new file mode 100644 index 00000000000..a5d0e5e8e6a --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbRollupCore.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IRollupCore} from "@arbitrum/nitro-contracts/src/rollup/IRollupCore.sol"; + +/// @dev to generate gethwrappers +interface IArbRollupCore is IRollupCore {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbSys.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbSys.sol new file mode 100644 index 00000000000..7d6afbc18e4 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbSys.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ArbSys} from "../../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; + +/// @dev to generate gethwrappers +interface IArbSys is ArbSys {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumGatewayRouter.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumGatewayRouter.sol new file mode 100644 index 00000000000..81fc2cb1b5e --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumGatewayRouter.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IGatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/gateway/IGatewayRouter.sol"; + +/// @dev to generate gethwrappers +interface IArbitrumGatewayRouter is IGatewayRouter {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumInbox.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumInbox.sol new file mode 100644 index 00000000000..a306ef21b1a --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumInbox.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IInboxBase} from "@arbitrum/nitro-contracts/src/bridge/IInboxBase.sol"; + +/// @dev to generate gethwrappers +interface IArbitrumInbox is IInboxBase {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumL1GatewayRouter.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumL1GatewayRouter.sol new file mode 100644 index 00000000000..49e7e45dd7d --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumL1GatewayRouter.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IL1GatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/gateway/IL1GatewayRouter.sol"; + +/// @dev to generate gethwrappers +interface IArbitrumL1GatewayRouter is IL1GatewayRouter {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumTokenGateway.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumTokenGateway.sol new file mode 100644 index 00000000000..0c1f2281890 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IArbitrumTokenGateway.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ITokenGateway} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/gateway/ITokenGateway.sol"; + +/// @dev to generate gethwrappers +interface IArbitrumTokenGateway is ITokenGateway {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IL2ArbitrumGateway.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IL2ArbitrumGateway.sol new file mode 100644 index 00000000000..96a63a0dcd0 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IL2ArbitrumGateway.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {L2ArbitrumGateway} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/arbitrum/gateway/L2ArbitrumGateway.sol"; + +/// @dev to generate gethwrappers +abstract contract IL2ArbitrumGateway is L2ArbitrumGateway {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IL2ArbitrumMessenger.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IL2ArbitrumMessenger.sol new file mode 100644 index 00000000000..115882a2115 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/IL2ArbitrumMessenger.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {L2ArbitrumMessenger} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/arbitrum/L2ArbitrumMessenger.sol"; + +/// @dev to generate gethwrappers +abstract contract IL2ArbitrumMessenger is L2ArbitrumMessenger {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/INodeInterface.sol b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/INodeInterface.sol new file mode 100644 index 00000000000..79475cdf5d1 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/arbitrum/INodeInterface.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {NodeInterface} from "@arbitrum/nitro-contracts/src/node-interface/NodeInterface.sol"; + +/// @dev to generate gethwrappers +interface INodeInterface is NodeInterface {} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/DisputeTypes.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/DisputeTypes.sol new file mode 100644 index 00000000000..f0bd99fbcde --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/DisputeTypes.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/v1.7.0/packages/contracts-bedrock/src/libraries/DisputeTypes.sol +pragma solidity ^0.8.0; + +/// @notice A `GameType` represents the type of game being played. +type GameType is uint32; + +/// @notice A `GameId` represents a packed 1 byte game ID, an 11 byte timestamp, and a 20 byte address. +/// @dev The packed layout of this type is as follows: +/// ┌───────────┬───────────┐ +/// │ Bits │ Value │ +/// ├───────────┼───────────┤ +/// │ [0, 8) │ Game Type │ +/// │ [8, 96) │ Timestamp │ +/// │ [96, 256) │ Address │ +/// └───────────┴───────────┘ +type GameId is bytes32; + +/// @notice A dedicated timestamp type. +type Timestamp is uint64; + +/// @notice A claim represents an MPT root representing the state of the fault proof program. +type Claim is bytes32; diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol new file mode 100644 index 00000000000..2b5cc650724 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/f707883038d527cbf1e9f8ea513fe33255deadbc/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol#L153 +pragma solidity ^0.8.0; + +interface IOptimismCrossDomainMessenger { + /// @notice Emitted whenever a message is sent to the other chain. + /// @param target Address of the recipient of the message. + /// @param sender Address of the sender of the message. + /// @param message Message to trigger the recipient address with. + /// @param messageNonce Unique nonce attached to the message. + /// @param gasLimit Minimum gas limit that the message can be executed with. + event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); + + /// @notice Relays a message that was sent by the other CrossDomainMessenger contract. Can only + /// be executed via cross-chain call from the other messenger OR if the message was + /// already received once and is currently being replayed. + /// @param _nonce Nonce of the message being relayed. + /// @param _sender Address of the user who sent the message. + /// @param _target Address that the message is targeted at. + /// @param _value ETH value to send with the message. + /// @param _minGasLimit Minimum amount of gas that the message can be executed with. + /// @param _message Message to send to the target. + function relayMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes calldata _message + ) external payable; +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismDisputeGameFactory.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismDisputeGameFactory.sol new file mode 100644 index 00000000000..f72e6456d3f --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismDisputeGameFactory.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/v1.7.0/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol +pragma solidity ^0.8.0; + +import {GameType, GameId, Timestamp, Claim} from "./DisputeTypes.sol"; + +interface IOptimismDisputeGameFactory { + /// @notice Information about a dispute game found in a `findLatestGames` search. + struct GameSearchResult { + uint256 index; + GameId metadata; + Timestamp timestamp; + Claim rootClaim; + bytes extraData; + } + + /// @notice Finds the `_n` most recent `GameId`'s of type `_gameType` starting at `_start`. If there are less than + /// `_n` games of type `_gameType` starting at `_start`, then the returned array will be shorter than `_n`. + /// @param _gameType The type of game to find. + /// @param _start The index to start the reverse search from. + /// @param _n The number of games to find. + function findLatestGames( + GameType _gameType, + uint256 _start, + uint256 _n + ) external view returns (GameSearchResult[] memory games_); + + /// @notice The total number of dispute games created by this factory. + /// @return gameCount_ The total number of dispute games created by this factory. + function gameCount() external view returns (uint256 gameCount_); +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol new file mode 100644 index 00000000000..3a518fcf798 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/f707883038d527cbf1e9f8ea513fe33255deadbc/packages/contracts-bedrock/src/L1/L1StandardBridge.sol +pragma solidity ^0.8.0; + +interface IOptimismL1StandardBridge { + /// @custom:legacy + /// @notice Deposits some amount of ETH into a target account on L2. + /// Note that if ETH is sent to a contract on L2 and the call fails, then that ETH will + /// be locked in the L2StandardBridge. ETH may be recoverable if the call can be + /// successfully replayed by increasing the amount of gas supplied to the call. If the + /// call will fail for any amount of gas, then the ETH will be locked permanently. + /// @param _to Address of the recipient on L2. + /// @param _minGasLimit Minimum gas limit for the deposit message on L2. + /// @param _extraData Optional data to forward to L2. + /// Data supplied here will not be used to execute any code on L2 and is + /// only emitted as extra data for the convenience of off-chain tooling. + function depositETHTo(address _to, uint32 _minGasLimit, bytes calldata _extraData) external payable; +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL2OutputOracle.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL2OutputOracle.sol new file mode 100644 index 00000000000..fa36863c5b7 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL2OutputOracle.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/v1.7.0/packages/contracts-bedrock/src/L1/L2OutputOracle.sol +pragma solidity ^0.8.0; + +import {Types} from "./Types.sol"; + +interface IOptimismL2OutputOracle { + /// @notice Returns the index of the L2 output that checkpoints a given L2 block number. + /// Uses a binary search to find the first output greater than or equal to the given + /// block. + /// @param _l2BlockNumber L2 block number to find a checkpoint for. + /// @return Index of the first checkpoint that commits to the given L2 block number. + function getL2OutputIndexAfter(uint256 _l2BlockNumber) external view returns (uint256); + + /// @notice Returns an output by index. Needed to return a struct instead of a tuple. + /// @param _l2OutputIndex Index of the output to return. + /// @return The output at the given index. + function getL2Output(uint256 _l2OutputIndex) external view returns (Types.OutputProposal memory); +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL2ToL1MessagePasser.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL2ToL1MessagePasser.sol new file mode 100644 index 00000000000..9ac6aebfb28 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL2ToL1MessagePasser.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/v1.7.0/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol +pragma solidity ^0.8.0; + +interface IOptimismL2ToL1MessagePasser { + /// @notice Emitted any time a withdrawal is initiated. + /// @param nonce Unique value corresponding to each withdrawal. + /// @param sender The L2 account address which initiated the withdrawal. + /// @param target The L1 account address the call will be send to. + /// @param value The ETH value submitted for withdrawal, to be forwarded to the target. + /// @param gasLimit The minimum amount of gas that must be provided when withdrawing. + /// @param data The data to be forwarded to the target on L1. + /// @param withdrawalHash The hash of the withdrawal. + event MessagePassed( + uint256 indexed nonce, + address indexed sender, + address indexed target, + uint256 value, + uint256 gasLimit, + bytes data, + bytes32 withdrawalHash + ); +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismPortal.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismPortal.sol new file mode 100644 index 00000000000..887025bac75 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismPortal.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/v1.7.0/packages/contracts-bedrock/src/L1/OptimismPortal.sol +pragma solidity ^0.8.0; + +import {Types} from "./Types.sol"; + +interface IOptimismPortal { + /// @notice Semantic version. + function version() external view returns (string memory); + + /// @notice Proves a withdrawal transaction. + /// @param _tx Withdrawal transaction to finalize. + /// @param _l2OutputIndex L2 output index to prove against. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract. + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + uint256 _l2OutputIndex, + Types.OutputRootProof calldata _outputRootProof, + bytes[] calldata _withdrawalProof + ) external; + + /// @notice Finalizes a withdrawal transaction. + /// @param _tx Withdrawal transaction to finalize. + function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismPortal2.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismPortal2.sol new file mode 100644 index 00000000000..165922b5aae --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismPortal2.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/v1.7.0/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +pragma solidity ^0.8.0; +import {GameType} from "./DisputeTypes.sol"; + +interface IOptimismPortal2 { + /// @notice The dispute game factory address. + /// @dev See https://github.com/ethereum-optimism/optimism/blob/f707883038d527cbf1e9f8ea513fe33255deadbc/packages/contracts-bedrock/src/L1/OptimismPortal2.sol#L79. + function disputeGameFactory() external view returns (address); + /// @notice The game type that the OptimismPortal consults for output proposals. + function respectedGameType() external view returns (GameType); +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol new file mode 100644 index 00000000000..2f9ef91d7c4 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/f707883038d527cbf1e9f8ea513fe33255deadbc/packages/contracts-bedrock/src/universal/StandardBridge.sol#L88 +pragma solidity ^0.8.0; + +interface IOptimismStandardBridge { + /// @notice Emitted when an ERC20 bridge is finalized on this chain. + /// @param localToken Address of the ERC20 on this chain. + /// @param remoteToken Address of the ERC20 on the remote chain. + /// @param from Address of the sender. + /// @param to Address of the receiver. + /// @param amount Amount of the ERC20 sent. + /// @param extraData Extra data sent with the transaction. + event ERC20BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + /// @notice Finalizes an ERC20 bridge on this chain. Can only be triggered by the other + /// StandardBridge contract on the remote chain. + /// @param _localToken Address of the ERC20 on this chain. + /// @param _remoteToken Address of the corresponding token on the remote chain. + /// @param _from Address of the sender. + /// @param _to Address of the receiver. + /// @param _amount Amount of the ERC20 being bridged. + /// @param _extraData Extra data to be sent with the transaction. Note that the recipient will + /// not be triggered with this data, but it will be emitted and can be used + /// to identify the transaction. + function finalizeBridgeERC20( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes calldata _extraData + ) external; +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/Types.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/Types.sol new file mode 100644 index 00000000000..bd8d5d3b630 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/Types.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/v1.7.0/packages/contracts-bedrock/src/libraries/Types.sol +pragma solidity ^0.8.0; + +/// @title Types +/// @notice Contains various types used throughout the Optimism contract system. +library Types { + /// @notice OutputProposal represents a commitment to the L2 state. The timestamp is the L1 + /// timestamp that the output root is posted. This timestamp is used to verify that the + /// finalization period has passed since the output root was submitted. + /// @custom:field outputRoot Hash of the L2 output. + /// @custom:field timestamp Timestamp of the L1 block that the output root was submitted in. + /// @custom:field l2BlockNumber L2 block number that the output corresponds to. + struct OutputProposal { + bytes32 outputRoot; + uint128 timestamp; + uint128 l2BlockNumber; + } + + /// @notice Struct representing the elements that are hashed together to generate an output root + /// which itself represents a snapshot of the L2 state. + /// @custom:field version Version of the output root. + /// @custom:field stateRoot Root of the state trie at the block of this output. + /// @custom:field messagePasserStorageRoot Root of the message passer storage trie. + /// @custom:field latestBlockhash Hash of the block this output was generated from. + struct OutputRootProof { + bytes32 version; + bytes32 stateRoot; + bytes32 messagePasserStorageRoot; + bytes32 latestBlockhash; + } + + /// @notice Struct representing a deposit transaction (L1 => L2 transaction) created by an end + /// user (as opposed to a system deposit transaction generated by the system). + /// @custom:field from Address of the sender of the transaction. + /// @custom:field to Address of the recipient of the transaction. + /// @custom:field isCreation True if the transaction is a contract creation. + /// @custom:field value Value to send to the recipient. + /// @custom:field mint Amount of ETH to mint. + /// @custom:field gasLimit Gas limit of the transaction. + /// @custom:field data Data of the transaction. + /// @custom:field l1BlockHash Hash of the block the transaction was submitted in. + /// @custom:field logIndex Index of the log in the block the transaction was submitted in. + //solhint-disable gas-struct-packing + struct UserDepositTransaction { + address from; + address to; + bool isCreation; + uint256 value; + uint256 mint; + uint64 gasLimit; + bytes data; + bytes32 l1BlockHash; + uint256 logIndex; + } + + /// @notice Struct representing a withdrawal transaction. + /// @custom:field nonce Nonce of the withdrawal transaction + /// @custom:field sender Address of the sender of the transaction. + /// @custom:field target Address of the recipient of the transaction. + /// @custom:field value Value to send to the recipient. + /// @custom:field gasLimit Gas limit of the transaction. + /// @custom:field data Data of the transaction. + struct WithdrawalTransaction { + uint256 nonce; + address sender; + address target; + uint256 value; + uint256 gasLimit; + bytes data; + } +} diff --git a/contracts/src/v0.8/liquiditymanager/ocr/OCR3Abstract.sol b/contracts/src/v0.8/liquiditymanager/ocr/OCR3Abstract.sol new file mode 100644 index 00000000000..44e5d89f7fb --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/ocr/OCR3Abstract.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +abstract contract OCR3Abstract is ITypeAndVersion { + // Maximum number of oracles the offchain reporting protocol is designed for + uint256 internal constant MAX_NUM_ORACLES = 31; + + /// @notice triggers a new run of the offchain reporting protocol + /// @param previousConfigBlockNumber block in which the previous config was set, to simplify historic analysis + /// @param configDigest configDigest of this configuration + /// @param configCount ordinal number of this config setting among all config settings over the life of this contract + /// @param signers ith element is address ith oracle uses to sign a report + /// @param transmitters ith element is address ith oracle uses to transmit a report via the transmit method + /// @param f maximum number of faulty/dishonest oracles the protocol can tolerate while still working correctly + /// @param onchainConfig serialized configuration used by the contract (and possibly oracles) + /// @param offchainConfigVersion version of the serialization format used for "offchainConfig" parameter + /// @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract + event ConfigSet( + uint32 previousConfigBlockNumber, + bytes32 configDigest, + uint64 configCount, + address[] signers, + address[] transmitters, + uint8 f, + bytes onchainConfig, + uint64 offchainConfigVersion, + bytes offchainConfig + ); + + /// @notice sets offchain reporting protocol configuration incl. participating oracles + /// @param signers addresses with which oracles sign the reports + /// @param transmitters addresses oracles use to transmit the reports + /// @param f number of faulty oracles the system can tolerate + /// @param onchainConfig serialized configuration used by the contract (and possibly oracles) + /// @param offchainConfigVersion version number for offchainEncoding schema + /// @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract + function setOCR3Config( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external virtual; + + /// @notice information about current offchain reporting protocol configuration + /// @return configCount ordinal number of current config, out of all configs applied to this contract so far + /// @return blockNumber block at which this config was set + /// @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) + function latestConfigDetails() + external + view + virtual + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest); + + function _configDigestFromConfigData( + uint256 chainId, + address contractAddress, + uint64 configCount, + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) internal pure returns (bytes32) { + uint256 h = uint256( + keccak256( + abi.encode( + chainId, + contractAddress, + configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00 + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } + + /// @notice optionally emitted to indicate the latest configDigest and sequence number + /// for which a report was successfully transmitted. Alternatively, the contract may + /// use latestConfigDigestAndEpoch with scanLogs set to false. + event Transmitted(bytes32 configDigest, uint64 sequenceNumber); + + /// @notice transmit is called to post a new report to the contract + /// @param report serialized report, which the signatures are signing. + /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param rawVs ith element is the the V component of the ith signature + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external virtual; +} diff --git a/contracts/src/v0.8/liquiditymanager/ocr/OCR3Base.sol b/contracts/src/v0.8/liquiditymanager/ocr/OCR3Base.sol new file mode 100644 index 00000000000..b856f734e7b --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/ocr/OCR3Base.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; +import {OCR3Abstract} from "./OCR3Abstract.sol"; + +/// @notice Onchain verification of reports from the offchain reporting protocol +/// @dev For details on its operation, see the offchain reporting protocol design +/// doc, which refers to this contract as simply the "contract". +abstract contract OCR3Base is OwnerIsCreator, OCR3Abstract { + error InvalidConfig(string message); + error WrongMessageLength(uint256 expected, uint256 actual); + error ConfigDigestMismatch(bytes32 expected, bytes32 actual); + error ForkedChain(uint256 expected, uint256 actual); + error WrongNumberOfSignatures(); + error SignaturesOutOfRegistration(); + error UnauthorizedTransmitter(); + error UnauthorizedSigner(); + error NonUniqueSignatures(); + error OracleCannotBeZeroAddress(); + error NonIncreasingSequenceNumber(uint64 sequenceNumber, uint64 latestSequenceNumber); + + // Packing these fields used on the hot path in a ConfigInfo variable reduces the + // retrieval of all of them to a minimum number of SLOADs. + struct ConfigInfo { + bytes32 latestConfigDigest; + uint8 f; + uint8 n; + } + + // Used for s_oracles[a].role, where a is an address, to track the purpose + // of the address, or to indicate that the address is unset. + enum Role { + // No oracle role has been set for address a + Unset, + // Signing address for the s_oracles[a].index'th oracle. I.e., report + // signatures from this oracle should ecrecover back to address a. + Signer, + // Transmission address for the s_oracles[a].index'th oracle. I.e., if a + // report is received by OCR3Aggregator.transmit in which msg.sender is + // a, it is attributed to the s_oracles[a].index'th oracle. + Transmitter + } + + struct Oracle { + uint8 index; // Index of oracle in s_signers/s_transmitters + Role role; // Role of the address which mapped to this struct + } + + // The current config + ConfigInfo internal s_configInfo; + + // incremented each time a new config is posted. This count is incorporated + // into the config digest, to prevent replay attacks. + uint32 internal s_configCount; + // makes it easier for offchain systems to extract config from logs. + uint32 internal s_latestConfigBlockNumber; + + uint64 internal s_latestSequenceNumber; + + // signer OR transmitter address + mapping(address signerOrTransmitter => Oracle oracle) internal s_oracles; + + // s_signers contains the signing address of each oracle + address[] internal s_signers; + + // s_transmitters contains the transmission address of each oracle, + // i.e. the address the oracle actually sends transactions to the contract from + address[] internal s_transmitters; + + // The constant-length components of the msg.data sent to transmit. + // See the "If we wanted to call sam" example on for example reasoning + // https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html + uint16 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT = + 4 + // function selector + 32 * + 3 + // 3 words containing reportContext + 32 + // word containing start location of abiencoded report value + 32 + // word containing location start of abiencoded rs value + 32 + // word containing start location of abiencoded ss value + 32 + // rawVs value + 32 + // word containing length of report + 32 + // word containing length rs + 32; // word containing length of ss + + uint256 internal immutable i_chainID; + + constructor() { + i_chainID = block.chainid; + } + + // Reverts transaction if config args are invalid + modifier checkConfigValid( + uint256 numSigners, + uint256 numTransmitters, + uint256 f + ) { + if (numSigners > MAX_NUM_ORACLES) revert InvalidConfig("too many signers"); + if (f == 0) revert InvalidConfig("f must be positive"); + if (numSigners != numTransmitters) revert InvalidConfig("oracle addresses out of registration"); + if (numSigners <= 3 * f) revert InvalidConfig("faulty-oracle f too high"); + _; + } + + /// @notice sets offchain reporting protocol configuration incl. participating oracles + /// @param signers addresses with which oracles sign the reports + /// @param transmitters addresses oracles use to transmit the reports + /// @param f number of faulty oracles the system can tolerate + /// @param onchainConfig encoded on-chain contract configuration + /// @param offchainConfigVersion version number for offchainEncoding schema + /// @param offchainConfig encoded off-chain oracle configuration + function setOCR3Config( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external override checkConfigValid(signers.length, transmitters.length, f) onlyOwner { + _beforeSetConfig(onchainConfig); + uint256 oldSignerLength = s_signers.length; + for (uint256 i = 0; i < oldSignerLength; ++i) { + delete s_oracles[s_signers[i]]; + delete s_oracles[s_transmitters[i]]; + } + + uint256 newSignersLength = signers.length; + for (uint256 i = 0; i < newSignersLength; ++i) { + // add new signer/transmitter addresses + address signer = signers[i]; + if (s_oracles[signer].role != Role.Unset) revert InvalidConfig("repeated signer address"); + if (signer == address(0)) revert OracleCannotBeZeroAddress(); + s_oracles[signer] = Oracle(uint8(i), Role.Signer); + + address transmitter = transmitters[i]; + if (s_oracles[transmitter].role != Role.Unset) revert InvalidConfig("repeated transmitter address"); + if (transmitter == address(0)) revert OracleCannotBeZeroAddress(); + s_oracles[transmitter] = Oracle(uint8(i), Role.Transmitter); + } + + s_signers = signers; + s_transmitters = transmitters; + + s_configInfo.f = f; + s_configInfo.n = uint8(newSignersLength); + s_configInfo.latestConfigDigest = _configDigestFromConfigData( + block.chainid, + address(this), + ++s_configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + + uint32 previousConfigBlockNumber = s_latestConfigBlockNumber; + s_latestConfigBlockNumber = uint32(block.number); + s_latestSequenceNumber = 0; + + emit ConfigSet( + previousConfigBlockNumber, + s_configInfo.latestConfigDigest, + s_configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + } + + /// @dev Hook that is run from setOCR3Config() right after validating configuration. + /// Empty by default, please provide an implementation in a child contract if you need additional configuration processing + function _beforeSetConfig(bytes memory _onchainConfig) internal virtual {} + + /// @return list of addresses permitted to transmit reports to this contract + /// @dev The list will match the order used to specify the transmitter during setConfig + function getTransmitters() external view returns (address[] memory) { + return s_transmitters; + } + + /// @notice transmit is called to post a new report to the contract + /// @param report serialized report, which the signatures are signing. + /// @param rs ith element is the R components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param ss ith element is the S components of the ith signature on report. Must have at most MAX_NUM_ORACLES entries + /// @param rawVs ith element is the the V component of the ith signature + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external override { + uint64 sequenceNumber = uint64(uint256(reportContext[1])); + if (sequenceNumber <= s_latestSequenceNumber) { + revert NonIncreasingSequenceNumber(sequenceNumber, s_latestSequenceNumber); + } + + // Scoping this reduces stack pressure and gas usage + { + _report(report, sequenceNumber); + } + + s_latestSequenceNumber = sequenceNumber; + // reportContext consists of: + // reportContext[0]: ConfigDigest + // reportContext[1]: 24 byte padding, 8 byte sequence number + bytes32 configDigest = reportContext[0]; + ConfigInfo memory configInfo = s_configInfo; + + if (configInfo.latestConfigDigest != configDigest) { + revert ConfigDigestMismatch(configInfo.latestConfigDigest, configDigest); + } + // If the cached chainID at time of deployment doesn't match the current chainID, we reject all signed reports. + // This avoids a (rare) scenario where chain A forks into chain A and A', A' still has configDigest + // calculated from chain A and so OCR reports will be valid on both forks. + if (i_chainID != block.chainid) revert ForkedChain(i_chainID, block.chainid); + + emit Transmitted(configDigest, sequenceNumber); + + if (rs.length != configInfo.f + 1) revert WrongNumberOfSignatures(); + if (rs.length != ss.length) revert SignaturesOutOfRegistration(); + + // Scoping this reduces stack pressure and gas usage + { + Oracle memory transmitter = s_oracles[msg.sender]; + // Check that sender is authorized to report + if (!(transmitter.role == Role.Transmitter && msg.sender == s_transmitters[transmitter.index])) + revert UnauthorizedTransmitter(); + } + // Scoping this reduces stack pressure and gas usage + { + uint256 expectedDataLength = uint256(TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT) + + report.length + // one byte pure entry in _report + rs.length * + 32 + // 32 bytes per entry in _rs + ss.length * + 32; // 32 bytes per entry in _ss) + if (msg.data.length != expectedDataLength) revert WrongMessageLength(expectedDataLength, msg.data.length); + } + + // Verify signatures attached to report + bytes32 h = keccak256(abi.encodePacked(keccak256(report), reportContext)); + bool[MAX_NUM_ORACLES] memory signed; + + uint256 numberOfSignatures = rs.length; + for (uint256 i = 0; i < numberOfSignatures; ++i) { + // Safe from ECDSA malleability here since we check for duplicate signers. + address signer = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]); + // Since we disallow address(0) as a valid signer address, it can + // never have a signer role. + Oracle memory oracle = s_oracles[signer]; + if (oracle.role != Role.Signer) revert UnauthorizedSigner(); + if (signed[oracle.index]) revert NonUniqueSignatures(); + signed[oracle.index] = true; + } + } + + /// @notice information about current offchain reporting protocol configuration + /// @return configCount ordinal number of current config, out of all configs applied to this contract so far + /// @return blockNumber block at which this config was set + /// @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) + function latestConfigDetails() + external + view + override + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) + { + return (s_configCount, s_latestConfigBlockNumber, s_configInfo.latestConfigDigest); + } + + /// @notice gets the latest sequence number accepted by the contract + /// @return sequenceNumber the monotomically incremenenting number associated with OCR reports + function latestSequenceNumber() external view virtual returns (uint64 sequenceNumber) { + return s_latestSequenceNumber; + } + + function _report(bytes calldata report, uint64 sequenceNumber) internal virtual; +} diff --git a/contracts/src/v0.8/liquiditymanager/test/LiquidityManager.t.sol b/contracts/src/v0.8/liquiditymanager/test/LiquidityManager.t.sol new file mode 100644 index 00000000000..73c9ba74455 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/LiquidityManager.t.sol @@ -0,0 +1,945 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {ILiquidityManager} from "../interfaces/ILiquidityManager.sol"; +import {IBridgeAdapter} from "../interfaces/IBridge.sol"; + +import {LockReleaseTokenPool} from "../../ccip/pools/LockReleaseTokenPool.sol"; +import {LiquidityManager} from "../LiquidityManager.sol"; +import {MockL1BridgeAdapter} from "./mocks/MockBridgeAdapter.sol"; +import {LiquidityManagerBaseTest} from "./LiquidityManagerBaseTest.t.sol"; +import {LiquidityManagerHelper} from "./helpers/LiquidityManagerHelper.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +// FOUNDRY_PROFILE=liquiditymanager forge test --match-path src/v0.8/liquiditymanager/test/LiquidityManager.t.sol + +contract LiquidityManagerSetup is LiquidityManagerBaseTest { + event FinalizationStepCompleted( + uint64 indexed ocrSeqNum, + uint64 indexed remoteChainSelector, + bytes bridgeSpecificData + ); + event LiquidityTransferred( + uint64 indexed ocrSeqNum, + uint64 indexed fromChainSelector, + uint64 indexed toChainSelector, + address to, + uint256 amount, + bytes bridgeSpecificPayload, + bytes bridgeReturnData + ); + event FinalizationFailed( + uint64 indexed ocrSeqNum, + uint64 indexed remoteChainSelector, + bytes bridgeSpecificData, + bytes reason + ); + event FinanceRoleSet(address financeRole); + event LiquidityAddedToContainer(address indexed provider, uint256 indexed amount); + event LiquidityRemovedFromContainer(address indexed remover, uint256 indexed amount); + // Liquidity container event + event LiquidityAdded(address indexed provider, uint256 indexed amount); + event LiquidityRemoved(address indexed remover, uint256 indexed amount); + + error NonceAlreadyUsed(uint256 nonce); + + LiquidityManagerHelper internal s_liquidityManager; + LockReleaseTokenPool internal s_lockReleaseTokenPool; + MockL1BridgeAdapter internal s_bridgeAdapter; + + // LiquidityManager that rebalances weth. + LiquidityManagerHelper internal s_wethRebalancer; + LockReleaseTokenPool internal s_wethLockReleaseTokenPool; + MockL1BridgeAdapter internal s_wethBridgeAdapter; + + function setUp() public virtual override { + LiquidityManagerBaseTest.setUp(); + + s_bridgeAdapter = new MockL1BridgeAdapter(s_l1Token, false); + s_lockReleaseTokenPool = new LockReleaseTokenPool(s_l1Token, new address[](0), address(1), true, address(123)); + s_liquidityManager = new LiquidityManagerHelper( + s_l1Token, + i_localChainSelector, + s_lockReleaseTokenPool, + 0, + FINANCE + ); + + s_lockReleaseTokenPool.setRebalancer(address(s_liquidityManager)); + + s_wethBridgeAdapter = new MockL1BridgeAdapter(IERC20(address(s_l1Weth)), true); + s_wethLockReleaseTokenPool = new LockReleaseTokenPool( + IERC20(address(s_l1Weth)), + new address[](0), + address(1), + true, + address(123) + ); + s_wethRebalancer = new LiquidityManagerHelper( + IERC20(address(s_l1Weth)), + i_localChainSelector, + s_wethLockReleaseTokenPool, + 0, + FINANCE + ); + + s_wethLockReleaseTokenPool.setRebalancer(address(s_wethRebalancer)); + } +} + +contract LiquidityManager_addLiquidity is LiquidityManagerSetup { + function test_addLiquiditySuccess() external { + address caller = STRANGER; + vm.startPrank(caller); + + uint256 amount = 12345679; + deal(address(s_l1Token), caller, amount); + + s_l1Token.approve(address(s_liquidityManager), amount); + + vm.expectEmit(); + emit LiquidityAddedToContainer(caller, amount); + + s_liquidityManager.addLiquidity(amount); + + assertEq(s_l1Token.balanceOf(address(s_lockReleaseTokenPool)), amount); + } +} + +contract LiquidityManager_removeLiquidity is LiquidityManagerSetup { + function test_removeLiquiditySuccess() external { + uint256 amount = 12345679; + deal(address(s_l1Token), address(s_lockReleaseTokenPool), amount); + + vm.expectEmit(); + emit LiquidityRemovedFromContainer(FINANCE, amount); + + vm.startPrank(FINANCE); + s_liquidityManager.removeLiquidity(amount); + + assertEq(s_l1Token.balanceOf(address(s_liquidityManager)), 0); + } + + function test_InsufficientLiquidityReverts() external { + uint256 balance = 923; + uint256 requested = balance + 1; + + deal(address(s_l1Token), address(s_lockReleaseTokenPool), balance); + + vm.expectRevert(abi.encodeWithSelector(LiquidityManager.InsufficientLiquidity.selector, requested, balance, 0)); + + vm.startPrank(FINANCE); + s_liquidityManager.removeLiquidity(requested); + } + + function test_OnlyFinanceRoleReverts() external { + vm.stopPrank(); + + vm.expectRevert(LiquidityManager.OnlyFinanceRole.selector); + + s_liquidityManager.removeLiquidity(123); + } +} + +contract LiquidityManager__report is LiquidityManagerSetup { + function test_EmptyReportReverts() external { + ILiquidityManager.LiquidityInstructions memory instructions = ILiquidityManager.LiquidityInstructions({ + sendLiquidityParams: new ILiquidityManager.SendLiquidityParams[](0), + receiveLiquidityParams: new ILiquidityManager.ReceiveLiquidityParams[](0) + }); + + vm.expectRevert(LiquidityManager.EmptyReport.selector); + + s_liquidityManager.report(abi.encode(instructions), 123); + } +} + +contract LiquidityManager_rebalanceLiquidity is LiquidityManagerSetup { + uint256 internal constant AMOUNT = 12345679; + + function test_rebalanceLiquiditySuccess() external { + deal(address(s_l1Token), address(s_lockReleaseTokenPool), AMOUNT); + + LiquidityManager.CrossChainRebalancerArgs[] memory args = new LiquidityManager.CrossChainRebalancerArgs[](1); + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(s_liquidityManager), + localBridge: s_bridgeAdapter, + remoteToken: address(s_l2Token), + remoteChainSelector: i_remoteChainSelector, + enabled: true + }); + s_liquidityManager.setCrossChainRebalancers(args); + + vm.expectEmit(); + emit Transfer(address(s_lockReleaseTokenPool), address(s_liquidityManager), AMOUNT); + + vm.expectEmit(); + emit Approval(address(s_liquidityManager), address(s_bridgeAdapter), AMOUNT); + + vm.expectEmit(); + emit Transfer(address(s_liquidityManager), address(s_bridgeAdapter), AMOUNT); + + vm.expectEmit(); + bytes memory encodedNonce = abi.encode(uint256(1)); + emit LiquidityTransferred( + type(uint64).max, + i_localChainSelector, + i_remoteChainSelector, + address(s_liquidityManager), + AMOUNT, + bytes(""), + encodedNonce + ); + + vm.startPrank(FINANCE); + s_liquidityManager.rebalanceLiquidity(i_remoteChainSelector, AMOUNT, 0, bytes("")); + + assertEq(s_l1Token.balanceOf(address(s_liquidityManager)), 0); + assertEq(s_l1Token.balanceOf(address(s_bridgeAdapter)), AMOUNT); + assertEq(s_l1Token.allowance(address(s_liquidityManager), address(s_bridgeAdapter)), 0); + } + + /// @notice this test sets up a circular system where the liquidity container of + /// the local Liquidity manager is the bridge adapter of the remote liquidity manager + /// and the other way around for the remote liquidity manager. This allows us to + /// rebalance funds between the two liquidity managers on the same chain. + function test_rebalanceBetweenPoolsSuccess() external { + uint256 amount = 12345670; + + s_liquidityManager = new LiquidityManagerHelper(s_l1Token, i_localChainSelector, s_bridgeAdapter, 0, FINANCE); + + MockL1BridgeAdapter mockRemoteBridgeAdapter = new MockL1BridgeAdapter(s_l1Token, false); + LiquidityManager mockRemoteRebalancer = new LiquidityManager( + s_l1Token, + i_remoteChainSelector, + mockRemoteBridgeAdapter, + 0, + FINANCE + ); + + LiquidityManager.CrossChainRebalancerArgs[] memory args = new LiquidityManager.CrossChainRebalancerArgs[](1); + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(mockRemoteRebalancer), + localBridge: mockRemoteBridgeAdapter, + remoteToken: address(s_l1Token), + remoteChainSelector: i_remoteChainSelector, + enabled: true + }); + + s_liquidityManager.setCrossChainRebalancers(args); + + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(s_liquidityManager), + localBridge: s_bridgeAdapter, + remoteToken: address(s_l1Token), + remoteChainSelector: i_localChainSelector, + enabled: true + }); + + mockRemoteRebalancer.setCrossChainRebalancers(args); + + deal(address(s_l1Token), address(s_bridgeAdapter), amount); + + vm.startPrank(FINANCE); + s_liquidityManager.rebalanceLiquidity(i_remoteChainSelector, amount, 0, bytes("")); + + assertEq(s_l1Token.balanceOf(address(s_bridgeAdapter)), 0); + assertEq(s_l1Token.balanceOf(address(mockRemoteBridgeAdapter)), amount); + assertEq(s_l1Token.allowance(address(s_liquidityManager), address(s_bridgeAdapter)), 0); + + // attach a bridge fee and see the relevant adapter's ether balance change. + // the bridge fee is sent along with the sendERC20 call. + uint256 bridgeFee = 123; + vm.deal(address(mockRemoteRebalancer), bridgeFee); + mockRemoteRebalancer.rebalanceLiquidity(i_localChainSelector, amount, bridgeFee, bytes("")); + + assertEq(s_l1Token.balanceOf(address(s_bridgeAdapter)), amount); + assertEq(s_l1Token.balanceOf(address(mockRemoteBridgeAdapter)), 0); + assertEq(address(s_bridgeAdapter).balance, bridgeFee); + + // Assert partial rebalancing works correctly + s_liquidityManager.rebalanceLiquidity(i_remoteChainSelector, amount / 2, 0, bytes("")); + + assertEq(s_l1Token.balanceOf(address(s_bridgeAdapter)), amount / 2); + assertEq(s_l1Token.balanceOf(address(mockRemoteBridgeAdapter)), amount / 2); + } + + function test_rebalanceBetweenPoolsSuccess_AlreadyFinalized() external { + // set up a rebalancer on another chain, an "L2". + // note we use the L1 bridge adapter because it has the reverting logic + // when finalization is already done. + MockL1BridgeAdapter remoteBridgeAdapter = new MockL1BridgeAdapter(s_l2Token, false); + LockReleaseTokenPool remotePool = new LockReleaseTokenPool( + s_l2Token, + new address[](0), + address(1), + true, + address(123) + ); + LiquidityManager remoteRebalancer = new LiquidityManager(s_l2Token, i_remoteChainSelector, remotePool, 0, FINANCE); + + // set rebalancer role on the pool. + remotePool.setRebalancer(address(remoteRebalancer)); + + // set up the cross chain rebalancer on "L1". + LiquidityManager.CrossChainRebalancerArgs[] memory args = new LiquidityManager.CrossChainRebalancerArgs[](1); + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(remoteRebalancer), + localBridge: s_bridgeAdapter, + remoteToken: address(s_l2Token), + remoteChainSelector: i_remoteChainSelector, + enabled: true + }); + + s_liquidityManager.setCrossChainRebalancers(args); + + // set up the cross chain rebalancer on "L2". + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(s_liquidityManager), + localBridge: remoteBridgeAdapter, + remoteToken: address(s_l1Token), + remoteChainSelector: i_localChainSelector, + enabled: true + }); + + remoteRebalancer.setCrossChainRebalancers(args); + + // deal some L1 tokens to the L1 bridge adapter so that it can send them to the rebalancer + // when the withdrawal gets finalized. + deal(address(s_l1Token), address(s_bridgeAdapter), AMOUNT); + // deal some L2 tokens to the remote token pool so that we can withdraw it when we rebalance. + deal(address(s_l2Token), address(remotePool), AMOUNT); + + uint256 nonce = 1; + uint64 maxSeqNum = type(uint64).max; + bytes memory bridgeSendReturnData = abi.encode(nonce); + bytes memory bridgeSpecificPayload = bytes(""); + vm.expectEmit(); + emit LiquidityRemoved(address(remoteRebalancer), AMOUNT); + vm.expectEmit(); + emit LiquidityTransferred( + maxSeqNum, + i_remoteChainSelector, + i_localChainSelector, + address(s_liquidityManager), + AMOUNT, + bridgeSpecificPayload, + bridgeSendReturnData + ); + vm.startPrank(FINANCE); + remoteRebalancer.rebalanceLiquidity(i_localChainSelector, AMOUNT, 0, bridgeSpecificPayload); + + // available liquidity has been moved to the remote bridge adapter from the token pool. + assertEq(s_l2Token.balanceOf(address(remoteBridgeAdapter)), AMOUNT, "remoteBridgeAdapter balance"); + assertEq(s_l2Token.balanceOf(address(remotePool)), 0, "remotePool balance"); + + // prove and finalize manually on the L1 bridge adapter. + // this should transfer the funds to the rebalancer. + MockL1BridgeAdapter.ProvePayload memory provePayload = MockL1BridgeAdapter.ProvePayload({nonce: nonce}); + MockL1BridgeAdapter.Payload memory payload = MockL1BridgeAdapter.Payload({ + action: MockL1BridgeAdapter.FinalizationAction.ProveWithdrawal, + data: abi.encode(provePayload) + }); + bool fundsAvailable = s_bridgeAdapter.finalizeWithdrawERC20( + address(0), + address(s_liquidityManager), + abi.encode(payload) + ); + assertFalse(fundsAvailable, "fundsAvailable must be false"); + MockL1BridgeAdapter.FinalizePayload memory finalizePayload = MockL1BridgeAdapter.FinalizePayload({ + nonce: nonce, + amount: AMOUNT + }); + payload = MockL1BridgeAdapter.Payload({ + action: MockL1BridgeAdapter.FinalizationAction.FinalizeWithdrawal, + data: abi.encode(finalizePayload) + }); + fundsAvailable = s_bridgeAdapter.finalizeWithdrawERC20( + address(0), + address(s_liquidityManager), + abi.encode(payload) + ); + assertTrue(fundsAvailable, "fundsAvailable must be true"); + + // available balance on the L1 bridge adapter has been moved to the rebalancer. + assertEq(s_l1Token.balanceOf(address(s_liquidityManager)), AMOUNT, "rebalancer balance 1"); + assertEq(s_l1Token.balanceOf(address(s_bridgeAdapter)), 0, "bridgeAdapter balance"); + + // try to finalize on L1 again + // bytes memory revertData = abi.encodeWithSelector(NonceAlreadyUsed.selector, nonce); + vm.expectEmit(); + emit FinalizationFailed( + maxSeqNum, + i_remoteChainSelector, + abi.encode(payload), + abi.encodeWithSelector(NonceAlreadyUsed.selector, nonce) + ); + vm.expectEmit(); + emit LiquidityAdded(address(s_liquidityManager), AMOUNT); + vm.expectEmit(); + emit LiquidityTransferred( + maxSeqNum, + i_remoteChainSelector, + i_localChainSelector, + address(s_liquidityManager), + AMOUNT, + abi.encode(payload), + bytes("") + ); + s_liquidityManager.receiveLiquidity(i_remoteChainSelector, AMOUNT, false, abi.encode(payload)); + + // available balance on the rebalancer has been injected into the token pool. + assertEq(s_l1Token.balanceOf(address(s_liquidityManager)), 0, "rebalancer balance 2"); + assertEq(s_l1Token.balanceOf(address(s_lockReleaseTokenPool)), AMOUNT, "lockReleaseTokenPool balance"); + } + + function test_rebalanceBetweenPools_MultiStageFinalization() external { + // set up a rebalancer on another chain, an "L2". + // note we use the L1 bridge adapter because it has the reverting logic + // when finalization is already done. + MockL1BridgeAdapter remoteBridgeAdapter = new MockL1BridgeAdapter(s_l2Token, false); + LockReleaseTokenPool remotePool = new LockReleaseTokenPool( + s_l2Token, + new address[](0), + address(1), + true, + address(123) + ); + LiquidityManager remoteRebalancer = new LiquidityManager(s_l2Token, i_remoteChainSelector, remotePool, 0, FINANCE); + + // set rebalancer role on the pool. + remotePool.setRebalancer(address(remoteRebalancer)); + + // set up the cross chain rebalancer on "L1". + LiquidityManager.CrossChainRebalancerArgs[] memory args = new LiquidityManager.CrossChainRebalancerArgs[](1); + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(remoteRebalancer), + localBridge: s_bridgeAdapter, + remoteToken: address(s_l2Token), + remoteChainSelector: i_remoteChainSelector, + enabled: true + }); + + s_liquidityManager.setCrossChainRebalancers(args); + + // set up the cross chain rebalancer on "L2". + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(s_liquidityManager), + localBridge: remoteBridgeAdapter, + remoteToken: address(s_l1Token), + remoteChainSelector: i_localChainSelector, + enabled: true + }); + + remoteRebalancer.setCrossChainRebalancers(args); + + // deal some L1 tokens to the L1 bridge adapter so that it can send them to the rebalancer + // when the withdrawal gets finalized. + deal(address(s_l1Token), address(s_bridgeAdapter), AMOUNT); + // deal some L2 tokens to the remote token pool so that we can withdraw it when we rebalance. + deal(address(s_l2Token), address(remotePool), AMOUNT); + + // initiate a send from remote rebalancer to s_liquidityManager. + uint256 nonce = 1; + uint64 maxSeqNum = type(uint64).max; + bytes memory bridgeSendReturnData = abi.encode(nonce); + bytes memory bridgeSpecificPayload = bytes(""); + vm.expectEmit(); + emit LiquidityRemoved(address(remoteRebalancer), AMOUNT); + vm.expectEmit(); + emit LiquidityTransferred( + maxSeqNum, + i_remoteChainSelector, + i_localChainSelector, + address(s_liquidityManager), + AMOUNT, + bridgeSpecificPayload, + bridgeSendReturnData + ); + vm.startPrank(FINANCE); + remoteRebalancer.rebalanceLiquidity(i_localChainSelector, AMOUNT, 0, bridgeSpecificPayload); + + // available liquidity has been moved to the remote bridge adapter from the token pool. + assertEq(s_l2Token.balanceOf(address(remoteBridgeAdapter)), AMOUNT, "remoteBridgeAdapter balance"); + assertEq(s_l2Token.balanceOf(address(remotePool)), 0, "remotePool balance"); + + // prove withdrawal on the L1 bridge adapter, through the rebalancer. + uint256 balanceBeforeProve = s_l1Token.balanceOf(address(s_lockReleaseTokenPool)); + MockL1BridgeAdapter.ProvePayload memory provePayload = MockL1BridgeAdapter.ProvePayload({nonce: nonce}); + MockL1BridgeAdapter.Payload memory payload = MockL1BridgeAdapter.Payload({ + action: MockL1BridgeAdapter.FinalizationAction.ProveWithdrawal, + data: abi.encode(provePayload) + }); + vm.expectEmit(); + emit FinalizationStepCompleted(maxSeqNum, i_remoteChainSelector, abi.encode(payload)); + s_liquidityManager.receiveLiquidity(i_remoteChainSelector, AMOUNT, false, abi.encode(payload)); + + // s_liquidityManager should have no tokens. + assertEq(s_l1Token.balanceOf(address(s_liquidityManager)), 0, "rebalancer balance 1"); + // balance of s_lockReleaseTokenPool should be unchanged since no liquidity got added yet. + assertEq( + s_l1Token.balanceOf(address(s_lockReleaseTokenPool)), + balanceBeforeProve, + "s_lockReleaseTokenPool balance should be unchanged" + ); + + // finalize withdrawal on the L1 bridge adapter, through the rebalancer. + MockL1BridgeAdapter.FinalizePayload memory finalizePayload = MockL1BridgeAdapter.FinalizePayload({ + nonce: nonce, + amount: AMOUNT + }); + payload = MockL1BridgeAdapter.Payload({ + action: MockL1BridgeAdapter.FinalizationAction.FinalizeWithdrawal, + data: abi.encode(finalizePayload) + }); + vm.expectEmit(); + emit LiquidityAdded(address(s_liquidityManager), AMOUNT); + vm.expectEmit(); + emit LiquidityTransferred( + maxSeqNum, + i_remoteChainSelector, + i_localChainSelector, + address(s_liquidityManager), + AMOUNT, + abi.encode(payload), + bytes("") + ); + s_liquidityManager.receiveLiquidity(i_remoteChainSelector, AMOUNT, false, abi.encode(payload)); + + // s_liquidityManager should have no tokens. + assertEq(s_l1Token.balanceOf(address(s_liquidityManager)), 0, "rebalancer balance 2"); + // balance of s_lockReleaseTokenPool should be updated + assertEq( + s_l1Token.balanceOf(address(s_lockReleaseTokenPool)), + balanceBeforeProve + AMOUNT, + "s_lockReleaseTokenPool balance should be updated" + ); + } + + function test_rebalanceBetweenPools_NativeRewrap() external { + // set up a rebalancer similar to the above on another chain, an "L2". + MockL1BridgeAdapter remoteBridgeAdapter = new MockL1BridgeAdapter(IERC20(address(s_l2Weth)), true); + LockReleaseTokenPool remotePool = new LockReleaseTokenPool( + IERC20(address(s_l2Weth)), + new address[](0), + address(1), + true, + address(123) + ); + LiquidityManager remoteRebalancer = new LiquidityManager( + IERC20(address(s_l2Weth)), + i_remoteChainSelector, + remotePool, + 0, + FINANCE + ); + + // set rebalancer role on the pool. + remotePool.setRebalancer(address(remoteRebalancer)); + + // set up the cross chain rebalancer on "L1". + LiquidityManager.CrossChainRebalancerArgs[] memory args = new LiquidityManager.CrossChainRebalancerArgs[](1); + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(remoteRebalancer), + localBridge: s_wethBridgeAdapter, + remoteToken: address(s_l2Weth), + remoteChainSelector: i_remoteChainSelector, + enabled: true + }); + + s_wethRebalancer.setCrossChainRebalancers(args); + + // set up the cross chain rebalancer on "L2". + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(s_wethRebalancer), + localBridge: remoteBridgeAdapter, + remoteToken: address(s_l1Weth), + remoteChainSelector: i_localChainSelector, + enabled: true + }); + + remoteRebalancer.setCrossChainRebalancers(args); + + // deal some ether to the L1 bridge adapter so that it can send them to the rebalancer + // when the withdrawal gets finalized. + vm.deal(address(s_wethBridgeAdapter), AMOUNT); + // deal some L2 tokens to the remote token pool so that we can withdraw it when we rebalance. + deal(address(s_l2Weth), address(remotePool), AMOUNT); + // deposit some eth to the weth contract on L2 from the remote bridge adapter + // so that the withdraw() call succeeds. + vm.deal(address(remoteBridgeAdapter), AMOUNT); + vm.startPrank(address(remoteBridgeAdapter)); + s_l2Weth.deposit{value: AMOUNT}(); + vm.stopPrank(); + + // switch to finance for the rest of the test to avoid reverts. + vm.startPrank(FINANCE); + + // initiate a send from remote rebalancer to s_wethRebalancer. + uint256 nonce = 1; + uint64 maxSeqNum = type(uint64).max; + bytes memory bridgeSendReturnData = abi.encode(nonce); + bytes memory bridgeSpecificPayload = bytes(""); + vm.expectEmit(); + emit LiquidityRemoved(address(remoteRebalancer), AMOUNT); + vm.expectEmit(); + emit LiquidityTransferred( + maxSeqNum, + i_remoteChainSelector, + i_localChainSelector, + address(s_wethRebalancer), + AMOUNT, + bridgeSpecificPayload, + bridgeSendReturnData + ); + remoteRebalancer.rebalanceLiquidity(i_localChainSelector, AMOUNT, 0, bridgeSpecificPayload); + + // available liquidity has been moved to the remote bridge adapter from the token pool. + assertEq(s_l2Weth.balanceOf(address(remoteBridgeAdapter)), AMOUNT, "remoteBridgeAdapter balance"); + assertEq(s_l2Weth.balanceOf(address(remotePool)), 0, "remotePool balance"); + + // prove withdrawal on the L1 bridge adapter, through the rebalancer. + uint256 balanceBeforeProve = s_l1Weth.balanceOf(address(s_wethLockReleaseTokenPool)); + MockL1BridgeAdapter.ProvePayload memory provePayload = MockL1BridgeAdapter.ProvePayload({nonce: nonce}); + MockL1BridgeAdapter.Payload memory payload = MockL1BridgeAdapter.Payload({ + action: MockL1BridgeAdapter.FinalizationAction.ProveWithdrawal, + data: abi.encode(provePayload) + }); + vm.expectEmit(); + emit FinalizationStepCompleted(maxSeqNum, i_remoteChainSelector, abi.encode(payload)); + s_wethRebalancer.receiveLiquidity(i_remoteChainSelector, AMOUNT, false, abi.encode(payload)); + + // s_wethRebalancer should have no tokens. + assertEq(s_l1Weth.balanceOf(address(s_wethRebalancer)), 0, "rebalancer balance 1"); + // balance of s_wethLockReleaseTokenPool should be unchanged since no liquidity got added yet. + assertEq( + s_l1Weth.balanceOf(address(s_wethLockReleaseTokenPool)), + balanceBeforeProve, + "s_wethLockReleaseTokenPool balance should be unchanged" + ); + + // finalize withdrawal on the L1 bridge adapter, through the rebalancer. + MockL1BridgeAdapter.FinalizePayload memory finalizePayload = MockL1BridgeAdapter.FinalizePayload({ + nonce: nonce, + amount: AMOUNT + }); + payload = MockL1BridgeAdapter.Payload({ + action: MockL1BridgeAdapter.FinalizationAction.FinalizeWithdrawal, + data: abi.encode(finalizePayload) + }); + vm.expectEmit(); + emit LiquidityAdded(address(s_wethRebalancer), AMOUNT); + vm.expectEmit(); + emit LiquidityTransferred( + maxSeqNum, + i_remoteChainSelector, + i_localChainSelector, + address(s_wethRebalancer), + AMOUNT, + abi.encode(payload), + bytes("") + ); + s_wethRebalancer.receiveLiquidity(i_remoteChainSelector, AMOUNT, true, abi.encode(payload)); + + // s_wethRebalancer should have no tokens. + assertEq(s_l1Weth.balanceOf(address(s_wethRebalancer)), 0, "rebalancer balance 2"); + // s_wethRebalancer should have no native tokens. + assertEq(address(s_wethRebalancer).balance, 0, "rebalancer native balance should be zero"); + // balance of s_wethLockReleaseTokenPool should be updated + assertEq( + s_l1Weth.balanceOf(address(s_wethLockReleaseTokenPool)), + balanceBeforeProve + AMOUNT, + "s_wethLockReleaseTokenPool balance should be updated" + ); + } + + // Reverts + + function test_InsufficientLiquidityReverts() external { + s_liquidityManager.setMinimumLiquidity(3); + deal(address(s_l1Token), address(s_lockReleaseTokenPool), AMOUNT); + vm.expectRevert(abi.encodeWithSelector(LiquidityManager.InsufficientLiquidity.selector, AMOUNT, AMOUNT, 3)); + + vm.startPrank(FINANCE); + s_liquidityManager.rebalanceLiquidity(0, AMOUNT, 0, bytes("")); + } + + function test_InvalidRemoteChainReverts() external { + deal(address(s_l1Token), address(s_lockReleaseTokenPool), AMOUNT); + + vm.expectRevert(abi.encodeWithSelector(LiquidityManager.InvalidRemoteChain.selector, i_remoteChainSelector)); + + vm.startPrank(FINANCE); + s_liquidityManager.rebalanceLiquidity(i_remoteChainSelector, AMOUNT, 0, bytes("")); + } +} + +contract LiquidityManager_setCrossChainRebalancer is LiquidityManagerSetup { + event CrossChainRebalancerSet( + uint64 indexed remoteChainSelector, + IBridgeAdapter localBridge, + address remoteToken, + address remoteRebalancer, + bool enabled + ); + + function test_setCrossChainRebalancerSuccess() external { + address newRebalancer = address(23892423); + uint64 remoteChainSelector = 12301293; + + uint64[] memory supportedChains = s_liquidityManager.getSupportedDestChains(); + assertEq(supportedChains.length, 0); + + LiquidityManager.CrossChainRebalancerArgs[] memory args = new LiquidityManager.CrossChainRebalancerArgs[](1); + args[0] = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: newRebalancer, + localBridge: s_bridgeAdapter, + remoteToken: address(190490124908), + remoteChainSelector: remoteChainSelector, + enabled: true + }); + + vm.expectEmit(); + emit CrossChainRebalancerSet( + remoteChainSelector, + args[0].localBridge, + args[0].remoteToken, + newRebalancer, + args[0].enabled + ); + + s_liquidityManager.setCrossChainRebalancers(args); + + assertEq(s_liquidityManager.getCrossChainRebalancer(remoteChainSelector).remoteRebalancer, newRebalancer); + + LiquidityManager.CrossChainRebalancerArgs[] memory got = s_liquidityManager.getAllCrossChainRebalancers(); + assertEq(got.length, 1); + assertEq(got[0].remoteRebalancer, args[0].remoteRebalancer); + assertEq(address(got[0].localBridge), address(args[0].localBridge)); + assertEq(got[0].remoteToken, args[0].remoteToken); + assertEq(got[0].remoteChainSelector, args[0].remoteChainSelector); + assertEq(got[0].enabled, args[0].enabled); + + supportedChains = s_liquidityManager.getSupportedDestChains(); + assertEq(supportedChains.length, 1); + assertEq(supportedChains[0], remoteChainSelector); + + address anotherRebalancer = address(123); + args[0].remoteRebalancer = anotherRebalancer; + + vm.expectEmit(); + emit CrossChainRebalancerSet( + remoteChainSelector, + args[0].localBridge, + args[0].remoteToken, + anotherRebalancer, + args[0].enabled + ); + + s_liquidityManager.setCrossChainRebalancer(args[0]); + + assertEq(s_liquidityManager.getCrossChainRebalancer(remoteChainSelector).remoteRebalancer, anotherRebalancer); + + supportedChains = s_liquidityManager.getSupportedDestChains(); + assertEq(supportedChains.length, 1); + assertEq(supportedChains[0], remoteChainSelector); + } + + function test_ZeroChainSelectorReverts() external { + LiquidityManager.CrossChainRebalancerArgs memory arg = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(9), + localBridge: s_bridgeAdapter, + remoteToken: address(190490124908), + remoteChainSelector: 0, + enabled: true + }); + + vm.expectRevert(LiquidityManager.ZeroChainSelector.selector); + + s_liquidityManager.setCrossChainRebalancer(arg); + } + + function test_ZeroAddressReverts() external { + LiquidityManager.CrossChainRebalancerArgs memory arg = ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(0), + localBridge: s_bridgeAdapter, + remoteToken: address(190490124908), + remoteChainSelector: 123, + enabled: true + }); + + vm.expectRevert(LiquidityManager.ZeroAddress.selector); + + s_liquidityManager.setCrossChainRebalancer(arg); + + arg.remoteRebalancer = address(9); + arg.localBridge = IBridgeAdapter(address(0)); + + vm.expectRevert(LiquidityManager.ZeroAddress.selector); + + s_liquidityManager.setCrossChainRebalancer(arg); + + arg.localBridge = s_bridgeAdapter; + arg.remoteToken = address(0); + + vm.expectRevert(LiquidityManager.ZeroAddress.selector); + + s_liquidityManager.setCrossChainRebalancer(arg); + } + + function test_OnlyOwnerReverts() external { + vm.stopPrank(); + + vm.expectRevert("Only callable by owner"); + + // Test the entrypoint that takes a list + s_liquidityManager.setCrossChainRebalancers(new LiquidityManager.CrossChainRebalancerArgs[](0)); + + vm.expectRevert("Only callable by owner"); + + // Test the entrypoint that takes a single item + s_liquidityManager.setCrossChainRebalancer( + ILiquidityManager.CrossChainRebalancerArgs({ + remoteRebalancer: address(9), + localBridge: s_bridgeAdapter, + remoteToken: address(190490124908), + remoteChainSelector: 124, + enabled: true + }) + ); + } +} + +contract LiquidityManager_setLocalLiquidityContainer is LiquidityManagerSetup { + event LiquidityContainerSet(address indexed newLiquidityContainer); + + function test_setLocalLiquidityContainerSuccess() external { + LockReleaseTokenPool newPool = new LockReleaseTokenPool( + s_l1Token, + new address[](0), + address(1), + true, + address(123) + ); + + vm.expectEmit(); + emit LiquidityContainerSet(address(newPool)); + + s_liquidityManager.setLocalLiquidityContainer(newPool); + + assertEq(s_liquidityManager.getLocalLiquidityContainer(), address(newPool)); + } + + function test_OnlyOwnerReverts() external { + vm.stopPrank(); + + vm.expectRevert("Only callable by owner"); + + s_liquidityManager.setLocalLiquidityContainer(LockReleaseTokenPool(address(1))); + } + + function test_ReverstWhen_CalledWithTheZeroAddress() external { + vm.expectRevert(LiquidityManager.ZeroAddress.selector); + s_liquidityManager.setLocalLiquidityContainer(LockReleaseTokenPool(address(0))); + } +} + +contract LiquidityManager_setMinimumLiquidity is LiquidityManagerSetup { + event MinimumLiquiditySet(uint256 oldBalance, uint256 newBalance); + + function test_setMinimumLiquiditySuccess() external { + vm.expectEmit(); + emit MinimumLiquiditySet(uint256(0), uint256(1000)); + s_liquidityManager.setMinimumLiquidity(1000); + assertEq(s_liquidityManager.getMinimumLiquidity(), uint256(1000)); + } + + function test_OnlyOwnerReverts() external { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + s_liquidityManager.setMinimumLiquidity(uint256(1000)); + } +} + +contract LiquidityManager_setFinanceRole is LiquidityManagerSetup { + event MinimumLiquiditySet(uint256 oldBalance, uint256 newBalance); + + function test_setFinanceRoleSuccess() external { + vm.expectEmit(); + address newFinanceRole = makeAddr("newFinanceRole"); + assertEq(s_liquidityManager.getFinanceRole(), FINANCE); + emit FinanceRoleSet(newFinanceRole); + s_liquidityManager.setFinanceRole(newFinanceRole); + assertEq(s_liquidityManager.getFinanceRole(), newFinanceRole); + } + + function test_OnlyOwnerReverts() external { + vm.stopPrank(); + vm.expectRevert("Only callable by owner"); + s_liquidityManager.setFinanceRole(address(1)); + } +} + +contract LiquidityManager_withdrawNative is LiquidityManagerSetup { + event NativeWithdrawn(uint256 amount, address destination); + + address private receiver = makeAddr("receiver"); + + function setUp() public override { + super.setUp(); + vm.deal(address(s_liquidityManager), 1); + } + + function test_withdrawNative_success() external { + assertEq(receiver.balance, 0); + vm.expectEmit(); + emit NativeWithdrawn(1, receiver); + vm.startPrank(FINANCE); + s_liquidityManager.withdrawNative(1, payable(receiver)); + assertEq(receiver.balance, 1); + } + + function test_OnlyFinanceRoleReverts() external { + vm.stopPrank(); + vm.expectRevert(LiquidityManager.OnlyFinanceRole.selector); + s_liquidityManager.withdrawNative(1, payable(receiver)); + } +} + +contract LiquidityManager_receive is LiquidityManagerSetup { + event NativeDeposited(uint256 amount, address depositor); + + address private depositor = makeAddr("depositor"); + + function test_receive_success() external { + vm.deal(depositor, 100); + uint256 before = address(s_liquidityManager).balance; + vm.expectEmit(); + emit NativeDeposited(100, depositor); + vm.startPrank(depositor); + payable(address(s_liquidityManager)).transfer(100); + assertEq(address(s_liquidityManager).balance, before + 100); + } +} + +contract LiquidityManager_withdrawERC20 is LiquidityManagerSetup { + function test_withdrawERC20Success() external { + uint256 amount = 100; + deal(address(s_otherToken), address(s_liquidityManager), amount); + assertEq(s_otherToken.balanceOf(address(1)), 0); + assertEq(s_otherToken.balanceOf(address(s_liquidityManager)), amount); + vm.startPrank(FINANCE); + s_liquidityManager.withdrawERC20(address(s_otherToken), amount, address(1)); + assertEq(s_otherToken.balanceOf(address(1)), amount); + assertEq(s_otherToken.balanceOf(address(s_liquidityManager)), 0); + } + + function test_withdrawERC20Reverts() external { + uint256 amount = 100; + deal(address(s_otherToken), address(s_liquidityManager), amount); + vm.startPrank(STRANGER); + vm.expectRevert(LiquidityManager.OnlyFinanceRole.selector); + s_liquidityManager.withdrawERC20(address(s_otherToken), amount, address(1)); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/test/LiquidityManagerBaseTest.t.sol b/contracts/src/v0.8/liquiditymanager/test/LiquidityManagerBaseTest.t.sol new file mode 100644 index 00000000000..128a03f255a --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/LiquidityManagerBaseTest.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {Test} from "forge-std/Test.sol"; + +import {WETH9} from "../../ccip/test/WETH9.sol"; + +import {ERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract LiquidityManagerBaseTest is Test { + // ERC20 events + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + IERC20 internal s_l1Token; + IERC20 internal s_l2Token; + IERC20 internal s_otherToken; + WETH9 internal s_l1Weth; + WETH9 internal s_l2Weth; + + uint64 internal immutable i_localChainSelector = 1234; + uint64 internal immutable i_remoteChainSelector = 9876; + + address internal constant FINANCE = address(0x00000fffffffffffffffffffff); + address internal constant OWNER = address(0x00000078772732723782873283); + address internal constant STRANGER = address(0x00000999999911111111222222); + + function setUp() public virtual { + s_l1Token = new ERC20("l1", "L1"); + s_l2Token = new ERC20("l2", "L2"); + s_otherToken = new ERC20("other", "OTHER"); + + s_l1Weth = new WETH9(); + s_l2Weth = new WETH9(); + + vm.startPrank(OWNER); + + vm.label(FINANCE, "FINANCE"); + vm.label(OWNER, "OWNER"); + vm.label(STRANGER, "STRANGER"); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/ArbitrumL1BridgeAdapter.t.sol b/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/ArbitrumL1BridgeAdapter.t.sol new file mode 100644 index 00000000000..8afea2d680d --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/ArbitrumL1BridgeAdapter.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IWrappedNative} from "../../../ccip/interfaces/IWrappedNative.sol"; + +import {ArbitrumL1BridgeAdapter, IOutbox} from "../../bridge-adapters/ArbitrumL1BridgeAdapter.sol"; +import "forge-std/Test.sol"; + +import {IL1GatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/gateway/IL1GatewayRouter.sol"; +import {IGatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/gateway/IGatewayRouter.sol"; +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +//contract ArbitrumL1BridgeAdapterSetup is Test { +// uint256 internal mainnetFork; +// uint256 internal arbitrumFork; +// +// string internal constant MAINNET_RPC_URL = ""; +// +// address internal constant L1_GATEWAY_ROUTER = 0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef; +// address internal constant L1_ERC20_GATEWAY = 0xa3A7B6F88361F48403514059F1F16C8E78d60EeC; +// address internal constant L1_INBOX = 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f; +// // inbox 0x5aED5f8A1e3607476F1f81c3d8fe126deB0aFE94? +// address internal constant L1_OUTBOX = 0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840; +// +// IERC20 internal constant L1_LINK = IERC20(0x514910771AF9Ca656af840dff83E8264EcF986CA); +// IWrappedNative internal constant L1_WRAPPED_NATIVE = IWrappedNative(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); +// +// address internal constant L2_GATEWAY_ROUTER = 0x5288c571Fd7aD117beA99bF60FE0846C4E84F933; +// address internal constant L2_ETH_WITHDRAWAL_PRECOMPILE = 0x0000000000000000000000000000000000000064; +// +// IERC20 internal constant L2_LINK = IERC20(0xf97f4df75117a78c1A5a0DBb814Af92458539FB4); +// IWrappedNative internal constant L2_WRAPPED_NATIVE = IWrappedNative(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); +// +// ArbitrumL1BridgeAdapter internal s_l1BridgeAdapter; +// +// uint256 internal constant TOKEN_BALANCE = 10e18; +// address internal constant OWNER = address(0xdead); +// +// function setUp() public { +// vm.startPrank(OWNER); +// +// mainnetFork = vm.createFork(MAINNET_RPC_URL); +// vm.selectFork(mainnetFork); +// +// s_l1BridgeAdapter = new ArbitrumL1BridgeAdapter( +// IL1GatewayRouter(L1_GATEWAY_ROUTER), +// IOutbox(L1_OUTBOX), +// L1_ERC20_GATEWAY +// ); +// +// deal(address(L1_LINK), OWNER, TOKEN_BALANCE); +// deal(address(L1_WRAPPED_NATIVE), OWNER, TOKEN_BALANCE); +// +// vm.label(OWNER, "Owner"); +// vm.label(L1_GATEWAY_ROUTER, "L1GatewayRouter"); +// vm.label(L1_ERC20_GATEWAY, "L1 ERC20 Gateway"); +// } +//} +// +//contract ArbitrumL1BridgeAdapter_sendERC20 is ArbitrumL1BridgeAdapterSetup { +// event TransferRouted(address indexed token, address indexed _userFrom, address indexed _userTo, address gateway); +// +// function test_sendERC20Success() public { +// L1_LINK.approve(address(s_l1BridgeAdapter), TOKEN_BALANCE); +// +// vm.expectEmit(); +// emit TransferRouted(address(L1_LINK), address(s_l1BridgeAdapter), OWNER, L1_ERC20_GATEWAY); +// +// uint256 expectedCost = s_l1BridgeAdapter.MAX_GAS() * +// s_l1BridgeAdapter.GAS_PRICE_BID() + +// s_l1BridgeAdapter.MAX_SUBMISSION_COST(); +// +// s_l1BridgeAdapter.sendERC20{value: expectedCost}(address(L1_LINK), OWNER, OWNER, TOKEN_BALANCE); +// } +// +// function test_BridgeFeeTooLowReverts() public { +// L1_LINK.approve(address(s_l1BridgeAdapter), TOKEN_BALANCE); +// uint256 expectedCost = s_l1BridgeAdapter.MAX_GAS() * +// s_l1BridgeAdapter.GAS_PRICE_BID() + +// s_l1BridgeAdapter.MAX_SUBMISSION_COST(); +// +// vm.expectRevert( +// abi.encodeWithSelector(ArbitrumL1BridgeAdapter.InsufficientEthValue.selector, expectedCost, expectedCost - 1) +// ); +// +// s_l1BridgeAdapter.sendERC20{value: expectedCost - 1}(address(L1_LINK), OWNER, OWNER, TOKEN_BALANCE); +// } +// +// function test_noApprovalReverts() public { +// uint256 expectedCost = s_l1BridgeAdapter.MAX_GAS() * +// s_l1BridgeAdapter.GAS_PRICE_BID() + +// s_l1BridgeAdapter.MAX_SUBMISSION_COST(); +// +// vm.expectRevert("SafeERC20: low-level call failed"); +// +// s_l1BridgeAdapter.sendERC20{value: expectedCost}(address(L1_LINK), OWNER, OWNER, TOKEN_BALANCE); +// } +//} diff --git a/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/ArbitrumL2BridgeAdapter.t.sol b/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/ArbitrumL2BridgeAdapter.t.sol new file mode 100644 index 00000000000..e34ff0480c0 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/ArbitrumL2BridgeAdapter.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {IWrappedNative} from "../../../ccip/interfaces/IWrappedNative.sol"; + +import {ArbitrumL2BridgeAdapter, IL2GatewayRouter} from "../../bridge-adapters/ArbitrumL2BridgeAdapter.sol"; +import "forge-std/Test.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +//contract ArbitrumL2BridgeAdapterSetup is Test { +// uint256 internal arbitrumFork; +// +// string internal constant ARBITRUM_RPC_URL = ""; +// +// address internal constant L2_GATEWAY_ROUTER = 0x5288c571Fd7aD117beA99bF60FE0846C4E84F933; +// address internal constant L2_ETH_WITHDRAWAL_PRECOMPILE = 0x0000000000000000000000000000000000000064; +// +// IERC20 internal constant L1_LINK = IERC20(0x514910771AF9Ca656af840dff83E8264EcF986CA); +// IERC20 internal constant L2_LINK = IERC20(0xf97f4df75117a78c1A5a0DBb814Af92458539FB4); +// IWrappedNative internal constant L2_WRAPPED_NATIVE = IWrappedNative(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); +// +// uint256 internal constant TOKEN_BALANCE = 10e18; +// address internal constant OWNER = address(0xdead); +// +// ArbitrumL2BridgeAdapter internal s_l2BridgeAdapter; +// +// function setUp() public { +// vm.startPrank(OWNER); +// +// arbitrumFork = vm.createFork(ARBITRUM_RPC_URL); +// +// vm.selectFork(arbitrumFork); +// s_l2BridgeAdapter = new ArbitrumL2BridgeAdapter(IL2GatewayRouter(L2_GATEWAY_ROUTER)); +// deal(address(L2_LINK), OWNER, TOKEN_BALANCE); +// deal(address(L2_WRAPPED_NATIVE), OWNER, TOKEN_BALANCE); +// +// vm.label(OWNER, "Owner"); +// vm.label(L2_GATEWAY_ROUTER, "L2GatewayRouterProxy"); +// vm.label(0xe80eb0238029333e368e0bDDB7acDf1b9cb28278, "L2GatewayRouter"); +// vm.label(L2_ETH_WITHDRAWAL_PRECOMPILE, "Precompile: ArbSys"); +// } +//} +// +//contract ArbitrumL2BridgeAdapter_sendERC20 is ArbitrumL2BridgeAdapterSetup { +// function test_sendERC20Success() public { +// L2_LINK.approve(address(s_l2BridgeAdapter), TOKEN_BALANCE); +// +// s_l2BridgeAdapter.sendERC20(address(L1_LINK), address(L2_LINK), OWNER, TOKEN_BALANCE); +// } +//} diff --git a/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/OptimismL1BridgeAdapter.t.sol b/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/OptimismL1BridgeAdapter.t.sol new file mode 100644 index 00000000000..cface1d5067 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/bridge-adapters/OptimismL1BridgeAdapter.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import "forge-std/Test.sol"; + +import {IWrappedNative} from "../../../ccip/interfaces/IWrappedNative.sol"; +import {WETH9} from "../../../ccip/test/WETH9.sol"; +import {OptimismL1BridgeAdapter} from "../../bridge-adapters/OptimismL1BridgeAdapter.sol"; +import {Types} from "../../interfaces/optimism/Types.sol"; +import {IOptimismPortal} from "../../interfaces/optimism/IOptimismPortal.sol"; + +import {IL1StandardBridge} from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract OptimismL1BridgeAdapterSetup is Test { + // addresses below are fake + address internal constant L1_STANDARD_BRIDGE = address(1234); + address internal constant OP_PORTAL = address(4567); + address internal constant OWNER = address(0xdead); + + OptimismL1BridgeAdapter internal s_adapter; + + function setUp() public { + vm.startPrank(OWNER); + + // deploy wrapped native + WETH9 weth = new WETH9(); + + // deploy bridge adapter + s_adapter = new OptimismL1BridgeAdapter( + IL1StandardBridge(L1_STANDARD_BRIDGE), + IWrappedNative(address(weth)), + IOptimismPortal(OP_PORTAL) + ); + } +} + +contract OptimismL1BridgeAdapter_finalizeWithdrawERC20 is OptimismL1BridgeAdapterSetup { + function testfinalizeWithdrawERC20proveWithdrawalSuccess() public { + // prepare payload + OptimismL1BridgeAdapter.OptimismProveWithdrawalPayload memory provePayload = OptimismL1BridgeAdapter + .OptimismProveWithdrawalPayload({ + withdrawalTransaction: Types.WithdrawalTransaction({ + nonce: 1, + sender: address(0xdead), + target: address(0xbeef), + value: 1234, + gasLimit: 4567, + data: hex"deadbeef" + }), + l2OutputIndex: 1234, + outputRootProof: Types.OutputRootProof({ + version: bytes32(0), + stateRoot: bytes32(uint256(500)), + messagePasserStorageRoot: bytes32(uint256(600)), + latestBlockhash: bytes32(uint256(700)) + }), + withdrawalProof: new bytes[](0) + }); + OptimismL1BridgeAdapter.FinalizeWithdrawERC20Payload memory payload; + payload.action = OptimismL1BridgeAdapter.FinalizationAction.ProveWithdrawal; + payload.data = abi.encode(provePayload); + + bytes memory encodedPayload = abi.encode(payload); + + // mock out call to optimism portal + vm.mockCall( + OP_PORTAL, + abi.encodeWithSelector( + IOptimismPortal.proveWithdrawalTransaction.selector, + provePayload.withdrawalTransaction, + provePayload.l2OutputIndex, + provePayload.outputRootProof, + provePayload.withdrawalProof + ), + "" + ); + + // call finalizeWithdrawERC20 + s_adapter.finalizeWithdrawERC20(address(0), address(0), encodedPayload); + } + + function testfinalizeWithdrawERC20FinalizeSuccess() public { + // prepare payload + OptimismL1BridgeAdapter.OptimismFinalizationPayload memory finalizePayload = OptimismL1BridgeAdapter + .OptimismFinalizationPayload({ + withdrawalTransaction: Types.WithdrawalTransaction({ + nonce: 1, + sender: address(0xdead), + target: address(0xbeef), + value: 1234, + gasLimit: 4567, + data: hex"deadbeef" + }) + }); + OptimismL1BridgeAdapter.FinalizeWithdrawERC20Payload memory payload; + payload.action = OptimismL1BridgeAdapter.FinalizationAction.FinalizeWithdrawal; + payload.data = abi.encode(finalizePayload); + + bytes memory encodedPayload = abi.encode(payload); + + // mock out call to optimism portal + vm.mockCall( + OP_PORTAL, + abi.encodeWithSelector( + IOptimismPortal.finalizeWithdrawalTransaction.selector, + finalizePayload.withdrawalTransaction + ), + "" + ); + + // call finalizeWithdrawERC20 + s_adapter.finalizeWithdrawERC20(address(0), address(0), encodedPayload); + } + + function testFinalizeWithdrawERC20Reverts() public { + // case 1: badly encoded payload + bytes memory payload = abi.encode(1, 2, 3); + vm.expectRevert(); + s_adapter.finalizeWithdrawERC20(address(0), address(0), payload); + + // case 2: invalid action + // can't prepare the payload in solidity + payload = hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000"; + vm.expectRevert(); + s_adapter.finalizeWithdrawERC20(address(0), address(0), payload); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/test/helpers/LiquidityManagerHelper.sol b/contracts/src/v0.8/liquiditymanager/test/helpers/LiquidityManagerHelper.sol new file mode 100644 index 00000000000..9b4654a07ff --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/helpers/LiquidityManagerHelper.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ILiquidityContainer} from "../../interfaces/ILiquidityContainer.sol"; + +import {LiquidityManager} from "../../LiquidityManager.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; + +contract LiquidityManagerHelper is LiquidityManager { + constructor( + IERC20 token, + uint64 localChainSelector, + ILiquidityContainer localLiquidityContainer, + uint256 targetTokens, + address finance + ) LiquidityManager(token, localChainSelector, localLiquidityContainer, targetTokens, finance) {} + + function report(bytes calldata rep, uint64 ocrSeqNum) external { + _report(rep, ocrSeqNum); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/test/helpers/OCR3Helper.sol b/contracts/src/v0.8/liquiditymanager/test/helpers/OCR3Helper.sol new file mode 100644 index 00000000000..b2cd2ef3712 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/helpers/OCR3Helper.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {OCR3Base} from "../../ocr/OCR3Base.sol"; + +contract OCR3Helper is OCR3Base { + function configDigestFromConfigData( + uint256 chainSelector, + address contractAddress, + uint64 configCount, + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) public pure returns (bytes32) { + return + _configDigestFromConfigData( + chainSelector, + contractAddress, + configCount, + signers, + transmitters, + f, + onchainConfig, + offchainConfigVersion, + offchainConfig + ); + } + + function _report(bytes calldata report, uint64 sequenceNumber) internal override {} + + function typeAndVersion() public pure override returns (string memory) { + return "OCR3BaseHelper 1.0.0"; + } + + function setLatestSeqNum(uint64 newSeqNum) external { + s_latestSequenceNumber = newSeqNum; + } +} diff --git a/contracts/src/v0.8/liquiditymanager/test/helpers/ReportEncoder.sol b/contracts/src/v0.8/liquiditymanager/test/helpers/ReportEncoder.sol new file mode 100644 index 00000000000..ff5e21f2e14 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/helpers/ReportEncoder.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ILiquidityManager} from "../../interfaces/ILiquidityManager.sol"; + +/// @dev this is needed to generate the types to help encode the report offchain +abstract contract ReportEncoder is ILiquidityManager { + /// @dev exposed so that we can encode the report for OCR offchain + function exposeForEncoding(ILiquidityManager.LiquidityInstructions memory instructions) public pure {} +} diff --git a/contracts/src/v0.8/liquiditymanager/test/mocks/MockBridgeAdapter.sol b/contracts/src/v0.8/liquiditymanager/test/mocks/MockBridgeAdapter.sol new file mode 100644 index 00000000000..f51c60fcf3d --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/mocks/MockBridgeAdapter.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: BUSL-1.1 +// solhint-disable one-contract-per-file +pragma solidity ^0.8.0; + +import {IBridgeAdapter} from "../../interfaces/IBridge.sol"; +import {ILiquidityContainer} from "../../interfaces/ILiquidityContainer.sol"; +import {IWrappedNative} from "../../../ccip/interfaces/IWrappedNative.sol"; + +import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice Mock multiple-stage finalization bridge adapter implementation. +/// @dev Funds are only made available after both the prove and finalization steps are completed. +/// Sends the L1 tokens from the msg sender to address(this). +contract MockL1BridgeAdapter is IBridgeAdapter, ILiquidityContainer { + using SafeERC20 for IERC20; + + error InsufficientLiquidity(); + error NonceAlreadyUsed(uint256 nonce); + error InvalidFinalizationAction(); + error NonceNotProven(uint256 nonce); + error NativeSendFailed(); + + /// @notice Payload to "prove" the withdrawal. + /// @dev This is just a mock setup, there's no real proving. This is so that + /// we can test the multi-step finalization code path. + /// @param nonce the nonce emitted on the remote chain. + struct ProvePayload { + uint256 nonce; + } + + /// @notice Payload to "finalize" the withdrawal. + /// @dev This is just a mock setup, there's no real finalization. This is so that + /// we can test the multi-step finalization code path. + /// @param nonce the nonce emitted on the remote chain. + struct FinalizePayload { + uint256 nonce; + uint256 amount; + } + + /// @notice The finalization action to take. + /// @dev This emulates Optimism's two-step withdrawal process. + enum FinalizationAction { + ProveWithdrawal, + FinalizeWithdrawal + } + + /// @notice The payload to use for the bridgeSpecificPayload in the finalizeWithdrawERC20 function. + struct Payload { + FinalizationAction action; + bytes data; + } + + IERC20 internal immutable i_token; + uint256 internal s_nonce = 1; + mapping(uint256 => bool) internal s_nonceProven; + mapping(uint256 => bool) internal s_nonceFinalized; + + /// @dev For test cases where we want to send pure native upon finalizeWithdrawERC20 being called. + /// This is to emulate the behavior of bridges that do not bridge wrapped native. + bool internal immutable i_holdNative; + + constructor(IERC20 token, bool holdNative) { + i_token = token; + i_holdNative = holdNative; + } + + /// @dev The receive function is needed for IWrappedNative.withdraw() to work. + receive() external payable {} + + /// @notice Simply transferFrom msg.sender the tokens that are to be bridged to address(this). + function sendERC20( + address localToken, + address /* remoteToken */, + address /* remoteReceiver */, + uint256 amount, + bytes calldata /* bridgeSpecificPayload */ + ) external payable override returns (bytes memory) { + IERC20(localToken).transferFrom(msg.sender, address(this), amount); + + // If the flag to hold native is set we assume that i_token points to a WETH contract + // and withdraw native. + // This way we can transfer the raw native back to the sender upon finalization. + if (i_holdNative) { + IWrappedNative(address(i_token)).withdraw(amount); + } + + bytes memory encodedNonce = abi.encode(s_nonce++); + return encodedNonce; + } + + function getBridgeFeeInNative() external pure returns (uint256) { + return 0; + } + + function provideLiquidity(uint256 amount) external { + i_token.safeTransferFrom(msg.sender, address(this), amount); + emit LiquidityAdded(msg.sender, amount); + } + + function withdrawLiquidity(uint256 amount) external { + if (i_token.balanceOf(address(this)) < amount) revert InsufficientLiquidity(); + i_token.safeTransfer(msg.sender, amount); + emit LiquidityRemoved(msg.sender, amount); + } + + /// @dev for easy encoding offchain + function encodeProvePayload(ProvePayload memory payload) external pure {} + + function encodeFinalizePayload(FinalizePayload memory payload) external pure {} + + function encodePayload(Payload memory payload) external pure {} + + /// @dev Test setup is trusted, so just transfer the tokens to the localReceiver, + /// which should be the local rebalancer. Infer the amount from the bridgeSpecificPayload. + /// Note that this means that this bridge adapter will need to have some tokens, + /// however this is ok in a test environment since we will have infinite tokens. + /// @param localReceiver the address to transfer the tokens to. + /// @param bridgeSpecificPayload the payload to use for the finalization or proving. + /// @return true if the transfer was successful, revert otherwise. + function finalizeWithdrawERC20( + address /* remoteSender */, + address localReceiver, + bytes calldata bridgeSpecificPayload + ) external override returns (bool) { + Payload memory payload = abi.decode(bridgeSpecificPayload, (Payload)); + if (payload.action == FinalizationAction.ProveWithdrawal) { + return _proveWithdrawal(payload); + } else if (payload.action == FinalizationAction.FinalizeWithdrawal) { + return _finalizeWithdrawal(payload, localReceiver); + } + revert InvalidFinalizationAction(); + } + + function _proveWithdrawal(Payload memory payload) internal returns (bool) { + ProvePayload memory provePayload = abi.decode(payload.data, (ProvePayload)); + if (s_nonceProven[provePayload.nonce]) revert NonceAlreadyUsed(provePayload.nonce); + s_nonceProven[provePayload.nonce] = true; + return false; + } + + function _finalizeWithdrawal(Payload memory payload, address localReceiver) internal returns (bool) { + FinalizePayload memory finalizePayload = abi.decode(payload.data, (FinalizePayload)); + if (!s_nonceProven[finalizePayload.nonce]) revert NonceNotProven(finalizePayload.nonce); + if (s_nonceFinalized[finalizePayload.nonce]) revert NonceAlreadyUsed(finalizePayload.nonce); + s_nonceFinalized[finalizePayload.nonce] = true; + // re-entrancy prevented by nonce checks above. + _transferTokens(finalizePayload.amount, localReceiver); + return true; + } + + function _transferTokens(uint256 amount, address localReceiver) internal { + if (i_holdNative) { + (bool success, ) = payable(localReceiver).call{value: amount}(""); + if (!success) { + revert NativeSendFailed(); + } + } else { + i_token.safeTransfer(localReceiver, amount); + } + } +} + +/// @notice Mock L2 Bridge adapter +/// @dev Sends the L2 tokens from the msg sender to address(this) +contract MockL2BridgeAdapter is IBridgeAdapter { + /// @notice Simply transferFrom msg.sender the tokens that are to be bridged. + function sendERC20( + address localToken, + address /* remoteToken */, + address /* recipient */, + uint256 amount, + bytes calldata /* bridgeSpecificPayload */ + ) external payable override returns (bytes memory) { + IERC20(localToken).transferFrom(msg.sender, address(this), amount); + return ""; + } + + function getBridgeFeeInNative() external pure returns (uint256) { + return 0; + } + + // No-op + function finalizeWithdrawERC20( + address /* remoteSender */, + address /* localReceiver */, + bytes calldata /* bridgeSpecificData */ + ) external pure override returns (bool) { + return true; + } +} diff --git a/contracts/src/v0.8/liquiditymanager/test/mocks/NoOpOCR3.sol b/contracts/src/v0.8/liquiditymanager/test/mocks/NoOpOCR3.sol new file mode 100644 index 00000000000..5e771f0ccd6 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/mocks/NoOpOCR3.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {OCR3Base} from "../../ocr/OCR3Base.sol"; + +// NoOpOCR3 is a mock implementation of the OCR3Base contract that does nothing +// This is so that we can generate gethwrappers for the contract and use the OCR3 ABI in +// Go code. +contract NoOpOCR3 is OCR3Base { + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables + string public constant override typeAndVersion = "NoOpOCR3 1.0.0"; + + constructor() OCR3Base() {} + + function _report(bytes calldata, uint64) internal override { + // do nothing + } +} diff --git a/contracts/src/v0.8/liquiditymanager/test/ocr/OCR3Base.t.sol b/contracts/src/v0.8/liquiditymanager/test/ocr/OCR3Base.t.sol new file mode 100644 index 00000000000..840e90fb876 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/ocr/OCR3Base.t.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.24; + +import {OCR3Setup} from "./OCR3Setup.t.sol"; +import {OCR3Base} from "../../ocr/OCR3Base.sol"; +import {OCR3Helper} from "../helpers/OCR3Helper.sol"; + +contract OCR3BaseSetup is OCR3Setup { + event ConfigSet( + uint32 previousConfigBlockNumber, + bytes32 configDigest, + uint64 configCount, + address[] signers, + address[] transmitters, + uint8 f, + bytes onchainConfig, + uint64 offchainConfigVersion, + bytes offchainConfig + ); + + OCR3Helper internal s_OCR3Base; + + bytes32[] internal s_rs; + bytes32[] internal s_ss; + bytes32 internal s_rawVs; + + uint40 internal s_latestEpochAndRound; + + function setUp() public virtual override { + OCR3Setup.setUp(); + s_OCR3Base = new OCR3Helper(); + + bytes32 testReportDigest = getTestReportDigest(); + + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](2); + uint8[] memory vs = new uint8[](2); + + // Calculate signatures + (vs[0], rs[0], ss[0]) = vm.sign(PRIVATE0, testReportDigest); + (vs[1], rs[1], ss[1]) = vm.sign(PRIVATE1, testReportDigest); + + s_rs = rs; + s_ss = ss; + s_rawVs = bytes32(bytes1(vs[0] - 27)) | (bytes32(bytes1(vs[1] - 27)) >> 8); + } + + function getBasicConfigDigest(uint8 f, uint64 currentConfigCount) internal view returns (bytes32) { + bytes memory configBytes = abi.encode(""); + return + s_OCR3Base.configDigestFromConfigData( + block.chainid, + address(s_OCR3Base), + currentConfigCount + 1, + s_valid_signers, + s_valid_transmitters, + f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + } + + function getTestReportDigest() internal view returns (bytes32) { + bytes32 configDigest = getBasicConfigDigest(s_f, 0); + bytes32[3] memory reportContext = [configDigest, configDigest, configDigest]; + return keccak256(abi.encodePacked(keccak256(REPORT), reportContext)); + } + + function getBasicConfigDigest( + address contractAddress, + uint8 f, + uint64 currentConfigCount, + bytes memory onchainConfig + ) internal view returns (bytes32) { + return + s_OCR3Base.configDigestFromConfigData( + block.chainid, + contractAddress, + currentConfigCount + 1, + s_valid_signers, + s_valid_transmitters, + f, + onchainConfig, + s_offchainConfigVersion, + abi.encode("") + ); + } +} + +contract OCR3Base_transmit is OCR3BaseSetup { + bytes32 internal s_configDigest; + + function setUp() public virtual override { + OCR3BaseSetup.setUp(); + bytes memory configBytes = abi.encode(""); + + s_configDigest = getBasicConfigDigest(s_f, 0); + s_OCR3Base.setOCR3Config( + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + } + + function testTransmit2SignersSuccess_gas() public { + vm.pauseGasMetering(); + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + vm.startPrank(s_valid_transmitters[0]); + vm.resumeGasMetering(); + s_OCR3Base.transmit(reportContext, REPORT, s_rs, s_ss, s_rawVs); + } + + // Reverts + + function testNonIncreasingSequenceNumberReverts() public { + bytes32[3] memory reportContext = [s_configDigest, bytes32(uint256(0)) /* sequence number */, s_configDigest]; + + vm.expectRevert(abi.encodeWithSelector(OCR3Base.NonIncreasingSequenceNumber.selector, 0, 0)); + s_OCR3Base.transmit(reportContext, REPORT, s_rs, s_ss, s_rawVs); + } + + function testForkedChainReverts() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + uint256 chain1 = block.chainid; + uint256 chain2 = chain1 + 1; + vm.chainId(chain2); + vm.expectRevert(abi.encodeWithSelector(OCR3Base.ForkedChain.selector, chain1, chain2)); + vm.startPrank(s_valid_transmitters[0]); + s_OCR3Base.transmit(reportContext, REPORT, s_rs, s_ss, s_rawVs); + } + + function testWrongNumberOfSignaturesReverts() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + vm.expectRevert(OCR3Base.WrongNumberOfSignatures.selector); + s_OCR3Base.transmit(reportContext, REPORT, new bytes32[](0), new bytes32[](0), s_rawVs); + } + + function testConfigDigestMismatchReverts() public { + bytes32 configDigest; + bytes32[3] memory reportContext = [configDigest, bytes32(uint256(1)) /* sequence number */, configDigest]; + + vm.expectRevert(abi.encodeWithSelector(OCR3Base.ConfigDigestMismatch.selector, s_configDigest, configDigest)); + s_OCR3Base.transmit(reportContext, REPORT, new bytes32[](0), new bytes32[](0), s_rawVs); + } + + function testSignatureOutOfRegistrationReverts() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](1); + + vm.expectRevert(OCR3Base.SignaturesOutOfRegistration.selector); + s_OCR3Base.transmit(reportContext, REPORT, rs, ss, s_rawVs); + } + + function testUnAuthorizedTransmitterReverts() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + bytes32[] memory rs = new bytes32[](2); + bytes32[] memory ss = new bytes32[](2); + + vm.expectRevert(OCR3Base.UnauthorizedTransmitter.selector); + s_OCR3Base.transmit(reportContext, REPORT, rs, ss, s_rawVs); + } + + function testNonUniqueSignatureReverts() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + bytes32[] memory rs = s_rs; + bytes32[] memory ss = s_ss; + + rs[1] = rs[0]; + ss[1] = ss[0]; + // Need to reset the rawVs to be valid + bytes32 rawVs = bytes32(bytes1(uint8(28) - 27)) | (bytes32(bytes1(uint8(28) - 27)) >> 8); + + vm.startPrank(s_valid_transmitters[0]); + vm.expectRevert(OCR3Base.NonUniqueSignatures.selector); + s_OCR3Base.transmit(reportContext, REPORT, rs, ss, rawVs); + } + + function testUnauthorizedSignerReverts() public { + bytes32[3] memory reportContext = [s_configDigest, s_configDigest, s_configDigest]; + bytes32[] memory rs = new bytes32[](2); + rs[0] = s_configDigest; + bytes32[] memory ss = rs; + + vm.startPrank(s_valid_transmitters[0]); + vm.expectRevert(OCR3Base.UnauthorizedSigner.selector); + s_OCR3Base.transmit(reportContext, REPORT, rs, ss, s_rawVs); + } +} + +contract OCR3Base_setOCR3Config is OCR3BaseSetup { + function testSetConfigSuccess() public { + vm.pauseGasMetering(); + bytes memory configBytes = abi.encode(""); + uint32 configCount = 0; + + bytes32 configDigest = getBasicConfigDigest(s_f, configCount++); + + address[] memory transmitters = s_OCR3Base.getTransmitters(); + assertEq(0, transmitters.length); + + s_OCR3Base.setLatestSeqNum(3); + uint64 seqNum = s_OCR3Base.latestSequenceNumber(); + assertEq(seqNum, 3); + + vm.expectEmit(); + emit ConfigSet( + 0, + configDigest, + configCount, + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + + s_OCR3Base.setOCR3Config( + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + + transmitters = s_OCR3Base.getTransmitters(); + assertEq(s_valid_transmitters, transmitters); + + configDigest = getBasicConfigDigest(s_f, configCount++); + + seqNum = s_OCR3Base.latestSequenceNumber(); + assertEq(seqNum, 0); + + vm.expectEmit(); + emit ConfigSet( + uint32(block.number), + configDigest, + configCount, + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + vm.resumeGasMetering(); + s_OCR3Base.setOCR3Config( + s_valid_signers, + s_valid_transmitters, + s_f, + configBytes, + s_offchainConfigVersion, + configBytes + ); + } + + // Reverts + function testRepeatAddressReverts() public { + address[] memory signers = new address[](10); + signers[0] = address(1245678); + address[] memory transmitters = new address[](10); + transmitters[0] = signers[0]; + + vm.expectRevert(abi.encodeWithSelector(OCR3Base.InvalidConfig.selector, "repeated transmitter address")); + s_OCR3Base.setOCR3Config(signers, transmitters, 2, abi.encode(""), 100, abi.encode("")); + } + + function testSignerCannotBeZeroAddressReverts() public { + uint256 f = 1; + address[] memory signers = new address[](3 * f + 1); + address[] memory transmitters = new address[](3 * f + 1); + for (uint160 i = 0; i < 3 * f + 1; ++i) { + signers[i] = address(i + 1); + transmitters[i] = address(i + 1000); + } + + signers[0] = address(0); + + vm.expectRevert(OCR3Base.OracleCannotBeZeroAddress.selector); + s_OCR3Base.setOCR3Config(signers, transmitters, uint8(f), abi.encode(""), 100, abi.encode("")); + } + + function testTransmitterCannotBeZeroAddressReverts() public { + uint256 f = 1; + address[] memory signers = new address[](3 * f + 1); + address[] memory transmitters = new address[](3 * f + 1); + for (uint160 i = 0; i < 3 * f + 1; ++i) { + signers[i] = address(i + 1); + transmitters[i] = address(i + 1000); + } + + transmitters[0] = address(0); + + vm.expectRevert(OCR3Base.OracleCannotBeZeroAddress.selector); + s_OCR3Base.setOCR3Config(signers, transmitters, uint8(f), abi.encode(""), 100, abi.encode("")); + } + + function testOracleOutOfRegisterReverts() public { + address[] memory signers = new address[](10); + address[] memory transmitters = new address[](0); + + vm.expectRevert(abi.encodeWithSelector(OCR3Base.InvalidConfig.selector, "oracle addresses out of registration")); + s_OCR3Base.setOCR3Config(signers, transmitters, 2, abi.encode(""), 100, abi.encode("")); + } + + function testFTooHighReverts() public { + address[] memory signers = new address[](0); + uint8 f = 1; + + vm.expectRevert(abi.encodeWithSelector(OCR3Base.InvalidConfig.selector, "faulty-oracle f too high")); + s_OCR3Base.setOCR3Config(signers, new address[](0), f, abi.encode(""), 100, abi.encode("")); + } + + function testFMustBePositiveReverts() public { + uint8 f = 0; + + vm.expectRevert(abi.encodeWithSelector(OCR3Base.InvalidConfig.selector, "f must be positive")); + s_OCR3Base.setOCR3Config(new address[](0), new address[](0), f, abi.encode(""), 100, abi.encode("")); + } + + function testTooManySignersReverts() public { + address[] memory signers = new address[](32); + + vm.expectRevert(abi.encodeWithSelector(OCR3Base.InvalidConfig.selector, "too many signers")); + s_OCR3Base.setOCR3Config(signers, new address[](0), 0, abi.encode(""), 100, abi.encode("")); + } +} diff --git a/contracts/src/v0.8/liquiditymanager/test/ocr/OCR3Setup.t.sol b/contracts/src/v0.8/liquiditymanager/test/ocr/OCR3Setup.t.sol new file mode 100644 index 00000000000..ee60c58dcc6 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/test/ocr/OCR3Setup.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {LiquidityManagerBaseTest} from "../LiquidityManagerBaseTest.t.sol"; + +contract OCR3Setup is LiquidityManagerBaseTest { + // Signer private keys used for these test + uint256 internal constant PRIVATE0 = 0x7b2e97fe057e6de99d6872a2ef2abf52c9b4469bc848c2465ac3fcd8d336e81d; + uint256 internal constant PRIVATE1 = 0xab56160806b05ef1796789248e1d7f34a6465c5280899159d645218cd216cee6; + uint256 internal constant PRIVATE2 = 0x6ec7caa8406a49b76736602810e0a2871959fbbb675e23a8590839e4717f1f7f; + uint256 internal constant PRIVATE3 = 0x80f14b11da94ae7f29d9a7713ea13dc838e31960a5c0f2baf45ed458947b730a; + + address[] internal s_valid_signers; + address[] internal s_valid_transmitters; + + uint64 internal constant s_offchainConfigVersion = 3; + uint8 internal constant s_f = 1; + bytes internal constant REPORT = abi.encode("testReport"); + + function setUp() public virtual override { + LiquidityManagerBaseTest.setUp(); + + s_valid_transmitters = new address[](4); + for (uint160 i = 0; i < 4; ++i) { + s_valid_transmitters[i] = address(4 + i); + } + + s_valid_signers = new address[](4); + s_valid_signers[0] = vm.addr(PRIVATE0); //0xc110458BE52CaA6bB68E66969C3218A4D9Db0211 + s_valid_signers[1] = vm.addr(PRIVATE1); //0xc110a19c08f1da7F5FfB281dc93630923F8E3719 + s_valid_signers[2] = vm.addr(PRIVATE2); //0xc110fdF6e8fD679C7Cc11602d1cd829211A18e9b + s_valid_signers[3] = vm.addr(PRIVATE3); //0xc11028017c9b445B6bF8aE7da951B5cC28B326C0 + } +} diff --git a/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol b/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol new file mode 100644 index 00000000000..4daefc5d4f2 --- /dev/null +++ b/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/ERC165Checker.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.2) (utils/introspection/ERC165Checker.sol) + +pragma solidity ^0.8.0; + +import "./IERC165.sol"; + +/** + * @dev Library used to query support of an interface declared via {IERC165}. + * + * Note that these functions return the actual result of the query: they do not + * `revert` if an interface is not supported. It is up to the caller to decide + * what to do in these cases. + */ +library ERC165Checker { + // As per the EIP-165 spec, no interface should ever match 0xffffffff + bytes4 private constant _INTERFACE_ID_INVALID = 0xffffffff; + + /** + * @dev Returns true if `account` supports the {IERC165} interface. + */ + function supportsERC165(address account) internal view returns (bool) { + // Any contract that implements ERC165 must explicitly indicate support of + // InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid + return + supportsERC165InterfaceUnchecked(account, type(IERC165).interfaceId) && + !supportsERC165InterfaceUnchecked(account, _INTERFACE_ID_INVALID); + } + + /** + * @dev Returns true if `account` supports the interface defined by + * `interfaceId`. Support for {IERC165} itself is queried automatically. + * + * See {IERC165-supportsInterface}. + */ + function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) { + // query support of both ERC165 as per the spec and support of _interfaceId + return supportsERC165(account) && supportsERC165InterfaceUnchecked(account, interfaceId); + } + + /** + * @dev Returns a boolean array where each value corresponds to the + * interfaces passed in and whether they're supported or not. This allows + * you to batch check interfaces for a contract where your expectation + * is that some interfaces may not be supported. + * + * See {IERC165-supportsInterface}. + * + * _Available since v3.4._ + */ + function getSupportedInterfaces(address account, bytes4[] memory interfaceIds) + internal + view + returns (bool[] memory) + { + // an array of booleans corresponding to interfaceIds and whether they're supported or not + bool[] memory interfaceIdsSupported = new bool[](interfaceIds.length); + + // query support of ERC165 itself + if (supportsERC165(account)) { + // query support of each interface in interfaceIds + for (uint256 i = 0; i < interfaceIds.length; i++) { + interfaceIdsSupported[i] = supportsERC165InterfaceUnchecked(account, interfaceIds[i]); + } + } + + return interfaceIdsSupported; + } + + /** + * @dev Returns true if `account` supports all the interfaces defined in + * `interfaceIds`. Support for {IERC165} itself is queried automatically. + * + * Batch-querying can lead to gas savings by skipping repeated checks for + * {IERC165} support. + * + * See {IERC165-supportsInterface}. + */ + function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) internal view returns (bool) { + // query support of ERC165 itself + if (!supportsERC165(account)) { + return false; + } + + // query support of each interface in interfaceIds + for (uint256 i = 0; i < interfaceIds.length; i++) { + if (!supportsERC165InterfaceUnchecked(account, interfaceIds[i])) { + return false; + } + } + + // all interfaces supported + return true; + } + + /** + * @notice Query if a contract implements an interface, does not check ERC165 support + * @param account The address of the contract to query for support of an interface + * @param interfaceId The interface identifier, as specified in ERC-165 + * @return true if the contract at account indicates support of the interface with + * identifier interfaceId, false otherwise + * @dev Assumes that account contains a contract that supports ERC165, otherwise + * the behavior of this method is undefined. This precondition can be checked + * with {supportsERC165}. + * + * Some precompiled contracts will falsely indicate support for a given interface, so caution + * should be exercised when using this function. + * + * Interface identification is specified in ERC-165. + */ + function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) { + // prepare call + bytes memory encodedParams = abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId); + + // perform static call + bool success; + uint256 returnSize; + uint256 returnValue; + assembly { + success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20) + returnSize := returndatasize() + returnValue := mload(0x00) + } + + return success && returnSize >= 0x20 && returnValue > 0; + } +}