Using CCIP local simulator in your Foundry project
You can use Chainlink Local to run CCIP in a localhost environment within your Foundry project. To get started quickly, you will use the CCIP Foundry Starter Kit. This project is a Foundry boilerplate that includes the Chainlink Local package and several CCIP examples.
Prerequisites
-
In a terminal, clone the CCIP Foundry Starter Kit repository and change directories.
git clone https://github.com/smartcontractkit/ccip-starter-kit-foundry.git && \ cd ./ccip-starter-kit-foundry/ -
Run
npm installto install the dependencies. This command installs the Chainlink Local package and other required packages:npm install -
Run
forge installto install packages:forge install -
Run
forge buildto compile the contracts:forge build
Test tokens transfers
You will run a test to transfer tokens between two accounts. The test file Example01.t.sol is located in the ./test/no-fork directory. This file contains two test cases:
-
Transfer with LINK fees: This test case transfers tokens from the sender account to the receiver account, paying fees in LINK. At the end of the test, it verifies that the sender account was debited and the receiver account was credited.
-
Transfer with native gas fees: This test case transfers tokens from the sender account to the receiver account, paying fees in native gas. At the end of the test, it verifies that the sender account was debited and the receiver account was credited.
For a detailed explanation of the test file, refer to the Examine the code section.
In your terminal, run the following command to execute the test:
forge test --match-contract Example01Test
Example output:
$ forge test --match-contract Example01Test
[⠊] Compiling...
No files changed, compilation skipped
Ran 2 tests for test/no-fork/Example01.t.sol:Example01Test
[PASS] test_transferTokensFromEoaToEoaPayFeesInLink() (gas: 167576)
[PASS] test_transferTokensFromEoaToEoaPayFeesInNative() (gas: 122348)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 8.79ms (976.54µs CPU time)
Ran 1 test suite in 201.00ms (8.79ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
Examine the code
Setup
To transfer tokens using CCIP, we need the following:
- Destination chain selector
- Source CCIP router
- LINK token for paying CCIP fees
- A test token contract (such as CCIP-BnM)
- A sender account (Alice)
- A receiver account (Bob)
The setUp() function is invoked before each test case to reinitialize all the variables, ensuring a clean state for each test:
-
Initialize the CCIP local simulator contract:
ccipLocalSimulator = new CCIPLocalSimulator(); -
Invoke the
configurationfunction to retrieve the configuration details for the pre-deployed contracts and services needed for local CCIP simulations:( uint64 chainSelector, IRouterClient sourceRouter, , , LinkToken linkToken, BurnMintERC677Helper ccipBnM, ) = ccipLocalSimulator.configuration();Note: The
configurationfunction also returns the destination router, WETH9, and CCIP-LnM contracts, but we are not using them in these test cases. Hence, there are commas in the return values. -
Initialize the sender and receiver accounts:
alice = makeAddr("alice") bob = makeAddr("bob") -
All the variables are stored in the contract state for use in the test cases.
Prepare scenario (helper function)
The prepareScenario() function is invoked at the beginning of each test case. It performs the following actions:
-
Request CCIP-BnM tokens for Alice (sender):
ccipBnMToken.drip(alice); -
Approve the source router to spend tokens on behalf of Alice (sender):
amountToSend = 100; ccipBnMToken.approve(address(router), amountToSend); -
Create an array Client.EVMTokenAmount[] to specify the token transfer details. This array and the amount to send are returned by the prepareScenario() function for use in the calling test case:
tokensToSendDetails = new Client.EVMTokenAmount[](1); Client.EVMTokenAmount memory tokenToSendDetails = Client.EVMTokenAmount({token: address(ccipBnMToken), amount: amountToSend}); tokensToSendDetails[0] = tokenToSendDetails;
Test case 1: Transfer with LINK fees
The test_transferTokensFromEoaToEoaPayFeesInLink function tests the transfer of tokens between two externally owned accounts (EOA) while paying fees in LINK.
Here are the steps involved in this test case:
-
Invoke the
prepareScenario()function to set up the necessary variables:(Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) = prepareScenario(); -
Record the initial token balances for Alice (sender) and Bob (receiver):
uint256 balanceOfAliceBefore = ccipBnMToken.balanceOf(alice); uint256 balanceOfBobBefore = ccipBnMToken.balanceOf(bob); -
Begin impersonating Alice (sender) to perform the subsequent actions:
vm.startPrank(alice); -
Request 5 LINK tokens from the CCIP Local Simulator faucet for Alice (sender):
ccipLocalSimulator.requestLinkFromFaucet(alice, 5 ether); -
Construct the Client.EVM2AnyMessage structure with the receiver, token amounts, and other necessary details.
- Set the
dataparameter to an empty string because you are not sending any arbitrary data, only tokens. - In
extraArgs, set the gas limit to0. This gas limit is for execution of receiver logic, which doesn't apply here because you're sending tokens to an EOA.
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ receiver: abi.encode(bob), data: abi.encode(""), tokenAmounts: tokensToSendDetails, extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})), feeToken: address(linkToken) }); - Set the
-
Calculate the required fees for the transfer and approve the router to spend LINK tokens for these fees:
uint256 fees = router.getFee(destinationChainSelector, message); linkToken.approve(address(router), fees); -
Send the CCIP transfer request to the router:
router.ccipSend(destinationChainSelector, message); -
Stop impersonating (sender):
vm.stopPrank(); -
Record the final token balances for Alice (sender) and Bob (receiver):
uint256 balanceOfAliceAfter = ccipBnMToken.balanceOf(alice); uint256 balanceOfBobAfter = ccipBnMToken.balanceOf(bob); -
Verify that Alice's balance has decreased by the amount sent and Bob's balance has increased by the same amount:
assertEq(balanceOfAliceAfter, balanceOfAliceBefore - amountToSend); assertEq(balanceOfBobAfter, balanceOfBobBefore + amountToSend);
Test case 2: Transfer with native gas fees
The test_transferTokensFromEoaToEoaPayFeesInNative function tests the transfer of tokens between two externally owned accounts (EOA) while paying fees in native gas.
Here are the steps involved in this test case:
-
Invoke the
prepareScenario()function to set up the necessary variables. (This step is the same as in the previous test case.) -
Record the initial token balances of Alice (sender) and Bob (receiver). (This step is the same as in the previous test case.)
-
Begin impersonating Alice (sender) and provide her with native gas to pay for the fees:
vm.startPrank(alice); deal(alice, 5 ether); -
Construct the Client.EVM2AnyMessage structure. This step is the same as in the previous test case. The main difference is that the
feeTokenis set withaddress(0)to indicate that the fees are paid in native gas:Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ receiver: abi.encode(bob), data: abi.encode(""), tokenAmounts: tokensToSendDetails, extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})), feeToken: address(0) }); -
Calculate the required fees for the transfer and send the CCIP transfer request along with the necessary native gas:
uint256 fees = router.getFee(destinationChainSelector, message); router.ccipSend{value: fees}(destinationChainSelector, message); -
Stop impersonating Alice (sender) and verify the token balances for Alice (sender) and Bob (receiver). (This step is the same as in the previous test case.)
Next steps
For more advanced scenarios, please refer to other test files in the ./test/no-fork directory. To learn how to use Chainlink Local in forked environments, refer to
the guide on Using the CCIP Local Simulator in your Foundry project with forked environments.