Using the CCIP Local Simulator to fork mainnets
This guide explains how to use the CCIP Local Simulator to test cross-chain transactions in forked mainnet environments using Foundry. You'll learn how to leverage the Register.sol
contract to streamline the simulation process and properly configure mainnet details that aren't included by default.
Understanding Network Registration
The Register.sol
smart contract contains pre-configured details for test networks to help accelerate cross-chain transaction simulations in Foundry. However, mainnet details are intentionally excluded and must be manually configured.
Here's an example of how to verify network details for a test network such as Sepolia:
pragma solidity ^0.8.0;
import { Test, console } from "forge-std/Test.sol"
import {
CCIPLocalSimulatorFork,
Register
} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol"
contract ExampleTest is Test {
CCIPLocalSimulatorFork public ccipLocalSimulatorFork;
uint256 public ethSepoliaFork;
Register.NetworkDetails public ethSepoliaNetworkDetails;
function setUp() public {
// Create a fork of the Sepolia network
string memory SEPOLIA_RPC_URL = vm.envString("SEPOLIA_RPC_URL");
ethSepoliaFork = vm.createFork(SEPOLIA_RPC_URL);
// Initialize the CCIP simulator
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
vm.makePersistent(address(ccipLocalSimulatorFork));
// Select and verify the fork
vm.selectFork(ethSepoliaFork);
assertEq(block.chainid, 11155111);
// Verify network details match the CCIP Directory
ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid);
assertEq(ethSepoliaNetworkDetails.chainSelector, 16015286601757825753);
assertEq(ethSepoliaNetworkDetails.routerAddress, 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59);
}
}
Working with Mainnet Forks
The CCIP Local Simulator supports any forked environment where CCIP contracts exist. Since mainnet details aren't included in Register.sol
, you'll need to configure them manually. Let's walk through a complete example that demonstrates how to simulate cross-chain messages between Ethereum and Polygon mainnets. This example shows:
- Setting up mainnet forks
- Configuring network details
- Creating and sending a cross-chain message
- Routing the message between chains
Here's the complete implementation:
pragma solidity ^0.8.0;
import { Test, console } from "forge-std/Test.sol";
import { CCIPLocalSimulatorFork, Register } from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol";
import { IRouterClient } from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import { Client } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract ExampleTest is Test {
CCIPLocalSimulatorFork public ccipLocalSimulatorFork;
uint256 public ethereumMainnetForkId;
uint256 public polygonMainnetForkId;
function setUp() public {
// Create forks of both networks
string memory ETHEREUM_MAINNET_RPC_URL = vm.envString("ETHEREUM_MAINNET_RPC_URL");
string memory POLYGON_MAINNET_RPC_URL = vm.envString("POLYGON_MAINNET_RPC_URL");
ethereumMainnetForkId = vm.createFork(ETHEREUM_MAINNET_RPC_URL);
polygonMainnetForkId = vm.createFork(POLYGON_MAINNET_RPC_URL);
address ethereumMainnetCcipRouterAddress = 0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D;
uint64 polygonMainnetChainSelector = 4051577828743386545;
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
ccipLocalSimulatorFork.setNetworkDetails(
polygonMainnetForkId,
Register.NetworkDetails({
chainSelector: polygonMainnetChainSelector,
routerAddress: polygonMainnetCcipRouterAddress,
linkAddress: address(0), // not needed for this test
wrappedNativeAddress: address(0), // not needed for this test
ccipBnMAddress: address(0), // not needed for this test
ccipLnMAddress: address(0), // not needed for this test
rmnProxyAddress: address(0), // not needed for this test
registryModuleOwnerCustomAddress: address(0), // not needed for this test
tokenAdminRegistryAddress: address(0) // not needed for this test
})
);
vm.makePersistent(address(ccipLocalSimulatorFork));
}
function test_example() public {
// Set up the source chain (Ethereum)
vm.selectFork(ethereumMainnetForkId);
Register.NetworkDetails memory polygonMainnetNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(
polygonMainnetForkId
);
address alice = makeAddr("alice");
vm.deal(alice, 1 ether);
// Prepare the cross-chain message
vm.startPrank(alice);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(ccipReceiverAddress),
data: abi.encode("Hello world"),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: "",
feeToken: address(0)
});
// Send the message using CCIP
IRouterClient(ethereumMainnetCcipRouterAddress).ccipSend(polygonMainnetNetworkDetails.routerAddress, message);
vm.stopPrank();
// Route the message to Polygon
ccipLocalSimulatorFork.switchChainAndRouteMessage(polygonMainnetForkId);
}
}
Code explanation:
-
Network Setup: The
setUp()
function creates forks of both Ethereum and Polygon mainnets and initializes the CCIP simulator. -
Network Configuration: We configure Polygon mainnet details using
setNetworkDetails()
. This step is crucial because mainnet details aren't included inRegister.sol
. The configuration includes:chainSelector
: The unique CCIP identifier for the Polygon network. Check the CCIP Directory for the correct value.routerAddress
: The address of the CCIP router on Polygon. Check the CCIP Directory for the correct value.- Parameters that can be set to
address(0)
because they are optional for messaging.
-
Message Transfer: The
test_example()
function demonstrates:- Setting up a test user (alice) with funds
- Creating a cross-chain message
- Sending the message through CCIP
- Routing the message to the destination chain
After this configuration, you can simulate cross-chain messages between mainnet forks, enabling thorough testing of your cross-chain applications in a local environment.