Exercise 3: Testing Cross-Chain contracts using Chainlink Local

You might noticed so far that building with CCIP directly on test networks is not ideal due to extra points of friction - like setting up a wallet, getting testnet tokens, waiting for cross-chain transactions to complete, etc. This is because test networks are for testing and local environments (like Foundry, Hardhat or Remix IDE) are for building and unit testing.

To address this issue, we created Chainlink Local - the Chainlink CCIP Local Simulator, and in this chapter you will learn how to simulate your cross-chain transactions locally and build with Chainlink CCIP 2000x quicker compared to working on test networks!

Chainlink Local is an installable dependency, like OpenZeppelin for example. It provides a tool (the Chainlink Local Simulator) that developers import into their Foundry or Hardhat or Remix IDE projects. This tool runs Chainlink CCIP locally which means developers can rapidly explore, prototype and iterate CCIP dApps off-chain in a local environment, and move to testnet only when they're ready to test in a live environment.

Most importantly, smart contracts tested with Chainlink Local can be deployed to test networks without any modifications (assuming network specific contract addresses such as Router contracts and LINK token addresses are passed in via a constructor).

To view more detailed documentation and more examples, visit the Chainlink Local Documentation and Chainlink Local YouTube Playlist.

Local Mode vs Forked Mode

The simulator supports two modes:

  1. Local Mode - working with mock contracts on a locally running development blockchain node running on localhost, and

  2. Forked Mode - working with deployed Chainlink CCIP contracts using multiple forked networks.

Local Mode

When working in local simulation mode, the simulator pre-deploys a set of smart contracts to a blank Hardhat/Anvil network EVM state and exposes their details via a call to the configuration() function. Even though there are two Router contracts exposed, sourceRouter and destinationRouter, to support the developer's mental model of routing cross-chain messages through two different Routers, both are actually the same contract running on the locally development blockchain node.

Forked Mode

When working in fork mode, you will need to create multiple locally running blockchain networks (you need an archive node that has historical network state in the pinned block from which you have forked for local development - see here) and interact with the contract addresses provided in the Official Chainlink Documentation.

Chainlink Local is so fast because it does not spin any type of offchain component needed for cross-chain transfers. That's why CCIP Local Simulator Fork (smart contract for Foundry, and typescript script for Hardhat) exposes functionality to switch between forks and route messages to the destination blockchain directly by developers - so don't forget this step 😉.

Coding time: Let's write a unit test for our XNFT.sol smart contract

The full example is available at: https://github.com/smartcontractkit/ccip-cross-chain-nft

Creating a new Foundry project

To get started let's first create a new Foundry project by running:

If this command fails, make sure you have Foundry installed.

Installing necessary dependencies

After that we will need to install the following dependencies. If you don't want to commit changes after each installment, append the --no-commit flag to each of the following commands

@chainlink/contracts

@chainlink/contracts-ccip

@openzeppelin/contracts

@chainlink/local

And then set the following remappings in your foundry.toml or remappings.txt file:

Delete src/Counter.sol, test/Counter.t.sol and script/Counter.s.sol files. Create new XNFT.sol file in src folder and paste the content of the XNFT smart contract from the previous page/exercise. Run forge build to compile that contract. This should work if you installed dependencies properly.

Getting Ethereum Sepolia and Arbitrum Sepolia RPC URLs

Create a new file by copying the .env.example file, and name it .env. Fill in with Ethereum Sepolia and Arbitrum Sepolia RPC URLs using either local archive nodes or a service that provides archival data, like Infura or Alchemy.

Then add the rpc_endpoints section to your foundry.toml file. Its final version should look like this:

In the previous exercise, we performed 7 steps on two different test networks in order to deploy, mint and transfer XNFT from one account to another. Using Chainlink Local, you can write a test that will perform the same action, using exactly the same CCIP contracts from test networks, in your local environment, much faster.

Create a new test/XNFT.t.sol file. Paste the following content:

If you try to compile this contract you should encounter in an error because there is no EncodeExtraArgs.sol helper smart contract from the previous exercise. Fix that by creating a new file test/utils/EncodeExtraArgs.sol and paste the following code.

Let's analyze the code so far. We've imported CCIPLocalSimulatorFork from the @chainlink/local package, meaning that we are using the Forked Mode. We created a new instance of it and made it persistent across all different forks so it can work properly.

For your homework however, you will need to import CCIPLocalSimulator from the @chainlink/local package.

We also created two forks (copies of the blockchain networks at the latest block - it is the best practice to pin this block number instead of always using the latest one) of Ethereum Sepolia and Arbitrum Sepolia test networks. We can now switch between multiple forks in Foundry, but we selected Ethereum Sepolia for start.

Fork ID is Foundry's way to manage different forked networks in a same test and those are usually numbers from 1, 2, 3... etc. Fork ID is not the same thing as block.chainid - that's the actuall Chain ID of a blockchain network you can get in Solidity - 11155111 for Ethereum Sepolia, for example.

CCIPLocalSimulatorFork in Foundry only, comes up with this Register helper smart contract that contains NetworkDetails struct which looks like this:

It is pre-populated with these details for some test networks and NO MAIN NETWORKS (on purpose) so it is the best practice to always validate these information - otherwise simulations won't work:

If there is no Network Details (for main networks for example) you must add those manually using the setNetworkDetails function.

We are going to perform Steps 1) and 2) from previous exercise directly in the setup() function, like this:

And the rest of the steps from the previous exercise directly in a new test function, like this:

Full Final code:

Last updated