How to use Chainlink CCIP

The minimal code needed to send and receive CCIP Messages

The minimal CCIP architecture

To recap, with Chainlink CCIP, one can:

  • Transfer (supported) tokens

  • Send any kind of data

  • Send both tokens and data

CCIP receiver can be:

  • EOA

  • Any smart contract that implements CCIPReceiver.sol

Basic CCIP Architecture

Note: If you send a message and token(s) to EOA, only tokens will arrive.

For now, you can consider CCIP as a "black-box" component and be aware of the Router contract only. We will explain the Chainlink CCIP architecture in the following chapters.

Getting started

You can use Chainlink CCIP with any blockchain development framework. For this Masterclass, we prepared the steps for Hardhat, Foundry, and Remix IDE.

Let's create a new project

Make sure you have Node.js and NPM installed. To check, run the following command:

Create a new folder and name it ccip-masterclass

Navigate to it

Create a hew Hardhat project by running:

And then select either "Create a JavaScript project" or "Create a TypeScript project".

Alternatively, you can clone:

To use Chainlink CCIP, you need to interact with Chainlink CCIP-specific contracts from the @chainlink/contracts-ccip NPM package.

To install it, follow steps specific to the development environment you will use for this Masterclass.

Basic interface

Although, as being said, CCIP sender and receiver can be EOA and smart contract, and all combinations are possible, we are going to cover the most complex use-case where both CCIP sender and receiver are smart contracts on different blockchains.

Source blockchain

To send CCIP Messages, the smart contract on the source blockchain must call the ccipSend() function, which is defined the IRouterClient.sol interface.

The CCIP Message which is being sent is a type of EVM2AnyMessage Solidity struct from the Client library.

Let's now understand what each property of the EVM2AnyMessage struct we are sending represents and how to use it.

receiver

Receiver address. It can be a smart contract or an EOA. Use abi.encode(receiver) to encode the address to the bytes Solidity data-type.

data

Payload sent within the CCIP message. This is that "any type of data" one can send as a CCIP Message we are referring to from the start. It can be anything from simple text like "Hello, world!" to Solidity structs or function selectors.

tokenAmounts

Tokens and their amounts in the source chain representation. Here we are specifying which tokens (out of supported ones) we are sending and how much of it. This is the array of a EVMTokenAmount struct, which consists of two properties only:

  • token - Address of a token we are sending on the local (source) blockchain

  • amount The amount of tokens we are sending. The sender must approve the CCIP router to spend this amount on behalf of the sender, otherwise the call to the ccipSend function will revert.

Currently, the maximum number of tokens one can send in a single CCIP send transaction is five.

feeToken

Address of feeToken. CCIP supports fee payments in LINK and in alternative assets, which currently include native blockchain gas coins and their ERC20 wrapped versions. For developers, this means you can simply pay on the source chain, and CCIP will take care of execution on the destination chain. Set address(0) to pay in native gas coins such as ETH on Ethereum or MATIC on Polygon. Keep in mind that even if you are paying for fees in the native asset, nodes in the Chainlink DON will be rewarded in LINK only.

extraArgs

Users fill in the EVMExtraArgsV1 struct and then encode it to bytes using the _argsToBytes function. The struct consists of two properties:

  • gasLimit - The maximum amount of gas CCIP can consume to execute ccipReceive() on the contract located on the destination blockchain. Unspent gas is not refunded. This means that if you are sending tokens to EOA, for example, you should put 0 as a gasLimit value because EOAs can't implement the ccipReceive() (or any other) function. To estimate the accurate gas limit for your destination contract, consider Leveraging Ethereum client RPC by applying eth_estimateGas on receiver.ccipReceive() function, or use the Hardhat plugin for gas tests, or conduct Foundry gas tests.

  • strict - Used for strict sequencing. You should set it to false. CCIP will always process messages sent from a specific sender to a specific destination blockchain in the order they were sent. If you set strict: true in the extraArgs part of the message, and if the ccipReceive fails (reverts), it will prevent any following messages from the same sender from being processed until the current message is successfully executed. You should be very careful when using this feature to avoid unintentionally stopping messages from the sender from being processed. The strict sequencing feature is currently experimental, and there is no guarantee of its maintenance or further development in the future.

If extraArgs are left empty, a.k.a extraArgs: "", a default of 200_000 gasLimit will be set with no strict sequencing. For production deployments, make sure that extraArgs is mutable. This allows you to build it off-chain and pass it in a call to a function or store it in a variable that you can update on demand. This makes extraArgs compatible with future CCIP upgrades.

Destination blockchain

To receive CCIP Messages, the smart contract on the destination blockchain must implement the IAny2EVMMessageReceiver interface. The @chainlink/contracts-ccip NPM package comes up with the contract which implements it in the right way, called CCIPReceiver.sol, but we are going to talk more about it in the next chapter. For now, let's understand which functions from the IAny2EVMMessageReceiver interface must be implemented in the general-case scenario.

As you can see, the ccipReceive() function from the IAny2EVMMessageReceiver interface accepts object of the Any2EVMMessage struct from the Client library. This struct is the Solidity representation of the received CCIP Message. Please note that this struct, Any2EVMMessage is different than the one we used to send on the source blockchain - EVM2AnyMessage. They are not the same.

Let's now understand what each property of the Any2EVMMessage struct we are receiving represents and how to use it.

  • messageId - CCIP Message Id, generated on the source chain.

  • sourceChainSelector - Source chain selector.

  • sender - Sender address. abi.decode(sender, (address)) if the source chain is an EVM chain.

  • data - Payload sent within the CCIP message. For example, "Hello, world!"

  • tokenAmounts - Received tokens and their amounts in their destination chain representation.

To recap, here's the diagram with the minimal architecture needed to send & receive the Chainlink CCIP Message:

Developer Interfaces


Coding time 🎉

Now that we understand what basic CCIP architecture looks like and how to use it let's write our first CCIP Sender & CCIP Receiver contracts. Keep in mind that this is the minimal code needed to send & receive CCIP Messages and that it is very unsafe for production usage, but we will cover that in the following chapters.

We are going to use Avalanche Fuji -> Ethereum Sepolia lane because it is the fastest one. The idea is to send a simple text message as a data payload.

Develop CCIP Sender contract

Follow the steps to create a basic CCIP Sender contract.

Create a new file inside the contracts folder and name it CCIPSender_Unsafe.sol

Start with the development by setting the Solidity compiler version and importing necessary contracts from the @chainlink/contracts-ccip NPM package.

Since we are importing the LinkTokenInterface interface from the @chainlink/contracts package, which we haven't installed yet, the compilation will fail. To solve the potential issue, let's install the @chainlink/contracts NPM package:

Now let's add storage variables for the Router.sol smart contract address and theLINK token address which we will use for fees. Also, we will approve the Router.sol to spend the maximum possible amount of LINK tokens this contract poses. This is an extremely bad practice, and in the next chapter, you will see how to approve the exact amount needed for fees, but for the sake of simplicity and better code readability, we are choosing the current path. Our goal for this lecture is to understand minimal principles when it comes to sending & receiving CCIP Messages.

Finally, let's write a function to send a CCIP Message. We will pass the address of the Receiver contract (to be developed & deployed) as a function argument alongside the destination chain selector (although we know that it is going to be the Ethereum Sepolia's one) and the simple text we want to send.

We are not sending any tokens, so we are assigning the empty Client.EVMTokenAmount array to the tokenAmounts field. For the sake of simplicity and code readability, we will not set the extraArgs field either, which will then default to 200_000 for the gasLimit and false for sequencing. Finally, we will use LINK tokens to pay for CCIP fees.

Now try to compile the contract by running the following command inside your Terminal (make sure that solidity version inside the hardhat.config file is set to at least 0.8.19 or higher:

Develop CCIP Receiver contract

Follow the steps to create a basic CCIP Receiver contract.

Create a new file inside the contracts folder and name it CCIPReceiver_Unsafe.sol

Start with the development by setting the Solidity compiler version and importing necessary contracts from the @chainlink/contracts-ccip NPM package.

Now let's add storage variables to track the latest received CCIP Message data and sender. We will mark them intentionally as public because Solidity will automatically develop getter functions for them, thus making our codebase smaller and more readable. We want to call these getter functions to confirm that CCIP Message has been successfully received.

Finally, let's implement the _ccipReceive() function from the CCIPReceiver.sol contract. Note that while we were explaining, in the previous chapter, how to develop the basic CCIP receiver contract by implementing the IAny2EVMMessageReceiver interface, we mentioned that there is something called the CCIPReceiver.sol smart contract. The CCIPReceiver.sol smart contract from the @chainlink/contracts-ccip NPM package is the smart contract that properly implements the following interface, following the major best practices, and therefore we are going to use it because it will make our code much more readable and easier to understand.

Once you receive the CCIP Message, you can do whatever you want with it. For the purpose of this very first example, we are just going to assign two storage variables we previously defined.

Now try to compile the contract by running the following command inside your Terminal (make sure that solidity version inside the hardhat.config file is set to at least 0.8.19 or higher:

Prepare for deployment

Follow the steps to add the necessary environment variables for deploying these contracts and sending your first CCIP Message.

We are going to use the @chainlink/env-enc package for extra security. It encrypts sensitive data instead of storing them as plain text in the .env file by creating a new .env.enc file. Although it's not recommended to push this file online, if that accidentally happens, your secrets will still be encrypted.

Install the package by running the following command:

Set a password for encrypting and decrypting the environment variable file. You can change it later by typing the same command.

Now set the following environment variables: PRIVATE_KEY, Source Blockchain RPC URL, Destination Blockchain RPC URL. For this example, we are going to use Avalanche Fuji and Ethereum Sepolia.

To set these variables, type the following command and follow the instructions in the terminal:

After you are done, the .env.enc file will be automatically generated. If you want to validate your inputs, you can always run the next command:

Finally, expand the hardhat.config to support these two networks:

Deploy CCIP Receiver to Ethereum Sepolia

Follow the steps to deploy the CCIPRecevier_Unsafe smart contract to the Ethereum Sepolia network.

Create a new file under the scripts folder and name it deployReceiver.ts or deployReceiver.js depends on whether you work with TypeScript or JavaScript Hardhat projects.

Note that deployment of the CCIPReceiver_Unsafe smart contract is hard coded to Ethereum Sepolia for this example, but feel free to refactor the following deployment script to support other networks or even make it fully customizable by rewriting it to Hardhat task with (optional) parameters. You can check CCIP Starter Kit (Hardhat version for reference.

Deploy CCIPReceiver_Unsafe smart contract by running:

or for JavaScript:

Deploy CCIP Sender to Avalanche Fuji

Follow the steps to deploy the CCIPSender_Unsafe smart contract to the Avalanche Fuji network.

Create a new file under the scripts folder and name it deploySender.ts or deploySender.js depends on whether you work with TypeScript or JavaScript Hardhat projects.

Note that deployment of the CCIPSender_Unsafe smart contract is hard coded to Avalanche Fuji for this example, but feel free to refactor the following deployment script to support other networks or even make it fully customizable by rewriting it to Hardhat task with (optional) parameters. You can check CCIP Starter Kit (Hardhat version for reference.

Deploy CCIPSender_Unsafe smart contract by running:

or for JavaScript:

Send your first CCIP Message

Follow the steps to send the CCIP Message from the CCIPSender_Unsafe smart contract on the Avalanche Fuji network to the CCIPReceiver_Unsafe smart contract on the Ethereum Sepolia network.

First of all, you will need to fund your CCIPSender_Unsafe smart contract with 1 LINK. To get it, navigate to the https://faucets.chain.link/fuji

Chainlink Faucet

Now fund the CCIPSender_Unsafe smart contract by sending 1 LINK from your wallet to it.

Fund CCIPSender_Unsafe.sol with 1 LINK

And finally, send your first CCIP Cross-Chain Message:

Prepare:

  • The address of the address of the CCIPReceiver_Unsafe.sol smart contract you previously deployed to Ethereum Sepolia, as the receiver parameter;

  • The Text Message you want to send, for example "CCIP Masterclass", as the someText parameter;

  • 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the destinationChainSelector parameter.

Create a new JavaScript/TypeScript file under the scripts folder and name it sendMessage.js/sendMessage.ts

Send your first CCIP Message by running the following command:

Or for JavaScript:

You can now monitor live the status of your CCIP Cross-Chain Message via CCIP Explorer. Just paste the transaction hash into the search bar and open the message details.

CCIP Explorer

Recap

To build using Chainlink CCIP, one needs to use the @chainlink/contracts-ccip NPM package.

To send a CCIP Message, one needs to call the ccipSend() function from the IRouterClient interface, on the CCIP Router.sol smart contract by passing the EVM2AnyMessage struct from the Client library.

To receive a CCIP Message, one needs to implement the IAny2EVMMessageReceiver interface, which consists of the ccipReceive() function that needs to be overridden, which receives the Any2EVMMessage struct from the Client library as a function argument.

The @chainlink/contracts-ccip NPM package comes up with the already implemented receiver smart contract called CCIPReceiver.

Last updated