# Using Chainlink Local simulator in your Foundry project
Source: https://docs.chain.link/chainlink-local/build/ccip/foundry/local-simulator

> 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 a localhost environment within your Foundry project. To get started quickly, you will use the
[CCIP Foundry Starter Kit](https://github.com/smartcontractkit/ccip-starter-kit-foundry). This project is a Foundry boilerplate that includes the Chainlink Local
package (V3, `v0.3.1`) and CCIP 2.0 examples (`Example01`-`Example10`) covering token transfers, Fast Transfers, programmable token transfers, and Cross-Chain Token (CCT) setup.

In local mode, the simulator delivers messages synchronously inside `ccipSend` and applies the CCIP 2.0 OnRamp and OffRamp message rules: finality, one token per message, no zero amounts, and receiver CCV config validation. Read [CCIP 2.0 behavior in local mode](/chainlink-local/learn/ccip-v2-local) for the full list of what is and is not enforced.

## Prerequisites

1. In a terminal, clone the [CCIP Foundry Starter Kit](https://github.com/smartcontractkit/ccip-starter-kit-foundry) repository and change directories. Clone without `--recursive` and initialize the submodules one level deep:

   ```shell
   git clone https://github.com/smartcontractkit/ccip-starter-kit-foundry.git && \
   cd ./ccip-starter-kit-foundry/ && \
   git submodule update --init
   ```

2. Run `forge build` to compile the contracts:

   ```shell
   forge build
   ```

The kit pins `solc 0.8.26` with `evm_version = cancun` in `foundry.toml`. CCIP 2.0 contracts need Cancun opcodes, so keep that setting in your own project.

## Test token transfers

You will run a test that transfers tokens between two accounts using `ExtraArgsV3` with Fast Transfers (non-zero block confirmations). The test file `Example01.t.sol` is located in the `./test` directory. It runs the real `Example01` script (`script/examples/Example01.s.sol`) against the Chainlink Local simulator, so a regression in the script fails the test. The file contains four test cases:

1. **Transfer with native fees**: Transfers CCIP-BnM from the sender to a receiver EOA, paying the fee in native gas, and verifies the receiver was credited and the router collected the fee.
2. **Transfer with LINK fees**: Same transfer, paying the fee in LINK.
3. **Revert on zero block confirmations**: `blockConfirmations == 0` means default finality; the script requires `> 0` for Faster Than Finality.
4. **Revert on zero amount**: The CCIP 2.0 rules reject zero-amount token transfers.

For a detailed explanation of the test file, refer to the [Examine the code](#examine-the-code) section.

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

```shell
forge test --match-contract Example01Test
```

Example output:

```text
$ forge test --match-contract Example01Test
[⠊] Compiling...
No files changed, compilation skipped

Ran 4 tests for test/Example01.t.sol:Example01Test
[PASS] test_revertsWhenAmountIsZero() (gas: 41283)
[PASS] test_revertsWhenBlockConfirmationsIsZero() (gas: 41309)
[PASS] test_transfersTokenToEoaPayingLinkFee() (gas: 1430441)
[PASS] test_transfersTokenToEoaPayingNativeFee() (gas: 1350710)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 2.54ms (1.81ms CPU time)

Ran 1 test suite in 6.09ms (2.54ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)
```

No RPC URLs are needed: `forge test` runs only the no-fork Chainlink Local tests.

## Examine the code

### Setup

To transfer tokens using CCIP, the test needs:

- The local chain selector
- The local CCIP router
- A test token contract (CCIP-BnM)
- A sender account (the broadcaster)
- A receiver account

The shared base contract `test/utils/LocalSimulatorTest.sol` deploys the simulator and exposes the router, LINK and BnM. The `setUp()` function is invoked before each test case to reinitialize all the variables, ensuring a clean state for each:

1. Initialize the [Chainlink Local simulator](/chainlink-local/api-reference/v0.3.1/ccip-local-simulator#cciplocalsimulator) contract and read its [`configuration`](/chainlink-local/api-reference/v0.3.1/ccip-local-simulator#configuration):

   ```solidity
   simulator = new CCIPLocalSimulator();
   (uint64 selector, IRouterClient sourceRouter,,, LinkToken linkToken, BurnMintERC677Helper bnm,) =
       simulator.configuration();
   ```

   **Note**: The [`configuration`](/chainlink-local/api-reference/v0.3.1/ccip-local-simulator#configuration) function also returns the destination router, WETH9 and CCIP-LnM contracts, which these tests do not use. Hence, there are commas in the return values.

2. The example scripts wrap their calls in `vm.startBroadcast()` with no argument, so inside a test the transaction sender is `DEFAULT_SENDER`. That account is exposed as `broadcaster` and must hold the tokens and ETH the script spends:

   ```solidity
   address internal broadcaster = DEFAULT_SENDER;
   ```

3. `Example01Test.setUp()` funds the broadcaster with CCIP-BnM and creates the receiver:

   ```solidity
   function setUp() public override {
       super.setUp();
       example = new Example01();
       receiver = makeAddr("receiver");
       ccipBnM.drip(broadcaster);
   }
   ```

### Test case 1: Transfer with native fees

The `test_transfersTokenToEoaPayingNativeFee` function tests the transfer of tokens to an EOA while paying the fee in native gas. Here are the steps involved:

1. Set the fee the local router charges and fund the broadcaster with native gas. The local router charges only what `router.setFee(...)` sets (default 0) and uses a mock message ID:

   ```solidity
   uint256 fee = 0.01 ether;
   router.setFee(fee);
   deal(broadcaster, 1 ether);
   ```

2. Record the sender's initial token balance, then run the `Example01` script with the router, chain selector, receiver, token, amount, gas limit, block confirmations and fee token:

   ```solidity
   uint256 senderBefore = ccipBnM.balanceOf(broadcaster);

   bytes32 messageId = example.run(
       address(router),
       chainSelector,
       receiver,
       address(ccipBnM),
       AMOUNT,
       EOA_GAS_LIMIT,
       BLOCK_CONFIRMATIONS,
       address(0)
   );
   ```

   The constants used:

   ```solidity
   uint256 internal constant AMOUNT = 1e17;
   uint32 internal constant EOA_GAS_LIMIT = 0;
   uint16 internal constant BLOCK_CONFIRMATIONS = 5;
   ```

   - `EOA_GAS_LIMIT` is `0` because the receiver is an EOA: there is no `ccipReceive` logic to execute. The sender is billed for the specified gas limit, not actual usage, so do not set a gas limit for token-only transfers.
   - `BLOCK_CONFIRMATIONS` is `5` (any value `> 0`): this requests a Fast Transfer, delivered before full chain finality. `0` would mean default finality.

3. Inside the script, the `ExtraArgsV3` are encoded with the `EncodeExtraArgsOffchain` helper, and the [Client.EVM2AnyMessage](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage) is built with the receiver, token amounts, extraArgs and fee token:

   ```solidity
   EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain();
   bytes memory extraArgs = extraArgsEncoder.encodeV3Basic(gasLimit, blockConfirmations);

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

   Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({
       receiver: abi.encode(receiver),
       data: "",
       tokenAmounts: tokenAmounts,
       extraArgs: extraArgs,
       feeToken: feeTokenAddress
   });
   ```

   `encodeV3Basic` maps `blockConfirmations` to the `requestedFinalityConfig` with `FinalityCodec._encodeBlockDepth`. Read more about `extraArgs` formats in the [CCIP best practices](/ccip/evm/concepts/best-practices#using-extraargs).

4. The script quotes the fee, approves the router to spend the token, and sends. With `feeTokenAddress == address(0)` the fee is paid in native gas:

   ```solidity
   uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage);
   vm.startBroadcast();
   IERC20(tokenToSend).approve(sourceRouter, amount);
   messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage);
   vm.stopBroadcast();
   ```

5. The test verifies the transfer: the receiver was credited, the sender was debited, and the router collected the fee. Because local mode delivers synchronously inside `ccipSend`, the assertions run immediately after the script:

   ```solidity
   assertTrue(messageId != bytes32(0));
   assertEq(ccipBnM.balanceOf(receiver), AMOUNT);
   assertEq(ccipBnM.balanceOf(broadcaster), senderBefore - AMOUNT);
   assertEq(address(router).balance, fee);
   ```

### Test case 2: Transfer with LINK fees

The `test_transfersTokenToEoaPayingLinkFee` function tests the same transfer paying the fee in LINK. The differences from the previous test case:

1. Fund the broadcaster with LINK from the simulator faucet instead of native gas:

   ```solidity
   simulator.requestLinkFromFaucet(broadcaster, 1 ether);
   ```

2. Pass the LINK token address as the fee token, so the script approves and pays in LINK:

   ```solidity
   bytes32 messageId = example.run(
       address(router),
       chainSelector,
       receiver,
       address(ccipBnM),
       AMOUNT,
       EOA_GAS_LIMIT,
       BLOCK_CONFIRMATIONS,
       address(link)
   );
   ```

3. Verify the router collected the fee in LINK:

   ```solidity
   assertEq(link.balanceOf(address(router)), fee);
   ```

### Revert test cases

The remaining two test cases verify the CCIP 2.0 send-time rules the local router enforces:

1. `test_revertsWhenBlockConfirmationsIsZero` expects the script's own require message, because `0` means default finality, not a Fast Transfer:

   ```solidity
   vm.expectRevert("blockConfirmations == 0 means default finality; use > 0 for Faster Than Finality");
   example.run(address(router), chainSelector, receiver, address(ccipBnM), AMOUNT, EOA_GAS_LIMIT, 0, address(0));
   ```

2. `test_revertsWhenAmountIsZero` expects the zero-amount rejection:

   ```solidity
   vm.expectRevert("amount must be > 0");
   ```

## Next steps

- Send a data-only message to a receiver contract and see the local router enforce its CCV config and minimum block depth in the [receiver CCV config guide](/chainlink-local/build/ccip/foundry/data-messages-receiver-ccv-config).
- For the other test files in the `./test` directory (`Example03.t.sol` programmable token transfers, `Example04.t.sol` sender contract, `Example05.t.sol` manual execution path), see the [CCIP starter kit examples map](/chainlink-local/learn/starter-kit-examples).
- To learn how to use Chainlink Local in forked environments, refer to
  the guide on [Using the Chainlink Local Simulator in your Foundry project with forked environments](/chainlink-local/build/ccip/foundry/local-simulator-fork).