# Transfer Tokens Between Chains from Smart Contracts
Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/transfer-tokens-from-contract
Last Updated: 2026-05-01

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

In this tutorial, we will use CCIP to transfer `CCIP-BnM` tokens from a smart contract on *Ethereum Sepolia* to an externally owned account (EOA) on *Arbitrum Sepolia*. The example uses off-chain `extraArgs` encoding so the scripts can select the correct finality configuration for the lane.

We will send the transfer twice:

1. Once, paying CCIP fees in native gas tokens `ETH` and using faster than finality (`BLOCK_DEPTH=32`).
2. And again, paying CCIP fees in `LINK` and using finalized finality (default).

> **NOTE: CCIP 2.0 supports Faster-Than-Finality (FTF)**
>
> 1. Chainlink CCIP 2.0 supports [Faster-Than-Finality (FTF)](/ccip/concepts/execution-latency/ftf), which lets
>    supported lanes process messages with a requested finality mode instead of always waiting for full finality.
> 2. In these tutorials, the scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`.
> 3. Supplying extraArgs that were encoded off-chain keeps the smart contract compatible with future CCIP versions
>    without requiring an upgrade. We recommend against hardcoding extraArgs inside a smart contract.
> 4. We demonstrate FTF with a numeric block depth of `32` (`BLOCK_DEPTH=32`) in this tutorial; omit `BLOCK_DEPTH` (or
>    set it to `0`/`DEFAULT`) to request `finalized` finality (`WAIT_FOR_FINALITY_FLAG`), which is the default.

## Before you begin

1. You should understand how to write, compile, deploy, and fund a smart contract. Go through this [tutorial](/quickstarts/deploy-your-first-contract) to get started.
2. Your account must have some `ETH` and `LINK` tokens on *Ethereum Sepolia*. Learn how to [Acquire testnet LINK](/resources/acquire-link).
3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different set of source and destination chains/tokens.
4. [Acquire CCIP test tokens](/ccip/getting-started/test-tokens#evm-chains). You should have `CCIP-BnM` tokens, and `CCIP-BnM` should appear in the list of your tokens in MetaMask.

## Examine the code

#### 1. Initializing the contract

When deploying the contract, you provide the CCIP router address for the source chain.
The router address is useful for the following:

- Calls the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee) to estimate CCIP fees.
- Calls the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend) to send CCIP messages.

Some key things to note:

- `OwnerIsCreator` sets the deployer as the owner of the contract.
- This is a sender-only contract: it does not inherit from `CCIPReceiver` because no receiver callback is executed on the destination chain. Tokens can be sent to any address (EOA or contract) on the destination chain.
- `sendMessage` is `payable` and open to any caller (not restricted to the owner). It handles both LINK and native fee payments: pass the LINK token address as `_feeTokenAddress` to pay in LINK, or `address(0)` to pay in native gas. The function accepts pre-encoded `_extraArgs` bytes built off-chain, making the contract forward-compatible with any extraArgs version.
- Access control is enforced through allowlisting: destination chains are gated by the `onlyAllowlistedDestinationChain` modifier, and `validateReceiver` prevents sending to the zero address.

```solidity filename="TokenTransferor.sol"
contract TokenTransferor is OwnerIsCreator {
    using SafeERC20 for IERC20;

    IRouterClient private s_router;

    constructor(address _router) {
        s_router = IRouterClient(_router);
    }

    // ... modifiers, allowlist admin functions ...

    function sendMessage(
        uint64 _destinationChainSelector,
        address _receiver,
        address _token,
        uint256 _amount,
        address _feeTokenAddress,
        bytes calldata _extraArgs
    )
        external
        payable
        onlyAllowlistedDestinationChain(_destinationChainSelector)
        validateReceiver(_receiver)
        returns (bytes32 messageId)
    {
        messageId = _sendCCIPMessage(
            _destinationChainSelector, _receiver, _token, _amount, _feeTokenAddress, _extraArgs
        );
    }

    function getFee(
        uint64 _destinationChainSelector,
        address _receiver,
        address _token,
        uint256 _amount,
        address _feeTokenAddress,
        bytes calldata _extraArgs
    ) external view returns (uint256 fees) {
        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
            _receiver, _token, _amount, _feeTokenAddress, _extraArgs
        );
        fees = s_router.getFee(_destinationChainSelector, evm2AnyMessage);
    }

    // ... internal helpers (message building, fee handling, approvals) ...
}
```

#### 2. Build transaction payload

`_sendCCIPMessage` calls the `_buildCCIPMessage` helper to build a CCIP message payload using `EVM2AnyMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage). \
This payload is then passed to the router's `getFee` and `ccipSend` functions.

The payload includes:

- `receiver`: ABI-encoded destination address (`abi.encode(_receiver)`).
- `data`: Empty (`""`) because this tutorial transfers tokens only (no data payload).
- `tokenAmounts`: A 1-element array containing the token address and amount to transfer.
- `extraArgs`: Pre-encoded message execution parameters built off-chain by a helper script. For finalized finality (default), the scripts encode V3 extraArgs with finalized finality. For faster than finality requests (`BLOCK_DEPTH=32`), the scripts detect lane support and use V3 extraArgs with `requestedFinalityConfig` on FTF-capable lanes, or V2 extraArgs on pre-v2.0 lanes.
- `feeToken`: The token used to pay CCIP fees (`_feeTokenAddress`). Pass the LINK token address to pay in LINK, or `address(0)` to pay in native gas.

> **NOTE: ExtraArgs are encoded off-chain**
>
> 1. The contract accepts pre-encoded `_extraArgs` bytes, so it does not need to know about specific encoding versions.
>    The scripts handle lane detection and encoding.
> 2. This makes the contract forward-compatible with future CCIP versions.

```solidity filename="_buildCCIPMessage"
    function _buildCCIPMessage(
        address _receiver,
        address _token,
        uint256 _amount,
        address _feeTokenAddress,
        bytes calldata _extraArgs
    ) private pure returns (Client.EVM2AnyMessage memory) {
        Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);
        tokenAmounts[0] = Client.EVMTokenAmount({token: _token, amount: _amount});

        return Client.EVM2AnyMessage({
            receiver: abi.encode(_receiver),
            data: "",
            tokenAmounts: tokenAmounts,
            extraArgs: _extraArgs,
            feeToken: _feeTokenAddress
        });
    }
```

> **CAUTION: Best Practices**
>
> This example is simplified for educational purposes. For production code, follow these best practices:
>
> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs).
>
> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain).
>
> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*)
>
> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0.
>
> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact).

#### 3. Sending tokens

The public `sendMessage` function delegates to `_sendCCIPMessage`, which performs four operations:

1. Builds the message payload by calling `_buildCCIPMessage`. See **Build transaction payload** for details.
2. Computes the fees by invoking the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee).
3. Pulls tokens from the caller and grants the router the required approvals by calling `_handleFeeAndTokenApprovals`. See **Handling fees and token approvals** for details.
4. Dispatches the CCIP message by executing the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend). If paying in native gas (`_feeTokenAddress == address(0)`), the fee is forwarded via `{value: ccipFee}`.

**Note**: As a security measure, `sendMessage` is protected by the `onlyAllowlistedDestinationChain` and `validateReceiver` modifiers. Any caller can invoke it -- access is governed by the destination chain allowlist, not ownership.

```solidity filename="_sendCCIPMessage"
    function _sendCCIPMessage(
        uint64 _destinationChainSelector,
        address _receiver,
        address _token,
        uint256 _amount,
        address _feeTokenAddress,
        bytes calldata _extraArgs
    ) private returns (bytes32 messageId) {
        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
            _receiver, _token, _amount, _feeTokenAddress, _extraArgs
        );

        uint256 ccipFee = s_router.getFee(_destinationChainSelector, evm2AnyMessage);

        _handleFeeAndTokenApprovals(s_router, _token, _amount, _feeTokenAddress, ccipFee);

        if (_feeTokenAddress == address(0)) {
            messageId = s_router.ccipSend{value: ccipFee}(_destinationChainSelector, evm2AnyMessage);
        } else {
            messageId = s_router.ccipSend(_destinationChainSelector, evm2AnyMessage);
        }

        emit TokensTransferred(
            messageId, _destinationChainSelector, _receiver, _token, _amount, _feeTokenAddress, ccipFee
        );

        return messageId;
    }
```

#### 4. Handling fees and token approvals

The contract uses a **pull-from-caller** model: when a user calls `sendMessage`, the contract pulls the required tokens from `msg.sender` via `safeTransferFrom`, then approves the Router to spend them via `forceApprove`. The caller (EOA or upstream contract) must `approve` this contract **before** calling `sendMessage`.

`_handleFeeAndTokenApprovals` handles three scenarios:

1. **Native fee** (`_feeTokenAddress == address(0)`): Validates that `msg.value` covers the CCIP fee. Pulls the transfer token from the caller via `safeTransferFrom` and approves the Router.
2. **Same token for fee and transfer** (`_token == _feeTokenAddress`): Pulls the combined total (`ccipFee + amount`) from the caller in one `safeTransferFrom`. Approves the Router for the combined total.
3. **Different ERC-20 tokens**: Pulls each token separately from the caller. Approves the Router for each.

> **NOTE: Approve the contract, not the Router**
>
> Because the contract pulls tokens from the caller, your scripts must call `approve(contractAddress, amount)` --
> **not** `approve(routerAddress, amount)`. This is a key difference from older CCIP patterns where the contract was
> pre-funded.

```solidity filename="_handleFeeAndTokenApprovals"
    function _handleFeeAndTokenApprovals(
        IRouterClient _router,
        address _token,
        uint256 _amount,
        address _feeTokenAddress,
        uint256 _ccipFee
    ) private {
        if (_feeTokenAddress == address(0)) {
            if (msg.value < _ccipFee) {
                revert InsufficientNativeForFees(msg.value, _ccipFee);
            }

            IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
            IERC20(_token).forceApprove(address(_router), _amount);
        } else if (_token == _feeTokenAddress) {
            uint256 totalAmount = _ccipFee + _amount;
            IERC20(_token).safeTransferFrom(msg.sender, address(this), totalAmount);
            IERC20(_token).forceApprove(address(_router), totalAmount);
        } else {
            IERC20(_feeTokenAddress).safeTransferFrom(msg.sender, address(this), _ccipFee);
            IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
            IERC20(_feeTokenAddress).forceApprove(address(_router), _ccipFee);
            IERC20(_token).forceApprove(address(_router), _amount);
        }
    }
```

[TokenTransferor.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/token-transfers/TokenTransferor.sol)

Check out the complete contract code on Github.

## Tutorial

Let's get started! Choose your preferred development environment below.

### Foundry

Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework.

> **NOTE: Want to set up a dev environment from scratch?&#x20;**
>
> Check out the [Getting started page](/ccip/evm/getting-started#foundry) for detailed instructions on setting up a
> Foundry dev environment from scratch.

#### 1. Bootstrap a new Foundry project <a id="foundry-bootstrap-a-new-foundry-project"></a>

[Foundry Starter Kit](https://github.com/smartcontractkit/docs-ccip)

Clone the Foundry Starter Kit for a smoother setup.

1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory:

```bash filename="Terminal"
git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip
```

2. If you don't already have a Foundry keystore, use the `cast` command to create a new one. Here, `your_keystore_name` is the alias you assign to this keystore entry -- Foundry will prompt you to enter the actual private key and a password to encrypt it:

```bash filename="Terminal"
cast wallet import your_keystore_name --interactive
```

And use the `cast wallet list` command to verify:

![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png)

> **CAUTION: Note**
>
> As of writing, Foundry supports keystore encryption only for private keys (not generic strings), so in this tutorial
> we only encrypt the private key and store the keystore alias in `KEYSTORE_NAME`.

3. Install dependencies:

```bash filename="Terminal"
npm install
```

4. Create a `.env` file by copying the example file, and fill in your values:

```bash filename="Terminal"
cp .env.example .env
```

Set `KEYSTORE_NAME` to the name of the keystore entry you created above, and provide RPC endpoints for the chains you will use:

```env filename=".env"
# Keystore name
KEYSTORE_NAME=your_keystore_name

# RPC URLs (add the ones you need)
ETHEREUM_SEPOLIA_RPC_URL=
ARBITRUM_SEPOLIA_RPC_URL=

# Etherscan API key (required only if you pass --verify to deployment scripts)
ETHERSCAN_API_KEY=
```

5. Load the environment variables:

```bash filename="Terminal"
source .env
```

> **CAUTION: Run this in every new terminal session**
>
> If you open a new terminal, you must run `source .env` again. To verify, run `echo $ETHEREUM_SEPOLIA_RPC_URL` and
> confirm it prints your URL.

6. Run the following command to compile all the contracts:

```bash filename="Terminal"
forge build
```

#### 2. Deploy your contract <a id="foundry-deploy-your-contract"></a>

In this section, you will deploy `TokenTransferor` on the source chain (*Ethereum Sepolia*). \
Unlike the programmable token transfers tutorial, no destination contract is needed because tokens are transferred to an address (EOA or contract) on the destination chain.

Run the script:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA forge script foundry/scripts/tutorials/token-transfers/deploy/Deploy.s.sol \
--account $KEYSTORE_NAME \
--broadcast -vv
```

Your terminal output will look like this:

```bash filename="Terminal"
========================================
🚀 Deploy TokenTransferor Contract
========================================
Source Chain: Ethereum Sepolia
========================================


[Step 1] Deploying TokenTransferor on Ethereum Sepolia
Contract deployed at: 0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901
https://sepolia.etherscan.io/address/0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901

========================================
✅ Deployment Complete!
========================================
Chain: Ethereum Sepolia
Contract: 0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901
https://sepolia.etherscan.io/address/0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901

Run this command to set the environment variable:
export ETHEREUM_SEPOLIA_CONTRACT=0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901
========================================

** Next Step: Configuration **

Allowlist the destination chain:
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA forge script foundry/scripts/tutorials/token-transfers/configure/Configure.s.sol:Configure --account $KEYSTORE_NAME --broadcast -vv
========================================
```

Save the exported contract address from the output:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_CONTRACT=<your-deployed-contract-address>
```

[Deploy.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/token-transfers/deploy/Deploy.s.sol)

Check out the complete script code on Github.

#### 3. Configure allowlists <a id="foundry-configure-allowlists"></a>

As a best practice, allowlist the destination chain before sending tokens.

[Configure.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/token-transfers/configure/Configure.s.sol)

Check out the complete script code on Github.

1. If you haven't already, export the address of the deployed contract so that it's available in the terminal:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_CONTRACT=<your-deployed-contract-address>
```

2. Run the configure script to allowlist the destination chain:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
forge script foundry/scripts/tutorials/token-transfers/configure/Configure.s.sol \
--account $KEYSTORE_NAME --broadcast -vv
```

Your terminal output will look like this:

```bash filename="Terminal"
========================================
⚙️ Configure TokenTransferor
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0xD18B78Ce9f4dc76b65ECD22F784352041B92Adc1
Destination Chain: Arbitrum Sepolia
========================================

Configuring TokenTransferor on Ethereum Sepolia
Allowlisting Arbitrum Sepolia as destination chain...
✅ Destination chain allowlisted: Arbitrum Sepolia

========================================
✅ Configuration Complete!
========================================
Ethereum Sepolia can send tokens to Arbitrum Sepolia

** Next Step: Transfer Tokens **
```

#### 4. Fund your wallet with test tokens <a id="foundry-fund-your-wallet-with-test-tokens"></a>

[DripBnMToken.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/faucet/DripBnMToken.s.sol)

Check out the faucet script on Github.

> **NOTE: Convenient way to print your wallet address**
>
> If you don't have the public address of your wallet handy, run `cast wallet address --account $KEYSTORE_NAME` to print
> it from the terminal.

Before sending a CCIP message, you need `CCIP-BnM` test tokens in your wallet. The send scripts transfer `CCIP-BnM` from
your EOA to the contract, so your wallet must hold a balance.

Use the faucet script included in the starter kit to drip `CCIP-BnM` tokens to your address:

```bash filename="Terminal"
CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS=<your-wallet-address> forge script foundry/scripts/faucet/DripBnMToken.s.sol --account $KEYSTORE_NAME --broadcast -vv
```

> **NOTE: Note**
>
> If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the
> [Chainlink faucet](https://faucets.chain.link/).

#### 5. Transfer Tokens <a id="foundry-transfer-tokens"></a>

`SendMessage.s.sol` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script:

- Detects the lane version and encodes the correct extraArgs (V2 for pre-v2.0 lanes, V3 for v2.0+ lanes) via `ExtraArgsHelper.buildExtraArgs`.
- Approves the contract to spend the caller's tokens (the contract then pulls via `safeTransferFrom`).
- Sends the CCIP token transfer.

##### Faster Than Finality (block depth)

> **NOTE: ExtraArgsV3**
>
> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields:
>
> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers.
> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery.
>   These tutorials use a depth of 32, which encodes as `0x00000020`.
>
> For the complete list of supported parameters, see the
>
> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference,
> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository.
>
> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored.

Set the receiver address. This is the EOA or contract on the destination chain that will receive the transferred `CCIP-BnM` tokens:

```bash filename="Terminal"
export RECEIVER_ADDRESS=<receiver-address>
```

##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`)

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
RECEIVER_ADDRESS=$RECEIVER_ADDRESS \
BLOCK_DEPTH=32 \
forge script foundry/scripts/tutorials/token-transfers/interact/SendMessage.s.sol \
--account $KEYSTORE_NAME --broadcast -vv
```

Your terminal should look like this:

```bash filename="Terminal"
========================================
📡 CCIP Token Transfer - Pay with Native
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901
Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
Fee Token: Native (ETH)
========================================


[Pre-validation] Detecting lane version and building extraArgs...
Token pool ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s))
Receiver is an EOA — no receiver constraint.
✅ Using V3 extraArgs with FTF (gasLimit=0, finalityConfig=0x00000020 (BLOCK_DEPTH: 32 block(s))).

[Step 1] Approving contract to spend CCIP-BnM...
✅ Contract approved to spend CCIP-BnM


[Step 2] Sending CCIP token transfer with native token fee ( ETH )...
Required CCIP fee (in WEI): 130463107598943

========================================
✅ Tokens transferred successfully!
========================================
CCIP messageId: 0xc1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0xc1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6
```

##### Example 2: Pay with LINK + finalized finality (default)

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
RECEIVER_ADDRESS=$RECEIVER_ADDRESS \
FEE_TOKEN=LINK \
forge script foundry/scripts/tutorials/token-transfers/interact/SendMessage.s.sol \
--account $KEYSTORE_NAME --broadcast -vv
```

Your terminal should look like this:

```bash filename="Terminal"
========================================
📡 CCIP Token Transfer - Pay with LINK
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x1D4CC0abA2C8401382497FBa4e8d4CEAabE73901
Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
Fee Token: LINK
========================================


[Pre-validation] Detecting lane version and building extraArgs...
✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=0, finalityConfig=0x00000000).

[Step 1] Approving contract to spend fee token for CCIP fees...
Required CCIP fee (in token units): 26110523180283030
✅ Contract approved to spend fee token


[Step 2] Approving contract to spend CCIP-BnM...
✅ Contract approved to spend CCIP-BnM


[Step 3] Sending CCIP token transfer...

========================================
✅ Tokens transferred successfully!
========================================
```

##### Environment variables

| Variable            | Description                                                                                                          | Default                    |
| ------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `KEYSTORE_NAME`     | Foundry encrypted keystore name used by `--account $KEYSTORE_NAME`                                                   | `your_keystore_name`       |
| `SOURCE_CHAIN`      | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`)                                                       | Not set                    |
| `DEST_CHAIN`        | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`)                                                  | Not set                    |
| `{CHAIN}_RPC_URL`   | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set                    |
| `{CHAIN}_CONTRACT`  | Deployed tutorial contract address on the source chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`)                    | Not set                    |
| `RECEIVER_ADDRESS`  | Destination receiver address for the token transfer and balance verification                                         | Not set                    |
| `FEE_TOKEN`         | `LINK` or `NATIVE`                                                                                                   | `NATIVE`                   |
| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN`                            | Not set                    |
| `TOKEN_AMOUNT`      | Amount of CCIP-BnM to transfer (in wei)                                                                              | `1000000000000000` (0.001) |
| `BLOCK_DEPTH`       | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality                         | `DEFAULT`                  |

[SendMessage.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/token-transfers/interact/SendMessage.s.sol)

Check out the complete script code on Github.

#### 6. Verify transfer on the destination chain <a id="foundry-verify-transfer-on-the-destination-chain"></a>

After sending the transfer, you can verify it was successful:

1. Use the **CCIP Explorer** link printed in the send output to track the message status in real time.

2. Once the message is marked as delivered, check the receiver's token balance on the destination chain. You can use a block explorer or the following command:

```bash filename="Terminal"
cast call <CCIP_BNM_TOKEN_ADDRESS_ON_DESTINATION> "balanceOf(address)(uint256)" $RECEIVER_ADDRESS --rpc-url $ARBITRUM_SEPOLIA_RPC_URL --chain arbitrum-sepolia
```

Find the CCIP-BnM token address for each chain in the [CCIP Directory](/ccip/directory).

### Hardhat

Best for developers who want a mature, TypeScript-based smart contract development framework.

> **NOTE: Want to set up a dev environment from scratch?&#x20;**
>
> Check out the [Getting started page](/ccip/evm/getting-started#hardhat-3) for detailed instructions on setting up a
> Hardhat dev environment from scratch.

#### 1. Bootstrap a new Hardhat project <a id="hardhat-bootstrap-a-new-hardhat-project"></a>

[CCIP Starter Kit](https://github.com/smartcontractkit/docs-ccip)

Clone the starter kit (contains both Hardhat and Foundry code) for a smoother setup.

1. Clone the [CCIP 2.0 template](https://github.com/smartcontractkit/docs-ccip) repository, and open a terminal inside the project directory:

```bash filename="Terminal"
git clone https://github.com/smartcontractkit/docs-ccip.git && cd docs-ccip
```

2. Copy the example environment file and fill in your values:

```bash filename="Terminal"
cp .env.example .env
```

Set `KEYSTORE_NAME` to the keystore alias you will create later in this section, and provide RPC endpoints for the chains you will use:

```env filename=".env"
# Keystore name
KEYSTORE_NAME=your_keystore_name

# RPC URLs (add the ones you need)
ETHEREUM_SEPOLIA_RPC_URL=
ARBITRUM_SEPOLIA_RPC_URL=

# Etherscan API key (required only if you pass --verify to deployment scripts)
ETHERSCAN_API_KEY=
```

3. Install dependencies and compile:

```bash filename="Terminal"
npm install && npx hardhat compile
```

4. Create a Hardhat keystore entry for your private key. Use the same name as `KEYSTORE_NAME` in your `.env` file. Hardhat will prompt you to enter the private key and a password to encrypt it:

```bash filename="Terminal"
npx hardhat keystore set your_keystore_name
```

> **CAUTION: Private key format**
>
> When prompted for your private key, include the `0x` prefix.

#### 2. Deploy your contract <a id="hardhat-deploy-your-contract"></a>

[deploy.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/token-transfers/deploy/deploy.ts)

Check out the complete script code on Github.

In this section, you will deploy `TokenTransferor` on the source chain (*Ethereum Sepolia*). No destination deployment is required.

Run the script:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA npx hardhat run hardhat/scripts/tutorials/token-transfers/deploy/deploy.ts
```

Your terminal output will look like this:

```bash filename="Terminal"
========================================
🚀 Deploy TokenTransferor Contract
========================================
Source Chain: Ethereum Sepolia
========================================


[Step 1] Deploying TokenTransferor on Ethereum Sepolia
Contract deployed at: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1
https://sepolia.etherscan.io/address/0x67b6e59e7b3d36ee90890935a1d13e87734e94d1

========================================
✅ Deployment Complete!
========================================
Chain: Ethereum Sepolia
Contract: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1
https://sepolia.etherscan.io/address/0x67b6e59e7b3d36ee90890935a1d13e87734e94d1

Run this command to set the environment variable:
export ETHEREUM_SEPOLIA_CONTRACT=0x67b6e59e7b3d36ee90890935a1d13e87734e94d1
========================================

** Next Step: Configuration **

Allowlist the destination chain:
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA npx hardhat run hardhat/scripts/tutorials/token-transfers/configure/configure.ts
========================================
```

Before moving on, export the address of the deployed contract so that it's available in the terminal:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_CONTRACT=<your-deployed-contract-address>
```

#### 3. Configure allowlists <a id="hardhat-configure-allowlists"></a>

As a best practice, allowlist the destination chain before sending tokens.

[configure.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/token-transfers/configure/configure.ts)

Check out the complete script code on Github.

1. The `configure.ts` script allowlists the destination chain on the sender contract:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
npx hardhat run hardhat/scripts/tutorials/token-transfers/configure/configure.ts
```

Your terminal output will look like this:

```bash filename="Terminal"
========================================
⚙️ Configure TokenTransferor
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1
Destination Chain: Arbitrum Sepolia
========================================

Configuring TokenTransferor on Ethereum Sepolia
Allowlisting Arbitrum Sepolia as destination chain...
✅ Destination chain allowlisted: Arbitrum Sepolia

========================================
✅ Configuration Complete!
========================================
Ethereum Sepolia can send tokens to Arbitrum Sepolia

** Next Step: Transfer Tokens **

Transfer tokens (pay with native gas, default):
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA RECEIVER_ADDRESS=$RECEIVER_ADDRESS BLOCK_DEPTH=DEFAULT npx hardhat run hardhat/scripts/tutorials/token-transfers/interact/send-message.ts

Or pay with LINK:
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA RECEIVER_ADDRESS=$RECEIVER_ADDRESS FEE_TOKEN=LINK BLOCK_DEPTH=DEFAULT npx hardhat run hardhat/scripts/tutorials/token-transfers/interact/send-message.ts
========================================
```

#### 4. Fund your wallet with test tokens <a id="hardhat-fund-your-wallet-with-test-tokens"></a>

[drip-bnm-token.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/faucet/drip-bnm-token.ts)

Check out the faucet script on Github.

Before sending a CCIP message, you need `CCIP-BnM` test tokens in your wallet. The send scripts transfer `CCIP-BnM` from your EOA to the contract, so your wallet must hold a balance.

Use the faucet script included in the starter kit to drip `CCIP-BnM` tokens to your address:

```bash filename="Terminal"
CHAIN=ETHEREUM_SEPOLIA RECIPIENT_ADDRESS=<your-wallet-address> \
npx hardhat run hardhat/scripts/faucet/drip-bnm-token.ts
```

If you plan to pay CCIP fees in LINK (instead of native gas), you also need LINK tokens. Get test LINK from the [Chainlink faucet](https://faucets.chain.link/).

#### 5. Transfer Tokens <a id="hardhat-transfer-tokens"></a>

`send-message.ts` is a unified send script that handles both native and LINK fee payments. Set `FEE_TOKEN=LINK` to pay with LINK, or omit it (defaults to `NATIVE`) to pay with the native gas token. The script:

- Queries lane features via `@chainlink/ccip-sdk` to encode the correct extraArgs (V2 for pre-v2.0 lanes, V3 for v2.0+ lanes).
- Approves the contract to spend the caller's tokens (the contract then pulls via `safeTransferFrom`).
- Sends the CCIP token transfer.

##### Faster Than Finality (block depth)

> **NOTE: ExtraArgsV3**
>
> On CCIP 2.0 EVM lanes, the `extraArgs` field of a CCIP message is encoded as **`ExtraArgsV3`**, which allow you to use CCIP 2.0 specific features. The tutorial scripts set two of its fields:
>
> - **`gasLimit`** (`uint32`): the gas made available to the receiver's `ccipReceive` callback on the destination chain. The sender is billed for the gas specified, not the gas actually used, and unspent gas is not refunded. A gas limit of zero combined with no message data skips the callback entirely, which is what you would want for pure token transfers or EOA receivers.
> - **`requestedFinalityConfig`** (`bytes4`): the finality the sender requests for this message, encoded with `FinalityCodec`. `0x00000000` requests `Finalized` (full finality, the default). A numeric block depth instead requests [faster-than-finality](/ccip/concepts/execution-latency/ftf) delivery.
>   These tutorials use a depth of 32, which encodes as `0x00000020`.
>
> For the complete list of supported parameters, see the
>
> 1. [`ExtraArgsCodec`](/ccip/evm/api-reference/v2.0.0/extra-args-codec) API reference,
> 2. or the [`ExtraArgsCodec.sol`](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/evm/contracts/libraries/ExtraArgsCodec.sol) source in the chainlink-ccip repository.
>
> On pre-2.0 lanes, the scripts fall back to legacy `GenericExtraArgsV2` extraArgs, where only the `gasLimit` field is honored.

Set the receiver address. This is the EOA or contract on the destination chain that will receive the transferred `CCIP-BnM` tokens:

```bash filename="Terminal"
export RECEIVER_ADDRESS=<receiver-address>
```

##### Example 1: Pay with native gas + faster than finality (`BLOCK_DEPTH=32`)

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
RECEIVER_ADDRESS=$RECEIVER_ADDRESS \
BLOCK_DEPTH=32 \
npx hardhat run hardhat/scripts/tutorials/token-transfers/interact/send-message.ts
```

Your terminal should look like this:

```bash filename="Terminal"
========================================
📡 CCIP Token Transfer - Pay with ETH
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1
Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
Fee Token: Native (ETH)
========================================


[Pre-validation] Querying lane features and building extraArgs...
Token pool ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s))
Receiver is an EOA — no receiver constraint.
✅ Using V3 extraArgs with FTF (gasLimit=0, finalityConfig=32 block(s)).
[Pre-validation] CCIP fee: 130463107598943

[Step 1] Approving contract to spend CCIP-BnM...
✅ Contract approved to spend CCIP-BnM


[Step 2] Sending CCIP token transfer with native token fee (ETH)...
Required CCIP fee (in WEI): 130463107598943

========================================
✅ Tokens transferred successfully!
========================================
CCIP messageId: 0xd2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0xd2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607
```

##### Example 2: Pay with LINK + finalized finality (default)

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
RECEIVER_ADDRESS=$RECEIVER_ADDRESS \
FEE_TOKEN=LINK \
npx hardhat run hardhat/scripts/tutorials/token-transfers/interact/send-message.ts
```

Your terminal should look like this:

```bash filename="Terminal"
========================================
📡 CCIP Token Transfer - Pay with LINK
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x67b6e59e7b3d36ee90890935a1d13e87734e94d1
Receiver: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
Fee Token: LINK
========================================


[Pre-validation] Querying lane features and building extraArgs...
✅ Using default finality (BLOCK_DEPTH=DEFAULT). V3 extraArgs (gasLimit=0, finalityConfig=0x00000000).
[Pre-validation] CCIP fee: 26110523180283030

[Step 1] Approving contract to spend LINK for CCIP fees...
Required CCIP fee (in LINK units): 26110523180283030
✅ Contract approved to spend LINK


[Step 2] Approving contract to spend CCIP-BnM...
✅ Contract approved to spend CCIP-BnM


[Step 3] Sending CCIP token transfer...

========================================
✅ Tokens transferred successfully!
========================================
CCIP messageId: 0xe3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0xe3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718
```

##### Environment variables

| Variable            | Description                                                                                                          | Default                    |
| ------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `KEYSTORE_NAME`     | Hardhat keystore entry name, set in `.env`                                                                           | `your_keystore_name`       |
| `SOURCE_CHAIN`      | Source chain name identifier (for example, `ETHEREUM_SEPOLIA`)                                                       | Not set                    |
| `DEST_CHAIN`        | Destination chain name identifier (for example, `ARBITRUM_SEPOLIA`)                                                  | Not set                    |
| `{CHAIN}_RPC_URL`   | RPC URL for each chain you run scripts against (for example, `ETHEREUM_SEPOLIA_RPC_URL`, `ARBITRUM_SEPOLIA_RPC_URL`) | Not set                    |
| `{CHAIN}_CONTRACT`  | Deployed tutorial contract address on the source chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`)                    | Not set                    |
| `RECEIVER_ADDRESS`  | Destination receiver address for the token transfer and balance verification                                         | Not set                    |
| `FEE_TOKEN`         | `LINK` or `NATIVE`                                                                                                   | `NATIVE`                   |
| `FEE_TOKEN_ADDRESS` | ERC-20 address of a CCIP-supported fee token on the lane. Takes priority over `FEE_TOKEN`                            | Not set                    |
| `TOKEN_AMOUNT`      | Amount of CCIP-BnM to transfer (in wei)                                                                              | `1000000000000000` (0.001) |
| `BLOCK_DEPTH`       | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality                         | `DEFAULT`                  |

[send-message.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/token-transfers/interact/send-message.ts)

Check out the complete script code on Github.

#### 6. Verify transfer on the destination chain <a id="hardhat-verify-transfer-on-the-destination-chain"></a>

After sending the transfer, you can verify it was successful:

1. Use the **CCIP Explorer** link printed in the send output to track the message status in real time.

2. Once the message is marked as delivered, check the receiver's token balance on the destination chain using a block explorer or any on-chain query tool.

Find the CCIP-BnM token address for each chain in the [CCIP Directory](/ccip/directory).

> **CAUTION: Educational Example Disclaimer**
>
> Please note, this page 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](https://chain.link/terms), which provides important information and disclosures. By using this code, you
> acknowledge and agree to these terms.