Getting Started

A simple use case for Chainlink CCIP is sending data between smart contracts on different blockchains. This guide shows you how to deploy a CCIP sender contract and a CCIP receiver contract to two different blockchains and send data from the sender contract to the receiver contract. You pay the CCIP fees using LINK.

Fees can also be paid in alternative assets, which currently include the native gas tokens of the source blockchain and their ERC20 wrapped version. For example, you can pay ETH or WETH when you send transactions to the CCIP router on Ethereum and AVAX or WAVAX when you send transactions to the CCIP router on Avalanche.

Before you begin

Deploy the sender contract

Deploy the Sender.sol contract on Avalanche Fuji. To see a detailed explanation of this contract, read the Code Explanation section.

  1. Open the Sender.sol contract in Remix.

  2. Compile the contract.

  3. Deploy the sender contract on Avalanche Fuji:

    1. Open MetaMask and select the Avalanche Fuji network.

    2. In Remix under the Deploy & Run Transactions tab, 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 addresses for your specific blockchain. You can find both of these addresses on the CCIP Directory. The LINK token contract address is also listed on the LINK Token Contracts page. For Avalanche Fuji, the router address is 0xF694E193200268f9a4868e4Aa017A0118C9a8177 and the LINK address is 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846.

      Chainlink CCIP deploy sender Avalanche Fuji
    4. Click the transact button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to Avalanche Fuji.

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

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

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

Deploy the receiver contract

Deploy the receiver contract on Ethereum Sepolia. You will use this contract to receive data from the sender that you deployed on Avalanche Fuji. To see a detailed explanation of this contract, read the Code Explanation section.

  1. Open the Receiver.sol contract in Remix.

  2. Compile the contract.

  3. Deploy the receiver contract on Ethereum Sepolia:

    1. Open MetaMask and select the Ethereum Sepolia network.

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

    3. Under the Deploy section, fill in the router address field. For Ethereum Sepolia, the Router address is 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59. You can find the addresses for each network on the CCIP Directory.

      Chainlink CCIP Deploy receiver Sepolia
    4. Click the Deploy button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to Ethereum Sepolia.

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

      Chainlink CCIP deployed receiver Sepolia

You now have one sender contract on Avalanche Fuji and one receiver contract on Ethereum Sepolia. You sent 70 LINK to the sender contract to pay the CCIP fees. Next, send data from the sender contract to the receiver contract.

Send data

Send a Hello World! string from your contract on Avalanche Fuji to the contract you deployed on Ethereum Sepolia:

  1. Open MetaMask and select the Avalanche Fuji network.

  2. In Remix under the Deploy & Run Transactions tab, expand the first contract in the Deployed Contracts section.

  3. Expand the sendMessage function and fill in the following arguments:

    ArgumentDescriptionValue (Ethereum Sepolia)
    destinationChainSelectorCCIP Chain identifier of the target blockchain. You can find each network's chain selector on the CCIP Directory16015286601757825753
    receiverThe destination smart contract addressYour deployed contract address
    textAny stringHello World!
    Chainlink CCIP Sepolia send message
  4. Click the transact button to run the function. MetaMask prompts you to confirm the transaction.

  5. After the transaction is successful, note the transaction hash. Here is an example of a successful transaction on Avalanche Fuji.

After the transaction is finalized on the source chain, it will take a few minutes for CCIP to deliver the data to Ethereum Sepolia and call the ccipReceive function on your receiver contract. You can use the CCIP explorer to see the status of your CCIP transaction and then read data stored by your receiver contract.

  1. Open the CCIP explorer and use the transaction hash that you copied to search for your cross-chain transaction. The explorer provides several details about your request.

    Chainlink CCIP Explorer transaction details
  2. When the status of the transaction is marked with a "Success" status, the CCIP transaction and the destination transaction are complete.

    Chainlink CCIP Explorer transaction details success

Read data

Read data stored by the receiver contract on Ethereum Sepolia:

  1. Open MetaMask and select the Ethereum Sepolia network.

  2. In Remix under the Deploy & Run Transactions tab, expand the receiver contract deployed on Ethereum Sepolia.

  3. Click the getLastReceivedMessageDetails function button to read the stored data. In this example, it is "Hello World!".

    Chainlink CCIP Sepolia message details

Congratulations! You just sent your first cross-chain data using CCIP. Next, examine the example code to learn how this contract works.

Examine the example code

Sender code

The smart contract in this tutorial is designed to interact with CCIP to send data. The contract code includes comments to clarify the various functions, events, and underlying logic. However, this section explains the key elements. You can see the full contract code below.

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

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 {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/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 contract for sending string data across chains.
contract Sender is OwnerIsCreator {
    // Custom errors to provide more descriptive revert messages.
    error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance.

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

    IRouterClient private s_router;

    LinkTokenInterface 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) {
        s_router = IRouterClient(_router);
        s_linkToken = LinkTokenInterface(_link);
    }

    /// @notice Sends data to receiver on the destination chain.
    /// @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 string text to be sent.
    /// @return messageId The ID of the message that was sent.
    function sendMessage(
        uint64 destinationChainSelector,
        address receiver,
        string calldata text
    ) external onlyOwner returns (bytes32 messageId) {
        // 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: new Client.EVMTokenAmount[](0), // Empty array indicating no tokens are being sent
            extraArgs: Client._argsToBytes(
                // Additional arguments, setting gas limit
                Client.EVMExtraArgsV2({
                    gasLimit: 200_000, // Gas limit for the callback on the destination chain
                    allowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages from the same sender
                })
            ),
            // Set the feeToken  address, indicating LINK will be used for fees
            feeToken: address(s_linkToken)
        });

        // Get the fee required to send the 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 message through the router and store the returned message ID
        messageId = s_router.ccipSend(destinationChainSelector, evm2AnyMessage);

        // Emit an event with message details
        emit MessageSent(
            messageId,
            destinationChainSelector,
            receiver,
            text,
            address(s_linkToken),
            fees
        );

        // Return the message ID
        return messageId;
    }
}

Initializing the contract

When deploying the contract, you define the router address and the LINK contract address of the blockchain where you choose to deploy the contract.

The router address provides functions that are required for this example:

  • The getFee function to estimate the CCIP fees.
  • The ccipSend function to send CCIP messages.

Sending data

The sendMessage function completes several operations:

  1. Construct a CCIP-compatible message using the EVM2AnyMessage struct:

    • The receiver address is encoded in bytes format to accommodate non-EVM destination blockchains with distinct address formats. The encoding is achieved through abi.encode.
    • The data is encoded from a string text to bytes using abi.encode.
    • The tokenAmounts is an array. Each element comprises a struct that contains the token address and amount. In this example, the array is empty because no tokens are sent.
    • The extraArgs specify the gasLimit for relaying the CCIP message to the recipient contract on the destination blockchain. In this example, the gasLimit is set to 200000.
    • The feeToken designates the token address used for CCIP fees. Here, address(linkToken) signifies payment in LINK.
  2. Compute the fees by invoking the router's getFee function.

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

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

  5. Dispatch the CCIP message to the destination chain by executing the router's ccipSend function.

Receiver code

The smart contract in this tutorial is designed to interact with CCIP to receive data. The contract code includes comments to clarify the various functions, events, and underlying logic. However, this section explains the key elements. You can see the full contract code below.

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

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

/**
 * 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 contract for receiving string data across chains.
contract Receiver is CCIPReceiver {
    // Event emitted when a message is received from another chain.
    event MessageReceived(
        bytes32 indexed messageId, // The unique ID of the 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.

    /// @notice Constructor initializes the contract with the router address.
    /// @param router The address of the router contract.
    constructor(address router) CCIPReceiver(router) {}

    /// handle a received message
    function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    ) internal override {
        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 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);
    }
}

Initializing the contract

When you deploy the contract, you define the router address. The receiver contract inherits from the CCIPReceiver.sol contract, which uses the router address.

Receiving data

On the destination blockchain:

  1. The CCIP Router invokes the ccipReceive function. Note: This function is protected by the onlyRouter modifier, which ensures that only the router can call the receiver contract.

  2. The ccipReceive function calls an internal function _ccipReceive function. The receiver contract implements this function.

  3. This _ccipReceive function expects an Any2EVMMessage struct that contains the following values:

    • The CCIP messageId.
    • The sourceChainSelector.
    • The sender address in bytes format. The sender is a contract deployed on an EVM-compatible blockchain, so the address is decoded from bytes to an Ethereum address using the ABI specification.
    • The data is also in bytes format. A string is expected, so the data is decoded from bytes to a string using the ABI specification.

What's next

Get the latest Chainlink content straight to your inbox.