# 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

[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)

[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)

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

[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)

[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)

[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)

[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

[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)

[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)

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

[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)

[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)

[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)

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