NEW

Join us at SmartCon 2023—where Web3 gets real. Get your ticket.

Transfer Tokens and Data Across Chains

In this tutorial, you will use Chainlink CCIP to transfer tokens and arbitrary data between smart contracts on different blockchains. First, you will pay for the CCIP fees on the source blockchain using LINK. Then, you will use the same contract to pay CCIP fees in native gas tokens. For example, you would use ETH on Ethereum or MATIC on Polygon.

Before you begin

  1. You should understand how to write, compile, deploy, and fund a smart contract. If you need to brush up on the basics, read this tutorial, which will guide you through using the Solidity programming language, interacting with the MetaMask wallet and working within the Remix Development Environment.
  2. Your account must have some ETH and LINK tokens on Ethereum Sepolia and MATIC tokens on Polygon Mumbai. Learn how to Acquire testnet LINK.
  3. Check the Supported Networks page to confirm that the tokens you will transfer are supported for your lane. In this example, you will transfer tokens from Ethereum Sepolia to Polygon Mumbai so check the list of supported tokens here.
  4. Learn how to acquire CCIP test tokens. Following this guide, you should have CCIP-BnM tokens, and CCIP-BnM should appear in the list of your tokens in MetaMask.
  5. Learn how to fund your contract. This guide shows how to fund your contract in LINK, but you can use the same guide for funding your contract with any ERC20 tokens as long as they appear in the list of tokens in MetaMask.
  6. Follow the previous tutorial: Transfer tokens between chains.

Tutorial

In this tutorial, you will send a string text and CCIP-BnM tokens between smart contracts on Ethereum Sepolia and Polygon Mumbai using CCIP. First, you will pay CCIP fees in LINK, then you will pay CCIP fees in native gas.

// 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.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.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 transferring/receiving tokens and data across chains.
contract ProgrammableTokenTransfers 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 to cover the fees.
    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 DestinationChainNotWhitelisted(uint64 destinationChainSelector); // Used when the destination chain has not been whitelisted by the contract owner.
    error SourceChainNotWhitelisted(uint64 sourceChainSelector); // Used when the source chain has not been whitelisted by the contract owner.
    error SenderNotWhitelisted(address sender); // Used when the sender has not been whitelisted by the contract owner.

    // 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 token, // The token address that was transferred.
        uint256 tokenAmount, // The token amount that was transferred.
        address feeToken, // the token address used to pay CCIP fees.
        uint256 fees // The fees paid for sending the 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.
        address token, // The token address that was transferred.
        uint256 tokenAmount // The token amount that was transferred.
    );

    bytes32 private lastReceivedMessageId; // Store the last received messageId.
    address private lastReceivedTokenAddress; // Store the last received token address.
    uint256 private lastReceivedTokenAmount; // Store the last received amount.
    string private lastReceivedText; // Store the last received text.

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

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

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

    LinkTokenInterface 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) {
        linkToken = LinkTokenInterface(_link);
    }

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

    /// @dev Modifier that checks if the chain with the given sourceChainSelector is whitelisted.
    /// @param _sourceChainSelector The selector of the destination chain.
    modifier onlyWhitelistedSourceChain(uint64 _sourceChainSelector) {
        if (!whitelistedSourceChains[_sourceChainSelector])
            revert SourceChainNotWhitelisted(_sourceChainSelector);
        _;
    }

    /// @dev Modifier that checks if the chain with the given sourceChainSelector is whitelisted.
    /// @param _sender The address of the sender.
    modifier onlyWhitelistedSenders(address _sender) {
        if (!whitelistedSenders[_sender]) revert SenderNotWhitelisted(_sender);
        _;
    }

    /// @dev Whitelists a chain for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _destinationChainSelector The selector of the destination chain to be whitelisted.
    function whitelistDestinationChain(
        uint64 _destinationChainSelector
    ) external onlyOwner {
        whitelistedDestinationChains[_destinationChainSelector] = true;
    }

    /// @dev Denylists a chain for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _destinationChainSelector The selector of the destination chain to be denylisted.
    function denylistDestinationChain(
        uint64 _destinationChainSelector
    ) external onlyOwner {
        whitelistedDestinationChains[_destinationChainSelector] = false;
    }

    /// @dev Whitelists a chain for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _sourceChainSelector The selector of the source chain to be whitelisted.
    function whitelistSourceChain(
        uint64 _sourceChainSelector
    ) external onlyOwner {
        whitelistedSourceChains[_sourceChainSelector] = true;
    }

    /// @dev Denylists a chain for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _sourceChainSelector The selector of the source chain to be denylisted.
    function denylistSourceChain(
        uint64 _sourceChainSelector
    ) external onlyOwner {
        whitelistedSourceChains[_sourceChainSelector] = false;
    }

    /// @dev Whitelists a sender.
    /// @notice This function can only be called by the owner.
    /// @param _sender The address of the sender.
    function whitelistSender(address _sender) external onlyOwner {
        whitelistedSenders[_sender] = true;
    }

    /// @dev Denylists a sender.
    /// @notice This function can only be called by the owner.
    /// @param _sender The address of the sender.
    function denySender(address _sender) external onlyOwner {
        whitelistedSenders[_sender] = false;
    }

    /// @notice Sends data and transfer tokens to receiver on the destination chain.
    /// @notice Pay for fees in LINK.
    /// @dev Assumes your contract has sufficient LINK to pay for CCIP fees.
    /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
    /// @param _receiver The address of the recipient on the destination blockchain.
    /// @param _text The string data to be sent.
    /// @param _token token address.
    /// @param _amount token amount.
    /// @return messageId The ID of the CCIP message that was sent.
    function sendMessagePayLINK(
        uint64 _destinationChainSelector,
        address _receiver,
        string calldata _text,
        address _token,
        uint256 _amount
    )
        external
        onlyOwner
        onlyWhitelistedDestinationChain(_destinationChainSelector)
        returns (bytes32 messageId)
    {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        // address(linkToken) means fees are paid in LINK
        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
            _receiver,
            _text,
            _token,
            _amount,
            address(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 > linkToken.balanceOf(address(this)))
            revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);

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

        // approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token
        IERC20(_token).approve(address(router), _amount);

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

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

        // Return the message ID
        return messageId;
    }

    /// @notice Sends data and transfer tokens to receiver on the destination chain.
    /// @notice Pay for fees in native gas.
    /// @dev Assumes your contract has sufficient native gas like ETH on Ethereum or MATIC on Polygon.
    /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
    /// @param _receiver The address of the recipient on the destination blockchain.
    /// @param _text The string data to be sent.
    /// @param _token token address.
    /// @param _amount token amount.
    /// @return messageId The ID of the CCIP message that was sent.
    function sendMessagePayNative(
        uint64 _destinationChainSelector,
        address _receiver,
        string calldata _text,
        address _token,
        uint256 _amount
    )
        external
        onlyOwner
        onlyWhitelistedDestinationChain(_destinationChainSelector)
        returns (bytes32 messageId)
    {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        // address(0) means fees are paid in native gas
        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
            _receiver,
            _text,
            _token,
            _amount,
            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);

        // approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token
        IERC20(_token).approve(address(router), _amount);

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

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

        // Return the message ID
        return messageId;
    }

    /**
     * @notice Returns the details of the last CCIP received message.
     * @dev This function retrieves the ID, text, token address, and token amount of the last received CCIP message.
     * @return messageId The ID of the last received CCIP message.
     * @return text The text of the last received CCIP message.
     * @return tokenAddress The address of the token in the last CCIP received message.
     * @return tokenAmount The amount of the token in the last CCIP received message.
     */
    function getLastReceivedMessageDetails()
        public
        view
        returns (
            bytes32 messageId,
            string memory text,
            address tokenAddress,
            uint256 tokenAmount
        )
    {
        return (
            lastReceivedMessageId,
            lastReceivedText,
            lastReceivedTokenAddress,
            lastReceivedTokenAmount
        );
    }

    /// handle a received message
    function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    )
        internal
        override
        onlyWhitelistedSourceChain(any2EvmMessage.sourceChainSelector) // Make sure source chain is whitelisted
        onlyWhitelistedSenders(abi.decode(any2EvmMessage.sender, (address))) // Make sure the sender is whitelisted
    {
        lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
        lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text
        // Expect one token to be transferred at once, but you can transfer several tokens.
        lastReceivedTokenAddress = any2EvmMessage.destTokenAmounts[0].token;
        lastReceivedTokenAmount = any2EvmMessage.destTokenAmounts[0].amount;

        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)),
            any2EvmMessage.destTokenAmounts[0].token,
            any2EvmMessage.destTokenAmounts[0].amount
        );
    }

    /// @notice Construct a CCIP message.
    /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for programmable tokens transfer.
    /// @param _receiver The address of the receiver.
    /// @param _text The string data to be sent.
    /// @param _token The token to be transferred.
    /// @param _amount The amount of the token to be transferred.
    /// @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 _token,
        uint256 _amount,
        address _feeTokenAddress
    ) internal pure returns (Client.EVM2AnyMessage memory) {
        // Set the token amounts
        Client.EVMTokenAmount[]
            memory tokenAmounts = new Client.EVMTokenAmount[](1);
        Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
            token: _token,
            amount: _amount
        });
        tokenAmounts[0] = tokenAmount;
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
            receiver: abi.encode(_receiver), // ABI-encoded receiver address
            data: abi.encode(_text), // ABI-encoded string
            tokenAmounts: tokenAmounts, // The amount and type of token being transferred
            extraArgs: Client._argsToBytes(
                // Additional arguments, setting gas limit and non-strict sequencing mode
                Client.EVMExtraArgsV1({gasLimit: 200_000, strict: false})
            ),
            // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
            feeToken: _feeTokenAddress
        });
        return evm2AnyMessage;
    }

    /// @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).transfer(_beneficiary, amount);
    }
}

Deploy your contracts

To use this contract:

  1. Open the contract in Remix.

  2. Compile your contract.

  3. Deploy and fund your sender contract on Ethereum Sepolia:

    1. Open MetaMask and select the network Ethereum Sepolia.
    2. In Remix IDE, click on Deploy & Run Transactions and select Injected Provider - MetaMask from the environment list. Remix will then interact with your MetaMask wallet to communicate with Ethereum Sepolia.
    3. Fill in your blockchain's router and LINK contract addresses. The router address can be found on the supported networks page and the LINK contract address on the LINK token contracts page. For Ethereum Sepolia, the router address is 0xD0daae2231E9CB96b94C8512223533293C3693Bf and the LINK contract address is 0x779877A7B0D9E8603169DdbD7836e478b4624789.
    4. Click the transact button. After you confirm the transaction, the contract address appears on the Deployed Contracts list. Note your contract address.
    5. Open MetaMask and fund your contract with CCIP-BnM tokens. You can transfer 0.002 CCIP-BnM to your contract.
    6. Enable your contract to send CCIP messages to Polygon Mumbai:
      1. In Remix IDE, under Deploy & Run Transactions, open the list of transactions of your smart contract deployed on Ethereum Sepolia.
      2. Call the whitelistDestinationChain with 12532609583862916517 as the destination chain selector. Each chain selector is found on the supported networks page.
  4. Deploy your receiver contract on Polygon Mumbai:

    1. Open MetaMask and select the network Polygon Mumbai.
    2. In Remix IDE, under Deploy & Run Transactions, make sure the environment is still Injected Provider - MetaMask.
    3. Fill in your blockchain's router and LINK contract addresses. The router address can be found on the supported networks page and the LINK contract address on the LINK token contracts page. For Polygon Mumbai, the router address is 0x70499c328e1E2a3c41108bd3730F6670a44595D1 and the LINK contract address is 0x326C977E6efc84E512bB9C30f76E30c160eD06FB.
    4. Click the transact button. After you confirm the transaction, the contract address appears on the Deployed Contracts list. Note your contract address.
    5. Enable your contract to receive CCIP messages from Ethereum Sepolia:
      1. In Remix IDE, under Deploy & Run Transactions, open the list of transactions of your smart contract deployed on Polygon Mumbai.
      2. Call the whitelistSourceChain with 16015286601757825753 as the source chain selector. Each chain selector is found on the supported networks page.
    6. Enable your contract to receive CCIP messages from the contract that you deployed on Ethereum Sepolia:
      1. In Remix IDE, under Deploy & Run Transactions, open the list of transactions of your smart contract deployed on Polygon Mumbai.
      2. Call the whitelistSender with the contract address of the contract that you deployed on Ethereum Sepolia.
  5. At this point, you have one sender contract on Ethereum Sepolia and one receiver contract on Polygon Mumbai. As security measures, you enabled the sender contract to send CCIP messages to Polygon Mumbai and the receiver contract to receive CCIP messages from the sender and Ethereum Sepolia.

    Note: Another security measure enforces that only the router can call the _ccipReceive function. Read the explanation section for more details.

You will transfer 0.001 CCIP-BnM and a text. The CCIP fees for using CCIP will be paid in LINK. Read this explanation for a detailed description of the code example.

  1. Open MetaMask and connect to Ethereum Sepolia. Fund your contract with LINK tokens. You can transfer 0.01 LINK to your contract. In this example, LINK is used to pay the CCIP fees.

  2. Send a string data with tokens from Ethereum Sepolia:

    1. Open MetaMask and select the network Ethereum Sepolia.

    2. In Remix IDE, under Deploy & Run Transactions, open the list of transactions of your smart contract deployed on Ethereum Sepolia.

    3. Fill in the arguments of the sendMessagePayLINK function:


      ArgumentValue and Description
      _destinationChainSelector12532609583862916517
      CCIP Chain identifier of the destination blockchain (Polygon Mumbai in this example). You can find each chain selector on the supported networks page.
      _receiverYour receiver contract address at Polygon Mumbai.
      The destination contract address.
      _textHello World!
      Any string
      _token0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05
      The CCIP-BnM contract address at the source chain (Ethereum Sepolia in this example). You can find all the addresses for each supported blockchain on the supported networks page.
      _amount1000000000000000
      The token amount (0.001 CCIP-BnM).
    4. Click on transact and confirm the transaction on MetaMask.

    5. After the transaction is successful, record the transaction hash. Here is an example of a transaction on Ethereum Sepolia.

    Note: During gas price spikes, your transaction might fail, requiring more than 0.01 LINK to proceed. If your transaction fails, fund your contract with more LINK tokens and try again.

  3. Open the CCIP explorer and search your cross-chain transaction using the transaction hash.


    Chainlink CCIP Explorer transaction details
  4. The CCIP transaction is completed once the status is marked as "Success". In this example, the CCIP message ID is 0x0269fd4a8f21d298c6d73b14a5b1051e87178518bdd0f1fce36f9c6c658808b1.


    Chainlink CCIP Explorer transaction details success
  5. Check the receiver contract on the destination chain:

    1. Open MetaMask and select the network Polygon Mumbai.

    2. In Remix IDE, under Deploy & Run Transactions, open the list of transactions of your smart contract deployed on Polygon Mumbai.

    3. Call the getLastReceivedMessageDetails function.


      Chainlink CCIP Mumbai message details
    4. Notice the received messageId is 0x0269fd4a8f21d298c6d73b14a5b1051e87178518bdd0f1fce36f9c6c658808b1, the received text is Hello World!, the token address is 0xf1E3A5842EeEF51F2967b3F05D45DD4f4205FF40 (CCIP-BnM token address on Polygon Mumbai) and the token amount is 1000000000000000 (0.001 CCIP-BnM).

Note: These example contracts are designed to work bi-directionally. As an exercise, you can use them to transfer tokens and data from Ethereum Sepolia to Polygon Mumbai and from Polygon Mumbai back to Ethereum Sepolia.

Transfer and Receive tokens and data and pay in native

You will transfer 0.001 CCIP-BnM and a text. The CCIP fees for using CCIP will be paid in Sepolia's native ETH. Read this explanation for a detailed description of the code example.

  1. Open MetaMask and connect to Ethereum Sepolia. Fund your contract with ETH tokens. You can transfer 0.01 ETH to your contract. The native gas tokens are used to pay the CCIP fees.

  2. Send a string data with tokens from Ethereum Sepolia:

    1. Open MetaMask and select the network Ethereum Sepolia.

    2. In Remix IDE, under Deploy & Run Transactions, open the list of transactions of your smart contract deployed on Ethereum Sepolia.

    3. Fill in the arguments of the sendMessagePayNative function:


      ArgumentValue and Description
      _destinationChainSelector12532609583862916517
      CCIP Chain identifier of the destination blockchain (Polygon Mumbai in this example). You can find each chain selector on the supported networks page.
      _receiverYour receiver contract address at Polygon Mumbai.
      The destination contract address.
      _textHello World!
      Any string
      _token0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05
      The CCIP-BnM contract address at the source chain (Ethereum Sepolia in this example). You can find all the addresses for each supported blockchain on the supported networks page.
      _amount1000000000000000
      The token amount (0.001 CCIP-BnM).
    4. Click on transact and confirm the transaction on MetaMask.

    5. Once the transaction is successful, note the transaction hash. Here is an example of a transaction on Ethereum Sepolia.

    Note: During gas price spikes, your transaction might fail, requiring more than 0.01 ETH to proceed. If your transaction fails, fund your contract with more ETH and try again.

  3. Open the CCIP explorer and search your cross-chain transaction using the transaction hash.


    Chainlink CCIP Explorer transaction details
  4. The CCIP transaction is completed once the status is marked as "Success". In this example, the CCIP message ID is 0x3a2cde0d0dd07f2b2e7841b550221101d57792eb581a75ed11419022f13fd865. Note that CCIP fees are denominated in LINK. Even if CCIP fees are paid using native gas tokens, node operators will be paid in LINK.


    Chainlink CCIP Explorer transaction details success
  5. Check the receiver contract on the destination chain:

    1. Open MetaMask and select the network Polygon Mumbai.

    2. In Remix IDE, under Deploy & Run Transactions, open the list of transactions of your smart contract deployed on Polygon Mumbai.

    3. Call the getLastReceivedMessageDetails function.


      Chainlink CCIP Mumbai message details
    4. Notice the received messageId is 0x3a2cde0d0dd07f2b2e7841b550221101d57792eb581a75ed11419022f13fd865, the received text is Hello World!, the token address is 0xf1E3A5842EeEF51F2967b3F05D45DD4f4205FF40 (CCIP-BnM token address on Polygon Mumbai) and the token amount is 1000000000000000 (0.001 CCIP-BnM).

Note: These example contracts are designed to work bi-directionally. As an exercise, you can use them to transfer tokens and data from Ethereum Sepolia to Polygon Mumbai and from Polygon Mumbai back to Ethereum Sepolia.

Explanation

The smart contract featured in this tutorial is designed to interact with CCIP to transfer and receive tokens and data. The contract code contains supporting comments clarifying the functions, events, and underlying logic. Here we will further explain initializing the contract and sending data with tokens.

Initializing the contract

When deploying the contract, we define the router address and LINK contract address of the blockchain we deploy the contract on. Defining the router address is useful for the following:

  • Sender part:

    • Calls the router's getFee function to estimate the CCIP fees.
    • Calls the router's ccipSend function to send CCIP messages.
  • Receiver part:

    • The contract inherits from CCIPReceiver, which serves as a base contract for receiver contracts. This contract requires that child contracts implement the _ccipReceive function. _ccipReceive is called by the ccipReceive function, which ensures that only the router can deliver CCIP messages to the receiver contract.

The sendMessagePayLINK function undertakes six primary operations:

  1. Call the _buildCCIPMessage internal function to construct a CCIP-compatible message using the EVM2AnyMessage struct:

    • The _receiver address is encoded in bytes to accommodate non-EVM destination blockchains with distinct address formats. The encoding is achieved through abi.encode.

    • The data is encoded from a string to bytes using abi.encode.

    • The tokenAmounts is an array, with each element comprising an EVMTokenAmount struct containing the token address and amount. The array contains one element where the _token (token address) and _amount (token amount) are passed by the user when calling the transferTokensPayNative function.

    • The extraArgs specifies the gasLimit for relaying the message to the recipient contract on the destination blockchain, along with a strict parameter. In this example, the gasLimit is set to 200000 and strict is set to false. Note: If strict is true and ccipReceive reverts on the destination blockchain, subsequent messages from the same sender will be blocked by the CCIP until the reverted message can be executed.

    • The _feeTokenAddress designates the token address used for CCIP fees. Here, address(linkToken) signifies payment in LINK.

  2. Computes the message fees by invoking the router's getFee function.

  3. Ensures your contract balance in LINK is enough to cover the fees.

  4. Grants the router contract permission to deduct the fees from the contract's LINK balance.

  5. Grants the router contract permission to deduct the amount from the contract's CCIP-BnM balance.

  6. Dispatches the CCIP message to the destination chain by executing the router's ccipSend function.

Note: As a security measure, the sendMessagePayLINK function is protected by the onlyWhitelistedDestinationChain, ensuring the contract owner has whitelisted a destination chain.

Transferring tokens and data and pay in native

The sendMessagePayNative function undertakes five primary operations:

  1. Call the _buildCCIPMessage internal function to construct a CCIP-compatible message using the EVM2AnyMessage struct:

    • The _receiver address is encoded in bytes to accommodate non-EVM destination blockchains with distinct address formats. The encoding is achieved through abi.encode.

    • The data is encoded from a string to bytes using abi.encode.

    • The tokenAmounts is an array, with each element comprising an EVMTokenAmount struct containing the token address and amount. The array contains one element where the _token (token address) and _amount (token amount) are passed by the user when calling the transferTokensPayNative function.

    • The extraArgs specifies the gasLimit for relaying the message to the recipient contract on the destination blockchain, along with a strict parameter. In this example, the gasLimit is set to 200000 and strict is set to false. Note: If strict is true and ccipReceive reverts on the destination blockchain, subsequent messages from the same sender will be blocked by CCIP until the reverted message can be executed.

    • The _feeTokenAddress designates the token address used for CCIP fees. Here, address(0) signifies payment in native gas tokens (ETH).

  2. Computes the message fees by invoking the router's getFee function.

  3. Ensures your contract balance in native gas is enough to cover the fees.

  4. Grants the router contract permission to deduct the amount from the contract's CCIP-BnM balance.

  5. Dispatches the CCIP message to the destination chain by executing the router's ccipSend function. Note: msg.value is set because you pay in native gas.

Note: As a security measure, the sendMessagePayNative function is protected by the onlyWhitelistedDestinationChain, ensuring the contract owner has whitelisted a destination chain.

Receiving messages

On the destination blockchain, the router invokes the _ccipReceive function which expects a Any2EVMMessage struct that contains:

  • The CCIP messageId.
  • The sourceChainSelector.
  • The sender address in bytes format. Given that the sender is known to be a contract deployed on an EVM-compatible blockchain, the address is decoded from bytes to an Ethereum address using the ABI specifications.
  • The tokenAmounts is an array containing received tokens and their respective amounts. Given that only one token transfer is expected, the first element of the array is extracted.
  • The data, which is also in bytes format. Given a string is expected, the data is decoded from bytes to a string using the ABI specifications.

Note: Three important security measures are applied:

  • _ccipReceive is called by the ccipReceive function, which ensures that only the router can deliver CCIP messages to the receiver contract. See the onlyRouter modifier for more information.
  • The modifier onlyWhitelistedSourceChain ensures that only a call from a whitelisted source chain is accepted.
  • The modifier onlyWhitelistedSenders ensures that only a call from a whitelisted sender is accepted.

What's next

Stay updated on the latest Chainlink news