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.
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).
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
forge soldeer install chainlink-local~v0.3.0 https://github.com/smartcontractkit/chainlink-local.git
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") },
},
},
});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/
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.
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);
}
}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;parisfails withEvmError: NotActivated). Use a fork-only profile so your deployed bytecode is not affected:[profile.fork]withevm_version = "cancun"infoundry.toml, thenFOUNDRY_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.
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 (
getSupportedTokensis informational); - manual execution (
NO_EXECUTION_ADDRESSmessages are executed immediately); - the lane's
maxPerMsgGasLimit(only V1/V2 extraArgs gas limits aboveuint32revertMessageGasLimitTooHigh).
Use fork mode to test these against the real contracts.
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.