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

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

This tutorial will teach you how to use Chainlink CCIP to send arbitrary data between smart contracts on different blockchains and how to track the status of each sent message in the sender contract on the source chain. Tracking the status of sent messages allows your smart contracts to execute actions after the receiver acknowledges it received the message. In this example, the sender contract emits an event after it receives acknowledgment from the receiver.

**Note**: For simplicity, this tutorial demonstrates this pattern for sending arbitrary data. However, you are not limited to this application. You can apply the same pattern to programmable token transfers.

## Before you begin

- This tutorial assumes you have completed the [Send Arbitrary Data](/ccip/tutorials/evm/send-arbitrary-data) tutorial.
- Your account must have some AVAX tokens on *Avalanche Fuji* and ETH tokens on *Ethereum Sepolia*.
- Learn how to [Acquire testnet LINK](/resources/acquire-link) and [Fund your contract with LINK](/resources/fund-your-contract).

## Tutorial

> **NOTE: Optimize your development with the CCIP local simulator**
>
> Enhance your development workflow using the [Chainlink CCIP local
> simulator](https://github.com/smartcontractkit/chainlink-local), an installable package designed to simulate Chainlink
> CCIP locally within your Hardhat and Foundry projects. It provides a robust smart contracts and scripts suite,
> enabling you to build, deploy, and execute CCIP token transfers and arbitrary messages on a local Hardhat or Anvil
> development node. With Chainlink Local, you can also work on forked nodes, ensuring a seamless transition of your
> contracts to test networks without modifications. Start integrating Chainlink Local today to streamline your
> development process and validate your CCIP implementations effectively.

In this tutorial, you will deploy a *message tracker* contract on the source blockchain (Avalanche Fuji) and an *acknowledger* on the destination blockchain (Ethereum Sepolia). Throughout the tutorial, you will pay for CCIP fees using LINK tokens. Here is a step-by-step breakdown:

1. **Sending and building a CCIP message:** Initiate and send a message from the *message tracker* contract on Avalanche Fuji to the *acknowledger* contract on Ethereum Sepolia. The *message tracker* contract constructs a CCIP message that encapsulates a text string and establishes a tracking status for this message before sending it off.
2. **Receiving and acknowledging the message:** After the *acknowledger* contract receives the text on Ethereum Sepolia, it sends back a CCIP message to the *message tracker* contract as an acknowledgment of receipt.
3. **Updating tracking status:** After the *message tracker* receives the acknowledgment, the contract updates the tracking status of the initial CCIP message and emits an event to signal completion.

### Deploy the *message tracker* (sender) contract

Deploy the `MessageTracker.sol` contract on *Avalanche Fuji* and enable it to send and receive CCIP messages to and from *Ethereum Sepolia*. You must also enable your contract to receive CCIP messages from the *acknowledger* contract.

1. [Open the MessageTracker.sol contract](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/MessageTracker.sol) in Remix.

   [Open MessageTracker.sol in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/MessageTracker.sol)

   Note: The contract code is also available in the [Examine the code](/ccip/tutorials/evm/send-arbitrary-data-receipt-acknowledgment#messagetrackersol) section.

2. Compile the contract.

3. Deploy the contract on *Avalanche Fuji*:
   1. Open MetaMask and select the *Avalanche Fuji* network.

   2. On the **Deploy & Run Transactions** tab in Remix, select *Injected Provider - MetaMask* in the **Environment** list. Remix will use the MetaMask wallet to communicate with *Avalanche Fuji*.

   3. Under the **Deploy** section, fill in the router address and the LINK token contract address for your specific blockchain. You can find both of these addresses on the [CCIP Directory](/ccip/directory). The LINK token contract address is also listed on the [LINK Token Contracts](/resources/link-token-contracts) page. For *Avalanche Fuji*:
      - The router address is 0xF694E193200268f9a4868e4Aa017A0118C9a8177
      - The LINK token address is 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846

   4. Click **transact** to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract on *Avalanche Fuji*.

   5. After you confirm the transaction, the contract address appears in the **Deployed Contracts** list. Copy your contract address.

   6. Open MetaMask and send 70 LINK to the contract address you copied. Your contract will pay CCIP fees in LINK.

      **Note:** This transaction fee is significantly higher than normal due to gas spikes on Sepolia. To run this example, you can get additional testnet LINK
      from [faucets.chain.link](https://faucets.chain.link) or use a supported testnet other than Sepolia.

4. Allow the *Ethereum Sepolia* chain selector for both destination and source chains.
   1. On the **Deploy & Run Transactions** tab in Remix, expand the *message tracker* contract in the **Deployed Contracts** section.
   2. Call the `allowlistDestinationChain` function with 16015286601757825753 as the destination chain selector for *Ethereum Sepolia* and true as allowed.
   3. Call the `allowlistSourceChain` function with 16015286601757825753 as the source chain selector for *Ethereum Sepolia* and true as allowed.
      You can find each network's chain selector on the [CCIP Directory](/ccip/directory).

### Deploy the acknowledger (receiver) contract

Deploy the `Acknowledger.sol` contract on *Ethereum Sepolia* and enable it to send and receive CCIP messages to and from *Avalanche Fuji*. You must also enable your contract to receive CCIP messages from the *message tracker* contract.

1. [Open the Acknowledger.sol](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/Acknowledger.sol) contract in Remix.

   [Open Acknowledger.sol in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/Acknowledger.sol)

   Note: The contract code is also available in the [Examine the code](/ccip/tutorials/evm/send-arbitrary-data-receipt-acknowledgment#acknowledgersol) section.

2. Compile the contract.

3. Deploy the contract on *Ethereum Sepolia*:
   1. Open MetaMask and select the *Ethereum Sepolia* network.

   2. On the **Deploy & Run Transactions** tab in Remix, make sure the **Environment** is still set to *Injected Provider - MetaMask*.

   3. Under the **Deploy** section, fill in the router address and the LINK token contract address for your specific blockchain. You can find both of these addresses on the [CCIP Directory](/ccip/directory). The LINK token contract address is also listed on the [LINK Token Contracts](/resources/link-token-contracts) page. For *Ethereum Sepolia*:
      - The Router address is 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59.
      - The LINK token address is 0x779877A7B0D9E8603169DdbD7836e478b4624789.

   4. Click **transact** to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to *Ethereum Sepolia*.

   5. After you confirm the transaction, the contract address appears in the **Deployed Contracts** list. Copy this contract address.

   6. Open MetaMask and send 70 LINK to the contract address that you copied. Your contract will pay CCIP fees in LINK.

      **Note:** This transaction fee is significantly higher than normal due to gas spikes on Sepolia. To run this example, you can get additional testnet LINK
      from [faucets.chain.link](https://faucets.chain.link) or use a supported testnet other than Sepolia.

4. Allow the *Avalanche Fuji* chain selector for both destination and source chains. You must also enable your acknowledger contract to receive CCIP messages from the message tracker you deployed on *Avalanche Fuji*.
   1. On the **Deploy & Run Transactions** tab in Remix, expand the *acknowledger* contract in the **Deployed Contracts** section. Expand the `allowlistDestinationChain`, `allowlistSender`, and `allowlistSourceChain` functions and fill in the following arguments:

      | Function                  | Description                                                                                                                         | Value (*Avalanche Fuji*)             |
      | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
      | allowlistDestinationChain | CCIP Chain identifier of the target blockchain. You can find each network's chain selector on the [CCIP Directory](/ccip/directory) | 14767482510784806043, true           |
      | allowlistSender           | The address of the message tracker contract deployed on *Avalanche Fuji*                                                            | Your deployed contract address, true |
      | allowlistSourceChain      | CCIP Chain identifier of the source blockchain. You can find each network's chain selector on the [CCIP Directory](/ccip/directory) | 14767482510784806043, true           |

   2. Open MetaMask and select the *Ethereum Sepolia* network.

   3. For each function you expanded and filled in the arguments for, click the **transact** button to call the function. MetaMask prompts you to confirm the transaction. Wait for each transaction to succeed before calling the following function.

5. Finally, enable your *message tracker* contract to receive CCIP messages from the *acknowledger* contract you deployed on *Ethereum Sepolia*.
   1. On the **Deploy & Run Transactions** tab in Remix, expand the *message tracker* contract in the **Deployed Contracts** section. Expand the `allowlistSender` function and fill in your *acknowledger* contract address and true as allowed.

   2. Open MetaMask and select the *Avalanche Fuji* network.

   3. Click **transact** to call the function. MetaMask prompts you to confirm the transaction.

At this point, you have one *message tracker* (sender) contract on *Avalanche Fuji* and one *acknowledger* (receiver) contract on *Ethereum Sepolia*. You sent `70` LINK to the *message tracker* contract and `70` LINK to the *acknowledger* contract to pay the CCIP fees.

**Note:** This transaction fee is significantly higher than normal due to gas spikes on Sepolia.

### Send data and track the message status

#### Initial message

1. Send a `Hello World!` string from your *message tracker* contract on *Avalanche Fuji* to your *acknowledger* contract deployed on *Ethereum Sepolia*. You will track the status of this message during this tutorial.
   1. Open MetaMask and select the *Avalanche Fuji* network.

   2. On the **Deploy & Run Transactions** tab in Remix, expand the *message tracker* contract in the **Deployed Contracts** section.

   3. Expand the **sendMessagePayLINK** function and fill in the following arguments:

      | Argument                 | Description                                                                                                                         | Value (*Ethereum Sepolia*)                    |
      | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
      | destinationChainSelector | CCIP Chain identifier of the target blockchain. You can find each network's chain selector on the [CCIP Directory](/ccip/directory) | 16015286601757825753                          |
      | receiver                 | The destination smart contract address                                                                                              | Your deployed *acknowledger* contract address |
      | text                     | Any `string`                                                                                                                        | Hello World!                                  |

   4. Click **transact** to call the function. MetaMask prompts you to confirm the transaction.

> \*\*NOTE: Gas price spikes\*\*
>
>
>
> Under normal circumstances, transactions on the Ethereum Sepolia network require significantly fewer tokens to pay for gas. However, during exceptional periods of high gas price spikes, your transactions may fail if not sufficiently funded. In such cases, you may need to fund your contract with additional tokens. We recommend paying for your CCIP transactions in **LINK** tokens (rather than native tokens) as you can obtain extra LINK testnet tokens from [faucets.chain.link](https://faucets.chain.link/). If you encounter a transaction failure due to these gas price spikes, please add additional LINK tokens to your contract and try again.
> Alternatively, you can use a supported testnet other than Sepolia.

1. Upon transaction success, expand the last transaction in the Remix log and copy the transaction hash. In this example, it is `0x1f88abc33a4ab426a5466e01d9e5fe8a2b96d6a6e5cedb643a674489c74126b4`.

2. Open the [CCIP Explorer](https://ccip.chain.link/) and use the transaction hash that you copied to search for your cross-chain transaction.

   (Image: Image)

   After the transaction is finalized on the source chain, it will take a few minutes for CCIP to deliver the data to *Ethereum Sepolia* and call the `ccipReceive` function on your *acknowledger* contract.

3. Copy the message ID from the CCIP Explorer transaction details. You will use this message ID to track your message status on the *message tracker* contract. In this example, it is `0xdd8be2f5f5d5cf3b8640c62924025b311ae83c6144f0f2ed5c24637436d6aab8`.

4. On the **Deploy & Run Transactions** tab in Remix, expand your *message tracker* contract in the **Deployed Contracts** section.

5. Paste the message ID you copied from the CCIP explorer as the argument in the **messagesInfo** getter function. Click **messagesInfo** to read the message status.

   (Image: Image)

   Note the returned `status 1`. This value indicates that the *message tracker* contract has updated your message status to the `Sent` status as defined by the `MessageStatus` `enum` in the *message tracker* contract.

   ```solidity
   // Enum is used to track the status of messages sent via CCIP.
   // `NotSent` indicates a message has not yet been sent.
   // `Sent` indicates that a message has been sent to the Acknowledger contract but not yet acknowledged.
   // `ProcessedOnDestination` indicates that the Acknowledger contract has processed the message and that
   // the Message Tracker contract has received the acknowledgment from the Acknowledger contract.
   enum MessageStatus {
     NotSent, // 0
     Sent, // 1
     ProcessedOnDestination // 2
   }
   ```

6. When the transaction is marked with a "Success" status on the [CCIP Explorer](https://ccip.chain.link/), the CCIP transaction and the destination transaction are complete. The *acknowledger* contract has received the message from the *message tracker* contract.

   (Image: Image)

#### Acknowledgment message

The *acknowledger* contract processes the message, sends an acknowledgment message containing the initial message ID back to the *message tracker* contract, and emits an `AcknowledgmentSent` event. Read this [explanation](/ccip/tutorials/evm/send-arbitrary-data-receipt-acknowledgment#acknowledger-contract) for further description.

```solidity
// Emitted when an acknowledgment message is successfully sent back to the sender contract.
// This event signifies that the Acknowledger contract has recognized the receipt of an initial message
// and has informed the original sender contract by sending an acknowledgment message,
// including the original message ID.
event AcknowledgmentSent(
  bytes32 indexed messageId, // The unique ID of the CCIP message.
  uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
  address indexed receiver, // The address of the receiver on the destination chain.
  bytes32 data, // The data being sent back, usually containing the message ID of the original message to acknowledge its receipt.
  address feeToken, // The token address used to pay CCIP fees for sending the acknowledgment.
  uint256 fees // The fees paid for sending the acknowledgment message via CCIP.
);
```

1. Copy your *acknowledger* contract address from Remix. Open the [*Ethereum Sepolia* explorer](https://sepolia.etherscan.io/) and search for your deployed *acknowledger* contract. Click the **Events** tab to see the events log.

   (Image: Image)

   The first indexed topic (`topic1`) in the `AcknowledgmentSent` event is the acknowledgment message ID sent to the *message tracker* contract on *Avalanche Fuji*. In this example, the message ID is `0xd4d4a5d0db05dc714f8150c1af654ed34eb8c9f7547401fa9bf072a815f56ac1`.

2. Copy your own message ID from the indexed `topic1` and search for it in the [CCIP explorer](https://ccip.chain.link/).

   (Image: Image)

   When the transaction is marked with a "Success" status on the CCIP explorer, the CCIP transaction and the destination transaction are complete. The *message tracker* contract has received the message from the *acknowledger* contract.

#### Final status check

When the *message tracker* receives the acknowledgment message, the `ccipReceive` function updates the initial message status to `2`, which corresponds to the `ProcessedOnDestination` status as defined by the `MessageStatus` `enum`. The function emits a `MessageProcessedOnDestination` event.

1. Open MetaMask and select the *Avalanche Fuji* network.

2. On the **Deploy & Run Transactions** tab in Remix, expand your *message tracker* contract in the **Deployed Contracts** section.

3. Copy the **initial message ID** from the CCIP explorer (transaction from *Avalanche Fuji* to *Ethereum Sepolia*) and paste it as the argument in the **messagesInfo** getter function. Click **messagesInfo** to read the message status. It returns `status 2` and the acknowledgment message ID that confirms this status.

   (Image: Image)

4. Copy your *message tracker* contract address from Remix. Open the [*Avalanche Fuji* explorer](https://testnet.snowtrace.io/) and search for your deployed *message tracker* contract. Then, click on the **Events** tab.

   (Image: Image)

   The `MessageProcessedOnDestination` event is emitted with the acknowledged message ID `0xdd8be2f5f5d5cf3b8640c62924025b311ae83c6144f0f2ed5c24637436d6aab8` as indexed `topic2`.

   ```solidity
   // Event emitted when the sender contract receives an acknowledgment
   // that the receiver contract has successfully received and processed the message.
   event MessageProcessedOnDestination(
     bytes32 indexed messageId, // The unique ID of the CCIP acknowledgment message.
     bytes32 indexed acknowledgedMsgId, // The unique ID of the message acknowledged by the receiver.
     uint64 indexed sourceChainSelector, // The chain selector of the source chain.
     address sender // The address of the sender from the source chain.
   );
   ```

## Explanation

> \*\*NOTE: Integrate Chainlink CCIP v1.6.2 into your project\*\*
>
>
>
> <Tabs sharedStore="ccip-v1-6-2-package" client:visible>
>   <Fragment slot="tab.1">npm</Fragment>
>   <Fragment slot="tab.2">yarn</Fragment>
>   <Fragment slot="tab.3">foundry</Fragment>
>
>   <Fragment slot="panel.2">
>     If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/contracts-ccip NPM package](https://www.npmjs.com/package/@chainlink/contracts-ccip):
>
>     ```shell
>     yarn add @chainlink/contracts-ccip@1.6.2
>     ```
>   </Fragment>
> </Tabs>

The smart contracts featured in this tutorial are designed to interact with CCIP to send and receive messages with an acknowledgment of receipt mechanism. The contract code across both contracts contains supporting comments clarifying the functions, events, and underlying logic.

Refer to the [Send Arbitrary Data](/ccip/tutorials/evm/send-arbitrary-data#explanation) tutorial for more explanation about [initializing the contracts](/ccip/tutorials/evm/send-arbitrary-data#initializing-of-the-contract), [sending data, paying in LINK](/ccip/tutorials/evm/send-arbitrary-data#sending-data-and-pay-in-link), and [receiving data](/ccip/tutorials/evm/send-arbitrary-data#receiving-data).

Here, we will further explain the acknowledgment of receipt mechanism.

> **CAUTION: Best Practices**
>
> This example is simplified for educational purposes. For production code, please adhere to the following best practices:

- **Do Not Hardcode `extraArgs`**: In this example, `extraArgs` are hardcoded within the contract for simplicity. It is recommended to make `extraArgs` mutable. For instance, you can construct `extraArgs` off-chain and pass them into your function calls, or store them in a storage variable that can be updated as needed. This approach ensures that `extraArgs` remain backward compatible with future CCIP upgrades. Refer to the [Best Practices](/ccip/concepts/best-practices/evm) guide for more information.

- **Validate the Destination Chain**: Always ensure that the destination chain is valid and supported before sending messages.

- **Understand `allowOutOfOrderExecution` Usage**: This example sets `allowOutOfOrderExecution` to `true` (see [GenericExtraArgsV2](/ccip/api-reference/evm/v1.6.1/client#genericextraargsv2)). Read the [Best Practices: Setting `allowOutOfOrderExecution`](/ccip/concepts/best-practices/evm#setting-allowoutoforderexecution) to learn more about this parameter.

- **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/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).

Following these best practices ensures that your contract is robust, future-proof, and compliant with CCIP standards.

### Message acknowledgment of receipt mechanism

This mechanism ensures that a message sent by the *message tracker* (sender) contract is received and acknowledged by the *acknowledger* (receiver) contract. The message status is tracked and stored in the *message tracker* contract.

```solidity
// Enum is used to track the status of messages sent via CCIP.
// `NotSent` indicates a message has not yet been sent.
// `Sent` indicates that a message has been sent to the Acknowledger contract but not yet acknowledged.
// `ProcessedOnDestination` indicates that the Acknowledger contract has processed the message and that
// the Message Tracker contract has received the acknowledgment from the Acknowledger contract.
enum MessageStatus {
    NotSent, // 0
    Sent, // 1
    ProcessedOnDestination // 2
}

// Struct to store the status and acknowledger message ID of a message.
struct MessageInfo {
    MessageStatus status;
    bytes32 acknowledgerMessageId;
}

// Mapping to keep track of message IDs to their info (status & acknowledger message ID).
mapping(bytes32 => MessageInfo) public messagesInfo;
```

#### *Message tracker* contract

The *message tracker* contract acts as the sender, initiating cross-chain communication. It performs the following operations:

- **Message sending**: Constructs and sends messages to the *acknowledger* contract on another blockchain, using [`sendMessagePayLINK`](/ccip/tutorials/evm/send-arbitrary-data#sending-data-and-pay-in-link) function. On top of its [five primary operations](/ccip/tutorials/evm/send-arbitrary-data#sending-data-and-pay-in-link), the [`sendMessagePayLINK`](/ccip/tutorials/evm/send-arbitrary-data#sending-data-and-pay-in-link) function also updates the message status upon sending.

- **Status tracking**:
  - Upon sending a message, the *message tracker* updates its internal state to mark the message as `Sent` (status `1`). This status is pivotal for tracking the message lifecycle and awaiting acknowledgment.

    ```solidity
    // Update the message status to `Sent`
    messagesInfo[messageId].status = MessageStatus.Sent;
    ```

  - Upon receiving an acknowledgment message from the *acknowledger* contract, the *message tracker* contract updates the message status from `Sent` (status `1`) to `ProcessedOnDestination` (status `2`). This update indicates that the cross-chain communication cycle is complete, and the receiver successfully received and acknowledged the message.

    ```solidity
    // Update the message status to `ProcessedOnDestination`
    messagesInfo[messageId].status = MessageStatus.ProcessedOnDestination;
    ```

#### *Acknowledger* contract

The *acknowledger* contract receives the message, sends back an acknowledgment message, and emits an event. It performs the following operations:

- **Message receipt**: Upon receiving a message via CCIP, the `ccipReceive` function decodes it and calls the `acknowledgePayLINK` function nested within the [`ccipReceive`](/ccip/tutorials/evm/send-arbitrary-data#receiving-data) function.

- **Acknowledgment sending**: The `acknowledgePayLINK` function acts as a custom [`sendMessagePayLINK`](https://docs.chain.link/ccip/tutorials/evm/send-arbitrary-data#sending-data-and-pay-in-link) function nested within the `ccipReceive` function. It sends
  an acknowledgment (a CCIP message) to the *message tracker* contract upon the initial message receipt. The data transferred in this acknowledgment message is the initial message ID. It then emits an `AcknowledgmentSent` event.

### Security and integrity

Both contracts use allowlists to process only messages from and to allowed sources.

- **Allowlisting chains and senders**:
  - The `sendMessagePayLINK` function is protected by the `onlyAllowlistedDestinationChain` modifier, ensuring the contract owner has allowlisted a destination chain.
  - The `ccipReceive` function is protected by the `onlyAllowlisted` modifier, ensuring the contract owner has allowlisted a source chain and a sender.

- **Ensuring the initial message authenticity**: The *message tracker* contract first checks that the message awaiting acknowledgment was sent from the contract itself and is currently marked as `Sent`. Once confirmed, the message status is updated to `ProcessedOnDestination`.

## Examine the code

### MessageTracker.sol

```sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {CCIPReceiver} from "@chainlink/contracts-ccip/contracts/applications/CCIPReceiver.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {OwnerIsCreator} from "@chainlink/contracts@1.4.0/src/v0.8/shared/access/OwnerIsCreator.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

/// @title - A simple messenger contract for sending/receiving data across chains and tracking the status of sent
/// messages.
contract MessageTracker is CCIPReceiver, OwnerIsCreator {
  // Custom errors to provide more descriptive revert messages.
  error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough
  // balance.
  error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
  error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been
  // allowlisted by the contract owner.
  error SourceChainNotAllowlisted(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by
  // the contract owner.
  error SenderNotAllowlisted(address sender); // Used when the sender has not been allowlisted by the contract owner.
  error InvalidReceiverAddress(); // Used when the receiver address is 0.
  error MessageWasNotSentByMessageTracker(bytes32 msgId); // Triggered when attempting to confirm a message not
  // recognized as sent by this tracker.
  error MessageHasAlreadyBeenProcessedOnDestination(bytes32 msgId); // Triggered when trying to mark a message as
  // `ProcessedOnDestination` when it is already marked as such.

  // Enum is used to track the status of messages sent via CCIP.
  // `NotSent` indicates a message has not yet been sent.
  // `Sent` indicates that a message has been sent to the Acknowledger contract but not yet acknowledged.
  // `ProcessedOnDestination` indicates that the Acknowledger contract has processed the message and that
  // the Message Tracker contract has received the acknowledgment from the Acknowledger contract.
  enum MessageStatus {
    NotSent, // 0
    Sent, // 1
    ProcessedOnDestination // 2
  }

  // Struct to store the status and acknowledger message ID of a message.
  struct MessageInfo {
    MessageStatus status;
    bytes32 acknowledgerMessageId;
  }

  // Mapping to keep track of allowlisted destination chains.
  mapping(uint64 => bool) public allowlistedDestinationChains;

  // Mapping to keep track of allowlisted source chains.
  mapping(uint64 => bool) public allowlistedSourceChains;

  // Mapping to keep track of allowlisted senders.
  mapping(address => bool) public allowlistedSenders;

  // Mapping to keep track of message IDs to their info (status & acknowledger message ID).
  mapping(bytes32 => MessageInfo) public messagesInfo;

  // Event emitted when a message is sent to another chain.
  // The chain selector of the destination chain.
  // The address of the receiver on the destination chain.
  // The text being sent.
  // the token address used to pay CCIP fees.
  // The fees paid for sending the CCIP message.
  event MessageSent( // The unique ID of the CCIP message.
    bytes32 indexed messageId,
    uint64 indexed destinationChainSelector,
    address receiver,
    string text,
    address feeToken,
    uint256 fees
  );

  // Event emitted when the sender contract receives an acknowledgment
  // that the receiver contract has successfully received and processed the message.
  event MessageProcessedOnDestination( // The unique ID of the CCIP acknowledgment message.
    // The unique ID of the message acknowledged by the receiver.
    // The chain selector of the source chain.
    // The address of the sender from the source chain.
    bytes32 indexed messageId,
    bytes32 indexed acknowledgedMsgId,
    uint64 indexed sourceChainSelector,
    address sender
  );

  IERC20 private s_linkToken;

  /// @notice Constructor initializes the contract with the router address.
  /// @param _router The address of the router contract.
  /// @param _link The address of the link contract.
  constructor(
    address _router,
    address _link
  ) CCIPReceiver(_router) {
    s_linkToken = IERC20(_link);
  }

  /// @dev Modifier that checks if the chain with the given destinationChainSelector is allowlisted.
  /// @param _destinationChainSelector The selector of the destination chain.
  modifier onlyAllowlistedDestinationChain(
    uint64 _destinationChainSelector
  ) {
    if (!allowlistedDestinationChains[_destinationChainSelector]) {
      revert DestinationChainNotAllowlisted(_destinationChainSelector);
    }
    _;
  }

  /// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is
  /// allowlisted.
  /// @param _sourceChainSelector The selector of the destination chain.
  /// @param _sender The address of the sender.
  modifier onlyAllowlisted(
    uint64 _sourceChainSelector,
    address _sender
  ) {
    if (!allowlistedSourceChains[_sourceChainSelector]) {
      revert SourceChainNotAllowlisted(_sourceChainSelector);
    }
    if (!allowlistedSenders[_sender]) revert SenderNotAllowlisted(_sender);
    _;
  }

  /// @dev Modifier that checks the receiver address is not 0.
  /// @param _receiver The receiver address.
  modifier validateReceiver(
    address _receiver
  ) {
    if (_receiver == address(0)) revert InvalidReceiverAddress();
    _;
  }

  /// @dev Updates the allowlist status of a destination chain for transactions.
  function allowlistDestinationChain(
    uint64 _destinationChainSelector,
    bool allowed
  ) external onlyOwner {
    allowlistedDestinationChains[_destinationChainSelector] = allowed;
  }

  /// @dev Updates the allowlist status of a source chain for transactions.
  function allowlistSourceChain(
    uint64 _sourceChainSelector,
    bool allowed
  ) external onlyOwner {
    allowlistedSourceChains[_sourceChainSelector] = allowed;
  }

  /// @dev Updates the allowlist status of a sender for transactions.
  function allowlistSender(
    address _sender,
    bool allowed
  ) external onlyOwner {
    allowlistedSenders[_sender] = allowed;
  }

  /// @notice Sends data to receiver on the destination chain.
  /// @notice Pay for fees in LINK.
  /// @dev Assumes your contract has sufficient LINK.
  /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
  /// @param _receiver The address of the recipient on the destination blockchain.
  /// @param _text The text to be sent.
  /// @return messageId The ID of the CCIP message that was sent.
  function sendMessagePayLINK(
    uint64 _destinationChainSelector,
    address _receiver,
    string calldata _text
  )
    external
    onlyOwner
    onlyAllowlistedDestinationChain(_destinationChainSelector)
    validateReceiver(_receiver)
    returns (bytes32 messageId)
  {
    // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
    Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(_receiver, _text, address(s_linkToken));

    // Initialize a router client instance to interact with cross-chain router
    IRouterClient router = IRouterClient(this.getRouter());

    // Get the fee required to send the CCIP message
    uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);

    if (fees > s_linkToken.balanceOf(address(this))) {
      revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
    }

    // approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
    s_linkToken.approve(address(router), fees);

    // Send the CCIP message through the router and store the returned CCIP message ID
    messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);

    // Update the message status to `Sent`
    messagesInfo[messageId].status = MessageStatus.Sent;

    // Emit an event with message details
    emit MessageSent(messageId, _destinationChainSelector, _receiver, _text, address(s_linkToken), fees);

    // Return the CCIP message ID
    return messageId;
  }

  /**
   * @dev Receives and processes messages sent via the Chainlink CCIP from allowed chains and senders.
   * Upon receiving a message, this function checks if the message's associated data indicates a previously
   * sent message awaiting acknowledgment. If the message is valid (i.e., its status is `Sent`), it updates
   * the message's status to `ProcessedOnDestination`, thereby acknowledging its receipt. It then emits a
   * `MessageProcessedOnDestination`
   * event. If the message cannot be validated (e.g., it was not sent or has been tampered with), the function
   * reverts with a `MessageWasNotSentByMessageTracker` error. This mechanism ensures that only messages
   * genuinely sent and awaiting acknowledgment are marked as `ProcessedOnDestination`.
   * @param any2EvmMessage The CCIP message received, which includes the message ID, the data being acknowledged,
   * the source chain selector, and the sender's address.
   */
  function _ccipReceive(
    Client.Any2EVMMessage memory any2EvmMessage
  )
    internal
    override
    onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address))) // Ensure the
    // source chain and sender are allowlisted for added security

  {
    bytes32 initialMsgId = abi.decode(any2EvmMessage.data, (bytes32)); // Decode the data sent by the receiver
    bytes32 acknowledgerMsgId = any2EvmMessage.messageId;
    messagesInfo[initialMsgId].acknowledgerMessageId = acknowledgerMsgId; // Store the messageId of the received message

    if (messagesInfo[initialMsgId].status == MessageStatus.Sent) {
      // Updates the status of the message to 'ProcessedOnDestination' to reflect that an acknowledgment
      // of receipt has been received and emits an event to log this confirmation along with relevant details.
      messagesInfo[initialMsgId].status = MessageStatus.ProcessedOnDestination;
      emit MessageProcessedOnDestination(
        acknowledgerMsgId,
        initialMsgId,
        any2EvmMessage.sourceChainSelector,
        abi.decode(any2EvmMessage.sender, (address))
      );
    } else if (messagesInfo[initialMsgId].status == MessageStatus.ProcessedOnDestination) {
      // If the message is already marked as 'ProcessedOnDestination', this indicates an attempt to
      // re-confirm a message that has already been processed on the destination chain and marked as such.
      revert MessageHasAlreadyBeenProcessedOnDestination(initialMsgId);
    } else {
      // If the message status is neither 'Sent' nor 'ProcessedOnDestination', it implies that the
      // message ID provided for acknowledgment does not correspond to a valid, previously
      // sent message.
      revert MessageWasNotSentByMessageTracker(initialMsgId);
    }
  }

  /// @notice Construct a CCIP message.
  /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for sending a text.
  /// @param _receiver The address of the receiver.
  /// @param _text The string data to be sent.
  /// @param _feeTokenAddress The address of the token used for fees. Set address(0) for native gas.
  /// @return Client.EVM2AnyMessage Returns an EVM2AnyMessage struct which contains information for sending a CCIP
  /// message.
  function _buildCCIPMessage(
    address _receiver,
    string calldata _text,
    address _feeTokenAddress
  ) private pure returns (Client.EVM2AnyMessage memory) {
    // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
    return Client.EVM2AnyMessage({
      receiver: abi.encode(_receiver), // ABI-encoded receiver address
      data: abi.encode(_text), // ABI-encoded string
      tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array as no tokens are transferred
      extraArgs: Client._argsToBytes(
        // Additional arguments, setting gas limit and allowing out-of-order execution.
        // Best Practice: For simplicity, the values are hardcoded. It is advisable to use a more dynamic approach
        // where you set the extra arguments off-chain. This allows adaptation depending on the lanes, messages,
        // and ensures compatibility with future CCIP upgrades. Read more about it here:
        // https://docs.chain.link/ccip/concepts/best-practices/evm#using-extraargs
        Client.GenericExtraArgsV2({
          gasLimit: 300_000,
          allowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages
          // from
          // the same sender
        })
      ),
      // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
      feeToken: _feeTokenAddress
    });
  }

  /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
  /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
  /// @param _beneficiary The address to which the tokens will be sent.
  /// @param _token The contract address of the ERC20 token to be withdrawn.
  function withdrawToken(
    address _beneficiary,
    address _token
  ) public onlyOwner {
    // Retrieve the balance of this contract
    uint256 amount = IERC20(_token).balanceOf(address(this));

    // Revert if there is nothing to withdraw
    if (amount == 0) revert NothingToWithdraw();

    IERC20(_token).safeTransfer(_beneficiary, amount);
  }
}
```

### Acknowledger.sol

```sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {CCIPReceiver} from "@chainlink/contracts-ccip/contracts/applications/CCIPReceiver.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {OwnerIsCreator} from "@chainlink/contracts@1.4.0/src/v0.8/shared/access/OwnerIsCreator.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

/// @title - A simple acknowledger contract for receiving data and sending acknowledgement of receipt messages across
/// chains.
contract Acknowledger is CCIPReceiver, OwnerIsCreator {
  // Custom errors to provide more descriptive revert messages.
  error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough
  // balance.
  error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
  error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been
  // allowlisted by the contract owner.
  error InvalidReceiverAddress(); // Used when the receiver address is 0.
  error SourceChainNotAllowlisted(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by
  // the contract owner.
  error SenderNotAllowlisted(address sender); // Used when the sender has not been allowlisted by the contract owner.

  string private s_lastReceivedText; // Store the last received text.

  // Mapping to keep track of allowlisted destination chains.
  mapping(uint64 => bool) public allowlistedDestinationChains;

  // Mapping to keep track of allowlisted source chains.
  mapping(uint64 => bool) public allowlistedSourceChains;

  // Mapping to keep track of allowlisted senders.
  mapping(address => bool) public allowlistedSenders;

  // Emitted when an acknowledgment message is successfully sent back to the sender contract.
  // This event signifies that the Acknowledger contract has recognized the receipt of an initial message
  // and has informed the original sender contract by sending an acknowledgment message,
  // including the original message ID.
  // The chain selector of the destination chain.
  // The address of the receiver on the destination chain.
  // The data being sent back, containing the message ID of the initial message to acknowledge.
  // The token address used to pay CCIP fees for sending the acknowledgment.
  // The fees paid for sending the acknowledgment message via CCIP.
  event AcknowledgmentSent( // The unique ID of the CCIP message.
    bytes32 indexed messageId,
    uint64 indexed destinationChainSelector,
    address indexed receiver,
    bytes32 data,
    address feeToken,
    uint256 fees
  );

  IERC20 private s_linkToken;

  /// @notice Constructor initializes the contract with the router address.
  /// @param _router The address of the router contract.
  /// @param _link The address of the link contract.
  constructor(
    address _router,
    address _link
  ) CCIPReceiver(_router) {
    s_linkToken = IERC20(_link);
  }

  /// @dev Modifier that checks if the chain with the given destinationChainSelector is allowlisted.
  /// @param _destinationChainSelector The selector of the destination chain.
  modifier onlyAllowlistedDestinationChain(
    uint64 _destinationChainSelector
  ) {
    if (!allowlistedDestinationChains[_destinationChainSelector]) {
      revert DestinationChainNotAllowlisted(_destinationChainSelector);
    }
    _;
  }

  /// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is
  /// allowlisted.
  /// @param _sourceChainSelector The selector of the destination chain.
  /// @param _sender The address of the sender.
  modifier onlyAllowlisted(
    uint64 _sourceChainSelector,
    address _sender
  ) {
    if (!allowlistedSourceChains[_sourceChainSelector]) {
      revert SourceChainNotAllowlisted(_sourceChainSelector);
    }
    if (!allowlistedSenders[_sender]) revert SenderNotAllowlisted(_sender);
    _;
  }

  /// @dev Updates the allowlist status of a destination chain for transactions.
  function allowlistDestinationChain(
    uint64 _destinationChainSelector,
    bool allowed
  ) external onlyOwner {
    allowlistedDestinationChains[_destinationChainSelector] = allowed;
  }

  /// @dev Updates the allowlist status of a source chain for transactions.
  function allowlistSourceChain(
    uint64 _sourceChainSelector,
    bool allowed
  ) external onlyOwner {
    allowlistedSourceChains[_sourceChainSelector] = allowed;
  }

  /// @dev Updates the allowlist status of a sender for transactions.
  function allowlistSender(
    address _sender,
    bool allowed
  ) external onlyOwner {
    allowlistedSenders[_sender] = allowed;
  }

  /// @notice Sends an acknowledgment message back to the sender contract on the source chain
  /// and pays the fees using LINK tokens.
  /// @dev This function constructs and sends an acknowledgment message using CCIP,
  /// indicating the receipt and processing of an initial message. It emits the `AcknowledgmentSent` event
  /// upon successful sending. This function should be called after processing the received message
  /// to inform the sender contract about the successful message reception.
  /// @param _messageIdToAcknowledge The message ID of the initial message being acknowledged.
  /// @param _messageTrackerAddress The address of the message tracker contract on the source chain.
  /// @param _messageTrackerChainSelector The chain selector of the source chain.
  function _acknowledgePayLINK(
    bytes32 _messageIdToAcknowledge,
    address _messageTrackerAddress,
    uint64 _messageTrackerChainSelector
  ) private {
    if (_messageTrackerAddress == address(0)) {
      revert InvalidReceiverAddress();
    }

    // Construct the CCIP message for acknowledgment, including the message ID of the initial message.
    Client.EVM2AnyMessage memory acknowledgment = Client.EVM2AnyMessage({
      receiver: abi.encode(_messageTrackerAddress), // ABI-encoded receiver address
      data: abi.encode(_messageIdToAcknowledge), // ABI-encoded message ID to acknowledge
      tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array aas no tokens are transferred
      extraArgs: Client._argsToBytes(
        // Additional arguments, setting gas limit and allowing out-of-order execution.
        // Best Practice: For simplicity, the values are hardcoded. It is advisable to use a more dynamic approach
        // where you set the extra arguments off-chain. This allows adaptation depending on the lanes, messages,
        // and ensures compatibility with future CCIP upgrades. Read more about it here:
        // https://docs.chain.link/ccip/concepts/best-practices/evm#using-extraargs
        Client.GenericExtraArgsV2({
          gasLimit: 200_000,
          allowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages
          // from
          // the same sender.
        })
      ),
      // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
      feeToken: address(s_linkToken)
    });

    // Initialize a router client instance to interact with the cross-chain router.
    IRouterClient router = IRouterClient(this.getRouter());

    // Calculate the fee required to send the CCIP acknowledgment message.
    uint256 fees = router.getFee(
      _messageTrackerChainSelector, // The chain selector for routing the message.
      acknowledgment // The acknowledgment message data.
    );

    // Ensure the contract has sufficient balance to cover the message sending fees.
    if (fees > s_linkToken.balanceOf(address(this))) {
      revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
    }

    // Approve the router to transfer LINK tokens on behalf of this contract to cover the sending fees.
    s_linkToken.approve(address(router), fees);

    // Send the acknowledgment message via the CCIP router and capture the resulting message ID.
    bytes32 messageId = router.ccipSend(
      _messageTrackerChainSelector, // The destination chain selector.
      acknowledgment // The CCIP message payload for acknowledgment.
    );

    // Emit an event detailing the acknowledgment message sending, for external tracking and verification.
    emit AcknowledgmentSent(
      messageId, // The ID of the sent acknowledgment message.
      _messageTrackerChainSelector, // The destination chain selector.
      _messageTrackerAddress, // The receiver of the acknowledgment, typically the original sender.
      _messageIdToAcknowledge, // The original message ID that was acknowledged.
      address(s_linkToken), // The fee token used.
      fees // The fees paid for sending the message.
    );
  }

  /// @dev Handles a received CCIP message, processes it, and acknowledges its receipt.
  /// This internal function is called upon the receipt of a new message via CCIP from an allowlisted source chain and
  /// sender.
  /// It decodes the message and acknowledges its receipt by calling `_acknowledgePayLINK`.
  /// @param any2EvmMessage The CCIP message received
  function _ccipReceive(
    Client.Any2EVMMessage memory any2EvmMessage
  )
    internal
    override
    onlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address))) // Make sure
    // source chain and sender are allowlisted

  {
    bytes32 messageIdToAcknowledge = any2EvmMessage.messageId; // The message ID of the received message to acknowledge
    address messageTrackerAddress = abi.decode(any2EvmMessage.sender, (address)); // ABI-decoding of the message tracker
    // address
    uint64 messageTrackerChainSelector = any2EvmMessage.sourceChainSelector; // The chain selector of the received
    // message
    s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text

    _acknowledgePayLINK(messageIdToAcknowledge, messageTrackerAddress, messageTrackerChainSelector);
  }

  /// @notice Fetches the details of the last received message.
  /// @return text The last received text.
  function getLastReceivedMessage() external view returns (string memory text) {
    return (s_lastReceivedText);
  }

  /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
  /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
  /// @param _beneficiary The address to which the tokens will be sent.
  /// @param _token The contract address of the ERC20 token to be withdrawn.
  function withdrawToken(
    address _beneficiary,
    address _token
  ) public onlyOwner {
    // Retrieve the balance of this contract
    uint256 amount = IERC20(_token).balanceOf(address(this));

    // Revert if there is nothing to withdraw
    if (amount == 0) revert NothingToWithdraw();

    IERC20(_token).safeTransfer(_beneficiary, amount);
  }
}
```

## Final note

In this example, the *message tracker* contract emits an event when it receives the acknowledgment message confirming the initial message reception and processing on the counterpart chain. However, you could think of any other logic to execute when the *message tracker* receives the acknowledgment. This tutorial demonstrates the pattern for sending arbitrary data, but you can apply the same pattern to programmable token transfers.

> **CAUTION: Educational Example Disclaimer**
>
> This page includes an educational example to use a Chainlink system, product, or service and is provided to
> demonstrate how to interact with Chainlink's systems, products, and services to integrate them into your own. This
> template is provided "AS IS" and "AS AVAILABLE" without warranties of any kind, it has not been audited, and it may be
> missing key checks or error handling to make the usage of the system, product or service more clear. Do not use the
> code in this example in a production environment without completing your own audits and application of best practices.
> Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs
> that are generated due to errors in code.