Skip to content

add simple foundry scripts for live testing #825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/solidity-foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**'
Expand Down
1 change: 1 addition & 0 deletions chains/evm/.solhintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Test files run with a different solhint ruleset, ignore them here.
./**/*.t.sol
./**/*.s.sol
./node_modules/
62 changes: 62 additions & 0 deletions chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
pragma solidity ^0.8.24;

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 {
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming a user is supposed to fill every param here? There's no docs on anything so not 100% sure.

How useful is this script if you need to construct the tx manually?

Copy link
Member

@0xsuryansh 0xsuryansh Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to inject the values using env
We can make a .env.example file which shows all the keys which will be used to inject the values to these variables

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. There's no way to get the sequence number dynamically on chain from the script it needs to be provided manually. I will include some comments for the user that they can get the sequencer number from the CCIP explorer front-end as well. I don't think using an ENV is any simpler if you still have to fill in the data it just adds more friction if you need to change things.
  2. The script is useful for simulating and debugging manual executions in foundry locally. During the Ronin situation recently we had to test manual executions. You can get manual execution data from a variety of other tools and I will include links to those on how to use them.


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);
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();

executionState = s_offRamp.getExecutionState(s_sourceChainSelector, s_sequenceNumber);
if (executionState != Internal.MessageExecutionState.SUCCESS) revert ManualExecutionFailed();

console.log("Script completed...");
}
}
/* solhint-enable no-console */
103 changes: 103 additions & 0 deletions chains/evm/contracts/scripts/CCIPSendTestScript.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
pragma solidity ^0.8.24;

import {Router} from "../Router.sol";
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 */
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...");

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to take specific extra args fields as input (maybe through env) and build the struct and encode it in this script

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While that would be better, as the number of different extra Args structs grows as we add more non-evm chains this may become unwieldy and unnecessary complex. This pattern is also present in some of our other example contracts such as PingPong so I feel like it is reasonable to expect the user to generate this themselves if required.

});

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);
}
}

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 */
1 change: 1 addition & 0 deletions chains/evm/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ evm_version = 'paris'

src = 'contracts'
test = 'contracts'
script = 'contracts'
out = 'foundry-artifacts'
cache_path = 'foundry-cache'
libs = ['node_modules']
Expand Down
1 change: 1 addition & 0 deletions chains/evm/scripts/lcov_prune
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ exclusion_list_ccip=(
"contracts/libraries/MerkleMultiProof.sol"
"contracts/applications/CCIPClientExample.sol"
"contracts/test/*"
"contracts/scripts/*"
)

echo "Excluding the following files"
Expand Down
Loading