NEW

Developer Passes are now available for Chainlink SmartCon. Get yours today.

Send Multiple Messages in a Single Transaction

This tutorial will teach you how to send multiple messages to different chains within a single transaction using Chainlink CCIP. You will learn how to send messages immediately without storing them in the contract's state, and how to register messages first and dispatch them later, which can be useful for scenarios like scheduled or automated message sending.

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

Tutorial

Deploy the message dispatcher (sender) contract

Deploy the MessageDispatcher contract on the source blockchain (e.g., Avalanche Fuji) from which you want to send messages.

  1. Open the MessageDispatcher.sol contract in Remix.

    Note: The contract code is also available in the Examine the code 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 Supported Networks page. The LINK token contract address is also listed on the 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 will appear on the Deployed Contracts list. Copy your contract address.

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

  4. Allow the Arbitrum Sepolia and Ethereum Sepolia chains as destination chains for the message dispatcher contract:

    1. On the Deploy & Run Transactions tab in Remix, expand the message dispatcher contract in the Deployed Contracts section.
    2. Call the allowlistDestinationChain function with 3478487238524512106 as the destination chain selector for Arbitrum Sepolia and true as allowed.
    3. Once the transaction is confirmed, call the allowlistDestinationChain function with 16015286601757825753 as the destination chain selector for Ethereum Sepolia and true as allowed.

    Note: You can find each network's chain selector on the supported networks page.

Deploy the Messenger (receiver) contract

Deploy the Messenger contract on Arbitrum Sepolia

Deploy the Messenger contract on Arbitrum Sepolia and enable it to receive CCIP messages from Avalanche Fuji. You must also enable your contract to receive CCIP messages from the message dispatcher contract.

  1. Open the Messenger.sol contract in Remix.

    Note: The contract code is also available in the Examine the code section.

  2. Compile the contract.

  3. Deploy the contract on Arbitrum Sepolia:

    1. Open MetaMask and select the Arbitrum 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 Supported Networks page. The LINK token contract address is also listed on the LINK Token Contracts page. For Arbitrum Sepolia:

      • The Router address is 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165.
      • The LINK token address is 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E.
    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 Arbitrum Sepolia.

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

  4. Allow the Avalanche Fuji chain selector for the source chain. You must also enable your receiver contract to receive CCIP messages from the message dispatcher you deployed on Avalanche Fuji.

    1. On the Deploy & Run Transactions tab in Remix, expand the messenger contract in the Deployed Contracts section. Expand the allowlistSourceChain and allowlistSender functions and fill in the following arguments:

      FunctionDescriptionValue (Avalanche Fuji)
      allowlistSourceChainCCIP Chain identifier of the source blockchain. You can find each network's chain selector on the supported networks page 14767482510784806043, true
      allowlistSenderThe address of the message dispatcher contract deployed on Avalanche FujiYour deployed contract address, true
    2. Open MetaMask and select the Arbitrum 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.

Deploy the Messenger contract on Ethereum Sepolia

Repeat the steps above to deploy the Messenger.sol contract on Ethereum Sepolia. When you deploy the messenger contract on Ethereum Sepolia, you must use the Ethereum Sepolia router address and LINK token address:

  • Router address: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59.
  • LINK token address: 0x779877A7B0D9E8603169DdbD7836e478b4624789.

Note: Once your messenger contract is deployed on Ethereum Sepolia, remember to allow the Avalanche Fuji chain selector for the source chain and the message dispatcher contract as a sender.

At this point:

  • You have one message dispatcher (sender) contract on Avalanche Fuji, one messenger (receiver) contract on Arbitrum Sepolia, and one messenger (receiver) contract on Ethereum Sepolia.
  • You sent 0.5 LINK to the message dispatcher contract to pay the CCIP fees.
  • You allowed Arbitrum Sepolia and Ethereum Sepolia as destination chains for the message dispatcher contract.
  • You allowed Avalanche Fuji as a source chain for both your messenger contracts and allowed the message dispatcher contract as a sender.

Note: This setup allows you to send messages from the message dispatcher contract to messenger (receiver) contracts deployed on Arbitrum Sepolia and Ethereum Sepolia. You can extend this setup to send messages to as many messenger (receiver) contracts as you want on any supported chains.

Send multiple messages directly

The dispatchMessagesDirect function allows you to send directly multiple messages to a single chain or different chains in a single transaction. In this example, you will send two messages to your messenger contract on Arbitrum Sepolia and one to your messenger contract on Ethereum Sepolia.

  1. On the Deploy & Run Transactions tab in Remix, expand the message dispatcher contract in the Deployed Contracts section. Fill in the dispatchMessagesDirect function argument with the following tuple array:

    [
        [3478487238524512106, "<your_messenger_contract_address_on_Arbitrum_Sepolia>", "Hello Arbitrum Sepolia ! This is Message 1"],
        [3478487238524512106, "<your_messenger_contract_address_on_Arbitrum_Sepolia>", "Hello Arbitrum Sepolia ! This is Message 2"],
        [16015286601757825753, "<your_messenger_contract_address_on_Ethereum_Sepolia>", "Hello Ethereum Sepolia ! This is Message 3"]
    ]
    

    Where each tuple contains the following elements:

    • The chain selector for the destination chain
    • The address of the messenger (receiver) contract
    • The message to be sent
  2. Open MetaMask and select the Avalanche Fuji network.

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

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

    Remix Transaction Hash
  5. Open the CCIP Explorer and use the transaction hash that you copied to search for your cross-chain transaction.

    Chainlink CCIP Explorer - Transaction status

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

  6. Wait for each CCIP message to be marked with a "Success" status on the CCIP Explorer.

    Chainlink CCIP Explorer - Transaction status success

Register and dispatch messages later

The MessageDispatcher contract also allows you to register messages in the contract's state and dispatch them later.

In this example, you will register two messages in the message dispatcher contract before dispatching them.

  1. Open MetaMask and select the Avalanche Fuji network.

  2. In the Deploy & Run Transactions tab of Remix, locate the MessageDispatcher contract within the Deployed Contracts section. To register messages for dispatch, invoke the registerMessage function separately for each message you want to send. For each call, provide the following parameters:

    • The chain selector for the destination chain
    • The address of the messenger (receiver) contract
    • The message to be sent

    Each set of parameters is provided as a tuple consisting of a chain selector, a receiver address, and the message text. For example:

    • Call registerMessage with the following tuple for Arbitrum Sepolia:

      [3478487238524512106,"<your_messenger_contract_address_on_Arbitrum_Sepolia>","Hello Arbitrum Sepolia! This is Message 1"]
      

      Replace <your_messenger_contract_address_on_Arbitrum_Sepolia> with the address of your messenger contract on _Arbitrum Sepolia

    • Call registerMessage again with the following tuple for Ethereum Sepolia:

      [16015286601757825753,"<your_messenger_contract_address_on_Ethereum_Sepolia>","Hello Ethereum Sepolia! This is Message 2"]
      

      Replace <your_messenger_contract_address_on_Ethereum_Sepolia> with the address of your messenger contract on _Ethereum Sepolia

  3. When you are ready to send the registered messages, call the dispatchMessages function in the Deployed Contracts section of Remix. This will send all registered messages in a single transaction.

  4. Retrieve the transaction hash and monitor the status of the messages on the CCIP Explorer.

Explanation

Two methods for dispatching multiple messages in a single transaction

  • Immediate dispatch (dispatchMessagesDirect): This method is ideal for users who want to send multiple messages within the same transaction quickly, and without storing them in the contract's state.

  • Registered dispatch (registerMessage and dispatchMessages): This method is suitable for use cases where messages need to be stored and sent later, possibly triggered by an external event or at a specific time (e.g., using Chainlink Automation). It is useful for scheduling and automating message sending.

Security and integrity

Contracts use allowlists to process only messages from and to allowed sources.

  • The sendMessage function is protected by the onlyAllowlistedDestinationChain modifier, which ensures 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.

Examine the code

MessageDispatcher.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

/// @title MessageDispatcher
/// @notice Handles sending CCIP messages to multiple chains within a single transaction.
/// @dev Allows messages to be sent immediately or registered for later dispatch.
contract MessageDispatcher is OwnerIsCreator {
    /// @notice Thrown when the contract's balance is insufficient to cover the calculated fees.
    /// @param currentBalance The current LINK token balance of the contract.
    /// @param calculatedFees The required fees for the operation.
    error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);

    /// @notice Thrown when the destination chain is not allowlisted.
    /// @param destinationChainSelector The selector of the destination chain.
    error DestinationChainNotAllowlisted(uint64 destinationChainSelector);

    /// @notice Thrown when the receiver address is invalid (zero address).
    error InvalidReceiverAddress();

    /// @notice Thrown when there are no tokens available to withdraw.
    error NothingToWithdraw();

    /// @notice Thrown when no messages have been registered for dispatch.
    error NoMessagesRegistered();

    /// @notice Represents a message to be dispatched to another chain.
    struct Message {
        /// @notice The selector identifying the destination blockchain.
        uint64 chainSelector;
        /// @notice The recipient's address on the destination blockchain.
        address receiver;
        /// @notice The text content of the message.
        string text;
    }

    /// @notice Indicates whether a destination chain is allowlisted.
    /// @dev Mapping from chain selector to its allowlist status.
    mapping(uint64 => bool) public allowlistedDestinationChains;

    /// @notice Stores messages that have been registered for future dispatch.
    Message[] public registeredMessages;

    /// @notice Emitted when a message is registered for later dispatch.
    /// @param chainSelector The selector of the destination chain.
    /// @param receiver The recipient's address on the destination chain.
    /// @param text The text content of the message.
    event MessageRegistered(
        uint64 indexed chainSelector,
        address indexed receiver,
        string text
    );

    /// @notice Emitted when a message is sent to a destination chain.
    /// @param messageId The unique identifier of the CCIP message.
    /// @param destinationChainSelector The selector of the destination chain.
    /// @param receiver The recipient's address on the destination chain.
    /// @param text The text content of the message.
    /// @param feeToken The address of the token used to pay CCIP fees.
    /// @param fees The amount of fees paid for sending the CCIP message.
    event MessageSent(
        bytes32 indexed messageId,
        uint64 indexed destinationChainSelector,
        address receiver,
        string text,
        address feeToken,
        uint256 fees
    );

    IRouterClient private s_router;
    IERC20 private s_linkToken;

    /// @notice Initializes the contract with the specified router and LINK token addresses.
    /// @param _router The address of the Chainlink CCIP router contract.
    /// @param _link The address of the LINK token contract.
    constructor(address _router, address _link) {
        s_router = IRouterClient(_router);
        s_linkToken = IERC20(_link);
    }

    /// @notice Ensures that the destination chain is allowlisted.
    /// @param _destinationChainSelector The selector of the destination chain.
    modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {
        if (!allowlistedDestinationChains[_destinationChainSelector])
            revert DestinationChainNotAllowlisted(_destinationChainSelector);
        _;
    }

    /// @notice Validates that the receiver address is not the zero address.
    /// @param _receiver The address of the receiver.
    modifier validateReceiver(address _receiver) {
        if (_receiver == address(0)) revert InvalidReceiverAddress();
        _;
    }

    /// @notice Updates the allowlist status of a destination chain.
    /// @param _destinationChainSelector The selector of the destination chain.
    /// @param allowed Indicates whether the chain should be allowlisted (`true`) or removed (`false`).
    function allowlistDestinationChain(
        uint64 _destinationChainSelector,
        bool allowed
    ) external onlyOwner {
        allowlistedDestinationChains[_destinationChainSelector] = allowed;
    }

    /// @notice Registers a message for later dispatch to a specific chain.
    /// @param _chainSelector The selector of the destination blockchain.
    /// @param _receiver The recipient's address on the destination blockchain.
    /// @param _text The text content of the message.
    function registerMessage(
        uint64 _chainSelector,
        address _receiver,
        string calldata _text
    )
        external
        onlyOwner
        onlyAllowlistedDestinationChain(_chainSelector)
        validateReceiver(_receiver)
    {
        registeredMessages.push(
            Message({
                chainSelector: _chainSelector,
                receiver: _receiver,
                text: _text
            })
        );

        emit MessageRegistered(_chainSelector, _receiver, _text);
    }

    /// @notice Dispatches all registered messages to their respective destination chains.
    /// @dev Requires the contract to have sufficient LINK balance to cover fees.
    function dispatchMessages() external onlyOwner {
        uint256 messageCount = registeredMessages.length;
        if (messageCount == 0) {
            revert NoMessagesRegistered();
        }

        for (uint256 i = 0; i < messageCount; i++) {
            Message memory message = registeredMessages[i];

            string memory messageText = message.text;

            (bytes32 messageId, uint256 fees) = _sendMessage(
                message.chainSelector,
                message.receiver,
                messageText
            );

            emit MessageSent(
                messageId,
                message.chainSelector,
                message.receiver,
                messageText,
                address(s_linkToken),
                fees
            );
        }

        // Clear all registered messages after dispatching
        delete registeredMessages;
    }

    /// @notice Sends multiple messages directly to their respective destination chains in a single transaction.
    /// @dev Requires the contract to have sufficient LINK balance to cover all fees.
    /// @param messages An array of `Message` structs containing details for each message to be sent.
    function dispatchMessagesDirect(
        Message[] calldata messages
    ) external onlyOwner {
        uint256 messageCount = messages.length;
        if (messageCount == 0) {
            revert NoMessagesRegistered();
        }

        for (uint256 i = 0; i < messageCount; i++) {
            Message calldata message = messages[i];

            (bytes32 messageId, uint256 fees) = _sendMessage(
                message.chainSelector,
                message.receiver,
                message.text
            );

            emit MessageSent(
                messageId,
                message.chainSelector,
                message.receiver,
                message.text,
                address(s_linkToken),
                fees
            );
        }
    }

    /// @notice Internal function to handle the sending of a single message to a destination chain.
    /// @param _destinationChainSelector The selector of the destination blockchain.
    /// @param _receiver The recipient's address on the destination blockchain.
    /// @param _text The text content of the message.
    /// @return messageId The unique identifier of the sent CCIP message.
    /// @return fees The amount of LINK tokens paid for the message.
    function _sendMessage(
        uint64 _destinationChainSelector,
        address _receiver,
        string memory _text
    ) private returns (bytes32 messageId, uint256 fees) {
        // Construct the CCIP message with necessary details
        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
            _receiver,
            _text,
            address(s_linkToken)
        );

        // Retrieve the fee required to send the CCIP message
        fees = s_router.getFee(_destinationChainSelector, evm2AnyMessage);

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

        // Approve the router to spend the necessary LINK tokens if not already approved
        uint256 currentAllowance = s_linkToken.allowance(
            address(this),
            address(s_router)
        );
        if (currentAllowance < fees) {
            s_linkToken.safeApprove(address(s_router), fees - currentAllowance);
        }

        // Send the CCIP message via the router and obtain the message ID
        messageId = s_router.ccipSend(
            _destinationChainSelector,
            evm2AnyMessage
        );

        return (messageId, fees);
    }

    /// @notice Constructs a CCIP message with the specified parameters.
    /// @dev Prepares the `EVM2AnyMessage` struct with the receiver, data, and fee token.
    /// @param _receiver The recipient's address on the destination chain.
    /// @param _text The text content to be sent.
    /// @param _feeTokenAddress The address of the token used to pay fees. Use `address(0)` for native gas.
    /// @return Client.EVM2AnyMessage The constructed CCIP message.
    function _buildCCIPMessage(
        address _receiver,
        string memory _text,
        address _feeTokenAddress
    ) private pure returns (Client.EVM2AnyMessage memory) {
        return
            Client.EVM2AnyMessage({
                receiver: abi.encode(_receiver),
                data: abi.encode(_text),
                tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array as no tokens are transferred
                extraArgs: Client._argsToBytes(
                    Client.EVMExtraArgsV1({gasLimit: 300_000})
                ),
                feeToken: _feeTokenAddress
            });
    }

    /// @notice Enables the contract to receive Ether.
    /// @dev This is a fallback function with no additional logic.
    receive() external payable {}

    /// @notice Allows the contract owner to withdraw all tokens of a specified ERC20 token.
    /// @dev Reverts with `NothingToWithdraw` if the contract holds no tokens of the specified type.
    /// @param _beneficiary The address to receive the withdrawn tokens.
    /// @param _token The ERC20 token contract address to withdraw.
    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        uint256 amount = IERC20(_token).balanceOf(address(this));

        if (amount == 0) revert NothingToWithdraw();

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

Messenger.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";

/**
 * 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/receving string data across chains.
contract Messenger is CCIPReceiver, OwnerIsCreator {
    using SafeERC20 for IERC20;

    // 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 FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.
    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.

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

    // Event emitted when a message is received from another chain.
    event MessageReceived(
        bytes32 indexed messageId, // The unique ID of the CCIP message.
        uint64 indexed sourceChainSelector, // The chain selector of the source chain.
        address sender, // The address of the sender from the source chain.
        string text // The text that was received.
    );

    bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
    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;

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

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

        // Return the CCIP message ID
        return messageId;
    }

    /// @notice Sends data to receiver on the destination chain.
    /// @notice Pay for fees in native gas.
    /// @dev Assumes your contract has sufficient native gas tokens.
    /// @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 sendMessagePayNative(
        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(0)
        );

        // 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 > address(this).balance)
            revert NotEnoughBalance(address(this).balance, fees);

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

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

        // Return the CCIP message ID
        return messageId;
    }

    /// handle a received message
    function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    )
        internal
        override
        onlyAllowlisted(
            any2EvmMessage.sourceChainSelector,
            abi.decode(any2EvmMessage.sender, (address))
        ) // Make sure source chain and sender are allowlisted
    {
        s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
        s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text

        emit MessageReceived(
            any2EvmMessage.messageId,
            any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector)
            abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address,
            abi.decode(any2EvmMessage.data, (string))
        );
    }

    /// @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
                    Client.EVMExtraArgsV1({gasLimit: 200_000})
                ),
                // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
                feeToken: _feeTokenAddress
            });
    }

    /// @notice Fetches the details of the last received message.
    /// @return messageId The ID of the last received message.
    /// @return text The last received text.
    function getLastReceivedMessageDetails()
        external
        view
        returns (bytes32 messageId, string memory text)
    {
        return (s_lastReceivedMessageId, s_lastReceivedText);
    }

    /// @notice Fallback function to allow the contract to receive Ether.
    /// @dev This function has no function body, making it a default function for receiving Ether.
    /// It is automatically called when Ether is sent to the contract without any data.
    receive() external payable {}

    /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract.
    /// @dev This function reverts if there are no funds to withdraw or if the transfer fails.
    /// It should only be callable by the owner of the contract.
    /// @param _beneficiary The address to which the Ether should be sent.
    function withdraw(address _beneficiary) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = address(this).balance;

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

        // Attempt to send the funds, capturing the success status and discarding any return data
        (bool sent, ) = _beneficiary.call{value: amount}("");

        // Revert if the send failed, with information about the attempted transfer
        if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
    }

    /// @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);
    }
}

What's next

Stay updated on the latest Chainlink news