# Send Arbitrary Data
Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/send-arbitrary-data
Last Updated: 2026-05-01

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

In this tutorial, we will use CCIP to send arbitrary string data from one chain to another.

We will send a `string` payload from *Ethereum Sepolia* to *Arbitrum Sepolia* twice:

1. Once, using native gas tokens `ETH` and numeric faster than finality.
2. And then a second time, using `LINK` tokens and 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* and `ETH` tokens on *Arbitrum Sepolia*. Learn how to [Acquire testnet LINK](/resources/acquire-link).
3. Check the [CCIP Directory](/ccip/directory) if you want to configure a different source and destination chain pair.

## Examine the code

#### 1. Initializing the contract

When deploying the contract, we define the router address of the blockchain we deploy the contract on.
Defining the router address is useful for the following:

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

- Receiver part:
  - The contract inherits from [CCIPReceiver](/ccip/evm/api-reference/v2.0.0/ccip-receiver), which serves as a base contract for receiver contracts. This contract requires that child contracts implement the `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive).
  - `_ccipReceive` is called by the `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which ensures that only the router can deliver CCIP messages to the receiver contract.

Some key things to note:

- `OwnerIsCreator` sets the deployer as the owner of the contract.
- The constructor passes the router address into `CCIPReceiver` at deployment time.
- `sendMessage` is `payable` and open to any caller. 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:
  - outbound messages are restricted by destination chain selector (`onlyAllowlistedDestinationChain`)
  - inbound messages are restricted by source chain selector + source sender contract pair (`onlyAllowlisted`)
- The contract overrides `getCCVsAndFinalityConfig` from `CCIPReceiver` to advertise per-source-chain receiver finality policy to the OffRamp. See **Configure receiver finality policy** below.

```solidity filename="Messenger.sol"
// Imports

contract Messenger is CCIPReceiver, OwnerIsCreator {
    constructor(address _router) CCIPReceiver(_router) {}

    // ... state variables, modifiers, allowlist admin functions ...

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

    // ... internal helpers (fee handling and message building) ...

    function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage)
        internal
        override
        onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address)))
    {
        s_lastReceivedMessageId = any2EvmMessage.messageId;
        s_lastReceivedSender = abi.decode(any2EvmMessage.sender, (address));
        s_lastReceivedText = abi.decode(any2EvmMessage.data, (string));

        emit MessageReceived( /* ... */ );
    }

    // ... getCCVsAndFinalityConfig override and withdrawal utilities ...
}
```

#### 2. Build transaction payload

`_sendCCIPMessage` calls the `_buildCCIPMessage` helper to build a CCIP message payload using the `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`: ABI-encoded text payload (`abi.encode(_text)`).
- `tokenAmounts`: An empty `EVMTokenAmount` array because this tutorial sends data only.
- `extraArgs`: Pre-encoded message execution parameters built off-chain by a helper script. For finalized finality, the scripts encode V3 extraArgs with the finalized finality config. For non-default finality requests, the scripts probe whether the lane accepts V3 extraArgs for message-only sends and use V3 `requestedFinalityConfig` when available, 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. For message-only sends, there is no transfer token or token pool to query. The helper detects lane support by
>    probing the router's `getFee` function with V3 extraArgs, then falls back to V2 when needed.

```solidity filename="_buildCCIPMessage"
function _buildCCIPMessage(
    address _receiver,
    string calldata _text,
    address _feeTokenAddress,
    bytes calldata _extraArgs
) private pure returns (Client.EVM2AnyMessage memory) {
    return Client.EVM2AnyMessage({
        receiver: abi.encode(_receiver),
        data: abi.encode(_text),
        tokenAmounts: new Client.EVMTokenAmount[](0),
        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 messages

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 ERC-20 fee tokens from the caller and grants the router the required approval, or validates that `msg.value` covers the fee when paying in native gas.
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,
    string calldata _text,
    address _feeTokenAddress,
    bytes calldata _extraArgs
) private returns (bytes32 messageId) {
    Client.EVM2AnyMessage memory evm2AnyMessage =
        _buildCCIPMessage(_receiver, _text, _feeTokenAddress, _extraArgs);

    IRouterClient router = IRouterClient(this.getRouter());
    uint256 ccipFee = router.getFee(_destinationChainSelector, evm2AnyMessage);

    _handleFeeApprovals(router, _feeTokenAddress, ccipFee);

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

    emit MessageSent(messageId, _destinationChainSelector, _receiver, _text, _feeTokenAddress, ccipFee);

    return messageId;
}
```

#### 4. Handling fees and approvals

The contract uses a **pull-from-caller** model for ERC-20 fees: when a user calls `sendMessage` with an ERC-20 fee token, the contract pulls the required fee from `msg.sender` via `safeTransferFrom`, then approves the Router to spend it via `forceApprove`. The caller must `approve` this contract **before** calling `sendMessage`.

`_handleFeeApprovals` handles two scenarios:

1. **Native fee** (`_feeTokenAddress == address(0)`): Validates that `msg.value` covers the CCIP fee.
2. **ERC-20 fee token**: Pulls `ccipFee` from the caller and approves the Router for the same amount.

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

```solidity filename="_handleFeeApprovals"
function _handleFeeApprovals(IRouterClient _router, address _feeTokenAddress, uint256 _ccipFee) private {
    if (_feeTokenAddress == address(0)) {
        if (msg.value < _ccipFee) {
            revert InsufficientNativeForFees(msg.value, _ccipFee);
        }
    } else {
        IERC20(_feeTokenAddress).safeTransferFrom(msg.sender, address(this), _ccipFee);
        IERC20(_feeTokenAddress).forceApprove(address(_router), _ccipFee);
    }
}
```

#### 5. Receiving messages

On the destination blockchain, the router calls the inherited `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which verifies the caller is the router and then invokes the contract's internal `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive). The `_ccipReceive` function expects an `Any2EVMMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#any2evmmessage) that contains:

- The CCIP `messageId`.
- The `sourceChainSelector`.
- The `sender` address in bytes format. The address is decoded from bytes to an Ethereum address using the [ABI specifications](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html) and stored in `s_lastReceivedSender`.
- The `data`, which is also in bytes format. Given a `string` is expected, the data is decoded from bytes to a string using the [ABI specifications](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html).

**Note**: Two important security measures are applied:

- `_ccipReceive` is called by the `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive), which ensures that only the router can deliver CCIP messages to the receiver contract. See the `onlyRouter` [modifier](/ccip/evm/api-reference/v2.0.0/ccip-receiver#onlyrouter-modifier) for more information.
- The modifier `onlyAllowlisted` ensures that only a call from an allowlisted source chain and sender pair is accepted.

```solidity filename="_ccipReceive"
function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage)
    internal
    override
    onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address)))
{
    s_lastReceivedMessageId = any2EvmMessage.messageId;
    s_lastReceivedSender = abi.decode(any2EvmMessage.sender, (address));
    s_lastReceivedText = abi.decode(any2EvmMessage.data, (string));

    emit MessageReceived(
        any2EvmMessage.messageId,
        any2EvmMessage.sourceChainSelector,
        s_lastReceivedSender,
        s_lastReceivedText
    );
}
```

#### 6. Configure receiver finality policy

> **CAUTION: Keep the default verifier; add CCVs as defense in depth**
>
> CCIP verifies every message with its default **committee verifier**. Additional CCVs are defense in depth on top of
> it, not a replacement. The recommended security posture is to keep the committee verifier as your default CCV and
> require additional CCVs only where you need them. See [verification models](/ccip/concepts/ccvs/verification-models)
> and the [trust and responsibility model](/ccip/concepts/ccvs/trust-responsibility-model).

The receiver exposes an `allowedFinalityConfig` for each source chain. This value tells CCIP which finality modes the receiver accepts for messages from that chain. The sender scripts encode the requested mode into V3 `extraArgs` as `requestedFinalityConfig`, then validate the request against the receiver policy (and the token pool policy, when applicable) before sending.

CCIP 2.0 supports two finality request styles:

- **Default finality (finalized)**: Omit `BLOCK_DEPTH` (or set `BLOCK_DEPTH=DEFAULT`).
- **Numeric block depth (faster than finality)**: Set `BLOCK_DEPTH=32` (or higher). This tutorial standardizes on `32`.

This contract uses two functions to manage receiver-side finality policy:

1. **`setAllowedFinalityConfig`**: An owner-only setter that stores the `FinalityCodec`-encoded policy for a source chain.
2. **`getCCVsAndFinalityConfig`**: The OffRamp and scripts can call this hook to read the receiver's CCV and finality policy. This tutorial does not configure custom CCVs, so it returns empty CCV arrays and `optionalThreshold = 0`.

```solidity filename="Receiver finality policy"
function setAllowedFinalityConfig(uint64 _sourceChainSelector, bytes4 _allowedFinalityConfig) external onlyOwner {
    s_allowedFinalityConfig[_sourceChainSelector] = _allowedFinalityConfig;
    emit AllowedFinalityConfigSet(_sourceChainSelector, _allowedFinalityConfig);
}

function getCCVsAndFinalityConfig(uint64 sourceChainSelector, bytes calldata sender)
    external
    view
    override
    returns (
        address[] memory requiredCCVs,
        address[] memory optionalCCVs,
        uint8 optionalThreshold,
        bytes4 allowedFinalityConfig
    )
{
    address decodedSender = abi.decode(sender, (address));
    if (!allowlistedChainSenders[sourceChainSelector][decodedSender]) {
        revert SenderNotAllowedForChain(sourceChainSelector, decodedSender);
    }

    requiredCCVs = new address[](0);
    optionalCCVs = new address[](0);
    optionalThreshold = 0;
    allowedFinalityConfig = s_allowedFinalityConfig[sourceChainSelector];
}
```

[Messenger.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/send-arbitrary-data/Messenger.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
```

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 contracts <a id="foundry-deploy-your-contracts"></a>

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

Check out the complete script code on Github.

In this section, you will deploy the contracts on the source and destination chains. The next step configures the sender contract to allow the destination chain and the receiver contract to allow the source chain-sender pair.

The `Deploy.s.sol` script does the following:

- Deploys `Messenger` on *Ethereum Sepolia* (source).
- Deploys `Messenger` on *Arbitrum Sepolia* (destination).
- Returns the contract addresses in the terminal.

To run the script, use the following command:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
forge script foundry/scripts/tutorials/send-arbitrary-data/deploy/Deploy.s.sol \
--account $KEYSTORE_NAME \
--broadcast -vv
```

Your terminal should look something like this:

```bash filename="Terminal"
========================================
🚀 Deploy CCIP Messenger Contracts on Both Chains
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
========================================


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

========================================
✅ Deployment Complete on Ethereum Sepolia!
========================================
Source Contract Address: 0x385240F511f64c25739fD05fC28a523649086491


[Step 2] Deploying Messenger on Arbitrum Sepolia
Contract deployed at: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
https://sepolia.arbiscan.io/address/0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F

========================================
✅ Deployment Complete on Arbitrum Sepolia!
========================================
Destination Contract Address: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F

========================================
✅ All Deployments Complete!
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0x385240F511f64c25739fD05fC28a523649086491
https://sepolia.etherscan.io/address/0x385240F511f64c25739fD05fC28a523649086491

Destination Chain: Arbitrum Sepolia
Destination Contract: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
https://sepolia.arbiscan.io/address/0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F

Run this command to set both environment variables:
export ETHEREUM_SEPOLIA_CONTRACT=0x385240F511f64c25739fD05fC28a523649086491 && export ARBITRUM_SEPOLIA_CONTRACT=0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
========================================
```

Before moving on, export the addresses of the previously deployed contracts:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_CONTRACT=<sender-contract-address> && \
export ARBITRUM_SEPOLIA_CONTRACT=<receiver-contract-address>
```

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

As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair.

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

Check out the complete script code on Github.

The `Configure.s.sol` script handles both sides in a single command:

- allowlists the destination chain on the sender contract
- allowlists the chain-sender pair on the receiver contract
- sets the receiver contract's allowed finality config for the source chain

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \
forge script foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol \
--account $KEYSTORE_NAME \
--broadcast -vv
```

##### Finality configuration

> **NOTE: Faster Than Finality (FTF)**
>
> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain.
>
> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality.
>
> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message:
>
> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today.
> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**.
>
> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request:
>
> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent.
> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits.
> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution.
>
> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf).

To allow numeric faster than finality requests, set `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` and choose a minimum `ALLOWED_BLOCK_DEPTH`. In this tutorial, `ALLOWED_BLOCK_DEPTH=32` allows send-time requests of `BLOCK_DEPTH=32` (or higher).

Your terminal should look something like this:

```bash filename="Terminal"
========================================
⚙️ Configure CCIP Messenger Contracts on Both Chains
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0x385240F511f64c25739fD05fC28a523649086491
Destination Chain: Arbitrum Sepolia
Destination Contract: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
========================================


[Step 1] Configuring sender on Ethereum Sepolia
Allowlisting Arbitrum Sepolia as destination chain...
✅ Destination chain allowlisted: Arbitrum Sepolia

========================================
✅ Configuration Complete on Ethereum Sepolia!
========================================


[Step 2] Configuring receiver on Arbitrum Sepolia
Allowlisting sender 0x385240F511f64c25739fD05fC28a523649086491 from Ethereum Sepolia...
✅ Chain-sender pair allowlisted: Ethereum Sepolia -> 0x385240F511f64c25739fD05fC28a523649086491
Setting allowed finality config to 0x00000020 (BLOCK_DEPTH=32)...
✅ Allowed finality config set to 0x00000020 (BLOCK_DEPTH=32) for Ethereum Sepolia

========================================
✅ Configuration Complete on Arbitrum Sepolia!
========================================

========================================
✅ All Configurations Complete!
========================================
Ethereum Sepolia can send messages to Arbitrum Sepolia
Arbitrum Sepolia can receive messages from Ethereum Sepolia
```

**Optional: enable bidirectional messaging:** If you want the destination contract to also send messages back to the source, run the reversed command:

```bash filename="Terminal"
SOURCE_CHAIN=ARBITRUM_SEPOLIA DEST_CHAIN=ETHEREUM_SEPOLIA ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \
forge script foundry/scripts/tutorials/send-arbitrary-data/configure/Configure.s.sol \
--account $KEYSTORE_NAME \
--broadcast -vv
```

#### 4. Send a message <a id="foundry-send-a-message"></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:

- Builds off-chain `extraArgs` for the lane. Foundry uses `ExtraArgsHelper.buildExtraArgs` to detect the lane version and encode V2 or V3.
- Approves the contract to spend the caller's ERC-20 fee token when paying with LINK or another ERC-20.
- Sends the CCIP message.

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

The `BLOCK_DEPTH` environment variable controls send-side finality behavior:

- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality.
- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth.

The scripts detect lane support and encode V2 or V3 `extraArgs` as needed.

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

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
FEE_TOKEN=NATIVE GAS_LIMIT=200000 BLOCK_DEPTH=32 \
MESSAGE="Hello from Foundry" \
forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol \
--account $KEYSTORE_NAME \
--broadcast -vv
```

Your terminal should look like this:

```bash filename="Terminal"
========================================
📡 CCIP Data-Only Message - Pay with Native
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x385240F511f64c25739fD05fC28a523649086491
Receiver: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
Fee Token: Native (ETH)
========================================


[Pre-validation] Detecting lane version and building extraArgs...
Gas limit (override): 200000
V3 extraArgs accepted by lane.
Receiver contract ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s))
✅ Using V3 extraArgs with FTF (gasLimit=200000, finalityConfig=0x00000020 (BLOCK_DEPTH: 32 block(s))).

[Step 1] Sending CCIP message with native fee ( ETH )...
Required CCIP fee (in WEI): 10463107598943

========================================
✅ Message sent successfully!
========================================
CCIP messageId: 0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607
```

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

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
FEE_TOKEN=LINK GAS_LIMIT=200000 \
MESSAGE="Hello from Foundry" \
forge script foundry/scripts/tutorials/send-arbitrary-data/interact/SendMessage.s.sol \
--account $KEYSTORE_NAME \
--broadcast -vv
```

Your terminal should look like this:

```bash filename="Terminal"
========================================
📡 CCIP Data-Only Message - Pay with LINK
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x385240F511f64c25739fD05fC28a523649086491
Receiver: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
Fee Token: LINK
========================================


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

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


[Step 2] Sending CCIP message...

========================================
✅ Message sent successfully!
========================================
CCIP messageId: 0x2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60719
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0x2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60719
```

If you use LINK or another ERC-20 fee token, the script approves the `Messenger` contract before sending.

##### 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 per chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`, `ARBITRUM_SEPOLIA_CONTRACT`)                                      | 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                                         |
| `GAS_LIMIT`               | Gas limit for the destination callback                                                                                                                    | `200000`                                        |
| `BLOCK_DEPTH`             | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality                                                              | `DEFAULT`                                       |
| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality config (used by the configure step). Set `BLOCK_DEPTH` to allow numeric `BLOCK_DEPTH` requests. Omit for default-finality-only. | Not set                                         |
| `ALLOWED_BLOCK_DEPTH`     | Receiver minimum block depth (used by the configure step). Required when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`. Use `32` in this tutorial.                | Not set                                         |
| `MESSAGE`                 | Text payload to send                                                                                                                                      | `Hello World From Foundry Script for CCIP 2.0!` |
| `CHAIN`                   | Chain name identifier used by the receipt verification scripts (the chain where the receiver is deployed)                                                 | Not set                                         |

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

Check out the complete script code on Github.

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

Check out the lane-aware extraArgs helper on Github.

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

The `GetLastReceivedMessageDetails.s.sol` script queries the receiver contract for the last received message. This is a read-only operation: no transaction is broadcast.

1. Run the script, passing the `CHAIN` where you deployed the receiver:

```bash filename="Terminal"
CHAIN=ARBITRUM_SEPOLIA \
forge script foundry/scripts/tutorials/send-arbitrary-data/interact/GetLastReceivedMessageDetails.s.sol
```

Your terminal should look something like this:

```bash filename="Terminal"
========================================
🔍 Verify Received Message
========================================
Chain: Arbitrum Sepolia
Receiver Address: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
========================================

Checking for received message...

========================================
✅ Message Received Successfully!
========================================
Message ID: 0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607
Sender: 0x385240F511f64c25739fD05fC28a523649086491
Received Text: "Hello from Foundry"
========================================
```

[GetLastReceivedMessageDetails.s.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/foundry/scripts/tutorials/send-arbitrary-data/interact/GetLastReceivedMessageDetails.s.sol)

Check out the complete script code on Github.

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

5. 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 contracts <a id="hardhat-deploy-your-contracts"></a>

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

Check out the complete script code on Github.

In this section, you will deploy the contracts on the source and destination chains. The next step configures the sender contract to allow the destination chain and the receiver contract to allow the source chain-sender pair.

The `deploy.ts` script does the following:

- Deploys `Messenger` on *Ethereum Sepolia* (source).
- Deploys `Messenger` on *Arbitrum Sepolia* (destination).
- Returns the contract addresses in the terminal.

To run the script, use the following command:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/deploy/deploy.ts
```

Your terminal should look something like this:

```bash filename="Terminal"
========================================
🚀 Deploy CCIP Messenger Contracts on Both Chains
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
========================================

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

========================================
✅ Deployment Complete on Ethereum Sepolia!
========================================
Source Contract Address: 0x385240F511f64c25739fD05fC28a523649086491

[Step 2] Deploying Messenger on Arbitrum Sepolia
Contract deployed at: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
https://sepolia.arbiscan.io/address/0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F

========================================
✅ Deployment Complete on Arbitrum Sepolia!
========================================
Destination Contract Address: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F

========================================
✅ All Deployments Complete!
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0x385240F511f64c25739fD05fC28a523649086491
https://sepolia.etherscan.io/address/0x385240F511f64c25739fD05fC28a523649086491

Destination Chain: Arbitrum Sepolia
Destination Contract: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
https://sepolia.arbiscan.io/address/0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F

Run this command to set both environment variables:
export ETHEREUM_SEPOLIA_CONTRACT=0x385240F511f64c25739fD05fC28a523649086491 && export ARBITRUM_SEPOLIA_CONTRACT=0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
========================================
```

Before moving on, export the addresses of the previously deployed contracts:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_CONTRACT=<sender-contract-address> && \
export ARBITRUM_SEPOLIA_CONTRACT=<receiver-contract-address>
```

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

As a best practice, configure allowlists before sending messages: the sender contract restricts outbound sends by destination chain, and the receiver contract restricts inbound delivery by source chain-sender pair.

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

Check out the complete script code on Github.

The `configure.ts` script handles both sides in a single command:

- allowlists the destination chain on the sender contract
- allowlists the chain-sender pair on the receiver contract
- sets the receiver contract's allowed finality config for the source chain

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/configure/configure.ts
```

##### Finality configuration

> **NOTE: Faster Than Finality (FTF)**
>
> By default, CCIP waits for a message's source-chain transaction to reach **full finality** (the point where the chain guarantees it can no longer be reverted) before the message is verified and executed on the destination chain.
>
> CCIP 2.0 lanes also support **Faster-Than-Finality (FTF)**, an opt-in mode where the sender requests that the message be processed once the source chain has reached a chosen level of assurance *short of* full finality.
>
> The finality setting is expressed in one of two ways, and a sender can choose **exactly one** mode per message:
>
> - **By name (a string):** `Finalized` requests full finality. This is the default. Omit it and CCIP waits for full finality. Other named modes are reserved in the encoding for future protocol use; none are activated on lanes today.
> - **By numeric block depth:** a number from 1 to 65,535 that tells CCIP to wait until N blocks have been built on top of the transaction's block. These tutorials use a minimum block depth of **32**.
>
> Receivers, token pools, and Cross-Chain Verifiers do not request finality; they configure which modes they **allow**. Every gate along the message path must permit the request:
>
> - **Token pool** (token transfers): Validates the request at send time. A request the pool does not permit reverts `ccipSend`, and nothing is sent.
> - **Cross-Chain Verifiers:** Each required verifier attests only messages whose requested finality it permits.
> - **Receiver** (messages that call `ccipReceive`): The receiver's allowed finality is checked at delivery. A mismatch fails execution.
>
> Read more in [Understanding Faster-Than-Finality (FTF) transfers](/ccip/concepts/execution-latency/ftf).

To allow numeric faster than finality requests, set `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH` and choose a minimum `ALLOWED_BLOCK_DEPTH`. In this tutorial, `ALLOWED_BLOCK_DEPTH=32` allows send-time requests of `BLOCK_DEPTH=32` (or higher).

Your terminal should look something like this:

```bash filename="Terminal"
========================================
⚙️ Configure CCIP Messenger Contracts on Both Chains
========================================
Source Chain: Ethereum Sepolia
Source Contract: 0x385240F511f64c25739fD05fC28a523649086491
Destination Chain: Arbitrum Sepolia
Destination Contract: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
========================================

[Step 1] Configuring sender on Ethereum Sepolia
Allowlisting Arbitrum Sepolia as destination chain...
✅ Destination chain allowlisted: Arbitrum Sepolia

========================================
✅ Configuration Complete on Ethereum Sepolia!
========================================

[Step 2] Configuring receiver on Arbitrum Sepolia
Allowlisting sender 0x385240F511f64c25739fD05fC28a523649086491 from Ethereum Sepolia...
✅ Chain-sender pair allowlisted: Ethereum Sepolia -> 0x385240F511f64c25739fD05fC28a523649086491
Setting allowed finality config to 0x00000020 (BLOCK_DEPTH=32)...
✅ Allowed finality config set to 0x00000020 (BLOCK_DEPTH=32) for Ethereum Sepolia

========================================
✅ Configuration Complete on Arbitrum Sepolia!
========================================

========================================
✅ All Configurations Complete!
========================================
Ethereum Sepolia can send messages to Arbitrum Sepolia
Arbitrum Sepolia can receive messages from Ethereum Sepolia
```

**Optional: enable bidirectional messaging:** If you want the destination contract to also send messages back to the source, run the reversed command:

```bash filename="Terminal"
SOURCE_CHAIN=ARBITRUM_SEPOLIA DEST_CHAIN=ETHEREUM_SEPOLIA ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH ALLOWED_BLOCK_DEPTH=32 \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/configure/configure.ts
```

#### 4. Send a message <a id="hardhat-send-a-message"></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:

- Builds off-chain `extraArgs` for the lane. Hardhat uses `buildMessageOnlyExtraArgs` from `hardhat/scripts/extra-args.ts` to detect whether the message-only lane accepts V3 extraArgs and to encode V2 or V3.
- Approves the contract to spend the caller's ERC-20 fee token when paying with LINK or another ERC-20.
- Sends the CCIP message.

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

The `BLOCK_DEPTH` environment variable controls send-side finality behavior:

- Omit `BLOCK_DEPTH`, or set `BLOCK_DEPTH=DEFAULT`/`BLOCK_DEPTH=0` (default): Use finalized finality.
- Set `BLOCK_DEPTH=32`: Request faster than finality using numeric block depth.

The scripts detect lane support and encode V2 or V3 `extraArgs` as needed.

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

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
FEE_TOKEN=NATIVE GAS_LIMIT=200000 BLOCK_DEPTH=32 \
MESSAGE="Hello from Hardhat" \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/interact/send-message.ts
```

Your terminal should look like this:

```bash filename="Terminal"
========================================
📡 CCIP Data-Only Message - Pay with ETH
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x385240F511f64c25739fD05fC28a523649086491
Receiver: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
Fee Token: Native (ETH)
========================================


[Pre-validation] Detecting lane version and building extraArgs...
V3 extraArgs accepted by lane.
Receiver contract ALLOWED_FINALITY_CONFIG: 0x00000020 (BLOCK_DEPTH: 32 block(s))
✅ Using V3 extraArgs with FTF (gasLimit=200000, finalityConfig=32 block(s)).
[Pre-validation] CCIP fee: 10463107598943

[Step 1] Sending CCIP message with native token fee (ETH)...
Required CCIP fee (in WEI): 10463107598943

========================================
✅ Message sent successfully!
========================================
CCIP messageId: 0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607
```

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

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
FEE_TOKEN=LINK GAS_LIMIT=200000 \
MESSAGE="Hello from Hardhat" \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/interact/send-message.ts
```

Your terminal should look like this:

```bash filename="Terminal"
========================================
📡 CCIP Data-Only Message - Pay with LINK
========================================
Source Chain: Ethereum Sepolia
Destination Chain: Arbitrum Sepolia
Sender: 0x385240F511f64c25739fD05fC28a523649086491
Receiver: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
Fee Token: LINK
========================================


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

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


[Step 2] Sending CCIP message...

========================================
✅ Message sent successfully!
========================================
CCIP messageId: 0x2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60719
CCIP Explorer:
https://ccip.chain.link/#/side-drawer/msg/0x2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60719
```

If you use LINK or another ERC-20 fee token, the script approves the `Messenger` contract before sending.

##### 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 per chain (for example, `ETHEREUM_SEPOLIA_CONTRACT`, `ARBITRUM_SEPOLIA_CONTRACT`)                                      | 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                                         |
| `GAS_LIMIT`               | Gas limit for the destination callback                                                                                                                    | `200000`                                        |
| `BLOCK_DEPTH`             | Omit or set `DEFAULT` for finalized finality (default), or set `32` for faster than finality                                                              | `DEFAULT`                                       |
| `ALLOWED_FINALITY_CONFIG` | Receiver allowed finality config (used by the configure step). Set `BLOCK_DEPTH` to allow numeric `BLOCK_DEPTH` requests. Omit for default-finality-only. | Not set                                         |
| `ALLOWED_BLOCK_DEPTH`     | Receiver minimum block depth (used by the configure step). Required when `ALLOWED_FINALITY_CONFIG=BLOCK_DEPTH`. Use `32` in this tutorial.                | Not set                                         |
| `MESSAGE`                 | Text payload to send                                                                                                                                      | `Hello World From Hardhat Script for CCIP 2.0!` |
| `CHAIN`                   | Chain name identifier used by the receipt verification scripts (the chain where the receiver is deployed)                                                 | Not set                                         |

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

Check out the complete script code on Github.

[extra-args.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/extra-args.ts)

Check out the lane-aware extraArgs helper used by Hardhat scripts.

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

[get-last-received-message-details.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data/interact/get-last-received-message-details.ts)

Check out the complete script code on Github.

The `get-last-received-message-details.ts` script queries the receiver contract for the last received message. This is a read-only operation: no transaction is broadcast.

1. Run the script, passing the `CHAIN` where you deployed the receiver:

```bash filename="Terminal"
CHAIN=ARBITRUM_SEPOLIA \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data/interact/get-last-received-message-details.ts
```

Your terminal should look something like this:

```bash filename="Terminal"
========================================
🔍 Verify Received Message
========================================
Chain: Arbitrum Sepolia
Receiver Address: 0x7F3a1C8e2B5d9A4f6E0c3D7b1F9a2E5c8B4d6A0F
========================================

Checking for received message...

========================================
✅ Message Received Successfully!
========================================
Message ID: 0x1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607
Sender: 0x385240F511f64c25739fD05fC28a523649086491
Received Text: "Hello from Hardhat"
========================================
```

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