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 all 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/
83 changes: 83 additions & 0 deletions chains/evm/contracts/scripts/CCIPManualExecutionScript.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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
/// @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();

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;

// 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"));

// 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...");

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

Internal.MessageExecutionState executionState =
OffRamp(offRamp).getExecutionState(sourceChainSelector, sequenceNumber);
if (
executionState != Internal.MessageExecutionState.FAILURE
&& executionState != Internal.MessageExecutionState.UNTOUCHED
) revert ManualExecutionNotAllowed();

// Attempt the call to the offRamp to manually execute the message.
vm.startBroadcast(privateKey);
(bool success,) = address(offRamp).call(manualExecutionData);
vm.stopBroadcast();

// Revert if the execution was not successful.
if (!success) revert ManualExecutionFailed();
executionState = OffRamp(offRamp).getExecutionState(sourceChainSelector, sequenceNumber);
if (executionState != Internal.MessageExecutionState.SUCCESS) revert ManualExecutionFailed();
}
}
116 changes: 116 additions & 0 deletions chains/evm/contracts/scripts/CCIPSendTestScript.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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 */
/// @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;

error ChainNotSupported(uint64 destChainSelector);

// 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.

function run() public {
vm.createSelectFork(string.concat(chainIdentifier, "_RPC_URL"));

// 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...");

Client.EVM2AnyMessage memory message;

// 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.
{
bool isSupported = Router(router).isChainSupported(destChainSelector);
if (!isSupported) revert ChainNotSupported(destChainSelector);

address[] memory tokenAddresses = new address[](numTokens);
uint256[] memory tokenAmounts = new uint256[](numTokens);

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

// Construct the EVM2AnyMessage using the fields defined above.
message = Client.EVM2AnyMessage({
receiver: recipient,
data: data,
tokenAmounts: tokens,
feeToken: feeToken,
extraArgs: extraArgs
});
}

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

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