diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot index 6ee16f4622..a5fbdb2cef 100644 --- a/contracts/gas-snapshots/ccip.gas-snapshot +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -34,16 +34,16 @@ BurnWithFromMintTokenPool_lockOrBurn:test_ChainNotAllowed_Revert() (gas: 28675) BurnWithFromMintTokenPool_lockOrBurn:test_PoolBurnRevertNotHealthy_Revert() (gas: 55158) BurnWithFromMintTokenPool_lockOrBurn:test_PoolBurn_Success() (gas: 243568) BurnWithFromMintTokenPool_lockOrBurn:test_Setup_Success() (gas: 24260) -CCIPClientTest:test_HappyPath_Success() (gas: 192504) +CCIPClientTest:test_HappyPath_Success() (gas: 192126) CCIPClientTest:test_ccipSend_withNonNativeFeetoken_andDestTokens_Success() (gas: 326927) CCIPClientTest:test_ccipSend_withNonNativeFeetoken_andNoDestTokens_Success() (gas: 219968) CCIPClientTest:test_ccipSend_with_NativeFeeToken_andDestTokens_Success() (gas: 376836) CCIPClientTest:test_modifyFeeToken_Success() (gas: 74452) -CCIPClientWithACKTest:test_ccipReceiveAndSendAck_Success() (gas: 331795) -CCIPClientWithACKTest:test_ccipReceiver_ack_with_invalidAckMessageHeaderBytes_Revert() (gas: 438714) -CCIPClientWithACKTest:test_ccipSendAndReceiveAck_in_return_Success() (gas: 349366) -CCIPClientWithACKTest:test_ccipSend_withNonNativeFeetoken_andNoDestTokens_Success() (gas: 242666) -CCIPClientWithACKTest:test_send_tokens_that_are_not_feeToken_Success() (gas: 553089) +CCIPClientWithACKTest:test_ccipReceiveAndSendAck_Success() (gas: 331807) +CCIPClientWithACKTest:test_ccipReceiver_ack_with_invalidAckMessageHeaderBytes_Revert() (gas: 416038) +CCIPClientWithACKTest:test_ccipSendAndReceiveAck_in_return_Success() (gas: 348558) +CCIPClientWithACKTest:test_ccipSend_withNonNativeFeetoken_andNoDestTokens_Success() (gas: 242678) +CCIPClientWithACKTest:test_send_tokens_that_are_not_feeToken_Success() (gas: 553108) CCIPConfigSetup:test_getCapabilityConfiguration_Success() (gas: 9495) CCIPConfig_ConfigStateMachine:test__computeConfigDigest_Success() (gas: 70755) CCIPConfig_ConfigStateMachine:test__computeNewConfigWithMeta_InitToRunning_Success() (gas: 363647) @@ -100,21 +100,21 @@ CCIPConfig_validateConfig:test__validateConfig_TooManySigners_Reverts() (gas: 12 CCIPConfig_validateConfig:test__validateConfig_TooManyTransmitters_Reverts() (gas: 1214264) CCIPConfig_validateConfig:test_getCapabilityConfiguration_Success() (gas: 9562) CCIPReceiverTest:test_HappyPath_Success() (gas: 193794) -CCIPReceiverTest:test_Recovery_from_invalid_sender_Success() (gas: 430905) -CCIPReceiverTest:test_Recovery_with_intentional_Revert() (gas: 445121) -CCIPReceiverTest:test_disableChain_andRevert_onccipReceive_Revert() (gas: 199985) -CCIPReceiverTest:test_modifyRouter_Success() (gas: 26446) -CCIPReceiverTest:test_removeSender_from_approvedList_and_revert_Success() (gas: 427653) -CCIPReceiverTest:test_retryFailedMessage_Success() (gas: 454419) -CCIPReceiverTest:test_withdraw_nativeToken_to_owner_Success() (gas: 20667) -CCIPReceiverWithAckTest:test_ccipReceive_ack_message_Success() (gas: 56278) -CCIPReceiverWithAckTest:test_ccipReceive_and_respond_with_ack_Success() (gas: 331829) -CCIPReceiverWithAckTest:test_ccipReceiver_ack_with_invalidAckMessageHeaderBytes_Revert() (gas: 438738) -CCIPReceiverWithAckTest:test_feeTokenApproval_in_constructor_Success() (gas: 2956788) -CCIPReceiverWithAckTest:test_modifyFeeToken_Success() (gas: 74867) -CCIPSenderTest:test_ccipSend_withNonNativeFeetoken_andDestTokens_Success() (gas: 339615) -CCIPSenderTest:test_ccipSend_withNonNativeFeetoken_andNoDestTokens_Success() (gas: 224489) -CCIPSenderTest:test_ccipSend_with_NativeFeeToken_andDestTokens_Success() (gas: 368159) +CCIPReceiverTest:test_Recovery_from_invalid_sender_Success() (gas: 408968) +CCIPReceiverTest:test_Recovery_with_intentional_Revert() (gas: 414468) +CCIPReceiverTest:test_disableChain_andRevert_onccipReceive_Revert() (gas: 200263) +CCIPReceiverTest:test_modifyRouter_Success() (gas: 26449) +CCIPReceiverTest:test_removeSender_from_approvedList_and_revert_Success() (gas: 405385) +CCIPReceiverTest:test_retryFailedMessage_Success() (gas: 423644) +CCIPReceiverTest:test_withdraw_nativeToken_to_owner_Success() (gas: 20854) +CCIPReceiverWithAckTest:test_ccipReceive_ack_message_Success() (gas: 55404) +CCIPReceiverWithAckTest:test_ccipReceive_and_respond_with_ack_Success() (gas: 331763) +CCIPReceiverWithAckTest:test_ccipReceiver_ack_with_invalidAckMessageHeaderBytes_Revert() (gas: 416017) +CCIPReceiverWithAckTest:test_feeTokenApproval_in_constructor_Success() (gas: 2879800) +CCIPReceiverWithAckTest:test_modifyFeeToken_Success() (gas: 74845) +CCIPSenderTest:test_ccipSend_withNonNativeFeetoken_andDestTokens_Success() (gas: 339353) +CCIPSenderTest:test_ccipSend_withNonNativeFeetoken_andNoDestTokens_Success() (gas: 224534) +CCIPSenderTest:test_ccipSend_with_NativeFeeToken_andDestTokens_Success() (gas: 367897) CommitStore_constructor:test_Constructor_Success() (gas: 3091326) CommitStore_isUnpausedAndRMNHealthy:test_RMN_Success() (gas: 73420) CommitStore_report:test_InvalidIntervalMinLargerThanMax_Revert() (gas: 28670) @@ -219,7 +219,7 @@ EVM2EVMMultiOffRamp_executeSingleReport:test_NonExistingSourceChain_Revert() (ga EVM2EVMMultiOffRamp_executeSingleReport:test_ReceiverError_Success() (gas: 181628) EVM2EVMMultiOffRamp_executeSingleReport:test_RetryFailedMessageWithoutManualExecution_Revert() (gas: 190908) EVM2EVMMultiOffRamp_executeSingleReport:test_RootNotCommitted_Revert() (gas: 48050) -EVM2EVMMultiOffRamp_executeSingleReport:test_RouterYULCall_Revert() (gas: 1697359) +EVM2EVMMultiOffRamp_executeSingleReport:test_RouterYULCall_Revert() (gas: 1587918) EVM2EVMMultiOffRamp_executeSingleReport:test_SingleMessageNoTokensOtherChain_Success() (gas: 252012) EVM2EVMMultiOffRamp_executeSingleReport:test_SingleMessageNoTokensUnordered_Success() (gas: 174226) EVM2EVMMultiOffRamp_executeSingleReport:test_SingleMessageNoTokens_Success() (gas: 193899) @@ -245,8 +245,8 @@ EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_DoesNotRevertIfUntouche EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_FailedTx_Revert() (gas: 207956) EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_ForkedChain_Revert() (gas: 26001) EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_GasLimitMismatchMultipleReports_Revert() (gas: 152913) -EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_LowGasLimit_Success() (gas: 1768860) -EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_ReentrancyFails() (gas: 3413460) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_LowGasLimit_Success() (gas: 1659419) +EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_ReentrancyFails() (gas: 3343871) EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_Success() (gas: 210050) EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_WithGasOverride_Success() (gas: 210613) EVM2EVMMultiOffRamp_manuallyExecute:test_manuallyExecute_WithMultiReportGasOverride_Success() (gas: 670625) @@ -397,7 +397,7 @@ EVM2EVMOffRamp_execute:test_Paused_Revert() (gas: 101458) EVM2EVMOffRamp_execute:test_ReceiverError_Success() (gas: 165192) EVM2EVMOffRamp_execute:test_RetryFailedMessageWithoutManualExecution_Revert() (gas: 177948) EVM2EVMOffRamp_execute:test_RootNotCommitted_Revert() (gas: 41317) -EVM2EVMOffRamp_execute:test_RouterYULCall_Revert() (gas: 1663435) +EVM2EVMOffRamp_execute:test_RouterYULCall_Revert() (gas: 1553994) EVM2EVMOffRamp_execute:test_SingleMessageNoTokensUnordered_Success() (gas: 159863) EVM2EVMOffRamp_execute:test_SingleMessageNoTokens_Success() (gas: 175094) EVM2EVMOffRamp_execute:test_SingleMessageToNonCCIPReceiver_Success() (gas: 248764) @@ -429,14 +429,14 @@ EVM2EVMOffRamp_execute_upgrade:test_V2_Success() (gas: 131906) 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: 1744898) +EVM2EVMOffRamp_manuallyExecute:test_LowGasLimitManualExec_Success() (gas: 1635457) EVM2EVMOffRamp_manuallyExecute:test_ManualExecFailedTx_Revert() (gas: 186809) EVM2EVMOffRamp_manuallyExecute:test_ManualExecForkedChain_Revert() (gas: 25894) EVM2EVMOffRamp_manuallyExecute:test_ManualExecGasLimitMismatch_Revert() (gas: 43519) EVM2EVMOffRamp_manuallyExecute:test_ManualExecInvalidGasLimit_Revert() (gas: 26009) EVM2EVMOffRamp_manuallyExecute:test_ManualExecWithGasOverride_Success() (gas: 189003) EVM2EVMOffRamp_manuallyExecute:test_ManualExec_Success() (gas: 188464) -EVM2EVMOffRamp_manuallyExecute:test_ReentrancyManualExecuteFails() (gas: 3156679) +EVM2EVMOffRamp_manuallyExecute:test_ReentrancyManualExecuteFails() (gas: 3087887) EVM2EVMOffRamp_manuallyExecute:test_manuallyExecute_DoesNotRevertIfUntouched_Success() (gas: 144106) EVM2EVMOffRamp_metadataHash:test_MetadataHash_Success() (gas: 8871) EVM2EVMOffRamp_setDynamicConfig:test_NonOwner_Revert() (gas: 40429) @@ -703,10 +703,10 @@ 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: 380711) -PingPong_example_ccipReceive:test_CcipReceive_Success() (gas: 179093) -PingPong_example_plumbing:test_Pausing_Success() (gas: 17917) -PingPong_example_plumbing:test_typeAndVersion() (gas: 9786) -PingPong_example_startPingPong:test_StartPingPong_Success() (gas: 203459) +PingPong_example_ccipReceive:test_CcipReceive_Success() (gas: 179111) +PingPong_example_plumbing:test_Pausing_Success() (gas: 17833) +PingPong_example_plumbing:test_typeAndVersion() (gas: 9741) +PingPong_example_startPingPong:test_StartPingPong_Success() (gas: 203453) PriceRegistry_applyFeeTokensUpdates:test_ApplyFeeTokensUpdates_Success() (gas: 79823) PriceRegistry_applyFeeTokensUpdates:test_OnlyCallableByOwner_Revert() (gas: 12580) PriceRegistry_constructor:test_InvalidStalenessThreshold_Revert() (gas: 67418) @@ -853,8 +853,8 @@ 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: 291110) -SelfFundedPingPong_ccipReceive:test_Funding_Success() (gas: 445731) +SelfFundedPingPong_ccipReceive:test_FundingIfNotANop_Revert() (gas: 268794) +SelfFundedPingPong_ccipReceive:test_Funding_Success() (gas: 445605) SelfFundedPingPong_setCountIncrBeforeFunding:test_setCountIncrBeforeFunding() (gas: 20273) TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole_OnlyPendingAdministrator_Revert() (gas: 51085) TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole_Success() (gas: 43947) diff --git a/contracts/src/v0.8/ccip/applications/external/CCIPBase.sol b/contracts/src/v0.8/ccip/applications/external/CCIPBase.sol index 6e6b52b91b..1c18da79f2 100644 --- a/contracts/src/v0.8/ccip/applications/external/CCIPBase.sol +++ b/contracts/src/v0.8/ccip/applications/external/CCIPBase.sol @@ -12,7 +12,7 @@ import {Address} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/ut /// @dev This contract is abstract, but does not have any functions which must be implemented by a child. abstract contract CCIPBase is OwnerIsCreator { using SafeERC20 for IERC20; - using Address for address; + using Address for address payable; error ZeroAddressNotAllowed(); error InvalidRouter(address router); @@ -24,8 +24,10 @@ abstract contract CCIPBase is OwnerIsCreator { event TokensWithdrawnByOwner(address indexed token, address indexed to, uint256 amount); // Parameters are indexed to simplify indexing of cross-chain dapps where contracts may be deployed with the same address. - // Since the updateApprovedSenders() function should be used sparingly by the contract owner, the additional gas cost should be negligible. If this function is needed to be used constantly, or with a large number of - // contracts, then an alternative and more gas-efficient method should be implemented instead, e.g. with merkle trees or indexing the parameters can be removed. + // Since the updateApprovedSenders() function should be used sparingly by the contract owner, the additional gas cost + // should be negligible. If this function is needed to be used constantly, or with a large number of + // contracts, then an alternative and more gas-efficient method should be implemented instead, e.g. with merkle trees + // or removing the indexed parameters event ApprovedSenderAdded(uint64 indexed destChainSelector, bytes indexed recipient); event ApprovedSenderRemoved(uint64 indexed destChainSelector, bytes indexed recipient); @@ -108,7 +110,7 @@ abstract contract CCIPBase is OwnerIsCreator { return s_chainConfigs[sourceChainSelector].approvedSender[senderAddr]; } - // ================================================================ + // =============================================================== // │ Fee Token Management │ // =============================================================== @@ -116,17 +118,6 @@ abstract contract CCIPBase is OwnerIsCreator { /// @dev All the example applications accept prefunding. This function should be removed if prefunding in native fee token is not required. receive() external payable {} - /// @notice Allow the owner to recover any native-tokens sent to this contract out of error, or to withdraw any native-tokens which were used for pre-funding if the fee-token is switched away from native-tokens. - /// @dev Function should not be used to recover tokens from failed messages, abandonFailedMessage() should be used instead - /// @param to A payable address to send the recovered tokens to - /// @param amount the amount of native tokens to recover, denominated in wei - function withdrawNativeToken(address payable to, uint256 amount) external onlyOwner { - Address.sendValue(to, amount); - - // Use the same withdrawal event signature as withdrawTokens() but use address(0) to denote native-tokens. - emit TokensWithdrawnByOwner(address(0), to, amount); - } - /// @notice Allow the owner to recover any ERC-20 tokens sent to this contract out of error or withdraw any fee-tokens which were sent as a source of fee-token pre-funding /// @dev This should NOT be used for recovering tokens from a failed message. Token recoveries can happen only if /// the failed message is guaranteed to not succeed upon retry, otherwise this can lead to double spend. @@ -134,7 +125,11 @@ abstract contract CCIPBase is OwnerIsCreator { /// @param to A payable address to send the recovered tokens to /// @param amount the amount of native tokens to recover, denominated in wei function withdrawTokens(address token, address to, uint256 amount) external onlyOwner { function withdrawTokens(address token, address to, uint256 amount) external onlyOwner { - IERC20(token).safeTransfer(to, amount); + if (token == address(0)) { + payable(to).sendValue(amount); + } else { + IERC20(token).safeTransfer(to, amount); + } emit TokensWithdrawnByOwner(token, to, amount); } @@ -160,21 +155,23 @@ abstract contract CCIPBase is OwnerIsCreator { /// @notice Enable a remote-chain to send and receive messages to/from this contract via CCIP function applyChainUpdates(ChainUpdate[] calldata chains) external onlyOwner { for (uint256 i = 0; i < chains.length; ++i) { - if (!chains[i].allowed) { - delete s_chainConfigs[chains[i].chainSelector].recipient; - emit ChainRemoved(chains[i].chainSelector); + ChainUpdate memory chain = chains[i]; + + if (!chain.allowed) { + delete s_chainConfigs[chain.chainSelector].recipient; + emit ChainRemoved(chain.chainSelector); } else { // The existence of a stored recipient is used to denote a chain being enabled, so the length here cannot be zero - if (chains[i].recipient.length == 0) revert ZeroAddressNotAllowed(); + if (chain.recipient.length == 0) revert ZeroAddressNotAllowed(); - ChainConfig storage currentConfig = s_chainConfigs[chains[i].chainSelector]; + ChainConfig storage currentConfig = s_chainConfigs[chain.chainSelector]; - currentConfig.recipient = chains[i].recipient; + currentConfig.recipient = chain.recipient; // Set any additional args such as enabling out-of-order execution or manual gas-limit - if (chains[i].extraArgsBytes.length != 0) currentConfig.extraArgsBytes = chains[i].extraArgsBytes; + currentConfig.extraArgsBytes = chain.extraArgsBytes; - emit ChainAdded(chains[i].chainSelector, chains[i].recipient, chains[i].extraArgsBytes); + emit ChainAdded(chain.chainSelector, chain.recipient, chain.extraArgsBytes); } } } @@ -182,8 +179,7 @@ abstract contract CCIPBase is OwnerIsCreator { /// @notice Reverts if the specified chainSelector is not approved to send/receive messages to/from this contract /// @param chainSelector the CCIP specific chain selector for a given remote-chain. modifier isValidChain(uint64 chainSelector) { - // Must be storage and not memory because the struct contains a nested mapping which is not capable of being copied to memory - ChainConfig storage currentConfig = s_chainConfigs[chainSelector]; + ChainConfig storage currentConfig = s_chainConfigs[chainSelector]; // Must be storage because the nested mapping cannot be copied to memory if (currentConfig.recipient.length == 0) revert InvalidChain(chainSelector); _; } diff --git a/contracts/src/v0.8/ccip/applications/external/CCIPClient.sol b/contracts/src/v0.8/ccip/applications/external/CCIPClient.sol index f225a19808..9d2f82b7f3 100644 --- a/contracts/src/v0.8/ccip/applications/external/CCIPClient.sol +++ b/contracts/src/v0.8/ccip/applications/external/CCIPClient.sol @@ -20,6 +20,7 @@ contract CCIPClient is CCIPReceiver { event MessageSent(bytes32 messageId); event FeeTokenUpdated(address oldFeeToken, address newFeeToken); + /// @dev A check for the zero-address is not explicitly performed since it is included in the CCIPBase parent constructor constructor(address router, IERC20 feeToken) CCIPReceiver(router) { s_feeToken = feeToken; @@ -47,15 +48,16 @@ contract CCIPClient is CCIPReceiver { uint256 fee = IRouterClient(s_ccipRouter).getFee(destChainSelector, message); - // Additional tokens for fees do not need to be approved to the router since it is already handled by setting s_feeToken - // Fee transfers need first, that way balanceOf(address(this)) does not conflict with any tokens sent in tokenAmounts - // to support fee-token pre-funding + // To support pre-funding, if the contract already posesses enough tokens to pay the fee, then a transferFrom is + // not necesarry. This branch must be performed before transfering tokens in tokenAmounts[], since in the case + // where tokenAmounts[] contains the fee token, then balanceOf may double count tokens improperly and cause + // unexpected behavior. if ((address(s_feeToken) != address(0)) && (s_feeToken.balanceOf(address(this)) < fee)) { IERC20(s_feeToken).safeTransferFrom(msg.sender, address(this), fee); } for (uint256 i = 0; i < tokenAmounts.length; ++i) { - // Transfer the tokens to pay for tokens in tokenAmounts + // Transfer the tokens specified in TokenAmounts[] so that it can be forwarded to the router IERC20(tokenAmounts[i].token).safeTransferFrom(msg.sender, address(this), tokenAmounts[i].amount); // Do not approve the tokens if it is the feeToken, otherwise the approval amount may overflow @@ -75,17 +77,6 @@ contract CCIPClient is CCIPReceiver { return messageId; } - /// @notice Contains arbitrary application-logic for incoming CCIP messages. - /// @dev It has to be external because of the try/catch of ccipReceive() which invokes it - function processMessage(Client.Any2EVMMessage calldata message) - external - virtual - override - onlySelf - isValidSender(message.sourceChainSelector, message.sender) - isValidChain(message.sourceChainSelector) - {} - function updateFeeToken(address token) external onlyOwner { // If the current fee token is not-native, zero out the allowance to the router for safety if (address(s_feeToken) != address(0)) { diff --git a/contracts/src/v0.8/ccip/applications/external/CCIPClientWithACK.sol b/contracts/src/v0.8/ccip/applications/external/CCIPClientWithACK.sol index 9312760ed7..af882e582e 100644 --- a/contracts/src/v0.8/ccip/applications/external/CCIPClientWithACK.sol +++ b/contracts/src/v0.8/ccip/applications/external/CCIPClientWithACK.sol @@ -10,7 +10,7 @@ import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/ /// @title CCIPClientWithACK /// @notice This contract implements logic for sending and receiving CCIP Messages, as well as responding to incoming messages with an ACK-response pattern. It utilizes CCIPReceiver's defensive patterns by default. -/// @dev ccipSend functionality has been inherited from CCIPClient.sol, and _sendACK() from CCIPReceiverWithACK, so only processMessage() must be overridden to enable full functionality for processing incoming messages for ACKs +/// @dev ccipReceive functionality has been inherited fromCCIPReceiverWithACK, and so only ccipSend functionality must be implemented contract CCIPClientWithACK is CCIPReceiverWithACK { using SafeERC20 for IERC20; @@ -66,39 +66,4 @@ contract CCIPClientWithACK is CCIPReceiverWithACK { return messageId; } - - /// @notice Implementation of arbitrary logic to be executed when a CCIP message is received - /// @dev is only invoked by self on CCIPReceive, and should implement arbitrary dapp-specific logic - function processMessage(Client.Any2EVMMessage calldata message) - external - virtual - override - onlySelf - isValidSender(message.sourceChainSelector, message.sender) - { - (MessagePayload memory payload) = abi.decode(message.data, (MessagePayload)); - - if (payload.messageType == MessageType.OUTGOING) { - // Insert Processing workflow here. - - // If the message was outgoing, then send an ack response. - _sendAck(message); - } else if (payload.messageType == MessageType.ACK) { - // Decode message into the message-header and the messageId to ensure the message is encoded correctly - (string memory messageHeader, bytes32 messageId) = abi.decode(payload.data, (string, bytes32)); - - // Ensure Ack Message contains proper message header - if (keccak256(abi.encode(messageHeader)) != keccak256(abi.encode(ACK_MESSAGE_HEADER))) { - revert InvalidAckMessageHeader(); - } - - // Make sure the ACK message was originally sent by this contract - if (s_messageStatus[messageId] != MessageStatus.SENT) revert CannotAcknowledgeUnsentMessage(messageId); - - // Mark the message has finalized from a proper ack-message. - s_messageStatus[messageId] = MessageStatus.ACKNOWLEDGED; - - emit MessageAckReceived(messageId); - } - } } diff --git a/contracts/src/v0.8/ccip/applications/external/CCIPReceiver.sol b/contracts/src/v0.8/ccip/applications/external/CCIPReceiver.sol index f2c81a87e0..71ca39b502 100644 --- a/contracts/src/v0.8/ccip/applications/external/CCIPReceiver.sol +++ b/contracts/src/v0.8/ccip/applications/external/CCIPReceiver.sol @@ -6,14 +6,14 @@ import {CCIPBase} from "./CCIPBase.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"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableSet.sol"; /// @title CCIPReceiver /// @notice This contract is capable of receiving incoming messages from the CCIP Router. /// @dev This contract implements various "defensive" measures to enhance security and efficiency. These include the ability to implement custom-retry logic and ownership-based token-recovery functions. contract CCIPReceiver is CCIPBase { using SafeERC20 for IERC20; - using EnumerableMap for EnumerableMap.Bytes32ToUintMap; + using EnumerableSet for EnumerableSet.Bytes32Set; error OnlySelf(); error MessageNotFailed(bytes32 messageId); @@ -23,19 +23,11 @@ contract CCIPReceiver is CCIPBase { event MessageRecovered(bytes32 indexed messageId); event MessageAbandoned(bytes32 indexed messageId, address tokenReceiver); - enum ErrorCode { - RESOLVED, // RESOLVED is the default status for any incoming message, unless execution fails and it is marked as FAILED. - FAILED, // FAILED messages are messages which reverted during execution of processMessage() as part of the ccipReceive() try catch loop. - ABANDONED // ABANDONED messages are ones which cannot be properly processed, but any sent tokens are recoverable and - // can only be triggered by the contract owner. Only a message that was previously marked as FAILED can be abandoned. - - } - - // Failed messages are stored here. mapping(bytes32 messageId => Client.Any2EVMMessage contents) internal s_messageContents; - // Contains failed messages and their state. - EnumerableMap.Bytes32ToUintMap internal s_failedMessages; + // Contains the set of all messages in s_messageContents which failed to process properly. + // When a message is retried or abandoned it is removed from this set. + EnumerableSet.Bytes32Set internal s_failedMessages; constructor(address router) CCIPBase(router) {} @@ -55,12 +47,10 @@ contract CCIPReceiver is CCIPBase { { try this.processMessage(message) {} catch (bytes memory err) { - // If custom retry logic is desired, plus granting the owner the ability to extract tokens as a last resort for recovery, use this try-catch pattern in ccipReceiver. - // This make the message appear as a success to CCIP, and actual message state and any residual errors can be tracked within the dapp with greater granularity. - // If custom retry logic and token recovery functions are not needed, then this try-catch can be removed, - // and ccip manualExecution can be used a retry function instead. - - s_failedMessages.set(message.messageId, uint256(ErrorCode.FAILED)); + // If custom retry logic is desired, plus granting the owner the ability to extract tokens as a last resort for + // recovery, use this try-catch pattern in ccipReceiver. It will make the message appear as a success to CCIP, and + // actual message state and any residual errors can be tracked within the dapp. + s_failedMessages.add(message.messageId); // Store the message contents in case it needs to be retried or abandoned s_messageContents[message.messageId] = message; @@ -93,14 +83,15 @@ contract CCIPReceiver is CCIPBase { /// Function will revert if the messageId was not already stored as having failed its initial execution /// @param messageId the unique ID of the CCIP-message which failed initial-execution. function retryFailedMessage(bytes32 messageId) external { - if (s_failedMessages.get(messageId) != uint256(ErrorCode.FAILED)) 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)); + if (!s_failedMessages.contains(messageId)) revert MessageNotFailed(messageId); // Allow developer to implement arbitrary functionality on retried messages, such as just releasing the associated tokens Client.Any2EVMMessage memory message = s_messageContents[messageId]; + // Set remove the message from storage to disallow reentry and retry the same failed message multiple times. + delete s_messageContents[messageId]; + s_failedMessages.remove(messageId); + // Allow the user override the implementation, since different workflow may be desired for retrying a message _retryFailedMessage(message); @@ -118,13 +109,16 @@ contract CCIPReceiver is CCIPBase { /// @dev function will send tokens to destination, but will NOT invoke any arbitrary logic afterwards. /// function is only callable by the contract owner function abandonFailedMessage(bytes32 messageId, address receiver) external onlyOwner { - if (s_failedMessages.get(messageId) != uint256(ErrorCode.FAILED)) revert MessageNotFailed(messageId); + if (!s_failedMessages.contains(messageId)) revert MessageNotFailed(messageId); - s_failedMessages.set(messageId, uint256(ErrorCode.ABANDONED)); - Client.Any2EVMMessage memory message = s_messageContents[messageId]; + Client.EVMTokenAmount[] memory tokenAmounts = s_messageContents[messageId].destTokenAmounts; + + // Follow CEI and remove failed message from state before transferring in case of ERC-667 external calls + delete s_messageContents[messageId]; + s_failedMessages.remove(messageId); - for (uint256 i = 0; i < message.destTokenAmounts.length; ++i) { - IERC20(message.destTokenAmounts[i].token).safeTransfer(receiver, message.destTokenAmounts[i].amount); + for (uint256 i = 0; i < tokenAmounts.length; ++i) { + IERC20(tokenAmounts[i].token).safeTransfer(receiver, tokenAmounts[i].amount); } emit MessageAbandoned(messageId, receiver); @@ -140,9 +134,12 @@ contract CCIPReceiver is CCIPBase { return s_messageContents[messageId]; } + /// @notice Retrieve whether a message delivered by the CCIP router failed to process properly. + /// @dev Querying this function with message which was successfully retried or abandoned will return false /// @param messageId the ID of the message delivered by the CCIP Router - function getMessageStatus(bytes32 messageId) public view returns (uint256) { - return s_failedMessages.get(messageId); + /// @return bool Whether the previously-delivered message failed to process. + function isFailedMessage(bytes32 messageId) public view returns (bool) { + return s_failedMessages.contains(messageId); } modifier onlySelf() { diff --git a/contracts/src/v0.8/ccip/applications/external/CCIPReceiverWithACK.sol b/contracts/src/v0.8/ccip/applications/external/CCIPReceiverWithACK.sol index 415e411939..1304f10091 100644 --- a/contracts/src/v0.8/ccip/applications/external/CCIPReceiverWithACK.sol +++ b/contracts/src/v0.8/ccip/applications/external/CCIPReceiverWithACK.sol @@ -93,16 +93,17 @@ contract CCIPReceiverWithACK is CCIPReceiver { // message type is a concept with ClientWithACK if (payload.messageType == MessageType.OUTGOING) { - // Insert processing workflow here. + _processIncomingMessage(message); // If the message was outgoing on the source chain, then send an ack response. _sendAck(message); + return; } else if (payload.messageType == MessageType.ACK) { // Decode message into the message header and the messageId to ensure the message is encoded correctly (string memory messageHeader, bytes32 messageId) = abi.decode(payload.data, (string, bytes32)); // Ensure Ack Message contains proper message header. Must abi.encode() before hashing since its of the string type - if (keccak256(abi.encode(messageHeader)) != keccak256(abi.encode(ACK_MESSAGE_HEADER))) { + if (keccak256(bytes(messageHeader)) != keccak256(bytes(ACK_MESSAGE_HEADER))) { revert InvalidAckMessageHeader(); } @@ -116,15 +117,16 @@ contract CCIPReceiverWithACK is CCIPReceiver { } } + /// @notice Contains the arbitrary logic for processing incoming messages from an authorized sender & source-chain + function _processIncomingMessage(Client.Any2EVMMessage calldata incomingMessage) internal virtual {} + /// @notice Sends the acknowledgement message back through CCIP to original sender contract function _sendAck(Client.Any2EVMMessage calldata incomingMessage) internal { - Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](0); - // Build the outgoing ACK message, with no tokens, with data being the concatenation of the acknowledgement header and incoming-messageId Client.EVM2AnyMessage memory outgoingMessage = Client.EVM2AnyMessage({ receiver: incomingMessage.sender, data: abi.encode(ACK_MESSAGE_HEADER, incomingMessage.messageId), - tokenAmounts: tokenAmounts, + tokenAmounts: new Client.EVMTokenAmount[](0), extraArgs: s_chainConfigs[incomingMessage.sourceChainSelector].extraArgsBytes, feeToken: address(s_feeToken) }); diff --git a/contracts/src/v0.8/ccip/applications/external/CCIPSender.sol b/contracts/src/v0.8/ccip/applications/external/CCIPSender.sol index eb9dd54c38..ef21876490 100644 --- a/contracts/src/v0.8/ccip/applications/external/CCIPSender.sol +++ b/contracts/src/v0.8/ccip/applications/external/CCIPSender.sol @@ -9,7 +9,7 @@ import {CCIPBase} from "./CCIPBase.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 Example of a client which supports EVM/non-EVM chains +/// @notice Example of a client which supports sending messages to 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. @@ -25,9 +25,6 @@ import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/ contract CCIPSender is CCIPBase { using SafeERC20 for IERC20; - error InvalidConfig(); - error InsufficientNativeFeeTokenAmount(); - event MessageSent(bytes32 messageId); event MessageReceived(bytes32 messageId); @@ -36,8 +33,8 @@ contract CCIPSender is CCIPBase { /// @notice sends a message through CCIP to the router function ccipSend( uint64 destChainSelector, - Client.EVMTokenAmount[] calldata tokenAmounts, - bytes calldata data, + Client.EVMTokenAmount[] memory tokenAmounts, + bytes memory data, address feeToken ) public payable isValidChain(destChainSelector) returns (bytes32 messageId) { Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ diff --git a/contracts/src/v0.8/ccip/applications/internal/CCIPReceiverLegacy.sol b/contracts/src/v0.8/ccip/applications/internal/CCIPReceiverLegacy.sol new file mode 100644 index 0000000000..a83f187612 --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/internal/CCIPReceiverLegacy.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/internal/EtherSenderReceiver.sol b/contracts/src/v0.8/ccip/applications/internal/EtherSenderReceiver.sol new file mode 100644 index 0000000000..e569b6f59b --- /dev/null +++ b/contracts/src/v0.8/ccip/applications/internal/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 "./CCIPReceiverLegacy.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/internal/PingPongDemo.sol b/contracts/src/v0.8/ccip/applications/internal/PingPongDemo.sol index 72a6b11cd2..3e31e0c557 100644 --- a/contracts/src/v0.8/ccip/applications/internal/PingPongDemo.sol +++ b/contracts/src/v0.8/ccip/applications/internal/PingPongDemo.sol @@ -24,7 +24,7 @@ contract PingPongDemo is CCIPClient { constructor(address router, IERC20 feeToken) CCIPClient(router, feeToken) {} function typeAndVersion() external pure virtual returns (string memory) { - return "PingPongDemo 1.3.0"; + return "PingPongDemo 1.6.0"; } function startPingPong() external onlyOwner { @@ -43,11 +43,7 @@ contract PingPongDemo is CCIPClient { bytes memory data = abi.encode(pingPongCount); - ccipSend({ - destChainSelector: s_counterpartChainSelector, // destChain - tokenAmounts: new Client.EVMTokenAmount[](0), - data: data - }); + ccipSend({destChainSelector: s_counterpartChainSelector, tokenAmounts: new Client.EVMTokenAmount[](0), data: data}); } /// @notice This function the entrypoint for this contract to process messages. @@ -61,9 +57,8 @@ contract PingPongDemo is CCIPClient { onlySelf isValidSender(message.sourceChainSelector, message.sender) { - uint256 pingPongCount = abi.decode(message.data, (uint256)); if (!s_isPaused) { - _respond(pingPongCount + 1); + _respond(abi.decode(message.data, (uint256)) + 1); } } @@ -72,6 +67,8 @@ contract PingPongDemo is CCIPClient { // ================================================================ function setCounterpart(uint64 counterpartChainSelector, address counterpartAddress) external onlyOwner { + if (counterpartAddress == address(0) || counterpartChainSelector == 0) revert ZeroAddressNotAllowed(); + s_counterpartChainSelector = counterpartChainSelector; s_counterpartAddress = counterpartAddress; @@ -82,16 +79,6 @@ contract PingPongDemo is CCIPClient { s_chainConfigs[counterpartChainSelector].recipient = abi.encode(counterpartAddress); } - function setCounterpartChainSelector(uint64 counterpartChainSelector) external onlyOwner { - s_counterpartChainSelector = counterpartChainSelector; - } - - function setCounterpartAddress(address counterpartAddress) external onlyOwner { - s_counterpartAddress = counterpartAddress; - - s_chainConfigs[s_counterpartChainSelector].recipient = abi.encode(counterpartAddress); - } - function setPaused(bool pause) external onlyOwner { s_isPaused = pause; } diff --git a/contracts/src/v0.8/ccip/applications/internal/SelfFundedPingPong.sol b/contracts/src/v0.8/ccip/applications/internal/SelfFundedPingPong.sol index 888e5aa361..4ff48c4f70 100644 --- a/contracts/src/v0.8/ccip/applications/internal/SelfFundedPingPong.sol +++ b/contracts/src/v0.8/ccip/applications/internal/SelfFundedPingPong.sol @@ -9,7 +9,7 @@ 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"; + string public constant override typeAndVersion = "SelfFundedPingPong 1.6.0"; event Funded(); event CountIncrBeforeFundingSet(uint8 countIncrBeforeFunding); diff --git a/contracts/src/v0.8/ccip/test/applications/external/CCIPClientWithACKTest.t.sol b/contracts/src/v0.8/ccip/test/applications/external/CCIPClientWithACKTest.t.sol index a2f2ba645c..20c4611e5c 100644 --- a/contracts/src/v0.8/ccip/test/applications/external/CCIPClientWithACKTest.t.sol +++ b/contracts/src/v0.8/ccip/test/applications/external/CCIPClientWithACKTest.t.sol @@ -74,7 +74,7 @@ contract CCIPClientWithACKTest is EVM2EVMOnRampSetup { ); // Check that message status is failed - assertEq(s_sender.getMessageStatus(messageId), 1); + assertTrue(s_sender.isFailedMessage(messageId), "Message Should be marked as failed"); } function test_ccipReceiveAndSendAck_Success() public { diff --git a/contracts/src/v0.8/ccip/test/applications/external/CCIPReceiverTest.t.sol b/contracts/src/v0.8/ccip/test/applications/external/CCIPReceiverTest.t.sol index ec99aed57c..bc4a396d96 100644 --- a/contracts/src/v0.8/ccip/test/applications/external/CCIPReceiverTest.t.sol +++ b/contracts/src/v0.8/ccip/test/applications/external/CCIPReceiverTest.t.sol @@ -129,7 +129,7 @@ contract CCIPReceiverTest is EVM2EVMOnRampSetup { assertEq(failedMessage.destTokenAmounts[0].amount, amount); // Check that message status is failed - assertEq(s_receiver.getMessageStatus(messageId), 1); + assertTrue(s_receiver.isFailedMessage(messageId), "Message should be marked as failed"); uint256 tokenBalanceBefore = IERC20(token).balanceOf(OWNER); @@ -182,7 +182,7 @@ contract CCIPReceiverTest is EVM2EVMOnRampSetup { assertEq(failedMessage.destTokenAmounts[0].amount, amount); // Check that message status is failed - assertEq(s_receiver.getMessageStatus(messageId), 1); + assertTrue(s_receiver.isFailedMessage(messageId), "Message should be marked as failed"); vm.startPrank(OWNER); @@ -198,7 +198,7 @@ contract CCIPReceiverTest is EVM2EVMOnRampSetup { emit CCIPReceiver.MessageRecovered(messageId); s_receiver.retryFailedMessage(messageId); - assertEq(s_receiver.getMessageStatus(messageId), 0); + assertFalse(s_receiver.isFailedMessage(messageId), "Message should be marked as resolved"); } function test_HappyPath_Success() public { @@ -327,7 +327,7 @@ contract CCIPReceiverTest is EVM2EVMOnRampSetup { vm.startPrank(OWNER); - s_receiver.withdrawNativeToken(payable(OWNER), amount); + s_receiver.withdrawTokens(address(0), payable(OWNER), amount); assertEq(OWNER.balance, balanceBefore + amount); } diff --git a/contracts/src/v0.8/ccip/test/applications/external/CCIPReceiverWithAckTest.t.sol b/contracts/src/v0.8/ccip/test/applications/external/CCIPReceiverWithAckTest.t.sol index a5608814ea..b3ae03d496 100644 --- a/contracts/src/v0.8/ccip/test/applications/external/CCIPReceiverWithAckTest.t.sol +++ b/contracts/src/v0.8/ccip/test/applications/external/CCIPReceiverWithAckTest.t.sol @@ -150,7 +150,7 @@ contract CCIPReceiverWithAckTest is EVM2EVMOnRampSetup { ); // Check that message status is failed - assertEq(s_receiver.getMessageStatus(messageId), 1); + assertTrue(s_receiver.isFailedMessage(messageId), "Message should be marked as failed"); } function test_modifyFeeToken_Success() public { diff --git a/contracts/src/v0.8/ccip/test/applications/internal/PingPongDemoTest.t.sol b/contracts/src/v0.8/ccip/test/applications/internal/PingPongDemoTest.t.sol index 268680843a..fadbf2bb73 100644 --- a/contracts/src/v0.8/ccip/test/applications/internal/PingPongDemoTest.t.sol +++ b/contracts/src/v0.8/ccip/test/applications/internal/PingPongDemoTest.t.sol @@ -94,25 +94,6 @@ contract PingPong_example_ccipReceive is PingPongDappSetup { } contract PingPong_example_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()); @@ -122,6 +103,6 @@ contract PingPong_example_plumbing is PingPongDappSetup { } function test_typeAndVersion() public view { - assertEq(s_pingPong.typeAndVersion(), "PingPongDemo 1.3.0"); + assertEq(s_pingPong.typeAndVersion(), "PingPongDemo 1.6.0"); } } diff --git a/contracts/src/v0.8/ccip/test/helpers/EtherSenderReceiverHelper.sol b/contracts/src/v0.8/ccip/test/helpers/EtherSenderReceiverHelper.sol index ab9f74d16d..88afa9ff73 100644 --- a/contracts/src/v0.8/ccip/test/helpers/EtherSenderReceiverHelper.sol +++ b/contracts/src/v0.8/ccip/test/helpers/EtherSenderReceiverHelper.sol @@ -1,35 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; -import {CCIPSender} from "../../applications/external/CCIPSender.sol"; +import {EtherSenderReceiver} from "../../applications/internal/EtherSenderReceiver.sol"; import {Client} from "../../libraries/Client.sol"; -import {CCIPReceiverBasic} from "./receivers/CCIPReceiverBasic.sol"; -import {IWrappedNative} from "../../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"; - -interface CCIPRouter { - function getWrappedNative() external view returns (address); -} - -contract EtherSenderReceiverHelper is CCIPSender { - using SafeERC20 for IERC20; - - error InvalidTokenAmounts(uint256 gotAmounts); - error InvalidToken(address gotToken, address expectedToken); - error TokenAmountNotEqualToMsgValue(uint256 gotAmount, uint256 msgValue); - error InsufficientMsgValue(uint256 gotAmount, uint256 msgValue); - error InsufficientFee(uint256 gotFee, uint256 fee); - error GasLimitTooLow(uint256 minLimit, uint256 gotLimit); - - IWrappedNative public immutable i_weth; - - constructor(address router) CCIPSender(router) { - i_weth = IWrappedNative(CCIPRouter(router).getWrappedNative()); - IERC20(i_weth).safeApprove(router, type(uint256).max); - } +contract EtherSenderReceiverHelper is EtherSenderReceiver { + constructor(address router) EtherSenderReceiver(router) {} function validatedMessage(Client.EVM2AnyMessage calldata message) public view returns (Client.EVM2AnyMessage memory) { return _validatedMessage(message); @@ -39,47 +15,7 @@ contract EtherSenderReceiverHelper is CCIPSender { _validateFeeToken(message); } - 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 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 validMessage = message; - - if (validMessage.tokenAmounts.length != 1) { - revert InvalidTokenAmounts(validMessage.tokenAmounts.length); - } - - validMessage.data = abi.encode(msg.sender); - validMessage.tokenAmounts[0].token = address(i_weth); - - return validMessage; - } - function publicCcipReceive(Client.Any2EVMMessage memory message) public { _ccipReceive(message); } - - function ccipReceive(Client.Any2EVMMessage calldata message) external virtual onlyRouter { - _ccipReceive(message); - } - - /// @notice Override this function in your implementation. - /// @param message Any2EVMMessage - function _ccipReceive(Client.Any2EVMMessage memory message) internal virtual {} } diff --git a/contracts/src/v0.8/ccip/test/helpers/receivers/CCIPReceiverBasic.sol b/contracts/src/v0.8/ccip/test/helpers/receivers/CCIPReceiverBasic.sol index 7d7c54e89b..e331b9377c 100644 --- a/contracts/src/v0.8/ccip/test/helpers/receivers/CCIPReceiverBasic.sol +++ b/contracts/src/v0.8/ccip/test/helpers/receivers/CCIPReceiverBasic.sol @@ -3,17 +3,17 @@ pragma solidity ^0.8.0; import {IAny2EVMMessageReceiver} from "../../../interfaces/IAny2EVMMessageReceiver.sol"; -import {IERC165} from "../../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/introspection/IERC165.sol"; - import {CCIPBase} from "../../../applications/external/CCIPBase.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. contract CCIPReceiverBasic is CCIPBase, IAny2EVMMessageReceiver, IERC165 { constructor(address router) CCIPBase(router) {} function typeAndVersion() external pure virtual returns (string memory) { - return "CCIPReceiverBasic 1.0.0-dev"; + return "CCIPReceiverBasic 1.6.0-dev"; } /// @notice IERC165 supports an interfaceId