Skip to content

About

The Chainlink CCIP Local Simulator, visit documentation by clicking the link below:

Topics

Resources

Stars

109 stars

Watchers

9 watching

Forks

Latest commit

 

History

134 Commits

Folders and files

Repository files navigation

Chainlink Local

Chainlink Local is an installable dependency. It provides a tool (the Chainlink Local Simulator) that developers import into their Foundry or Hardhat or Remix 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.

The package exposes a set of smart contracts and scripts with which you build, deploy and execute CCIP token transfers and arbitrary messages on a local Remix, Hardhat or Anvil (Foundry) development node. Chainlink Local also supports forked nodes.

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

Watch the demo on YouTube

Installation

0.3.x (V3) supports Foundry, Hardhat 3 and Remix IDE. Hardhat 2 users should stay on 0.2.x (npm install @chainlink/local@0.2.9).

Foundry (git)

forge install smartcontractkit/chainlink-local@v0.3.0

and then set remappings to: @chainlink/local/=lib/chainlink-local/ in either remappings.txt or foundry.toml file

Foundry (soldeer)

forge soldeer install chainlink-local~v0.3.0 https://github.com/smartcontractkit/chainlink-local.git

Hardhat 3 (npm)

npm install @chainlink/local@0.3.0

Hardhat 3 (Node.js 22) Solidity tests need no remappings: the package ships its own remappings.txt, and its OpenZeppelin and forge-std dependencies are installed with it. Fork tests need evmVersion: "cancun" or later:

// hardhat.config.ts
import { defineConfig } from "hardhat/config";

export default defineConfig({
  solidity: { version: "0.8.24", settings: { evmVersion: "cancun" } },
});
npx hardhat test solidity

For the JavaScript/TypeScript fork helpers (@chainlink/local/scripts/CCIPLocalSimulatorFork.js), also install and register the ethers plugin, and add forked networks:

npm install --save-dev @nomicfoundation/hardhat-ethers ethers
import { configVariable, defineConfig } from "hardhat/config";
import hardhatEthers from "@nomicfoundation/hardhat-ethers";

export default defineConfig({
  plugins: [hardhatEthers],
  solidity: { version: "0.8.24", settings: { evmVersion: "cancun" } },
  networks: {
    sepoliaFork: {
      type: "edr-simulated",
      chainType: "generic",
      chainId: 11155111,
      forking: { url: configVariable("ETHEREUM_SEPOLIA_RPC_URL") },
    },
  },
});

Foundry (npm)

Installing from npm into a Foundry project works too; list the package's dependencies in your remappings.txt (Foundry does not load the remappings of packages under node_modules):

@chainlink/local/=node_modules/@chainlink/local/
@chainlink/contracts-ccip/=node_modules/@chainlink/contracts-ccip/
@chainlink/contracts/=node_modules/@chainlink/contracts/
@openzeppelin/contracts@4.8.3/=node_modules/@openzeppelin/contracts-4.8.3/
@openzeppelin/contracts@5.3.0/=node_modules/@openzeppelin/contracts-5.3.0/
forge-std/=node_modules/forge-std/src/

Remix IDE (local mode)

import "https://github.com/smartcontractkit/chainlink-local/blob/v0.3.0/src/ccip/CCIPLocalSimulator.sol";

Remix resolves the @chainlink/contracts and @chainlink/contracts-ccip imports to their latest npm versions.

Once you have installed CCIP Local, you are now ready to start using it with your project.

Usage

Import CCIPLocalSimulator.sol inside your tests or scripts, for example (this is test/smoke/ccip/ReadmeUsageExample.t.sol):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {
    CCIPLocalSimulator,
    IRouterClient,
    WETH9,
    LinkToken,
    BurnMintERC677Helper
} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {ExtraArgsCodec} from "@chainlink/contracts-ccip/contracts/libraries/ExtraArgsCodec.sol";
import {FinalityCodec} from "@chainlink/contracts-ccip/contracts/libraries/FinalityCodec.sol";

contract ReadmeUsageExampleTest is Test {
    CCIPLocalSimulator public ccipLocalSimulator;
    uint64 public chainSelector;
    IRouterClient public sourceRouter;
    LinkToken public linkToken;
    BurnMintERC677Helper public ccipBnM;

    function setUp() public {
        ccipLocalSimulator = new CCIPLocalSimulator();

        WETH9 wrappedNative;
        IRouterClient destinationRouter;
        BurnMintERC677Helper ccipLnM;
        (chainSelector, sourceRouter, destinationRouter, wrappedNative, linkToken, ccipBnM, ccipLnM) =
            ccipLocalSimulator.configuration();
    }

    function test_sendTokens() public {
        address alice = makeAddr("alice");
        address bob = makeAddr("bob");
        ccipLocalSimulator.requestLinkFromFaucet(alice, 5 ether);
        ccipBnM.drip(alice);

        Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);
        tokenAmounts[0] = Client.EVMTokenAmount({token: address(ccipBnM), amount: 1 ether});

        // Fast Transfers example with 5 block confirmations
        uint32 gasLimit = 0;
        uint16 blockConfirmations = 5;
        bytes4 finalityConfig = FinalityCodec._encodeBlockDepth(blockConfirmations);
        bytes memory extraArgsBytes = ExtraArgsCodec._getBasicEncodedExtraArgsV3(gasLimit, finalityConfig);

        Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
            receiver: abi.encode(bob),
            data: "",
            tokenAmounts: tokenAmounts,
            extraArgs: extraArgsBytes,
            feeToken: address(linkToken)
        });

        vm.startPrank(alice);
        uint256 fee = sourceRouter.getFee(chainSelector, message);
        linkToken.approve(address(sourceRouter), fee);
        ccipBnM.approve(address(sourceRouter), 1 ether);
        sourceRouter.ccipSend(chainSelector, message);
        vm.stopPrank();

        assertEq(ccipBnM.balanceOf(bob), 1 ether);
    }
}

Fork mode and CCIP 2.0

CCIPLocalSimulatorFork routes messages on forked networks for every CCIP era: pre-1.6 (EVM2EVMOnRamp), 1.6 and CCIP 2.0 (CCV-based lanes, which live testnet lanes run today). Requirements for fork tests:

  • Compile fork tests with evm_version = "cancun" or later (deployed CCIP 2.0 contracts use Cancun opcodes; paris fails with EvmError: NotActivated). Use a fork-only profile so your deployed bytecode is not affected: [profile.fork] with evm_version = "cancun" in foundry.toml, then FOUNDRY_PROFILE=fork forge test.
  • Foundry >= 1.5.1 for fork tests (Hardhat 3 requires Node.js 22).
CCIPLocalSimulatorFork ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
vm.makePersistent(address(ccipLocalSimulatorFork));

// Send through the router returned by getNetworkDetails(block.chainid).routerAddress, or through the dedicated
// CCIP 2.0 router where one exists: ccipLocalSimulatorFork.getCCIPV2RouterAddress(block.chainid).
// ...ccipSend(...)

ccipLocalSimulatorFork.switchChainAndRouteMessage(destinationForkId);

Routing is strict by default: switchChainAndRouteMessage reverts with CCIPLocalSimulatorFork__MessageNotRouted(messageId, reason) when a captured message cannot be routed to any of the given forks, and with CCIPLocalSimulatorFork__MessageExecutionFailed(messageId, reason) (the decoded revert data) when it does not execute successfully. Call setStrictRouting(false) to record failures instead and read them with getMessageStatus(messageId). With several destinations, either pass every destination fork at once with the uint256[] forkIds overload, or call switchChainAndRouteMessage once per destination fork: a 1.6 or 2.0 message to a chain that is not in the call stays QUEUED and is routed by the call that includes its destination fork.

Hardhat 3 JavaScript/TypeScript tests can route the same way with scripts/CCIPLocalSimulatorFork.js (requires @nomicfoundation/hardhat-ethers; TypeScript declarations ship next to it):

import { network } from "hardhat";
import { getCCIPMessages, routeMessage } from "@chainlink/local/scripts/CCIPLocalSimulatorFork.js";

const source = await network.connect({ network: "sepoliaFork" });
const destination = await network.connect({ network: "arbitrumSepoliaFork" });
const receipt = await (await sourceRouter.ccipSend(destChainSelector, message, { value: fee })).wait();
const [sent] = getCCIPMessages(source, receipt);
await routeMessage(destination, destinationRouterAddress, sent);
Environment 0.3.x 0.2.x
Foundry, Hardhat 3 (Solidity tests, local + fork) ✅ Foundry only
Hardhat 3 JavaScript/TypeScript helpers ✅ -
Hardhat 2 - (not supported: Hardhat 2 cannot resolve the @openzeppelin/contracts@4.8.3/ style imports) ✅ pre-1.6 fork routing only
Remix IDE (local mode) ✅ ✅

On CCIP 2.0 lanes the destination OffRamp selects the CCVs and executes the message (V2VerificationMode.OFFRAMP_DERIVED, the default); CCV attestations are simulated. Fast Transfer messages with data are only delivered to receivers that opt in through getCCVsAndFinalityConfig, as in production. See CHANGELOG.md for the 0.3.0 breaking changes, migration guide and known limitations.

Local mode limitations

CCIPLocalSimulator delivers messages synchronously inside ccipSend and applies the CCIP 2.0 OnRamp and OffRamp message rules (finality, one token per message, no zero amounts, no V3 tokenReceiver on EVM lanes, V2 receiver CCV config validation). It does not simulate:

  • token pools (pool finality policies, rate limits and pool-required CCVs are not enforced; tokens go directly to the receiver);
  • CCVs or block confirmations (receiver CCV lists are validated, not verified);
  • the supported-token list (getSupportedTokens is informational);
  • manual execution (NO_EXECUTION_ADDRESS messages are executed immediately);
  • the lane's maxPerMsgGasLimit (only V1/V2 extraArgs gas limits above uint32 revert MessageGasLimitTooHigh).

Use fork mode to test these against the real contracts.

Learn more

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

Disclaimer

Please note, this repo contains community examples only — these are not Chainlink products or services and are not supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is intended for demonstration and educational purposes only. It is provided "AS IS" and "AS AVAILABLE" without warranties of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this example code does so entirely at their own risk and must perform its own audits, security and code review, key management, and testing before any production deployment and ensure the operation and performance of such code matches expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely on this code without first conducting your own technical, engineering, and security review. This code is also outside the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation Terms of Service, which provides important information and disclosures. By using this code, you acknowledge and agree to these terms.

About

The Chainlink CCIP Local Simulator, visit documentation by clicking the link below:

Topics

Resources

Stars

109 stars

Watchers

9 watching

Forks

Releases

Packages

Used by

Contributors

Languages