# Using Chainlink Local Simulator in Your Hardhat Project - Forked Environments
Source: https://docs.chain.link/chainlink-local/build/ccip/hardhat/local-simulator-fork

> For the complete documentation index, see [llms.txt](/llms.txt).

> **NOTE: Integrate Chainlink Local into your project**
>
> ### npm
>
> If you use [NPM](https://www.npmjs.com/), install the [@chainlink/local NPM
> package](https://www.npmjs.com/package/@chainlink/local):
>
> ```shell
> npm install @chainlink/local@0.3.1
> ```
>
> ### yarn
>
> If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/local NPM
> package](https://www.npmjs.com/package/@chainlink/local):
>
> ```shell
> yarn add @chainlink/local@0.3.1
> ```
>
> ### foundry
>
> If you use [Foundry](https://book.getfoundry.sh/):
>
> ```shell
> forge install smartcontractkit/chainlink-local@v0.3.1
> ```
>
> and then set remappings to: `@chainlink/local/=lib/chainlink-local/` in either `remappings.txt` or `foundry.toml` file
>
> ### RemixIDE
>
> Import the `CCIPLocalSimulator` contract from the `@chainlink/local` package:
>
> ```solidity
> import {CCIPLocalSimulator} from "@chainlink/local@0.3.1/src/ccip/CCIPLocalSimulator.sol";
> ```

You can use Chainlink Local to run CCIP 2.0 in forked environments within your Hardhat 3 project. To get started quickly, you will use the [CCIP Hardhat Starter Kit](https://github.com/smartcontractkit/ccip-starter-kit-hardhat).
The kit's `test-fork` directory contains fork tests for the CCT examples (`Example06`-`Example10`), which run the real Ignition modules and tasks against pinned forks of *Ethereum Sepolia* and *Polygon Amoy* (a live CCIP 2.0 lane) and route each message to the destination OffRamp with the Chainlink Local JavaScript fork helper (`@chainlink/local/scripts/CCIPLocalSimulatorFork.js`).

The helper routes messages for every CCIP era: pre-1.6 (`EVM2EVMOnRamp`), 1.6 and CCIP 2.0 (CCV-based lanes, which live testnet lanes run today). On CCIP 2.0 lanes the destination OffRamp selects the CCVs and executes the message through the permissionless `execute` entrypoint; CCV attestations are simulated with a synthetic no-op verifier.

## Prerequisites

This guide assumes that you are familiar with the guide [Using Chainlink Local Simulator in Your Hardhat Project](/chainlink-local/build/ccip/hardhat/local-simulator).
If not, please get familiar with it and run all the prerequisites.

Fork tests have three requirements beyond the no-fork setup:

1. **Node.js 22 and the `@nomicfoundation/hardhat-ethers` plugin**, which the JavaScript fork helper uses (`connection.ethers`). The kit already registers the plugin in `hardhat.config.ts`.

2. **Archive-capable RPC URLs for both chains.** A free-tier Alchemy or Infura endpoint works; public non-archive endpoints fail with "historical state is not available". Provide them as environment variables or in the Hardhat keystore:

   ```shell
   npx hardhat keystore set ETHEREUM_SEPOLIA_RPC_URL
   npx hardhat keystore set POLYGON_AMOY_RPC_URL
   ```

3. **The `production` build profile**: the token pools exceed the 24 KB contract size limit without the optimizer. The kit's `test:fork` script sets it.

The fork networks are defined in `hardhat.config.ts` with pinned blocks, so a fresh clone runs the same chain state:

```ts
sepoliaFork: {
  type: "edr-simulated",
  chainType: "generic",
  chainId: 11155111,
  forking: {
    url: configVariable("ETHEREUM_SEPOLIA_RPC_URL"),
    blockNumber: Number(process.env.ETHEREUM_SEPOLIA_FORK_BLOCK ?? 11_920_292),
  },
},
```

Override the pins for a run with `ETHEREUM_SEPOLIA_FORK_BLOCK` / `POLYGON_AMOY_FORK_BLOCK`, for example to check whether the live lane has drifted.

## Test a CCT transfer across a CCIP 2.0 lane

You will run the fork test for Example 06 (CCT 01: `CrossChainToken` + BurnMint pool). The test file `Example06.fork.test.ts` is located in the `./test-fork` directory. It deploys a custom token and pool on both forks through the kit's Ignition module, registers them in the live TokenAdminRegistry, links the pools, and sends token transfers across the Sepolia → Amoy CCIP 2.0 lane. The file contains seven test cases, including:

1. **Deploy**: Creates the token and pool and registers them on both chains.
2. **Configure lane**: Sets the remote token and pool.
3. **Send with default finality**: Burns on the source and mints on the destination.
4. **Send with Faster Than Finality**: Reverts until the pool opts in, then delivers after both pools opt in.

In your terminal, run the following command to execute the test:

```shell
npm run test:fork
```

Example output:

```text
$ npm run test:fork

Example06 (fork)
  ✔ deploy creates the token and pool and registers them (1962ms)
  ✔ configureLane sets the remote token and pool (1200ms)
  ✔ configureLane rerun replaces the existing config (1167ms)
  ✔ send at default finality burns on the source and mints on the destination (1338ms)
  ✔ send with Faster Than Finality reverts until the pool opts in (1328ms)
  ✔ send with Faster Than Finality delivers after both pools opt in (1441ms)
  ✔ check-finality-gates finds the registered pool (1105ms)

...

27 passing (36481ms)
```

## Examine the code

### Setup

The shared setup lives in `test-fork/utils/cctFork.ts`. To route messages across forks, the test needs:

- A fork connection for the source chain (Ethereum Sepolia) and the destination chain (Polygon Amoy)
- The chain details (routers, RMN proxy, registries) for both chains
- A sender account (the broadcaster, Hardhat wallet 0) and a receiver account

The `setupForks()` function:

1. Connects to both fork networks and wraps each connection in a `Fork` object with the chain details, the public client, the broadcaster, and a `taskHre` whose `connect()` returns that fork (for running the example tasks against it):

   ```typescript
   const sepolia = await connectFork("sepoliaFork", SEPOLIA)
   const amoy = await connectFork("amoyFork", AMOY)
   ```

   The chain details (routers, RMN proxy, registries) are copied from `@chainlink/local`'s `RegisterData.sol`:

   ```typescript
   export const SEPOLIA = {
     chainSelector: 16015286601757825753n,
     routerAddress: "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59",
     rmnProxyAddress: "0xba3f6251de62dED61Ff98590cB2fDf6871FbB991",
     registryModuleOwnerCustomAddress: "0xa3c796d480638d7476792230da1E2ADa86e031b0",
     tokenAdminRegistryAddress: "0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82",
   } as const
   ```

2. Verifies the lane is the CCIP 2.0 lane the test expects. The test fails with a clear message if the pinned block or the lane is not what it describes:

   ```typescript
   const onRamp = await sepolia.publicClient.readContract({
     address: SEPOLIA.routerAddress,
     abi: routerOnRampAbi,
     functionName: "getOnRamp",
     args: [AMOY.chainSelector],
   })
   assert.notEqual(onRamp, ZERO_ADDRESS, "Sepolia router has no OnRamp for Amoy at the pinned block")
   assert.equal(
     await sepolia.publicClient.readContract({
       address: onRamp,
       abi: typeAndVersionAbi,
       functionName: "typeAndVersion",
     }),
     "OnRamp 2.0.0",
     "Sepolia -> Amoy is not a CCIP 2.0 lane"
   )
   ```

3. Deploys the kit's Ignition module on each fork with the per-chain parameters (router, RMN proxy, registries):

   ```typescript
   return (await fork.connection.ignition.deploy(module, {
     parameters: {
       $global: {
         routerAddress: fork.details.routerAddress,
         armProxy: fork.details.rmnProxyAddress,
         registryModuleOwnerCustom: fork.details.registryModuleOwnerCustomAddress,
         tokenAdminRegistry: fork.details.tokenAdminRegistryAddress,
         thresholdAmountForAdditionalCCVs: THRESHOLD,
       },
     },
   })) as Record<string, { address: Address }>
   ```

### Send and route the message

The `sendAndRoute()` helper shows the two-step flow used by every fork test:

1. On the source fork, run the book's send task. It builds an `ExtraArgsV3` message and calls `ccipSend` through the Sepolia router:

   ```typescript
   await quietly(() =>
     sendAction(
       {
         sourceRouter: SEPOLIA.routerAddress,
         destinationChainSelector: AMOY.chainSelector,
         receiver: forks.receiver,
         tokenToSend: token,
         amount,
         gasLimit: EOA_RECEIVER_GAS_LIMIT,
         blockConfirmations,
         feeTokenAddress: ZERO_ADDRESS,
       },
       forks.sepolia.taskHre
     )
   )
   ```

2. Fetch the `ccipSend` transaction receipt and extract the CCIP message with [`getCCIPMessages`](/chainlink-local/api-reference/v0.3.1/ccip-local-simulator-fork-js#getccipmessages), then route it on the destination connection with [`routeMessage`](/chainlink-local/api-reference/v0.3.1/ccip-local-simulator-fork-js#routemessage):

   ```typescript
   const receipt = await sepolia.connection.ethers.provider.getTransactionReceipt(sendTx.hash)
   const [sent] = getCCIPMessages(sepolia.connection, receipt)
   return routeMessage(amoy.connection, AMOY.routerAddress, sent)
   ```

   `getCCIPMessages` parses the send event for every CCIP era and returns a `CCIPSentMessage` (for CCIP 2.0, the raw `encodedMessage` and the receipts). `routeMessage` finds the destination OffRamp that serves the lane, points owner-configurable CCV resolvers at a synthetic no-op verifier, and executes the message through the OffRamp's permissionless `execute`. It returns `{ offRamp, executed, reason }` and throws if no OffRamp serves the lane or the execution fails (receiver revert, finality or CCV rejection).

   When the destination chain has a dedicated CCIP 2.0 router, pass both routers so both routers' OffRamps are searched. For example, when the destination is Ethereum Sepolia, which has a dedicated CCIP 2.0 router:

   ```typescript
   routeMessage(sepolia.connection, [SEPOLIA.routerAddress, CCIP_V2_ROUTERS[11155111]], sent)
   ```

### Faster Than Finality across the lane

The `send with Faster Than Finality delivers after both pools opt in` test case shows the CCIP 2.0 finality gates on a real lane:

1. New pools allow full finality only, so any block-depth request is rejected by the pool. The test first asserts the pool's default and expects the send to revert.

2. Opt both pools in by setting their allowed finality config (through the kit's `Example06-SetPoolFinality` task), then find the smallest depth the live lane accepts by quoting `getFee` for candidate depths:

   ```typescript
   export async function findLaneFtfDepth(forks: Forks, token: Address): Promise<number> {
     for (const depth of [1, 2, 3, 5, 10, 20, 50, 100]) {
       /* quote getFee with blockConfirmations: depth; return the first depth that does not revert */
     }
   }
   ```

3. Send with the custom finality (`blockConfirmations = depth`) and route. The transfer burns on the source and mints on the destination, same as the default-finality case, but delivered before full finality.

## What the fork helper does not model

Not modelled by the helper (and not asserted in the kit's fork tests): real CCV attestations, RMN and executor fees. CCVs on the lane are replaced by a synthetic verifier, and a manual execution does not charge the executor fee a real one costs. Pinned state never sees testnet upgrades, so green tests only prove the examples against that snapshot; re-pin the fork blocks when the lane's behavior changes or on every `@chainlink/contracts-ccip` or `@chainlink/local` bump.

## Next steps

- The other fork tests cover Lock & Release pools with a lock box (`Example07.fork.test.ts`), `AdvancedPoolHooks` (`Example08`-`Example10`). For pool-required CCVs on `AdvancedPoolHooks` in Foundry fork tests, read the [pool-based CCVs tutorial](/chainlink-local/build/ccip/foundry/pool-based-ccvs-fork).
- For the full CCT enabling procedure with the kit's tasks and Ignition modules, read the [CCT burn and mint tutorial](/chainlink-local/build/ccip/foundry/cct-burn-and-mint-fork) (the flow is the same in Hardhat; the kit's book chapters list the Hardhat task commands).
- For an index of every kit example and where its topic is documented, read the [CCIP starter kit examples map](/chainlink-local/learn/starter-kit-examples).