From 4ae66591ab1ecd8faa66a82ab259d3b49e4f9e0f Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 9 Apr 2025 12:59:46 -0400 Subject: [PATCH 1/9] add simple foundry scripts for off-chain testing --- chains/evm/.solhintignore | 1 + .../script/CCIPManualExecutionScript.s.sol | 61 +++++++++++ .../contracts/script/CCIPSendTestScript.s.sol | 102 ++++++++++++++++++ chains/evm/foundry.toml | 1 + 4 files changed, 165 insertions(+) create mode 100644 chains/evm/contracts/script/CCIPManualExecutionScript.s.sol create mode 100644 chains/evm/contracts/script/CCIPSendTestScript.s.sol diff --git a/chains/evm/.solhintignore b/chains/evm/.solhintignore index e8ec9a5534..9d3cdafb21 100644 --- a/chains/evm/.solhintignore +++ b/chains/evm/.solhintignore @@ -1,3 +1,4 @@ # Test files run with a different solhint ruleset, ignore them here. ./**/*.t.sol +./**/*.s.sol ./node_modules/ \ No newline at end of file diff --git a/chains/evm/contracts/script/CCIPManualExecutionScript.s.sol b/chains/evm/contracts/script/CCIPManualExecutionScript.s.sol new file mode 100644 index 0000000000..24a63bf316 --- /dev/null +++ b/chains/evm/contracts/script/CCIPManualExecutionScript.s.sol @@ -0,0 +1,61 @@ +pragma solidity ^0.8.24; + +import {Internal} from "../libraries/Internal.sol"; +import {OffRamp} from "../offRamp/OffRamp.sol"; + +import {IERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {Script} from "forge-std/Script.sol"; +import {Test} from "forge-std/Test.sol"; + +// solhint-disable-next-line no-console +import {console2 as console} from "forge-std/console2.sol"; + +/* solhint-disable no-console */ +contract CCIPSendTestScript is Script, Test { + using SafeERC20 for IERC20; + + // Ex: "ETHEREUM_RPC_URL" as defined in .env + string public RPC_IDENTIFIER; + + OffRamp public s_offRamp; + + bytes32 public s_messageId; + + uint64 public s_sourceChainSelector; + uint64 public s_sequenceNumber; + bytes public s_manualExecutionData; + + function run() public { + vm.createSelectFork(RPC_IDENTIFIER); + + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + + address sender = vm.rememberKey(privateKey); + + vm.startBroadcast(privateKey); + + console.log("Sender: %s", sender); + console.log("Starting Script..."); + + // Check that the messageId is not empty + Internal.MessageExecutionState executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber); + assertTrue( + executionState == Internal.MessageExecutionState.FAILURE + || executionState == Internal.MessageExecutionState.UNTOUCHED, + "Message is not ready for Manual Execution" + ); + + // Manual Execution data can be invoked from a different tool or front-end to avoid having to + // gather execution report data manually + (bool success,) = address(s_offRamp).call(s_manualExecutionData); + assertTrue(success, "Manual execution call reverted"); + + executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber); + assertTrue(executionState == Internal.MessageExecutionState.SUCCESS, "Message was not executed successfully"); + + console.log("Script completed..."); + } +} +/* solhint-enable no-console */ diff --git a/chains/evm/contracts/script/CCIPSendTestScript.s.sol b/chains/evm/contracts/script/CCIPSendTestScript.s.sol new file mode 100644 index 0000000000..ddca5a7b18 --- /dev/null +++ b/chains/evm/contracts/script/CCIPSendTestScript.s.sol @@ -0,0 +1,102 @@ +pragma solidity ^0.8.24; + +import {Router} from "../Router.sol"; +import {Client} from "../libraries/Client.sol"; + +import {IERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {Script} from "forge-std/Script.sol"; + +/* solhint-disable-next-line no-console */ +import {console2 as console} from "forge-std/console2.sol"; + +/* solhint-disable no-console */ +contract CCIPSendTestScript is Script { + using SafeERC20 for IERC20; + + address public ROUTER; + address public FEE_TOKEN; + + address public TOKEN0; + uint256 public TOKEN0_AMOUNT; + + uint64 public DESTINATION_CHAIN_SELECTOR; + uint64 public SOURCE_CHAIN_SELECTOR; + + // Ex: "ETHEREUM_RPC_URL" as defined in .env + string public RPC_DESCRIPTOR; + + bytes public s_extraArgs; + bytes public s_recipient; + bytes public s_data; + + function run() public { + vm.createSelectFork(RPC_DESCRIPTOR); + + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + + address sender = vm.rememberKey(privateKey); + s_recipient = abi.encode(sender); + + vm.startBroadcast(privateKey); + + console.log("Sender: %s", sender); + console.log("Starting Script..."); + + Client.EVMTokenAmount[] memory tokens; + if (TOKEN0 != address(0)) { + tokens = new Client.EVMTokenAmount[](1); + tokens[0] = Client.EVMTokenAmount({token: TOKEN0, amount: TOKEN0_AMOUNT}); + } + + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ + receiver: abi.encode(sender), + data: s_data, + tokenAmounts: tokens, + feeToken: address(0), + extraArgs: s_extraArgs + }); + + uint256 fee = Router(ROUTER).getFee(DESTINATION_CHAIN_SELECTOR, message); + + console.log("Fee in WEI: %s", fee); + + console.log("1. Approving Send Tokens..."); + + for (uint256 i = 0; i < tokens.length; i++) { + // Since sender may be an EOA with an existing approval, the allowance should be checked first + uint256 allowance = IERC20(tokens[i].token).allowance(sender, ROUTER); + + // Approving Tokens for Router if allowance is currently insufficient. + if (allowance < tokens[i].amount) { + console.log("Approving %i tokens to Router for %s", tokens[i].amount, tokens[i].token); + IERC20(tokens[i].token).safeIncreaseAllowance(ROUTER, tokens[i].amount); + } + } + + // Uncomment the following line for debugging purposes + console.log("--- Tokens Approved ---"); + + console.log("2. Approving Fee Token"); + if (FEE_TOKEN != address(0)) { + console.log("Approving Fee Token %s to Router", FEE_TOKEN); + IERC20(FEE_TOKEN).safeIncreaseAllowance(ROUTER, fee); + } + // --- Fee Token Approved --- + + console.log("3. Sending message from: %i to %i", SOURCE_CHAIN_SELECTOR, DESTINATION_CHAIN_SELECTOR); + + // Send the message, forwarding native tokens if necessary to pay the fee + bytes32 messageId = + Router(ROUTER).ccipSend{value: FEE_TOKEN == address(0) ? fee : 0}(DESTINATION_CHAIN_SELECTOR, message); + + console.log("--- Message sent: MessageId ---"); + + console.logBytes32(messageId); + vm.stopBroadcast(); + + console.log("Script completed..."); + } +} +/* solhint-enable no-console */ diff --git a/chains/evm/foundry.toml b/chains/evm/foundry.toml index 699cfff9cc..1d903884df 100644 --- a/chains/evm/foundry.toml +++ b/chains/evm/foundry.toml @@ -4,6 +4,7 @@ evm_version = 'paris' src = 'contracts' test = 'contracts' +script = 'contracts' out = 'foundry-artifacts' cache_path = 'foundry-cache' libs = ['node_modules'] From 680352e4013de43289961dcd3256faa7819dc7e8 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 9 Apr 2025 17:18:28 -0400 Subject: [PATCH 2/9] forge fmt and update github action to exclude scripts --- .github/workflows/solidity-foundry.yml | 1 + .../{script => scripts}/CCIPManualExecutionScript.s.sol | 4 ++-- .../contracts/{script => scripts}/CCIPSendTestScript.s.sol | 0 3 files changed, 3 insertions(+), 2 deletions(-) rename chains/evm/contracts/{script => scripts}/CCIPManualExecutionScript.s.sol (98%) rename chains/evm/contracts/{script => scripts}/CCIPSendTestScript.s.sol (100%) diff --git a/.github/workflows/solidity-foundry.yml b/.github/workflows/solidity-foundry.yml index 6dcbfd4152..c9e09f3363 100644 --- a/.github/workflows/solidity-foundry.yml +++ b/.github/workflows/solidity-foundry.yml @@ -93,6 +93,7 @@ jobs: - '!chains/evm/contracts/**/mocks/**' - '!chains/evm/contracts/**/*.t.sol' - '!chains/evm/contracts/*.t.sol' + - '!chains/evm/contracts/*.s.sol' - '!chains/evm/contracts/**/testhelpers/**' - '!chains/evm/contracts/testhelpers/**' - '!chains/evm/contracts/vendor/**' diff --git a/chains/evm/contracts/script/CCIPManualExecutionScript.s.sol b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol similarity index 98% rename from chains/evm/contracts/script/CCIPManualExecutionScript.s.sol rename to chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol index 24a63bf316..30c56b2fd0 100644 --- a/chains/evm/contracts/script/CCIPManualExecutionScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol @@ -18,7 +18,7 @@ contract CCIPSendTestScript is Script, Test { // Ex: "ETHEREUM_RPC_URL" as defined in .env string public RPC_IDENTIFIER; - + OffRamp public s_offRamp; bytes32 public s_messageId; @@ -47,7 +47,7 @@ contract CCIPSendTestScript is Script, Test { "Message is not ready for Manual Execution" ); - // Manual Execution data can be invoked from a different tool or front-end to avoid having to + // Manual Execution data can be invoked from a different tool or front-end to avoid having to // gather execution report data manually (bool success,) = address(s_offRamp).call(s_manualExecutionData); assertTrue(success, "Manual execution call reverted"); diff --git a/chains/evm/contracts/script/CCIPSendTestScript.s.sol b/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol similarity index 100% rename from chains/evm/contracts/script/CCIPSendTestScript.s.sol rename to chains/evm/contracts/scripts/CCIPSendTestScript.s.sol From fa8513d35200697e217b9558757cf8a87533dea4 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 10 Apr 2025 10:24:07 -0400 Subject: [PATCH 3/9] update github action to exclude scripts from coverage requirements --- .github/workflows/solidity-foundry.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/solidity-foundry.yml b/.github/workflows/solidity-foundry.yml index c9e09f3363..e9986c4b55 100644 --- a/.github/workflows/solidity-foundry.yml +++ b/.github/workflows/solidity-foundry.yml @@ -28,7 +28,7 @@ jobs: run: | cat < matrix.json [ - { "name": "ccip", "setup": { "run-coverage": true, "min-coverage": 98.7, "extra-coverage-params": "--no-match-path='*End2End*'", "run-gas-snapshot": true}} + { "name": "ccip", "setup": { "run-coverage": true, "min-coverage": 98.7, "extra-coverage-params": "--no-match-path='*End2End*|*.s.sol'", "run-gas-snapshot": true}} ] EOF From 3012c35c443edb589dd6a2d95a54da6af354b433 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 10 Apr 2025 10:47:50 -0400 Subject: [PATCH 4/9] attempt fix coverage requirement --- .../scripts/CCIPManualExecutionScript.s.sol | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol index 30c56b2fd0..18575b7677 100644 --- a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol @@ -7,15 +7,17 @@ import {IERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20 import {SafeERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; import {Script} from "forge-std/Script.sol"; -import {Test} from "forge-std/Test.sol"; // solhint-disable-next-line no-console import {console2 as console} from "forge-std/console2.sol"; /* solhint-disable no-console */ -contract CCIPSendTestScript is Script, Test { +contract CCIPSendTestScript is Script { using SafeERC20 for IERC20; + error ManualExecutionFailed(); + error ManualExecutionNotAllowed(); + // Ex: "ETHEREUM_RPC_URL" as defined in .env string public RPC_IDENTIFIER; @@ -41,19 +43,18 @@ contract CCIPSendTestScript is Script, Test { // Check that the messageId is not empty Internal.MessageExecutionState executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber); - assertTrue( - executionState == Internal.MessageExecutionState.FAILURE - || executionState == Internal.MessageExecutionState.UNTOUCHED, - "Message is not ready for Manual Execution" - ); + if( + executionState != Internal.MessageExecutionState.FAILURE + && executionState != Internal.MessageExecutionState.UNTOUCHED + ) revert ManualExecutionNotAllowed(); // Manual Execution data can be invoked from a different tool or front-end to avoid having to // gather execution report data manually (bool success,) = address(s_offRamp).call(s_manualExecutionData); - assertTrue(success, "Manual execution call reverted"); + if(!success) revert ManualExecutionFailed(); executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber); - assertTrue(executionState == Internal.MessageExecutionState.SUCCESS, "Message was not executed successfully"); + if(executionState != Internal.MessageExecutionState.SUCCESS) revert ManualExecutionFailed(); console.log("Script completed..."); } From 30047c321528d5eb88b1e1016bae6f1aeb42c94a Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 10 Apr 2025 11:18:41 -0400 Subject: [PATCH 5/9] forge fmt --- .../evm/contracts/scripts/CCIPManualExecutionScript.s.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol index 18575b7677..35bd172be3 100644 --- a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol @@ -43,7 +43,7 @@ contract CCIPSendTestScript is Script { // Check that the messageId is not empty Internal.MessageExecutionState executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber); - if( + if ( executionState != Internal.MessageExecutionState.FAILURE && executionState != Internal.MessageExecutionState.UNTOUCHED ) revert ManualExecutionNotAllowed(); @@ -51,10 +51,10 @@ contract CCIPSendTestScript is Script { // Manual Execution data can be invoked from a different tool or front-end to avoid having to // gather execution report data manually (bool success,) = address(s_offRamp).call(s_manualExecutionData); - if(!success) revert ManualExecutionFailed(); + if (!success) revert ManualExecutionFailed(); executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber); - if(executionState != Internal.MessageExecutionState.SUCCESS) revert ManualExecutionFailed(); + if (executionState != Internal.MessageExecutionState.SUCCESS) revert ManualExecutionFailed(); console.log("Script completed..."); } From 88fffb1ee6e2a6822534c4cfc6a706283dc0faf1 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 14 Apr 2025 11:25:31 -0400 Subject: [PATCH 6/9] adjust coverage CI policy --- .github/workflows/solidity-foundry.yml | 2 +- chains/evm/scripts/lcov_prune | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/solidity-foundry.yml b/.github/workflows/solidity-foundry.yml index e9986c4b55..c9e09f3363 100644 --- a/.github/workflows/solidity-foundry.yml +++ b/.github/workflows/solidity-foundry.yml @@ -28,7 +28,7 @@ jobs: run: | cat < matrix.json [ - { "name": "ccip", "setup": { "run-coverage": true, "min-coverage": 98.7, "extra-coverage-params": "--no-match-path='*End2End*|*.s.sol'", "run-gas-snapshot": true}} + { "name": "ccip", "setup": { "run-coverage": true, "min-coverage": 98.7, "extra-coverage-params": "--no-match-path='*End2End*'", "run-gas-snapshot": true}} ] EOF diff --git a/chains/evm/scripts/lcov_prune b/chains/evm/scripts/lcov_prune index f1daec4b41..2682d609e9 100755 --- a/chains/evm/scripts/lcov_prune +++ b/chains/evm/scripts/lcov_prune @@ -13,6 +13,7 @@ exclusion_list_ccip=( "contracts/libraries/MerkleMultiProof.sol" "contracts/applications/CCIPClientExample.sol" "contracts/test/*" + "contracts/scripts/*" ) echo "Excluding the following files" From 1c9f6a91f1b004671432fa22ab836b89555152e4 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 21 Apr 2025 10:01:54 -0500 Subject: [PATCH 7/9] fix imports --- .../evm/contracts/scripts/CCIPManualExecutionScript.s.sol | 4 ++-- chains/evm/contracts/scripts/CCIPSendTestScript.s.sol | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol index 35bd172be3..9cfbcd0191 100644 --- a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.24; import {Internal} from "../libraries/Internal.sol"; import {OffRamp} from "../offRamp/OffRamp.sol"; -import {IERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; import {Script} from "forge-std/Script.sol"; diff --git a/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol b/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol index ddca5a7b18..0933d53a4c 100644 --- a/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.24; import {Router} from "../Router.sol"; import {Client} from "../libraries/Client.sol"; -import {IERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; import {Script} from "forge-std/Script.sol"; @@ -44,12 +44,14 @@ contract CCIPSendTestScript is Script { console.log("Sender: %s", sender); console.log("Starting Script..."); + // Create the EVMTokenAmount array and populate with the first token Client.EVMTokenAmount[] memory tokens; if (TOKEN0 != address(0)) { tokens = new Client.EVMTokenAmount[](1); tokens[0] = Client.EVMTokenAmount({token: TOKEN0, amount: TOKEN0_AMOUNT}); } + // Construct the EVM2AnyMessage Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ receiver: abi.encode(sender), data: s_data, @@ -75,7 +77,6 @@ contract CCIPSendTestScript is Script { } } - // Uncomment the following line for debugging purposes console.log("--- Tokens Approved ---"); console.log("2. Approving Fee Token"); From 929a8463c5cbbf120785d8b8d0ef15e2b11e02f7 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 29 Apr 2025 10:59:35 -0500 Subject: [PATCH 8/9] formalize scripts to make public --- .../scripts/CCIPManualExecutionScript.s.sol | 87 ++++++--- .../scripts/CCIPSendTestScript.s.sol | 166 +++++++++++------- 2 files changed, 158 insertions(+), 95 deletions(-) diff --git a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol index 9cfbcd0191..31dd6f69d7 100644 --- a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol @@ -1,62 +1,91 @@ pragma solidity ^0.8.24; +import {Router} from "../Router.sol"; import {Internal} from "../libraries/Internal.sol"; import {OffRamp} from "../offRamp/OffRamp.sol"; import {IERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; - import {Script} from "forge-std/Script.sol"; - // solhint-disable-next-line no-console import {console2 as console} from "forge-std/console2.sol"; -/* solhint-disable no-console */ -contract CCIPSendTestScript is Script { +// solhint-disable no-console +/// @title CCIPManualExecutionScript +/// @notice A foundry script for manually executing undelivered messages on a destination chain in CCIP. +/// @dev This script has NOT been audited and is NOT intended for production usage. It is intended only for +/// local debugging and testing with existing deployed contracts. +/// @dev Usage: "forge script scripts/CCIPManualExecutionScript.s.sol:CCIPManualExecutionScript -vvvv" +contract CCIPManualExecutionScript is Script { using SafeERC20 for IERC20; error ManualExecutionFailed(); error ManualExecutionNotAllowed(); - // Ex: "ETHEREUM_RPC_URL" as defined in .env - string public RPC_IDENTIFIER; - - OffRamp public s_offRamp; - - bytes32 public s_messageId; - - uint64 public s_sourceChainSelector; - uint64 public s_sequenceNumber; - bytes public s_manualExecutionData; - function run() public { - vm.createSelectFork(RPC_IDENTIFIER); + // 1. Define which chain you would like to use (Ex: "ETHEREUM" as defined in .env) + // [INSERT BELOW] + string memory chainIdentifier; - uint256 privateKey = vm.envUint("PRIVATE_KEY"); + // 2. Retrieve the RPC-URL based on the identifier and defined in .env + // Ex: "ETHEREUM" -> "ETHEREUM_RPC_URL" + vm.createSelectFork(string.concat(chainIdentifier, "_RPC_URL")); + // 3. Acquire the private key from the .env file and derive address + uint256 privateKey = vm.envUint("PRIVATE_KEY"); address sender = vm.rememberKey(privateKey); - vm.startBroadcast(privateKey); - console.log("Sender: %s", sender); console.log("Starting Script..."); - // Check that the messageId is not empty - Internal.MessageExecutionState executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber); + // 4. Define the selector of the source chain the message originated on. + // [INSERT BELOW] + uint64 sourceChainSelector; + + // 5. Define the sequence number of the message to be manually executed. The sequence number can be found + // on the CCIP-Explorer page for the given message. + // Note: The OffRamp will use the sequencer number and NOT the messageId to acquire message status, so if this + // value is set incorrectly, unexpected behavior may result by this script. + uint64 sequenceNumber; + + // 6. Acquire the address of the OffRamp to send the manual execution call to + // [INSERT BELOW] + address router = vm.envAddress(string.concat(chainIdentifier, "_ROUTER")); + address offRamp; + Router.OffRamp[] memory offRamps = Router(router).getOffRamps(); + // Perform a linear search for the offRamp contract using the known source chain selector. + // Note: Given that this operation is performed off-chain, a linear search is an acceptable time-complexity. + for (uint256 i = 0; i < offRamps.length; ++i) { + if (offRamps[i].sourceChainSelector == sourceChainSelector) { + offRamp = offRamps[i].offRamp; + break; + } + } + + // 7. Check that the message status is appropriate for manual execution, revert otherwise. + Internal.MessageExecutionState executionState = + OffRamp(offRamp).getExecutionState(sourceChainSelector, sequenceNumber); if ( executionState != Internal.MessageExecutionState.FAILURE && executionState != Internal.MessageExecutionState.UNTOUCHED ) revert ManualExecutionNotAllowed(); - // Manual Execution data can be invoked from a different tool or front-end to avoid having to - // gather execution report data manually - (bool success,) = address(s_offRamp).call(s_manualExecutionData); - if (!success) revert ManualExecutionFailed(); + // 8. Define the manual execution data to be used. Given that manual execution data can be hard to derive manually, + // due to the existence of offChain data and various proof flags, the data for manual execution should be acquired + // from the CCIP-Explorer webpage or the ccip-tools-ts repository on Github (https://github.com/smartcontractkit/ccip-tools-ts) + // Manual Execution Tutorial: https://docs.chain.link/ccip/tutorials/manual-execution#trigger-manual-execution + // Note: The manual execution data should be formatted as calldata to be sent to the OffRamp and invoke the + // function "manuallyExecute" + bytes memory manualExecutionData; - executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber); - if (executionState != Internal.MessageExecutionState.SUCCESS) revert ManualExecutionFailed(); + // 9. Attempt the call to the offRamp to manually execute the message. + vm.startBroadcast(privateKey); + (bool success,) = address(offRamp).call(manualExecutionData); + vm.stopBroadcast(); - console.log("Script completed..."); + // 10. Revert if the execution was not successful. + if (!success) revert ManualExecutionFailed(); + executionState = OffRamp(offRamp).getExecutionState(sourceChainSelector, sequenceNumber); + if (executionState != Internal.MessageExecutionState.SUCCESS) revert ManualExecutionFailed(); } } -/* solhint-enable no-console */ diff --git a/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol b/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol index 0933d53a4c..715a88dfd3 100644 --- a/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol @@ -5,99 +5,133 @@ import {Client} from "../libraries/Client.sol"; import {IERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@chainlink/vendor/openzeppelin-solidity/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol"; - import {Script} from "forge-std/Script.sol"; -/* solhint-disable-next-line no-console */ +// solhint-disable-next-line no-console import {console2 as console} from "forge-std/console2.sol"; /* solhint-disable no-console */ +/// @title CCIPSendTestScript +/// @notice This is a foundry script for sending messages through CCIP. +/// @dev This script has NOT been audited, and is NOT intended for use in production. It is intended to aid in +/// local debugging and testing with existing deployed contracts. +/// @dev Usage: "forge script scripts/CCIPSendTestScript.s.sol:CCIPSendTestScript" contract CCIPSendTestScript is Script { using SafeERC20 for IERC20; - address public ROUTER; - address public FEE_TOKEN; - - address public TOKEN0; - uint256 public TOKEN0_AMOUNT; - - uint64 public DESTINATION_CHAIN_SELECTOR; - uint64 public SOURCE_CHAIN_SELECTOR; - - // Ex: "ETHEREUM_RPC_URL" as defined in .env - string public RPC_DESCRIPTOR; - - bytes public s_extraArgs; - bytes public s_recipient; - bytes public s_data; + error ChainNotSupported(uint64 destChainSelector); function run() public { - vm.createSelectFork(RPC_DESCRIPTOR); + // 1. Define which chain you would like to use (Ex: "ETHEREUM" as defined in .env) + // [INSERT BELOW] + string memory chainIdentifier; - uint256 privateKey = vm.envUint("PRIVATE_KEY"); + // Retrieve the RPC-URL based on the identifier and defined in .env + // Ex: "ETHEREUM" -> "ETHEREUM_RPC_URL" + vm.createSelectFork(string.concat(chainIdentifier, "_RPC_URL")); + uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Acquire the private key from the .env file and derive address address sender = vm.rememberKey(privateKey); - s_recipient = abi.encode(sender); vm.startBroadcast(privateKey); console.log("Sender: %s", sender); console.log("Starting Script..."); - // Create the EVMTokenAmount array and populate with the first token - Client.EVMTokenAmount[] memory tokens; - if (TOKEN0 != address(0)) { - tokens = new Client.EVMTokenAmount[](1); - tokens[0] = Client.EVMTokenAmount({token: TOKEN0, amount: TOKEN0_AMOUNT}); - } - - // Construct the EVM2AnyMessage - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(sender), - data: s_data, - tokenAmounts: tokens, - feeToken: address(0), - extraArgs: s_extraArgs - }); - - uint256 fee = Router(ROUTER).getFee(DESTINATION_CHAIN_SELECTOR, message); - - console.log("Fee in WEI: %s", fee); - - console.log("1. Approving Send Tokens..."); - - for (uint256 i = 0; i < tokens.length; i++) { - // Since sender may be an EOA with an existing approval, the allowance should be checked first - uint256 allowance = IERC20(tokens[i].token).allowance(sender, ROUTER); - - // Approving Tokens for Router if allowance is currently insufficient. - if (allowance < tokens[i].amount) { - console.log("Approving %i tokens to Router for %s", tokens[i].amount, tokens[i].token); - IERC20(tokens[i].token).safeIncreaseAllowance(ROUTER, tokens[i].amount); + Client.EVM2AnyMessage memory message; + address feeToken; + + // 1. Define the CCIP-Specific Chain Selector to send the message to. Automatic + // [INSERT BELOW] + uint64 destChainSelector; + + // 2. Get the router address based on the current local chain identifier + // Ex: "ETHEREUM" -> "ETHEREUM_ROUTER" + address router = vm.envAddress(string.concat(chainIdentifier, "_ROUTER")); + + // Scoping to prevent a "stack-too-deep" error. + { + // 3. Validate that the destination chain selector is supported by CCIP, and revert if not. + bool isSupported = Router(router).isChainSupported(destChainSelector); + if (!isSupported) revert ChainNotSupported(destChainSelector); + + // 4. Declare how many unique tokens should be sent in the message + // [INSERT BELOW] + uint256 numTokens; + + address[] memory tokenAddresses = new address[](numTokens); + uint256[] memory tokenAmounts = new uint256[](numTokens); + + // 5. Manually define the addresses and amounts of each token that should be sent. They will automatically + // be converted into a Client.EVMTokenAmount format for the CCIP-Message. + // Ex: tokenAddresses[0] = address(1); + // Ex: tokenAmounts[0] = 1e18; + // [INSERT HERE] + + console.log("1. Approving Send Tokens..."); + + Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](numTokens); + for (uint256 i = 0; i < tokens.length; ++i) { + if (tokenAddresses[i] != address(0)) { + // Since the sender may be an EOA with an existing approval, the allowance is checked first. + uint256 allowance = IERC20(tokens[i].token).allowance(sender, router); + + // If the existing allowance is insufficient, increase it to allow sending through CCIP. + if (allowance < tokens[i].amount) { + console.log("Approving %i tokens to Router for %s", tokenAmounts[i], tokenAddresses[i]); + IERC20(tokens[i].token).safeIncreaseAllowance(router, tokenAmounts[i]); + } + + // Once approval is granted, copy into the EVM Token Amount Array to be included in the message-proper. + tokens[i] = Client.EVMTokenAmount({token: tokenAddresses[i], amount: tokenAmounts[i]}); + } } + console.log("--- Tokens Approved ---"); + + // 6. Define the message recipient. + // [INSERT BELOW] + bytes memory recipient; + + // 7. Define the message data to be passed to the recipient if it is NOT an EOA. + // [INSERT BELOW] + bytes memory data; + + // 8. If any extraArgs are needed, define below. Due to different chains families having different + // extraArgs formats, they should be passed as raw bytes, and encoded separately. + // [INSERT BELOW] + bytes memory extraArgs; + + // 9. Define the fee token to pay for the message. + // Ex: feeToken = address(0); + // [INSERT BELOW] + + // 10. Construct the EVM2AnyMessage using the fields defined above. + message = Client.EVM2AnyMessage({ + receiver: recipient, + data: data, + tokenAmounts: tokens, + feeToken: feeToken, + extraArgs: extraArgs + }); } - console.log("--- Tokens Approved ---"); - - console.log("2. Approving Fee Token"); - if (FEE_TOKEN != address(0)) { - console.log("Approving Fee Token %s to Router", FEE_TOKEN); - IERC20(FEE_TOKEN).safeIncreaseAllowance(ROUTER, fee); + // 11. Calculate the fee in WEI for the message and approve the router if necessary. + // Note: Even if the token is not native, it will still be provided in WEI. + uint256 fee = Router(router).getFee(destChainSelector, message); + console.log("Fee in WEI: %s", fee); + if (feeToken != address(0)) { + console.log("Approving Fee Token %s to Router", feeToken); + IERC20(feeToken).safeIncreaseAllowance(router, fee); } - // --- Fee Token Approved --- - console.log("3. Sending message from: %i to %i", SOURCE_CHAIN_SELECTOR, DESTINATION_CHAIN_SELECTOR); - - // Send the message, forwarding native tokens if necessary to pay the fee - bytes32 messageId = - Router(ROUTER).ccipSend{value: FEE_TOKEN == address(0) ? fee : 0}(DESTINATION_CHAIN_SELECTOR, message); + // 12. Send the message, forwarding native tokens if necessary to pay the fee. + console.log("Sending message to %i", destChainSelector); + bytes32 messageId = Router(router).ccipSend{value: feeToken == address(0) ? fee : 0}(destChainSelector, message); + // Foundry's console library does not support including bytes32 as a parameter so it is printed separately. console.log("--- Message sent: MessageId ---"); - console.logBytes32(messageId); - vm.stopBroadcast(); - console.log("Script completed..."); + vm.stopBroadcast(); } } -/* solhint-enable no-console */ From 03d0bf244f52f88bedaabf1e47bda6930937fdba Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 5 May 2025 14:10:28 -0500 Subject: [PATCH 9/9] move user provided values to storage not inside function call --- .../scripts/CCIPManualExecutionScript.s.sol | 54 +++++++--------- .../scripts/CCIPSendTestScript.s.sol | 61 ++++++------------- 2 files changed, 43 insertions(+), 72 deletions(-) diff --git a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol index 31dd6f69d7..8b33a02fdf 100644 --- a/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol @@ -22,39 +22,40 @@ contract CCIPManualExecutionScript is Script { error ManualExecutionFailed(); error ManualExecutionNotAllowed(); - function run() public { - // 1. Define which chain you would like to use (Ex: "ETHEREUM" as defined in .env) - // [INSERT BELOW] - string memory chainIdentifier; + string public chainIdentifier; // The Chain to use (Ex: "ETHEREUM" as defined in the .env) + uint64 sourceChainSelector; // The CCIP chain selector the message originated from + + // Define the sequence number of the message to be manually executed. The sequence number can be found + // on the CCIP-Explorer page for the given message. + // Note: The OffRamp will use the sequencer number and NOT the messageId to acquire message status, so if this + // value is set incorrectly, unexpected behavior may result by this script. + uint64 public sequenceNumber; - // 2. Retrieve the RPC-URL based on the identifier and defined in .env - // Ex: "ETHEREUM" -> "ETHEREUM_RPC_URL" + // Define the manual execution data to be used. Given that manual execution data can be hard to derive manually, + // due to the existence of offChain data and various proof flags, the data for manual execution should be acquired + // from the CCIP-Explorer webpage or the ccip-tools-ts repository on Github (https://github.com/smartcontractkit/ccip-tools-ts) + // Manual Execution Tutorial: https://docs.chain.link/ccip/tutorials/manual-execution#trigger-manual-execution + // Note: The manual execution data should be formatted as calldata to be sent to the OffRamp and invoke the + // function "manuallyExecute" + bytes public manualExecutionData; + + function run() public { vm.createSelectFork(string.concat(chainIdentifier, "_RPC_URL")); - // 3. Acquire the private key from the .env file and derive address + // Acquire the private key from the .env file and derive address uint256 privateKey = vm.envUint("PRIVATE_KEY"); address sender = vm.rememberKey(privateKey); console.log("Sender: %s", sender); console.log("Starting Script..."); - // 4. Define the selector of the source chain the message originated on. - // [INSERT BELOW] - uint64 sourceChainSelector; - - // 5. Define the sequence number of the message to be manually executed. The sequence number can be found - // on the CCIP-Explorer page for the given message. - // Note: The OffRamp will use the sequencer number and NOT the messageId to acquire message status, so if this - // value is set incorrectly, unexpected behavior may result by this script. - uint64 sequenceNumber; - - // 6. Acquire the address of the OffRamp to send the manual execution call to - // [INSERT BELOW] address router = vm.envAddress(string.concat(chainIdentifier, "_ROUTER")); address offRamp; Router.OffRamp[] memory offRamps = Router(router).getOffRamps(); + // Perform a linear search for the offRamp contract using the known source chain selector. - // Note: Given that this operation is performed off-chain, a linear search is an acceptable time-complexity. + // Note: Given that this operation is being performed off-chain, and thus gas is not a consideration, + // a linear search is an acceptable time-complexity. for (uint256 i = 0; i < offRamps.length; ++i) { if (offRamps[i].sourceChainSelector == sourceChainSelector) { offRamp = offRamps[i].offRamp; @@ -62,7 +63,6 @@ contract CCIPManualExecutionScript is Script { } } - // 7. Check that the message status is appropriate for manual execution, revert otherwise. Internal.MessageExecutionState executionState = OffRamp(offRamp).getExecutionState(sourceChainSelector, sequenceNumber); if ( @@ -70,20 +70,12 @@ contract CCIPManualExecutionScript is Script { && executionState != Internal.MessageExecutionState.UNTOUCHED ) revert ManualExecutionNotAllowed(); - // 8. Define the manual execution data to be used. Given that manual execution data can be hard to derive manually, - // due to the existence of offChain data and various proof flags, the data for manual execution should be acquired - // from the CCIP-Explorer webpage or the ccip-tools-ts repository on Github (https://github.com/smartcontractkit/ccip-tools-ts) - // Manual Execution Tutorial: https://docs.chain.link/ccip/tutorials/manual-execution#trigger-manual-execution - // Note: The manual execution data should be formatted as calldata to be sent to the OffRamp and invoke the - // function "manuallyExecute" - bytes memory manualExecutionData; - - // 9. Attempt the call to the offRamp to manually execute the message. + // Attempt the call to the offRamp to manually execute the message. vm.startBroadcast(privateKey); (bool success,) = address(offRamp).call(manualExecutionData); vm.stopBroadcast(); - // 10. Revert if the execution was not successful. + // Revert if the execution was not successful. if (!success) revert ManualExecutionFailed(); executionState = OffRamp(offRamp).getExecutionState(sourceChainSelector, sequenceNumber); if (executionState != Internal.MessageExecutionState.SUCCESS) revert ManualExecutionFailed(); diff --git a/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol b/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol index 715a88dfd3..0d97037e84 100644 --- a/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol +++ b/chains/evm/contracts/scripts/CCIPSendTestScript.s.sol @@ -21,16 +21,23 @@ contract CCIPSendTestScript is Script { error ChainNotSupported(uint64 destChainSelector); - function run() public { - // 1. Define which chain you would like to use (Ex: "ETHEREUM" as defined in .env) - // [INSERT BELOW] - string memory chainIdentifier; + // For the script to run successfully, please define the following constants below + // [REQUIRED] + string public chainIdentifier; // The Chain to use (Ex: "ETHEREUM" as defined in the .env) + uint64 public destChainSelector; // The CCIP-Specific chain selector to send the message to + uint256 public numTokens; // The number of tokens to be sent + + bytes public recipient; // The recipient to receive both the tokens and the arbitrary data. + bytes public data; // Define the message data to be passed to the recipient if it is NOT an EOA + bytes public extraArgs; // If any extraArgs are needed, define below. Due to different chains families having different extraArgs formats, they should be passed as raw bytes, and encoded separately. + + address public feeToken; // The token to pay CCIP Message Fees in, address(0) for native. - // Retrieve the RPC-URL based on the identifier and defined in .env - // Ex: "ETHEREUM" -> "ETHEREUM_RPC_URL" + function run() public { vm.createSelectFork(string.concat(chainIdentifier, "_RPC_URL")); - uint256 privateKey = vm.envUint("PRIVATE_KEY"); // Acquire the private key from the .env file and derive address + // Acquire the private key from the .env file and derive address + uint256 privateKey = vm.envUint("PRIVATE_KEY"); address sender = vm.rememberKey(privateKey); vm.startBroadcast(privateKey); @@ -39,36 +46,25 @@ contract CCIPSendTestScript is Script { console.log("Starting Script..."); Client.EVM2AnyMessage memory message; - address feeToken; - - // 1. Define the CCIP-Specific Chain Selector to send the message to. Automatic - // [INSERT BELOW] - uint64 destChainSelector; - // 2. Get the router address based on the current local chain identifier - // Ex: "ETHEREUM" -> "ETHEREUM_ROUTER" + // Get the router using the chain identifier Ex: "ETHEREUM" -> "ETHEREUM_ROUTER" address router = vm.envAddress(string.concat(chainIdentifier, "_ROUTER")); // Scoping to prevent a "stack-too-deep" error. { - // 3. Validate that the destination chain selector is supported by CCIP, and revert if not. bool isSupported = Router(router).isChainSupported(destChainSelector); if (!isSupported) revert ChainNotSupported(destChainSelector); - // 4. Declare how many unique tokens should be sent in the message - // [INSERT BELOW] - uint256 numTokens; - address[] memory tokenAddresses = new address[](numTokens); uint256[] memory tokenAmounts = new uint256[](numTokens); - // 5. Manually define the addresses and amounts of each token that should be sent. They will automatically + // Since solidity does not support array literals being defined in storage or constant, manually define the addresses and amounts of each token that should be sent. They will automatically // be converted into a Client.EVMTokenAmount format for the CCIP-Message. // Ex: tokenAddresses[0] = address(1); // Ex: tokenAmounts[0] = 1e18; // [INSERT HERE] - console.log("1. Approving Send Tokens..."); + console.log("Approving Send Tokens..."); Client.EVMTokenAmount[] memory tokens = new Client.EVMTokenAmount[](numTokens); for (uint256 i = 0; i < tokens.length; ++i) { @@ -88,24 +84,7 @@ contract CCIPSendTestScript is Script { } console.log("--- Tokens Approved ---"); - // 6. Define the message recipient. - // [INSERT BELOW] - bytes memory recipient; - - // 7. Define the message data to be passed to the recipient if it is NOT an EOA. - // [INSERT BELOW] - bytes memory data; - - // 8. If any extraArgs are needed, define below. Due to different chains families having different - // extraArgs formats, they should be passed as raw bytes, and encoded separately. - // [INSERT BELOW] - bytes memory extraArgs; - - // 9. Define the fee token to pay for the message. - // Ex: feeToken = address(0); - // [INSERT BELOW] - - // 10. Construct the EVM2AnyMessage using the fields defined above. + // Construct the EVM2AnyMessage using the fields defined above. message = Client.EVM2AnyMessage({ receiver: recipient, data: data, @@ -115,7 +94,7 @@ contract CCIPSendTestScript is Script { }); } - // 11. Calculate the fee in WEI for the message and approve the router if necessary. + // Calculate the fee in WEI for the message and approve the router if necessary. // Note: Even if the token is not native, it will still be provided in WEI. uint256 fee = Router(router).getFee(destChainSelector, message); console.log("Fee in WEI: %s", fee); @@ -124,7 +103,7 @@ contract CCIPSendTestScript is Script { IERC20(feeToken).safeIncreaseAllowance(router, fee); } - // 12. Send the message, forwarding native tokens if necessary to pay the fee. + // Send the message, forwarding native tokens if necessary to pay the fee. console.log("Sending message to %i", destChainSelector); bytes32 messageId = Router(router).ccipSend{value: feeToken == address(0) ? fee : 0}(destChainSelector, message);