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 A contract for sending CCIP messages to multiple chains within the same transaction.
 * @dev This contract provides functionality to either send messages immediately or register them for later dispatch.
 */
contract MessageDispatcher is OwnerIsCreator {
    error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
    error DestinationChainNotAllowlisted(uint64 destinationChainSelector);
    error InvalidReceiverAddress();
    error NothingToWithdraw();

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

    /// @notice Struct to store message details.
    struct Message {
        uint64 chainSelector; // The chain selector for the destination blockchain.
        address receiver; // The address of the recipient on the destination blockchain.
        string text; // The text message to be sent.
    }

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

    /// @notice Array to store registered messages.
    Message[] public registeredMessages;

    /// @notice Event emitted when a message is sent to another chain.
    /// @param messageId The unique ID of the CCIP message.
    /// @param destinationChainSelector The chain selector of the destination chain.
    /// @param receiver The address of the receiver on the destination chain.
    /// @param text The text being sent.
    /// @param feeToken The token address used to pay CCIP fees.
    /// @param fees The 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 Constructor initializes the contract with the 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 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);
        _;
    }

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

    /// @notice Updates the allowlist status of a destination chain for transactions.
    /// @param _destinationChainSelector The identifier (selector) of the destination chain.
    /// @param allowed Boolean indicating whether the chain is allowed.
    function allowlistDestinationChain(
        uint64 _destinationChainSelector,
        bool allowed
    ) external onlyOwner {
        allowlistedDestinationChains[_destinationChainSelector] = allowed;
    }

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

    /// @notice Sends all registered messages to their respective chains.
    /// @dev This function assumes your contract has sufficient LINK.
    function dispatchMessages() external onlyOwner {
        uint256 messageCount = registeredMessages.length;
        if (messageCount == 0) {
            revert NoMessagesRegistered();
        }

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

            string memory messageText = message.text;

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

            // Emit event for each message sent
            emit MessageSent(
                messageId,
                message.chainSelector,
                message.receiver,
                messageText,
                address(s_linkToken),
                s_linkToken.balanceOf(address(this))
            );
        }

        // Clear registered messages after dispatching
        delete registeredMessages;
    }

    /// @notice Sends multiple messages to their respective chains in a single transaction.
    /// @dev This function assumes your contract has sufficient LINK.
    /// @param messages An array of `Message` structs containing the chain selector, receiver, and text for each message.
    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 = _sendMessage(
                message.chainSelector,
                message.receiver,
                message.text
            );

            // Emit event for each message sent
            emit MessageSent(
                messageId,
                message.chainSelector,
                message.receiver,
                message.text,
                address(s_linkToken),
                s_linkToken.balanceOf(address(this))
            );
        }
    }

    /// @notice Internal function to handle sending messages to different chains.
    /// @param _destinationChainSelector The identifier (selector) for the destination blockchain.
    /// @param _receiver The address of the recipient on the destination blockchain.
    /// @param _text The text to be sent to the destination blockchain.
    /// @return messageId The ID of the CCIP message that was sent.
    function _sendMessage(
        uint64 _destinationChainSelector,
        address _receiver,
        string memory _text
    ) private 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)
        );

        // Get the fee required to send the CCIP message
        uint256 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 transfer LINK tokens on contract's behalf. It will spend the fees in LINK
        s_linkToken.approve(address(s_router), fees);

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

        return messageId;
    }

    /// @notice Constructs a CCIP message.
    /// @dev This function creates 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 to `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 memory _text,
        address _feeTokenAddress
    ) private pure returns (Client.EVM2AnyMessage memory) {
        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: 300_000})
                ),
                feeToken: _feeTokenAddress
            });
    }

    /// @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.
    receive() external payable {}

    /// @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 {
        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 aas 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