# Send Arbitrary Data and Receive Transfer Confirmation: A -> B -> A
Source: https://docs.chain.link/ccip/evm/tutorials/application-developers/send-arbitrary-data-receipt-acknowledgment
Last Updated: 2026-05-26

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

In this tutorial, we will use CCIP to send arbitrary data from one chain to another and then have the receiver contract send back an acknowledgment message back to the sender:

- A MessageTracker contract on chain A sends a text message to an Acknowledger contract on chain B.
- After processing the message, the Acknowledger sends an acknowledgment message back to the MessageTracker.
- The MessageTracker updates onchain state to reflect the successful execution of the back and forth pattern.

By tracking acknowledgment onchain, your contracts can safely trigger follow-up actions only after the destination contract has confirmed the receipt.

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

- This tutorial assumes you have completed the [Send Arbitrary Data](/ccip/evm/tutorials/application-developers/send-arbitrary-data) tutorial.
- You will use *Ethereum Sepolia* (A) and *Arbitrum Sepolia* (B).
- Your wallet should have:
  - Sepolia ETH (for deploying and sending from Ethereum Sepolia)
  - Arbitrum Sepolia ETH (for deploying on Arbitrum Sepolia and funding the Acknowledger)
  - Optional: Sepolia LINK (if you want to pay the CCIP fee in LINK)
- Learn how to [Acquire testnet LINK](/resources/acquire-link).

## Examine the code

The contracts and scripts used in this tutorial live in the Chainlink CCIP 2.0 starter kit repository (`docs-ccip`).

[MessageTracker.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/MessageTracker.sol)

Sends the initial message A→B and tracks the acknowledgment B→A.

[Acknowledger.sol](https://github.com/smartcontractkit/docs-ccip/blob/main/contracts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/Acknowledger.sol)

Receives the message on B and sends an acknowledgment back to A.

#### 1. Initializing both contracts

When deploying each contract, you pass the Router address for that chain to `CCIPReceiver`:

- Chain A (source): `MessageTracker` (sends A→B and receives the B→A acknowledgment)
- Chain B (destination): `Acknowledger` (receives A→B and sends the B→A acknowledgment)

Some key things to note:

- Both contracts inherit from `CCIPReceiver`, so `ccipReceive` ensures that only the Router can deliver CCIP messages.
- Both contracts inherit from `OwnerIsCreator`, so the deployer is the owner and can configure allowlists and receiver finality policy.
- In the canonical code, `MessageTracker.sendMessage` is `onlyOwner`, so the tutorial scripts broadcast from the deployer/owner account.

```solidity filename="MessageTracker.sol"
contract MessageTracker is CCIPReceiver, OwnerIsCreator {
    constructor(address _router) CCIPReceiver(_router) {}

    function sendMessage(
        uint64 _destinationChainSelector,
        address _receiver,
        string calldata _text,
        address _feeTokenAddress,
        bytes calldata _extraArgs
    )
        external
        payable
        onlyOwner
        // ... allowlists + receiver validation ...
        returns (bytes32 messageId)
    {
        // ...
    }
}
```

```solidity filename="Acknowledger.sol"
contract Acknowledger is CCIPReceiver, OwnerIsCreator {
    constructor(address _router) CCIPReceiver(_router) {}
}
```

#### 2. Architecture and message lifecycle (A→B→A)

In this tutorial, we deploy:

- Chain A (source): *Ethereum Sepolia* → `MessageTracker`
- Chain B (destination): *Arbitrum Sepolia* → `Acknowledger`

The lifecycle of a tracked message is:

1. **A→B**: `MessageTracker.sendMessage(...)` sends a text payload to the Acknowledger.
2. **B→A**: the Acknowledger receives the message and sends an acknowledgment back, encoding the **initial** message ID.
3. **State update**: when the MessageTracker receives the acknowledgment, it marks the initial message as processed.

`MessageTracker` tracks messages by the **initial CCIP messageId** (A→B), and stores the acknowledgment messageId (B→A) alongside it:

```solidity filename="MessageTracker.sol"
enum MessageStatus {
    NotSent, // 0: default / unknown messageId
    Sent, // 1: sent to Acknowledger, awaiting acknowledgment
    ProcessedOnDestination // 2: acknowledgment received
}

struct MessageInfo {
    MessageStatus status;
    bytes32 acknowledgerMessageId; // CCIP messageId of the acknowledgment (B→A)
}

mapping(bytes32 => MessageInfo) public messagesInfo;
```

#### 3. MessageTracker: build payload and send (A→B)

`MessageTracker` sends a data-only CCIP message using the `Client.EVM2AnyMessage` struct.

The payload includes:

- `receiver`: ABI-encoded destination address (`abi.encode(_receiver)`).
- `data`: ABI-encoded `(text, extraArgs)` tuple (`abi.encode(_text, _extraArgs)`).
- `tokenAmounts`: an empty array (data-only).
- `extraArgs`: pre-encoded execution parameters built off-chain by the scripts (gas limit + requested finality config).
- `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.

```solidity filename="MessageTracker.sol"
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),
        // Pack (text, extraArgs) so the Acknowledger can reuse the same lane/finality config for the acknowledgment.
        data: abi.encode(_text, _extraArgs),
        tokenAmounts: new Client.EVMTokenAmount[](0),
        extraArgs: _extraArgs,
        feeToken: _feeTokenAddress
    });
}
```

> **NOTE: Why pack extraArgs into the data field?**
>
> The receiver's `Any2EVMMessage` does not include the original `extraArgs` field. Packing `extraArgs` into `data` lets
> the Acknowledger decode and forward the same bytes into the B→A acknowledgment message.

After `ccipSend`, the contract immediately marks the initial message as `Sent`:

```solidity filename="MessageTracker.sol"
if (_feeTokenAddress == address(0)) {
    messageId = router.ccipSend{value: ccipFee}(_destinationChainSelector, evm2AnyMessage);
} else {
    messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
}

messagesInfo[messageId].status = MessageStatus.Sent;
```

#### 4. Acknowledger: receive message and send acknowledgment (B→A)

On chain B, the router calls `Acknowledger.ccipReceive(...)`, which verifies the caller is the router and then invokes the contract's internal `_ccipReceive(...)`.

Two important details:

- The A→B payload packs `(text, extraArgs)` into `data`, so the Acknowledger decodes both values.
- The Acknowledger forwards the decoded `extraArgs` into the acknowledgment send so both legs use the same lane/finality config.

```solidity filename="Acknowledger.sol"
bytes memory ackExtraArgs;
(s_lastReceivedText, ackExtraArgs) = abi.decode(any2EvmMessage.data, (string, bytes));

bytes32 messageIdToAcknowledge = any2EvmMessage.messageId;
address messageTrackerAddress = abi.decode(any2EvmMessage.sender, (address));
uint64 messageTrackerChainSelector = any2EvmMessage.sourceChainSelector;

_sendAcknowledgment(messageIdToAcknowledge, messageTrackerAddress, messageTrackerChainSelector, ackExtraArgs);
```

The acknowledgment encodes the initial message ID and always pays fees with native gas held by the Acknowledger contract:

```solidity filename="Acknowledger.sol"
Client.EVM2AnyMessage memory acknowledgment = Client.EVM2AnyMessage({
    receiver: abi.encode(_messageTrackerAddress),
    data: abi.encode(_messageIdToAcknowledge),
    tokenAmounts: new Client.EVMTokenAmount[](0),
    extraArgs: _ackExtraArgs,
    feeToken: address(0) // pay with native (held by contract)
});

uint256 fees = router.getFee(_messageTrackerChainSelector, acknowledgment);
bytes32 messageId = router.ccipSend{value: fees}(_messageTrackerChainSelector, acknowledgment);
```

> **NOTE: Funding requirement**
>
> The Acknowledger pays CCIP fees for acknowledgment messages using native gas held by the contract, so you must fund it
> with Arbitrum Sepolia ETH before sending messages.

#### 5. MessageTracker: receive acknowledgment and update status

When the acknowledgment returns B→A, the MessageTracker decodes the initial message ID from the incoming message data.

It then:

- stores the acknowledgment messageId for auditability (`acknowledgerMessageId`)
- advances the state machine (`Sent → ProcessedOnDestination`)
- reverts if the initial message ID is unknown or already processed

```solidity filename="MessageTracker.sol"
bytes32 initialMsgId = abi.decode(any2EvmMessage.data, (bytes32));
bytes32 acknowledgerMsgId = any2EvmMessage.messageId;

messagesInfo[initialMsgId].acknowledgerMessageId = acknowledgerMsgId;

MessageStatus currentStatus = messagesInfo[initialMsgId].status;

if (currentStatus == MessageStatus.Sent) {
    messagesInfo[initialMsgId].status = MessageStatus.ProcessedOnDestination;
    emit MessageProcessedOnDestination(
        acknowledgerMsgId,
        initialMsgId,
        any2EvmMessage.sourceChainSelector,
        abi.decode(any2EvmMessage.sender, (address))
    );
} else if (currentStatus == MessageStatus.ProcessedOnDestination) {
    revert MessageHasAlreadyBeenProcessedOnDestination(initialMsgId);
} else {
    revert MessageWasNotSentByMessageTracker(initialMsgId);
}
```

The tutorial scripts use `getMessageInfo(initialMessageId)` to read:

- the status code (`0`, `1`, or `2`)
- the acknowledgment’s CCIP message ID (once it has arrived)

#### 6. Security: allowlists + receiver finality policy (both directions)

Because this tutorial is bidirectional, both contracts enforce allowlists in both directions:

- **Outbound sends** are restricted by destination chain selector:
  - `MessageTracker` requires the destination chain selector to be allowlisted.
  - `Acknowledger` requires the source chain selector (used as the acknowledgment destination) to be allowlisted.
- **Inbound delivery** is restricted by `(source chain selector, sender address)` via the `onlyAllowlisted` checks in both `_ccipReceive` implementations.

Both contracts also expose a receiver finality policy for each source chain via `getCCVsAndFinalityConfig(...)`. The configuration scripts set this policy on both contracts so numeric block-depth finality requests (minimum `32`) can be accepted when configured.

> **NOTE: Default finality is the default**
>
> If you omit the finality-related environment variables, the scripts use default finality behavior.

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

## 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 starter kit (contains both Hardhat and Foundry code) for a smoother setup.

1. Clone the CCIP starter kit and open a terminal inside the project directory:

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

2. Create a Foundry keystore entry for your private key. Foundry will prompt you to enter the 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. Compile all contracts:

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

#### 2. Deploy your contracts (A and B) <a id="foundry-deploy-your-contracts-a-and-b"></a>

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

Deploys MessageTracker on Ethereum Sepolia and Acknowledger on Arbitrum Sepolia.

Run the deploy script:

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

Your terminal should look something like this:

```bash filename="Terminal"
========================================
🚀 Deploy MessageTracker + Acknowledger Contracts
========================================
Source Chain (MessageTracker): Ethereum Sepolia
Destination Chain (Acknowledger): Arbitrum Sepolia
========================================


[Step 1] Deploying MessageTracker on Ethereum Sepolia...
MessageTracker deployed at: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
https://sepolia.etherscan.io/address/0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123

========================================
✅ MessageTracker Deployed on Ethereum Sepolia!
========================================
MessageTracker Address: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123


[Step 2] Deploying Acknowledger on Arbitrum Sepolia...
Acknowledger deployed at: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
https://sepolia.arbiscan.io/address/0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a

========================================
✅ Acknowledger Deployed on Arbitrum Sepolia!
========================================
Acknowledger Address: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a

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

Destination Chain: Arbitrum Sepolia
  Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
https://sepolia.arbiscan.io/address/0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a

Run this command to set both environment variables:
export ETHEREUM_SEPOLIA_CONTRACT=0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 && export ARBITRUM_SEPOLIA_CONTRACT=0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
========================================

IMPORTANT: Fund the Acknowledger contract with native gas tokens on Arbitrum Sepolia
  The Acknowledger pays CCIP fees (native) for sending acknowledgments back to the MessageTracker.
```

Export the contract addresses printed by the script:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_CONTRACT=0x...  # MessageTracker
export ARBITRUM_SEPOLIA_CONTRACT=0x...    # Acknowledger
```

#### 3. Fund the Acknowledger (required) <a id="foundry-fund-the-acknowledger-required"></a>

The Acknowledger pays CCIP fees for acknowledgment messages with native gas held by the contract.
Fund it with Arbitrum Sepolia ETH before sending messages.

Example (using cast):

```bash filename="Terminal"
cast send $ARBITRUM_SEPOLIA_CONTRACT \
--value 0.01ether \
--account $KEYSTORE_NAME \
--rpc-url $ARBITRUM_SEPOLIA_RPC_URL \
--chain arbitrum-sepolia
```

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

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

Allowlists both contracts and configures receiver finality policy.

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

Configure both contracts in a single command. This tutorial uses a block-depth policy with a minimum depth of 32.

```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-and-receive-transfer-confirmation/configure/Configure.s.sol:Configure \
--account $KEYSTORE_NAME \
--broadcast -vv
```

Your terminal should look something like this:

```bash filename="Terminal"
========================================
⚙️ Configure MessageTracker + Acknowledger Contracts
========================================
Source Chain: Ethereum Sepolia
  MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
Destination Chain: Arbitrum Sepolia
  Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
========================================


[Step 1] Configuring MessageTracker on Ethereum Sepolia
Allowlisting Arbitrum Sepolia as destination chain on MessageTracker...
✅ Destination chain allowlisted on MessageTracker: Arbitrum Sepolia
Allowlisting Acknowledger (0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a) from Arbitrum Sepolia on MessageTracker...
✅ Acknowledger allowlisted on MessageTracker: Arbitrum Sepolia -> 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
Setting allowed finality config on MessageTracker to 0x00000020 (BLOCK_DEPTH=32)...
✅ Allowed finality config set on MessageTracker for Arbitrum Sepolia

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


[Step 2] Configuring Acknowledger on Arbitrum Sepolia
Allowlisting MessageTracker (0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123) from Ethereum Sepolia on Acknowledger...
✅ MessageTracker allowlisted on Acknowledger: Ethereum Sepolia -> 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
Allowlisting Ethereum Sepolia as destination chain on Acknowledger...
✅ Source chain allowlisted as destination on Acknowledger: Ethereum Sepolia
Setting allowed finality config on Acknowledger to 0x00000020 (BLOCK_DEPTH=32)...
✅ Allowed finality config set on Acknowledger for Ethereum Sepolia

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

========================================
✅ All Configurations Complete!
========================================
Ethereum Sepolia (MessageTracker) can send messages to Arbitrum Sepolia (Acknowledger)
Arbitrum Sepolia (Acknowledger) can receive messages from Ethereum Sepolia and send acknowledgments back
```

#### 5. Send a message (A→B) <a id="foundry-send-a-message-a-b"></a>

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

Sends a message from MessageTracker to Acknowledger.

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

##### Example 1: Send a message paying with native:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT \
MESSAGE="Hello World From Foundry" \
forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/SendMessage.s.sol:SendMessage \
--account $KEYSTORE_NAME \
--broadcast -vv
```

To request numeric faster than finality (minimum 32), set `BLOCK_DEPTH=32`.

Example output:

```bash filename="Terminal"
========================================
📡 CCIP Message (A→B→A) - Pay with Native
========================================
Source Chain: Ethereum Sepolia
  MessageTracker: 0x6C16098831403AaC46eb0Bbd989454D0D890ECAb
Destination Chain: Arbitrum Sepolia
  Acknowledger: 0x4F8a2C6e0B4d8A1f6E3c9B5d7A2f4C8e0B6d3A5c
Fee Token: Native (ETH)
Message: "Hello World From Foundry"
========================================


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

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

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

Track message status (status 1 = Sent, 2 = ProcessedOnDestination):
SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5 forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/GetMessageStatus.s.sol:GetMessageStatus -vv
========================================
```

##### Example 2: Send a message paying with LINK (optional):

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
FEE_TOKEN=LINK GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT \
MESSAGE="Hello World From Foundry" \
forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/SendMessage.s.sol:SendMessage \
--account $KEYSTORE_NAME \
--broadcast -vv
```

To request numeric faster than finality (minimum 32), set `BLOCK_DEPTH=32`.

Example output:

```bash filename="Terminal"
========================================
📡 CCIP Message (A→B→A) - Pay with LINK
========================================
Source Chain: Ethereum Sepolia
  MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
Destination Chain: Arbitrum Sepolia
  Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
Fee Token: LINK
Message: "Hello World From Foundry"
========================================


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

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


[Step 2] Sending message via MessageTracker...

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

Track message status (status 1 = Sent, 2 = ProcessedOnDestination):
SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/GetMessageStatus.s.sol:GetMessageStatus -vv
========================================
```

Copy the `CCIP messageId` from the output.

##### 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 (MessageTracker on source, Acknowledger on destination)                                                      | 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                                                                                                                    | `500000`                                        |
| `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!` |
| `MESSAGE_ID`              | Message ID to query status (used by the status script)                                                                                                    | Not set                                         |
| `CHAIN`                   | Chain name identifier used by the withdraw script (the chain where the Acknowledger is deployed)                                                          | Not set                                         |

#### 6. Track status (B→A acknowledgment) <a id="foundry-track-status-b-a-acknowledgment"></a>

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

Reads message status from MessageTracker.

Track the message status on the source chain:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x... \
forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/GetMessageStatus.s.sol:GetMessageStatus -vv
```

Status values:

- `0`: `NotSent`
- `1`: `Sent`
- `2`: `ProcessedOnDestination`

Example output:

```bash filename="Terminal"
========================================
🔍 Check Message Status
========================================
Chain: Ethereum Sepolia
MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
Message ID: 0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6
========================================

========================================
Message Status: 2 (ProcessedOnDestination)
✅ Message acknowledged! The Acknowledger has processed the message and the MessageTracker has received the acknowledgment.
Acknowledger Message ID: 0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e8
Check the CCIP Explorer for the acknowledgment message:
https://ccip.chain.link/#/side-drawer/msg/0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e8
========================================
```

#### 7. Withdraw from Acknowledger (optional cleanup) <a id="foundry-withdraw-from-acknowledger-optional-cleanup"></a>

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

Withdraws native balance from the Acknowledger contract.

After testing, reclaim any remaining native balance from the Acknowledger:

```bash filename="Terminal"
CHAIN=ARBITRUM_SEPOLIA \
forge script foundry/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/WithdrawFromAcknowledger.s.sol:WithdrawFromAcknowledger \
--account $KEYSTORE_NAME \
--broadcast -vv
```

Example output:

```bash filename="Terminal"
========================================
💸 Withdraw from Acknowledger
========================================
Chain: Arbitrum Sepolia
Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
Native balance: 10000000000000000 wei
========================================

[Step 1] Withdrawing native balance to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994...
✅ Native withdrawal complete

========================================
✅ Withdraw Complete
========================================
Beneficiary: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
```

By default, funds are sent to the broadcasting account. Set `BENEFICIARY=0x...` to override the recipient.

### Hardhat

Best for devs looking for 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 starter kit 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
```

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 and B) <a id="hardhat-deploy-your-contracts-a-and-b"></a>

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

Deploys MessageTracker on Ethereum Sepolia and Acknowledger on Arbitrum Sepolia.

Run the deploy script:

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

Example output:

```bash filename="Terminal"
========================================
🚀 Deploy MessageTracker + Acknowledger Contracts
========================================
Source Chain (MessageTracker): Ethereum Sepolia
Destination Chain (Acknowledger): Arbitrum Sepolia
========================================

[Step 1] Deploying MessageTracker on Ethereum Sepolia
MessageTracker deployed at: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
https://sepolia.etherscan.io/address/0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123

========================================
✅ MessageTracker Deployed on Ethereum Sepolia!
========================================
MessageTracker Address: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123

[Step 2] Deploying Acknowledger on Arbitrum Sepolia
Acknowledger deployed at: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
https://sepolia.arbiscan.io/address/0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a

========================================
✅ Acknowledger Deployed on Arbitrum Sepolia!
========================================
Acknowledger Address: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a

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

Destination Chain: Arbitrum Sepolia
  Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
https://sepolia.arbiscan.io/address/0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a

Run this command to set both environment variables:
export ETHEREUM_SEPOLIA_CONTRACT=0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123 && export ARBITRUM_SEPOLIA_CONTRACT=0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
========================================

IMPORTANT: Fund the Acknowledger contract with native gas tokens on Arbitrum Sepolia
  The Acknowledger pays CCIP fees (native) for sending acknowledgments back to the MessageTracker.
```

Export the contract addresses printed by the script:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_CONTRACT=0x...  # MessageTracker
export ARBITRUM_SEPOLIA_CONTRACT=0x...    # Acknowledger
```

#### 3. Fund the Acknowledger (required) <a id="hardhat-fund-the-acknowledger-required"></a>

The Acknowledger pays CCIP fees for acknowledgment messages with native gas held by the contract. Fund it with Arbitrum Sepolia ETH before sending messages.

Example (using cast):

```bash filename="Terminal"
cast send $ARBITRUM_SEPOLIA_CONTRACT \
--value 0.01ether \
--account $KEYSTORE_NAME \
--rpc-url $ARBITRUM_SEPOLIA_RPC_URL \
--chain arbitrum-sepolia
```

If you don't have `cast` installed, you can fund the contract from any wallet by sending Arbitrum Sepolia ETH to `$ARBITRUM_SEPOLIA_CONTRACT`.

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

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

Allowlists both contracts and configures receiver finality policy.

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

Configure both contracts in a single command:

```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-and-receive-transfer-confirmation/configure/configure.ts
```

Example output:

```bash filename="Terminal"
========================================
⚙️ Configure MessageTracker + Acknowledger Contracts
========================================
Source Chain: Ethereum Sepolia
  MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
Destination Chain: Arbitrum Sepolia
  Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
========================================

[Step 1] Configuring MessageTracker on Ethereum Sepolia
Allowlisting Arbitrum Sepolia as destination chain on MessageTracker...
✅ Destination chain allowlisted on MessageTracker: Arbitrum Sepolia
Allowlisting Acknowledger (0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a) from Arbitrum Sepolia on MessageTracker...
✅ Acknowledger allowlisted on MessageTracker: Arbitrum Sepolia -> 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
Setting allowed finality config on MessageTracker to 0x00000020 (BLOCK_DEPTH=32)...
✅ Allowed finality config set on MessageTracker to 0x00000020 (BLOCK_DEPTH=32) for Arbitrum Sepolia

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

[Step 2] Configuring Acknowledger on Arbitrum Sepolia
Allowlisting MessageTracker (0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123) from Ethereum Sepolia on Acknowledger...
✅ MessageTracker allowlisted on Acknowledger: Ethereum Sepolia -> 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
Allowlisting Ethereum Sepolia as destination chain on Acknowledger (for ack messages)...
✅ Source chain allowlisted as destination on Acknowledger: Ethereum Sepolia
Setting allowed finality config on Acknowledger to 0x00000020 (BLOCK_DEPTH=32)...
✅ Allowed finality config set on Acknowledger to 0x00000020 (BLOCK_DEPTH=32) for Ethereum Sepolia

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

========================================
✅ All Configurations Complete!
========================================
Ethereum Sepolia (MessageTracker) can send messages to Arbitrum Sepolia (Acknowledger)
Arbitrum Sepolia (Acknowledger) can receive messages from Ethereum Sepolia and send acknowledgments back

Next Step: Send a Message
Send a message paying with LINK:
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA FEE_TOKEN=LINK GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT MESSAGE='Hello World From Hardhat Script for CCIP 2.0!' npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/send-message.ts
========================================
```

#### 5. Send a message (A→B) <a id="hardhat-send-a-message-a-b"></a>

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

Sends a message from MessageTracker to Acknowledger.

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

##### Example 1: Send a message paying with native:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT \
MESSAGE="Hello World From Hardhat" \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/send-message.ts
```

To request numeric faster than finality (minimum 32), set `BLOCK_DEPTH=32`.

Example output:

```bash filename="Terminal"
========================================
📡 CCIP Message (A→B→A) - Pay with ETH
========================================
Source Chain: Ethereum Sepolia
  MessageTracker: 0x6c16098831403aac46eb0bbd989454d0d890ecab
Destination Chain: Arbitrum Sepolia
  Acknowledger: 0x4f8a2c6e0b4d8a1f6e3c9b5d7a2f4c8e0b6d3a5c
Fee Token: Native (ETH)
Message: "Hello World From Hardhat"
========================================


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

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

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

Track message status (1 = Sent, 2 = ProcessedOnDestination):
SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0xaf1e2d3c4b5a69788796a5b4c3d2e1f00918273a4b5c6d7e8f90a1b2c3d4e5f6 npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/get-message-status.ts
========================================
```

##### Example 2: Send a message paying with LINK (optional):

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA DEST_CHAIN=ARBITRUM_SEPOLIA \
FEE_TOKEN=LINK GAS_LIMIT=500000 BLOCK_DEPTH=DEFAULT \
MESSAGE="Hello World From Hardhat" \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/send-message.ts
```

To request numeric faster than finality (minimum 32), set `BLOCK_DEPTH=32`.

Example output:

```bash filename="Terminal"
========================================
📡 CCIP Message (A→B→A) - Pay with LINK
========================================
Source Chain: Ethereum Sepolia
  MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
Destination Chain: Arbitrum Sepolia
  Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
Fee Token: LINK
Message: "Hello World From Hardhat"
========================================


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

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


[Step 2] Sending message via MessageTracker...

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

Track message status (1 = Sent, 2 = ProcessedOnDestination):
SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6 npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/get-message-status.ts
========================================
```

Copy the `CCIP messageId` from the output.

##### 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 (MessageTracker on source, Acknowledger on destination)                                                      | 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                                                                                                                    | `500000`                                        |
| `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!` |
| `MESSAGE_ID`              | Message ID to query status (used by the status script)                                                                                                    | Not set                                         |
| `CHAIN`                   | Chain name identifier used by the withdraw script (the chain where the Acknowledger is deployed)                                                          | Not set                                         |

#### 6. Track status (B→A acknowledgment) <a id="hardhat-track-status-b-a-acknowledgment"></a>

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

Reads message status from MessageTracker.

Track the message status on the source chain:

```bash filename="Terminal"
SOURCE_CHAIN=ETHEREUM_SEPOLIA MESSAGE_ID=0x... \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/get-message-status.ts
```

Example output:

```bash filename="Terminal"
========================================
🔍 Check Message Status
========================================
Chain: Ethereum Sepolia
MessageTracker: 0x5E4b6A1b2C3d4E5f67890123456789aBCdEf0123
Message ID: 0x8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6
========================================

========================================
Message Status: 2 (ProcessedOnDestination)
✅ Message acknowledged! The Acknowledger has processed the message and the MessageTracker has received the acknowledgment.
Acknowledger Message ID: 0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e8
Check the CCIP Explorer for the acknowledgment message:
https://ccip.chain.link/#/side-drawer/msg/0x9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e8
========================================
```

#### 7. Withdraw from Acknowledger (optional cleanup) <a id="hardhat-withdraw-from-acknowledger-optional-cleanup"></a>

[withdraw-from-acknowledger.ts](https://github.com/smartcontractkit/docs-ccip/blob/main/hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/withdraw-from-acknowledger.ts)

Withdraws native balance from the Acknowledger contract.

After testing, reclaim any remaining native balance from the Acknowledger:

```bash filename="Terminal"
CHAIN=ARBITRUM_SEPOLIA \
npx hardhat run hardhat/scripts/tutorials/send-arbitrary-data-and-receive-transfer-confirmation/interact/withdraw-from-acknowledger.ts
```

Example output:

```bash filename="Terminal"
========================================
💸 Withdraw from Acknowledger
========================================
Chain: Arbitrum Sepolia
Acknowledger: 0x9C2e4F6a8B0d1E3f5A7c9E2b4D6f8A0c2E4b6D8a
Native balance: 10000000000000000 wei
Beneficiary: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
========================================

[Step 1] Withdrawing native balance to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994...
✅ Native withdrawal complete (tx: 0xb0e1d2c3a49586778879a6b5c4d3e2f11029384756a6b7c8d9e0f1a2b3c4d5e6)

========================================
✅ Withdraw Complete
========================================
Beneficiary: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
```

By default, funds are sent to the deployer account. Set `BENEFICIARY=0x...` to override the recipient.

## Final note

This tutorial uses a simple acknowledgment message to confirm delivery end-to-end. You can adapt the same pattern to:

- programmable token transfers
- multi-step workflows that depend on destination-side execution
- onchain receipts that unlock follow-up actions on the source chain

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