The minimal code needed to send and receive CCIP Messages
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
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.
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 Foundry installed. To check, run the following command:
Create a new folder and name it ccip-masterclass
Navigate to it
Create a hew Foundry project by running:
Navigate to https://remix.ethereum.org/ and click the "Create new Workspace" button.
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.
Option 1)
We cannot use git submodules to install @chainlink/contracts-ccip
because the content of this package is not available as a separate GitHub repo. This essentially means that we cannot run a forge install
command.
Here's the workaround:
Add the following line to the .gitignore
file
Then run the following command in your Terminal:
Finally, add the following lines to the foundry.toml
file:
Option 2) [October 2023 UPDATE]
You can run
And after that set remappings in your foundry.toml
or remappings.txt
files to
Create a new Solidity file, and paste the following content. It is an empty contract that just imports one of the contracts from the @chainlink/contracts-ccip
package.
Compile it. If compiled successfully and new .deps/npm/@chainlink/contracts-ccip
folders are generated, that means we imported the @chainlink/contracts-ccip
package into the Remix IDE Workspace.
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.
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.
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: