Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Chainlink SmartCon is the convergence point for the worlds of blockchain and finance, where digital asset leaders, DeFi pioneers, and onchain builders come together to invent the future of global markets and Web3.
Build alongside onchain innovators developing real-world applications that scale blockchain tech to billions of users worldwide.
Get cutting-edge insights from global financial institutions, premier DeFi protocols, and game-changing startups.
Eric Schmidt, former CEO, Google
Jonathan Ehrenfeld Solé, Strategy Director, SWIFT
Balaji Srinivasan, Angel Investor and Former CTO, Coinbase
Laurence Moroney, AI Lead, Google
Joseph Chan, Under Secretary for Financial Services and The Treasury, Hong Kong SAR
Oliver Roucloux, Business Analyst & Product Owner, Euroclear
Rashmi Misra, General Manager AI & Emerging Technologies Business Development, Microsoft
Please make sure you have completed these steps ahead of Day 1:
GitHub profile (required for test tokens, and homework exercises)
if you have difficulties please use Bootcamp Faucet
password: BigMac777
If you have any questions, please ask them on discord: https://discord.gg/m9vSfK65
Transporter is a highly secure interface for bridging tokens across blockchains with total peace of mind. Transporter is built on top of Chainlink CCIP, the industry-leading cross-chain protocol for defense-in-depth security. Transporter is the cross-chain bridging app for transferring digital assets across blockchains and experiencing the best that each chain has to offer. In just a few clicks, you can bridge your tokens and cross chains with confidence knowing that Transporter is underpinned by Chainlink CCIP’s Level-5 Security, offers users real-time transaction tracking, and is available to provide 24/7 support.
Video tutorial: https://www.youtube.com/watch?v=dCDQ-xetzpM
Navigate to https://test.transporter.io/
Connect your wallet by clicking the "Connect wallet button"
Select your Source and Destination Network of choice. I am going to send tokens from Avalanche Fuji to Ethereum Sepolia.
Then select Token you want to send (we can also send Messages). I am going to use USDC.
If you need tesnet USDC, you can get 10 units from the https://faucet.circle.com/
Then select the amount of tokens you want to send. I am going to choose 1 USDC.
And approve Transporter to transfer that amount of tokens on behalf of you, by clicking the "Approve USDC" button.
The next step is to select token you would like to use to pay for CCIP fees. By default the native coin of the source blockchain is selected (AVAX), but I am going to use LINK because it is cheaper.
Finally, initiate the transfer by clicking the "Send" button.
You can now sit back, relax and monitor your first cross-chain transfer in real time!
Once your transaction is completed on the source blockchain (Avalanche Fuji in this example) you can monitor it on Chainlink CCIP Block Explorer as well - at https://ccip.chain.link.
Bookmark this URL, because we are going to use it a lot during this bootcamp.
To navigate to the Chainlink CCIP Block Explorer click the "View transaction" button.
Once the transaction on the source blockchain is finalized, Chainlink DONs can proceed with the cross-chain transfers. Finality is really important security concept in blockchain technology, so make sure you familiarize yourself with it by the end of this bootcamp.
Finally, we just need to wait for a destination transaction to be included in the next Ethereum Sepolia block.
And that's it! Transport is completed, simple as that!
You can of course always come back to Chainlink CCIP Block Explorer to see more details about the transfer.
Chainlink’s tokenization bootcamp offers an exciting journey for Web3 developers, elevating your skills in bringing real-world assets onchain using Chainlink's cutting-edge tokenization infrastructure.
Secure Your Spot Before Anyone Else. September 30 - October 2, 2024
👉
Join us for a three-day tokenization adventure, where you’ll tokenize a house onchain. You’ll learn to enhance it with diverse data points, securely transfer it across chains, and unlock its potential beyond the capabilities of traditional systems.
What You’ll Learn:
Steps to create and enrich tokenized assets
Key stages of onchain development for tokenized assets
Strategies to maximize the potential of tokenized assets
Powering a Unified Golden Record for tokenized assets
Warmest welcome to the first-ever CCIP Bootcamp! Designed for established Web3 developer, the CCIP Bootcamp is meant to elevate your cross-chain development skills to the next level.
X (Twitter):
Email:
X (Twitter):
LinkedIn:
👉
👉
👉
Another best practice that we will implement is so called Defensive Example, the pattern which allows us to reprocess failed messages without forcing the original transaction to fail. Let's explain how it works.
Receiving and processing messages
Upon receiving a message on the destination blockchain, the ccipReceive
function is called by the CCIP Router. This function serves as the entry point to the contract for processing incoming CCIP messages, enforcing crucial security checks through the onlyRouter
, and onlyAllowlisted
modifiers.
Here's the step-by-step breakdown of the process:
Entrance through ccipReceive
:
The ccipReceive
function is invoked with an Any2EVMMessage
struct containing the message to be processed.
Security checks ensure the call is from the authorized router, an allowlisted source chain, and an allowlisted sender.
Processing Message:
ccipReceive
calls the processMessage
function, which is external to leverage Solidity's try/catch error handling mechanism. Note: The onlySelf
modifier ensures that only the contract can call this function.
Inside processMessage
, a check is performed for a simulated revert condition using the s_simRevert
state variable. This simulation is toggled by the setSimRevert
function, callable only by the contract owner.
If s_simRevert
is false, processMessage
calls the _ccipReceive
function for further message processing.
Message Processing in _ccipReceive
:
_ccipReceive
extracts and stores various information from the message, such as the messageId
, decoded sender
address, token amounts, and data.
It then emits a MessageReceived
event, signaling the successful processing of the message.
Error Handling:
If an error occurs during the processing (or a simulated revert is triggered), the catch block within ccipReceive
is executed.
The messageId
of the failed message is added to s_failedMessages
, and the message content is stored in s_messageContents
.
A MessageFailed
event is emitted, which allows for later identification and reprocessing of failed messages.
Reprocessing of failed messages
The retryFailedMessage
function provides a mechanism to recover assets if a CCIP message processing fails. It's specifically designed to handle scenarios where message data issues prevent entire processing yet allow for token recovery:
Initiation:
Only the contract owner can call this function, providing the messageId
of the failed message and the tokenReceiver
address for token recovery.
Validation:
It checks if the message has failed using s_failedMessages.get(messageId)
. If not, it reverts the transaction.
Status Update:
The error code for the message is updated to RESOLVED
to prevent reentry and multiple retries.
Token Recovery:
Retrieves the failed message content using s_messageContents[messageId]
.
Transfers the locked tokens associated with the failed message to the specified tokenReceiver
as an escape hatch without processing the entire message again.
Event Emission:
An event MessageRecovered
is emitted to signal the successful recovery of the tokens.
This function showcases a graceful asset recovery solution, protecting user values even when message processing encounters issues.
Transfer Tokens With Data - Defensive Example
This tutorial extends the programmable token transfers example. It uses Chainlink CCIP to transfer tokens and arbitrary data between smart contracts on different blockchains, and focuses on defensive coding in the receiver contract. In the event of a specified error during the CCIP message reception, the contract locks the tokens. Locking the tokens allows the owner to recover and redirect them as needed. Defensive coding is crucial as it enables the recovery of locked tokens and ensures the protection of your users' assets.
Before You Begin
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.
Your account must have some AVAX and LINK tokens on Avalanche Fuji and ETH tokens on Ethereum Sepolia. Learn how to Acquire testnet LINK.
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 Avalanche Fuji to Ethereum Sepolia so check the list of supported tokens here.
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.
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.
Follow the previous tutorial: Transfer Tokens with Data to learn how to make programmable token transfers using CCIP.
Coding time!
In this exercise, we'll initiate a transaction from a smart contract on Avalanche Fuji, sending a string text and CCIP-BnM tokens to another smart contract on Ethereum Sepolia using CCIP. However, a deliberate failure in the processing logic will occur upon reaching the receiver contract. This tutorial will demonstrate a graceful error-handling approach, allowing the contract owner to recover the locked tokens.
CORRECTLY ESTIMATE YOUR GAS LIMIT
It is crucial to thoroughly test all scenarios to accurately estimate the required gas limit, including for failure scenarios. Be aware that the gas used to execute the error-handling logic for failure scenarios may be higher than that for successful scenarios.
To use this contract:
Compile your contract.
Deploy, fund your sender contract on Avalanche Fuji and enable sending messages to Ethereum Sepolia:
Open MetaMask and select the network Avalanche Fuji.
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 Avalanche Fuji.
Click the transact button. After you confirm the transaction, the contract address appears on the Deployed Contracts list. Note your contract address.
Enable your contract to send CCIP messages to Ethereum Sepolia:
In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Avalanche Fuji.
Deploy your receiver contract on Ethereum Sepolia and enable receiving messages from your sender contract:
Open MetaMask and select the network Ethereum Sepolia.
In Remix IDE, under Deploy & Run Transactions, make sure the environment is still Injected Provider - MetaMask.
Click the transact button. After you confirm the transaction, the contract address appears on the Deployed Contracts list. Note your contract address.
Enable your contract to receive CCIP messages from Avalanche Fuji:
In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Ethereum Sepolia.
Enable your contract to receive CCIP messages from the contract that you deployed on Avalanche Fuji:
In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Ethereum Sepolia.
Call the setSimRevert
function, passing true
as a parameter, then wait for the transaction to confirm. Setting s_simRevert
to true simulates a failure when processing the received message. Read the explanation section for more details.
At this point, you have one sender contract on Avalanche Fuji and one receiver contract on Ethereum Sepolia. As security measures, you enabled the sender contract to send CCIP messages to Ethereum Sepolia and the receiver contract to receive CCIP messages from the sender on Avalanche Fuji. The receiver contract cannot process the message, and therefore, instead of throwing an exception, it will lock the received tokens, enabling the owner to recover them.
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.
Send a string data with tokens from Avalanche Fuji:
Open MetaMask and select the network Avalanche Fuji.
In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Avalanche Fuji.
Fill in the arguments of the sendMessagePayLINK function:
Click on transact
and confirm the transaction on MetaMask.
After the transaction is successful, record the transaction hash. Here is an example of a transaction on Avalanche Fuji.
NOTE
During gas price spikes, your transaction might fail, requiring more than 0.5 LINK to proceed. If your transaction fails, fund your contract with more LINK tokens and try again.
Open the CCIP explorer and search your cross-chain transaction using the transaction hash.
The CCIP transaction is completed once the status is marked as "Success". In this example, the CCIP message ID is 0x120367995ef71f83d64a05bd7793862afda9d04049da4cb32851934490d03ae4.
Check the receiver contract on the destination chain:
Open MetaMask and select the network Ethereum Sepolia.
In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Ethereum Sepolia.
Notice the returned values are: 0x120367995ef71f83d64a05bd7793862afda9d04049da4cb32851934490d03ae4 (the message ID) and 1 (the error code indicating failure).
To recover the locked tokens, call the retryFailedMessage
function:
After confirming the transaction, you can open it in a block explorer. Notice that the locked funds were transferred to the tokenReceiver
address.
Note: These example contracts are designed to work bi-directionally. As an exercise, you can use them to transfer tokens with data from Avalanche Fuji to Ethereum Sepolia and from Ethereum Sepolia back to Avalanche Fuji.
The smart contract featured in this tutorial is designed to interact with CCIP to transfer and receive tokens and data. The contract code is similar to the Transfer Tokens with Data tutorial. Hence, you can refer to its code explanation. We will only explain the main differences.
The sendMessagePayLINK
function is similar to the sendMessagePayLINK
function in the Transfer Tokens with Data tutorial. The main difference is the increased gas limit to account for the additional gas required to process the error-handling logic.
Upon receiving a message on the destination blockchain, the ccipReceive
function is called by the CCIP router. This function serves as the entry point to the contract for processing incoming CCIP messages, enforcing crucial security checks through the onlyRouter
, and onlyAllowlisted
modifiers.
Here's the step-by-step breakdown of the process:
Entrance through ccipReceive
:
The ccipReceive
function is invoked with an Any2EVMMessage
struct containing the message to be processed.
Security checks ensure the call is from the authorized router, an allowlisted source chain, and an allowlisted sender.
Processing Message:
ccipReceive
calls the processMessage
function, which is external to leverage Solidity's try/catch error handling mechanism. Note: The onlySelf
modifier ensures that only the contract can call this function.
Inside processMessage
, a check is performed for a simulated revert condition using the s_simRevert
state variable. This simulation is toggled by the setSimRevert
function, callable only by the contract owner.
If s_simRevert
is false, processMessage
calls the _ccipReceive
function for further message processing.
Message Processing in _ccipReceive
:
_ccipReceive
extracts and stores various information from the message, such as the messageId
, decoded sender
address, token amounts, and data.
It then emits a MessageReceived
event, signaling the successful processing of the message.
Error Handling:
If an error occurs during the processing (or a simulated revert is triggered), the catch block within ccipReceive
is executed.
The messageId
of the failed message is added to s_failedMessages
, and the message content is stored in s_messageContents
.
A MessageFailed
event is emitted, which allows for later identification and reprocessing of failed messages.
The retryFailedMessage
function provides a mechanism to recover assets if a CCIP message processing fails. It's specifically designed to handle scenarios where message data issues prevent entire processing yet allow for token recovery:
Initiation:
Only the contract owner can call this function, providing the messageId
of the failed message and the tokenReceiver
address for token recovery.
Validation:
It checks if the message has failed using s_failedMessages.get(messageId)
. If not, it reverts the transaction.
Status Update:
The error code for the message is updated to RESOLVED
to prevent reentry and multiple retries.
Token Recovery:
Retrieves the failed message content using s_messageContents[messageId]
.
Transfers the locked tokens associated with the failed message to the specified tokenReceiver
as an escape hatch without processing the entire message again.
Event Emission:
An event MessageRecovered
is emitted to signal the successful recovery of the tokens.
This function showcases a graceful asset recovery solution, protecting user values even when message processing encounters issues.
General overview of the Chainlink CCIP
Interoperability is the ability to exchange information between different systems or networks, even if they are incompatible. Shared concepts on different networks ensure that each party understands and trusts the exchanged information. It also considers the concept of finality to establish trust in the exchanged information by validating its accuracy and integrity.
The Web3 ecosystem has become multi-chain, with the rise of layer-1 blockchains and layer-2 scaling solutions like appchains, subnets, and more, where each network has its own approach to scalability, security, and trust.
However, blockchains are isolated networks that operate independently and cannot communicate natively. To create a truly interoperable Web3 ecosystem, data and value must move seamlessly between chains. This is where bridges come in.
Traditional bridges are one of the biggest problems in today's Web3 ecosystem because they are centralized. When you are transferring funds from one chain to another using bridge, you are essentially giving your funds to some centralized entity, and you are trusting them that the funds will appear on the other side. And surprise, surprise, numerous bridge hacks have happened so far.
The Chainlink Cross-Chain Interoperability Protocol (CCIP) provides a single simple interface through which dApps and web3 entrepreneurs can securely meet all their cross-chain needs, including token transfers and arbitrary messaging.
Chainlink CCIP connects blockchain networks via lanes. The lane is a unique combination of source blockchain to destination blockchain path, e.g., from blockchain A to blockchain C. To transfer messages in reverse order, from blockchain C to blockchain A using Chianlink CCIP, you will need to use a different lane, the one which is unique to the C -> A path.
Chainlink CCIP will always support bi-directional lanes for each new chain added in the future. The logical question is how CCIP knows through which lane to transfer a CCIP cross-chain message. It's actually quite simple - each blockchain supported by CCIP has a unique chain selector.
We said multiple times that by using Chainlink CCIP, you can send cross-chain messages. But what can a cross-chain message consist of? With Chainlink CCIP, one can:
Transfer (supported) tokens
Send any kind of data
Send both tokens and data
CCIP sender can be:
EOA
Any smart contract
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.
The Router is the primary contract CCIP users interface with. This contract is responsible for initiating cross-chain interactions. One router contact exists per chain. When transferring tokens, callers have to approve the router contract to be able to "spend" (transfer) the caller's tokens. When a message is received on the destination chain, the router is the contract that “delivers” tokens to the user's account or the message to the receiver's smart contract.
The Chainlink Cross-Chain Interoperability Protocol provides a single interface to transfer tokens and data across multiple chains in a secure and decentralized manner.
CCIP Programmable Token Transfers enable cross-chain swap use cases, where any token can be effectively bridged over CCIP by connecting to liquidity pools / DEXs on the source and destination chains.
For example, a cross-chain swap app built on CCIP enables users holding Token A on Arbitrum to be swapped for Token B on Optimism by first swapping Token A on Arbitrum for USDC, bridging the USDC to Optimism along with data about the swap, and then automatically swapping the USDC to Token B and sending it to the user’s wallet. This is why CCIP’s support for native USDC is so powerful; it doesn’t just support the cross-chain transfer of native USDC via burn and mint, but also the simultaneous transmission of data on what to do with the USDC once it arrives on the destination chain—a unique feature of CCIP Programmable Token Transfers.
XSwap is a cross-chain swaps protocol and BUILD participant that uses CCIP for Programmable Token Transfers to enable cross-chain swaps between blockchain networks. USDC is used as the liquidity token for XSwap. Since its launch, XSwap users have initiated over $130M in CCIP Programmable Token Transfers.
Other users of CCIP Programmable Token Transfers include Transporter, ChainSwap, WEMIX PLAY, Amino Rewards, and more.
CCIP Programmable Token Transfers unlock innovation for cross-chain staking and restaking. End-users can stake/restake assets directly from a layer-2 network, where CCIP is used to transfer the native assets back to the layer-1 blockchain chain along with instructions to (re)stake the asset in a specified (re)staking protocol. This reduces gas costs for users and provides them the convenience to (re)stake from any chain.
For example, EigenPie is integrating Chainlink CCIP to enable its users to deposit ETH directly into their layer-2 contracts in order to receive the corresponding LRT (egETH) without ever having to leave the chain. Once users deposit ETH into the layer-2 contract, CCIP’s Programmable Token Transfers will bridge the tokens to Ethereum with instructions to restake them into Eigenlayer. CCIP is then used to lock the minted egETH on Ethereum and bridge it back to the L2 where it is minted and sent to the end user’s wallet address.
You can read more about Chainlink’s support for staking and restaking in the blog: How The Chainlink Platform Unlocks LST and LRT Adoption in DeFi. Learn how to implement Chainlink CCIP Programmable Token Transfers in the CCIP Masterclass: Cross-Chain Staking Edition for a more technical deep dive.
CCIP Programmable Token Transfers are critical to enabling cross-chain Delivery vs. Payment (DvP) transactions. DvP in traditional finance refers to the requirement that the delivery of assets (e.g., securities) and the payment for those assets happen simultaneously (i.e., atomic settlement). DvP is an important feature in reducing the risk that a counterparty won’t deliver on its leg of the transaction despite the other leg being fulfilled.
The Australia and New Zealand Banking Group Limited (ANZ) demonstrated how CCIP Programmable Token Transfers can enable a cross-border, cross-chain, cross-currency DvP transaction. In a single cross-chain transaction, a stablecoin backed by a local currency (NZ$DC) was converted to another stablecoin in a different national currency (A$DC), transferred from the buyer’s source chain to the seller’s destination chain along with the instruction to purchase a tokenized asset (e.g., reef credits), which was subsequently sent back to the customer’s wallet on the source chain.
To learn more, check out the case study Cross-Chain Settlement of Tokenized Assets Using CCIP written in collaboration with ANZ and the Sibos panel discussion between Chainlink Co-Founder Sergey Nazarov and ANZ’s Banking Services Lead Nigel Dobson.
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:
The Chainlink Cross-Chain Interoperability Protocol (CCIP) is far more than a simple token bridging solution. It’s a generalized cross-chain messaging protocol for transferring tokens (value), messages (data), or both tokens and messages simultaneously within a single cross-chain transaction—referred to as Programmable Token Transfers.
In effect, CCIP Programmable Token Transfers enable smart contracts to transfer tokens cross-chain along with instructions on what the receiving smart contract should do with those tokens once they arrive on the destination chain. This revolutionary concept of wrapping value and instructions together allows tokenized value to interact automatically and dynamically once it arrives at a destination, opening up a world of new possibilities.
In decentralized finance (DeFi), CCIP Programmable Token Transfers enable the creation of cross-chain native dApps, such as a smart contract that can automatically transfer tokens cross-chain and deposit them into the highest yield lending markets. Within traditional finance (TradFi), CCIP Programmable Token Transfers enable advanced use cases, such as a cross-chain delivery-vs-payment (DvP) transaction where an institution holding stablecoins on its private blockchain can purchase a tokenized asset issued on a different private or public chain.
Importantly, CCIP Programmable Token Transfers enable institutions to interact with smart contracts and tokenized assets on other blockchain networks without needing to integrate or directly interact with that blockchain. All they need to do is send instructions to CCIP on how to interact with that chain, greatly reducing their overhead and the risks associated with point-to-point integrations with each blockchain network.
Just as TCP/IP is a universal standard that underpins the Internet, Chainlink CCIP serves as a universal standard that underpins the Internet of Contracts. To support all the various cross-chain use cases that exist within DeFi and TradFi, CCIP allows for a variety of ways to transfer data and/or value across blockchains.
CCIP’s support for Arbitrary Messaging enables developers to transfer any arbitrary data (encoded as bytes) across blockchain networks. Developers utilize CCIP’s Arbitrary Messaging to make their smart contract applications cross-chain native.
With CCIP, smart contracts on a source chain can call any arbitrary function on any arbitrary smart contract on a destination chain to trigger any arbitrary action (and receive a callback on the source chain if needed). Developers can encode multiple instructions in a single message, empowering them to orchestrate complex, multi-step, multi-chain tasks.
CCIP Token Transfers enable the transfer of tokens between chains via highly audited and security-reviewed token pool contracts. Transactions can be initiated directly by externally owned accounts (EOAs), such as from user wallets via a bridging app like Transporter, or directly by a smart contract. Tokens can then be sent to an EOA or to a smart contract.
In order to ensure the highest level of security and a superior user experience, token issuers can use CCIP directly within their token’s smart contract to make it a native cross-chain token. As a result, any user or developer can use CCIP to transfer the official (canonical) version of that issuer’s token cross-chain. Various layer-1 blockchain and layer-2 rollups such as Wemix and Metis built upon this concept by integrating CCIP as their official cross-chain infrastructure to power their canonical token bridges. Every token transferred onto those blockchain networks via CCIP is the canonical representation of that token on that chain.
There are three primary ways developers can integrate CCIP for token transfers:
Burn and mint—Tokens are burned on a source chain, and an equivalent amount is minted on a destination chain. This enables the creation of cross-chain native tokens with a dynamic, unified supply across chains. CCIP supports Circle’s USDC via the burn and mint token transfer method.
Lock and mint—Tokens are locked on the chain they were natively issued on, and fully collateralized “wrapped” tokens are minted on destination chains. These wrapped tokens can be transferred across other non-native destination chains via burn and mint or be burned to unlock tokens back on the original issuing source chain. Truflation’s TRUF token utilizes lock and mint for its token transfers on CCIP.
Lock and unlock—Tokens are locked on a source blockchain, and an equivalent amount of tokens are released on the destination blockchain. This enables the support of tokens without a burn/mint function or tokens that would introduce challenges if wrapped, such as native blockchain gas tokens. CCIP supports native ETH transfers via the lock and unlock token transfer method.
Programmable Token Transfers combine Token Transfers with Arbitrary Messaging. This enables developers to transfer both tokens (value) and instructions (data) about what to do with those tokens cross-chain within a single transaction. Importantly, Programmable Token Transfers are built natively into CCIP to give users the best possible security, reliability, UX (e.g., composability), and risk management.
How to design a cross-chain NFT smart contract
A cross-chain NFT is a smart contract that can exist on any blockchain, abstracting away the need for users to understand which blockchain they’re using.
Typically, NFT movements from one chain to another are eye-catching events, but this simple fact brings up a larger question of the reliability and security of Web3’s middleware infrastructure. In a seamless cross-chain world, moving digital assets from one chain to another should be as normal as submitting a transaction on the same blockchain.
When an NFT moves from one blockchain to another, it becomes a cross-chain NFT.
At a high level, an NFT is a digital token on a blockchain with a unique identifier different from any other token on the chain.
Any NFT is implemented by a smart contract that is intrinsically connected to a single blockchain. The smart contract is arguably the most important part of this equation because it controls the NFT implementation: How many are minted, when, what conditions need to be met to distribute them, and more. This means that any cross-chain NFT implementation requires at least two smart contracts on two blockchains and interconnection between them.
This is what a cross-chain NFT looks like - equivalent NFTs that exist across multiple blockchains.
With this in mind, cross-chain NFTs can be implemented in three ways:
Burn-and-mint: An NFT owner puts their NFT into a smart contract on the source chain and burns it, in effect removing it from that blockchain. Once this is done, an equivalent NFT is created on the destination blockchain from its corresponding smart contract. This process can occur in both directions.
Lock-and-mint: An NFT owner locks their NFT into a smart contract on the source chain, and an equivalent NFT is created on the destination blockchain. When the owner wants to move their NFT back, they burn the NFT and it unlocks the NFT on the original blockchain.
Lock and unlock: The same NFT collection is minted on multiple blockchains. An NFT owner can lock their NFT on a source blockchain to unlock the equivalent NFT on a destination blockchain. This means only a single NFT can actively be used at any point in time, even if there are multiple instances of that NFT across blockchains.
At this Masterclass, we are going to implement the Burn-and-Mint mechanism
In each scenario, a cross-chain messaging protocol in the middle is necessary to send data instructions from one blockchain to another.
To build Cross-Chain NFTs with Chainlink CCIP, let's first understand what one can do with Chainlink CCIP. With Chainlink CCIP, one can:
Transfer (supported) tokens
Send any kind of data
Send both tokens and data
CCIP sender can be:
EOA
Any smart contract
CCIP receiver can be:
EOA
Any smart contract that implements CCIPReceiver.sol
To implement Burn-and-Mint model using Chainlink CCIP, we will on cross-chain transfer function burn an NFT on the source blockchain (Arbitrum Sepolia) and send the cross-chain message using Chainlink CCIP. We will need to encode to
and from
addresses, NFT's tokenId
and tokenURI
so we can mint exactly the same NFT on the destination blockchain once it receives a cross-chain message.
The Arbitrum Sepolia side:
The Ethereum Sepolia side:
This design allows the cross-chain transfer and vice-versa, from Ethereum Sepolia back to Arbitrum Sepolia using the exact same codebase and Burn-and-Mint mechanism.
For this Cross-Chain NFT we will use Four Chainlink Warriors hosted on IPFS, as a Metadata.
For this exercise we will try to follow some of the CCIP Best Practices. For the full list you should always refer to the Chainlink Official Documentation.
It's crucial for this exercise that sending cross-chain messages is between Cross-Chain NFT smart contracts. To accomplish that, we need to track a record of these addresses on different blockchains using their CCIP chain selectors.
When you implement the ccipReceive
method in the contract residing on the destination chain, validate that the msg.sender
is the correct Router address. This verification ensures that only the Router contract can call the ccipReceive
function on the receiver contract and is for developers that want to restrict which accounts are allowed to call ccipReceive
.
gasLimit
The gasLimit
specifies the maximum amount of gas CCIP can consume to execute ccipReceive()
on the contract located on the destination blockchain. It is the main factor in determining the fee to send a message. Unspent gas is not refunded.
extraArgs
The purpose of extraArgs
is to allow compatibility with future CCIP upgrades. To get this benefit, make sure that extraArgs
is mutable in production deployments. 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.
If extraArgs
are left empty, a default of 200000 gasLimit
will be set.
To make extraArgs
mutable, set them as described previously in the enableChain
function. To calculate which bytes
value to pass, you can create a helper script like this:
We will now see what the workflow of a CCIP Message looks like in a couple of steps.
The Sender prepares a CCIP Message (EVM2AnyMessage
) for their cross-chain transaction to a destination blockchain (chainSelector
) of choice. A CCIP message includes the following information:
Receiver
Data payload
Tokens and amounts
Fee token
Additional parameters (gasLimit
)
The Sender calls Router.getFee()
to receive the total fees (gas + premium) to pay CCIP and approves the requested fee amount.
The Sender calls Router.ccipSend()
, providing the CCIP Message they want to send along with their desired destination chainSelector
. In the case of token transfers, the amount to be transferred must be approved to the Router.
The Router validates the received Message (e.g., valid and supported destination chainId
and supported tokens at the destination chain).
The Router receives and transfers fees to the OnRamp
.
The Router receives and transfers tokens to its corresponding Token Pool. If the sender has not approved the tokens to the Router, this will fail.
The Router forwards the Message to the correct OnRamp
(based on destination chainSelector
) for processing:
Validate the message (number of tokens, gasLimit
, data length …)
[For token transfers] Ensure that the transferred value does not hit the aggregate rate limit of the lane.
Sequence the message with a sequence number.
For each Token included in the Message: instruct the token pool to lock/burn the tokens. This will also validate the token pool rate limit for this lane.
The OnRamp
emits an event containing the sequenced message. This triggers the DONs to process the message.
A messageId
is generated and returned to the Sender.
Nodes in the Committing DON listen for events of Messages that are ready to be sent
Messages must be finalized to be considered secure against reorg attacks.
Triggered by time or number of messages queued in the OnRamp
, the Committing DON creates a Report with a commitment of all messages ready to be sent, in a batch. This commitment takes the form of a Merkle Root
Upon consensus in the Committing DON, the Report containing the Merkle Root is transmitted to the CommitStore
contract on the destination chain
The Risk Management Network “blesses” the Merkle Root in the CommitStore
, to make sure it is a correct representation of the queued messages at the OnRamp
.
Merkle Root: the root hash of the Merkle Tree. It is a commitment to all the leaves (Messages M1
-M4
) in the tree. Each node in the tree is the hash of the nodes below it.
Merkle Proof: to prove that Message M1 is included in the Merkle Root (commitment), a Prover provides the following elements as proof to a Verifier (who only has the root hash):
M1
H(M2)
H(H(M3),H(M4))
Using this Merkle Proof, a Verifier can easily verify that, indeed, M1
is included in the commitment (root hash) that it possesses.
Nodes in the Executing DON listen for events of Messages that are ready to be sent, similar to the Committing DON
Messages must be finalized to be considered secure against reorg attacks.
In addition to the time or number of messages queued in the OnRamp
, the Executing DON also monitors the CommitStore
to make sure the messages are ready to be executed at the destination chain, i.e., are the messages included in a blessed on-chain commitment?
If conditions are met, the Executing DON creates a Report with all messages ready to be sent, in a batch. It accounts for the gasLimit
of each message in its batching logic. It also calculates a relevant Merkle Proof for each message to prove that the message is included in the Merkle Root submitted by the Committing DON in the CommitStore
. Note that Executing DON batches can be any subset of a Committing DON batch.
Upon consensus, the Report is transmitted to the OffRamp
contract on the destination chain.
For each message in the batch received, the OffRamp
verifies using the provided Merkle Proof whether the transaction is included in the blessed commitment in the CommitStore
.
If tokens are included in the transaction, the OffRamp
validates the aggregate rate limit of the lane and identifies the matching Token Pool(s).
OffRamp
calls the TokenPool
’s unlock
/mint
function. This will validate the token pool rate limit, unlock or mint the token and transfer them to the specified receiver.
If the receiver is not an EOA and has the correct interface implemented, the OffRamp
uses the Router to call the receiver’s ccipReceive()
function
The receiver processes the message and is informed where the message comes from (blockchain + sender), the tokens transferred, and the data payload (with relevant instructions).
Based on the data payload, the receiver might transfer the tokens to the final recipient (end-user)
Now, do you need to know in details how the processing of CCIP Message works? Absolutely not, you just need to send the cross-chain Message by interacting with the Router contract and Chainlink CCIP will deliver it, the same as when sending a package you just drop it at the Post Office, mark to whom you are sending it and pay for fees.
Just as international travel involves various checkpoints and procedures to ensure a safe and verified journey from one country to another, Chainlink CCIP ensures secure and verified communication between different blockchain networks.
Pre-Boarding at JFK Airport: The Committing Phase
Before any plane takes off, a series of rigorous checks are in place—mirroring the initiating phase of CCIP. Here, the Router acts as our check-in desk, where users present their cross-chain requests, akin to passengers confirming their flight details.
Travelers check in their luggage, which is then handled by the airport staff. As with any luggage on a long-haul flight, the cross-chain message undergoes a meticulous security protocol. Similarly, in CCIP, the OnRamp contract checks the validity of the destination blockchain address, message size, gas limits, and sequence numbers, ensuring that the 'luggage' (or data payload) is prepared correctly for its journey.
Passport and Visa verification are much comparable to the Commit Store smart contract, which stores the Merkle root of finalized messages and ensures they are 'blessed' by the Risk Management Network for authenticity and security before being executed on the destination chain.
In-Flight: The Cross-Chain Transit
During the flight, passengers and luggage are in transit, similar to the cross-chain message being propagated across networks. The Executing DON works behind the scenes, analogous to the flight crew, ensuring the smooth passage of the cross-chain interaction.
As we know, the duration of a flight can vary - flying east might chase the sun, shortening our perceived day, while westward travel extends it. Similarly, the delivery time of a cross-chain message depends on the source blockchain finality. We remember from the CCIP Masterclass #1 that finality refers to the state of irreversibility and permanent record of a transaction on the blockchain.
Touchdown at Hong Kong Airport: The Executing Phase
On arrival, the OffRamp contract ensures the message is authentic, verifying the proof against the committed and blessed Merkle root.
Similarly to the final checks by the Risk Management Network, behind the scenes (unseen by the passenger) the suitcase or our cross-chain message undergoes various security checks, ensuring that the message adheres to the rules and regulations of the destination blockchain.
Finally, the Router on the destination chain assumes the role of border control, delivering the message data and/or tokens to the receiver's address, akin to a traveler receiving their stamped passport and entering a new country.
This entire process, while complex, remains largely invisible to the end-user, who simply experiences the seamless transfer of their digital assets from one blockchain to another. From the moment of check-in to the joy of retrieval, the journey of a cross-chain message through CCIP is a marvel of modern cryptography, ensuring that what we entrust to the network is what we receive.
Please note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.
Please answer these 15 questions by the end of the bootcamp.
Submit your answers via
Easy:
What is CCIP Lane?
What is CCIP Chain Selector? How does it differ from Chain ID?
What is gasLimit in CCIP Messages used for?
How can one monitor CCIP Messages in real time?
What are the three main capabilities of Chainlink CCIP? Provide examples of potential use cases leveraging these capabilities.
Medium:
Detail the security best practices for verifying the integrity of incoming CCIP messages in your smart contracts. What specific checks should be implemented in the ccipReceive function, and why are these verifications crucial for the security of cross-chain dApps
Which token transfer mechanisms are supported by Chainlink CCIP?
Describe the role of the Risk Management Network in Chainlink CCIP and explain the process of "blessing" and "cursing".
Discuss the significance of the "finality" concept in the context of Chainlink CCIP. How does the finality of a source chain impact the end-to-end transaction time in CCIP?
Discuss the best practices for setting the gasLimit in CCIP messages. How can developers accurately estimate and optimize the gas limit to ensure reliable execution of cross-chain transactions?
Hard:
Explain the DefensiveExample Pattern and how to handle CCIP message failures gracefully.
List and explain the scenarios that would trigger the need for manual execution using the Chainlink CCIP Explorer.
Explain why it is a best practice to make the extraArgs mutable in Chainlink CCIP and describe how a developer can implement this in their smart contract.
What are CCIP Rate Limits? What considerations should developers keep in mind when designing applications to operate within these limits?
What is going to happen if you send arbitrary data alongside tokens to an Externally Owned Account using Chainlink CCIP?
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 Avalanche Fuji, the router address is 0xF694E193200268f9a4868e4Aa017A0118C9a8177
and the LINK contract address is 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846
.
Open MetaMask and fund your contract with CCIP-BnM tokens. You can transfer 0.002
CCIP-BnM to your contract.
Call the allowlistDestinationChain
with 16015286601757825753
as the destination chain selector, and true
as allowed. Each chain selector is found on the supported networks page.
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 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
and the LINK contract address is 0x779877A7B0D9E8603169DdbD7836e478b4624789
.
Call the allowlistSourceChain
with 14767482510784806043
as the source chain selector, and true
as allowed. Each chain selector is found on the supported networks page.
Call the allowlistSender
with the contract address of the contract that you deployed on Avalanche Fuji, and true
as allowed.
Open MetaMask and connect to Avalanche Fuji. Fund your contract with LINK tokens. You can transfer 0.5
LINK to your contract. In this example, LINK is used to pay the CCIP fees.
Argument | Value and Description |
---|---|
Call the getFailedMessages
function with an offset of 0
and a limit of 1
to retrieve the first failed message.
Argument | Description |
---|---|
Call again the getFailedMessages
function with an offset of 0
and a limit of 1
to retrieve the first failed message. Notice that the error code is now 0, indicating that the message was resolved.
New Terms | Meaning |
---|---|
Chainlink Elf, available at:
Chainlink Knight, available at:
Chainlink Orc, available at:
Chainlink Witch, available at:
X (Twitter):
Interoperability
The ability to exchange information between different systems or networks, even if they are incompatible
Chainlink CCIP
The protocol that allows you to send tokens and arbitrary messages across different blockchains
Lane
The unique combination of source blockchain to destination blockchain path
Chain Selector
The unique identifier of a blockchain in Chainlink CCIP
CCIP Message
The message that you can send across blockchains through CCIP lane which can consist of tokens and arbitrary data
Sender
A smart contract (of the User) or an EOA that sends the CCIP Message
Source blockchain
The blockchain the CCIP Message is sent from
Receiver
A smart contract or an EOA that receives the CCIP Message
Destination blockchain
The blockchain the CCIP Message is sent to
messageId
The unique identifier of the failed message.
tokenReceiver
The address to which the tokens will be sent.
Please note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.
Submit your public GitHub repo for the exercise below via this form
Using chainlink-local write a test for the https://github.com/smartcontractkit/ccip-cross-chain-name-service project. You can use https://cll-devrel.gitbook.io/chainlink-local-documentation for help.
During the Exercise #3 we used Chainlink Local's Forked Mode with Foundry. For this homework you should use Chainlink Local's Local Mode with Hardhat.
In the test you must:
Create an instance of CCIPLocalSimulator.sol
smart contract.
Call the configuration()
function to get Router contract address.
Create instances of CrossChainNameServiceRegister.sol
, CrossChainNameServiceReceiver.sol
and CrossChainNameServiceLookup.sol
smart contracts and call the enableChain()
function where needed.
Call the setCrossChainNameServiceAddress
function of the CrossChainNameServiceLookup.sol
smart contract "source" instance and provide the address of the CrossChainNameServiceRegister.sol
smart contract instance. Repeat the process for the CrossChainNameServiceLookup.sol
smart contract "receiver" instance and provide the address of the CrossChainNameServiceReceiver.sol
smart contract instance.
Call the register()
function and provide “alice.ccns” and Alice’s EOA address as function arguments.
Call the lookup()
function and provide “alice.ccns” as a function argument. Assert that the returned address is Alice’s EOA address.
Send a url pointing to the public Github repository.
DO NOT PROVIDE PRIVATE KEY OR TESTNET RPC DETAILS, USE HARDHAT NETWORK ONLY! npx hardhat test --network hardhat
Please note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.
Submit your public GitHub repo for the exercise below via this form
Following the https://docs.chain.link/ccip/tutorials/ccipreceive-gaslimit guide measure the gas consumption of the ccipReceive function. Once you have the number, increase it by 10% and provide as gasLimit parameter of the transferUsdc function instead of the currently hard-coded 500.000
_destinationChainSelector
_receiver
Your receiver contract address at Ethereum Sepolia. The destination contract address.
_text
_token
_amount
You might noticed so far that building with CCIP directly on test networks is not ideal due to extra points of friction - like setting up a wallet, getting testnet tokens, waiting for cross-chain transactions to complete, etc. This is because test networks are for testing and local environments (like Foundry, Hardhat or Remix IDE) are for building and unit testing.
To address this issue, we created Chainlink Local - the Chainlink CCIP Local Simulator, and in this chapter you will learn how to simulate your cross-chain transactions locally and build with Chainlink CCIP 2000x quicker compared to working on test networks!
Chainlink Local is an installable dependency, like OpenZeppelin for example. It provides a tool (the Chainlink Local Simulator) that developers import into their Foundry or Hardhat or Remix IDE projects. This tool runs Chainlink CCIP locally which means developers can rapidly explore, prototype and iterate CCIP dApps off-chain in a local environment, and move to testnet only when they're ready to test in a live environment.
Most importantly, smart contracts tested with Chainlink Local can be deployed to test networks without any modifications (assuming network specific contract addresses such as Router contracts and LINK token addresses are passed in via a constructor).
To view more detailed documentation and more examples, visit the Chainlink Local Documentation and Chainlink Local YouTube Playlist.
The simulator supports two modes:
Local Mode - working with mock contracts on a locally running development blockchain node running on localhost
, and
Forked Mode - working with deployed Chainlink CCIP contracts using multiple forked networks.
In this example we will use Forked Mode.
For homework you must use Local Mode.
When working in local simulation mode, the simulator pre-deploys a set of smart contracts to a blank Hardhat/Anvil network EVM state and exposes their details via a call to the configuration()
function. Even though there are two Router contracts exposed, sourceRouter
and destinationRouter
, to support the developer's mental model of routing cross-chain messages through two different Routers, both are actually the same contract running on the locally development blockchain node.
When working in fork mode, you will need to create multiple locally running blockchain networks (you need an archive node that has historical network state in the pinned block from which you have forked for local development - see here) and interact with the contract addresses provided in the Official Chainlink Documentation.
The full example is available at: https://github.com/smartcontractkit/ccip-cross-chain-nft
To get started let's first create a new Foundry project by running:
If this command fails, make sure you have Foundry installed.
After that we will need to install the following dependencies. If you don't want to commit changes after each installment, append the --no-commit
flag to each of the following commands
@chainlink/contracts
@chainlink/contracts-ccip
@openzeppelin/contracts
@chainlink/local
And then set the following remappings in your foundry.toml
or remappings.txt
file:
Delete src/Counter.sol
, test/Counter.t.sol
and script/Counter.s.sol
files. Create new XNFT.sol
file in src
folder and paste the content of the XNFT smart contract from the previous page/exercise. Run forge build
to compile that contract. This should work if you installed dependencies properly.
Create a new file by copying the .env.example
file, and name it .env
. Fill in with Ethereum Sepolia and Arbitrum Sepolia RPC URLs using either local archive nodes or a service that provides archival data, like Infura or Alchemy.
Then add the rpc_endpoints
section to your foundry.toml
file. Its final version should look like this:
In the previous exercise, we performed 7 steps on two different test networks in order to deploy, mint and transfer XNFT from one account to another. Using Chainlink Local, you can write a test that will perform the same action, using exactly the same CCIP contracts from test networks, in your local environment, much faster.
Create a new test/XNFT.t.sol
file. Paste the following content:
If you try to compile this contract you should encounter in an error because there is no EncodeExtraArgs.sol
helper smart contract from the previous exercise. Fix that by creating a new file test/utils/EncodeExtraArgs.sol
and paste the following code.
Let's analyze the code so far. We've imported CCIPLocalSimulatorFork
from the @chainlink/local
package, meaning that we are using the Forked Mode. We created a new instance of it and made it persistent across all different forks so it can work properly.
For your homework however, you will need to import CCIPLocalSimulator
from the @chainlink/local
package.
We also created two forks (copies of the blockchain networks at the latest block - it is the best practice to pin this block number instead of always using the latest one) of Ethereum Sepolia and Arbitrum Sepolia test networks. We can now switch between multiple forks in Foundry, but we selected Ethereum Sepolia for start.
Fork ID is Foundry's way to manage different forked networks in a same test and those are usually numbers from 1, 2, 3... etc. Fork ID is not the same thing as block.chainid
- that's the actuall Chain ID of a blockchain network you can get in Solidity - 11155111 for Ethereum Sepolia, for example.
CCIPLocalSimulatorFork
in Foundry only, comes up with this Register
helper smart contract that contains NetworkDetails
struct which looks like this:
It is pre-populated with these details for some test networks and NO MAIN NETWORKS (on purpose) so it is the best practice to always validate these information - otherwise simulations won't work:
If there is no Network Details (for main networks for example) you must add those manually using the setNetworkDetails function.
We are going to perform Steps 1) and 2) from previous exercise directly in the setup()
function, like this:
And the rest of the steps from the previous exercise directly in a new test
function, like this:
Full Final code:
Coding Time 🎉
Arbitrum Sepolia | Ethereum Sepolia | |
---|---|---|
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-3
Navigate to it
Create a hew Foundry project by running:
Navigate to https://remix.ethereum.org/ and click the "Create new Workspace" button. Select "Blank" template and name the workspace as "CCIP Masterclass 3".
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.
We will need a standard @chainlink/contracts NPM package for this Module as well, so let's install it too while we are here by running the following command:
Finally, for this exercise we will need to install and the @openzeppelin/contracts NPM package, as well. To do so, run:
Since Foundry is designed to run with Solidity, NPM packages, though usable, ,can be replaced with directly installying Solidity contract packages from Github source repositories. We will install Chainlink CCIP contracts and then the contracts for other Chainlink Services.
First the CCIP Contracts.
And after that set remappings in your foundry.toml
or remappings.txt
files to
Next we install the other Chainlink Services contracts by running the following command:
And set remmapings to
Finally, for this exercise we will need to install and the @openzeppelin/contracts NPM package, as well. To do so, run:
And set remappings to @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
in foundry.toml
or remappings.txt
files.
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
, .deps/npm/@chainlink/contracts
and .deps/npm/@openzeppelin/contracts
folders are generated, that means we imported all of the necessary packages into the Remix IDE Workspace.
To pay for CCIP Fees you can use either LINK token or native/wrapped native asset on a given blockchain. For this exercise we will need at least 1 LINK or Arbitrum Sepolia testnet. To get it, navigate to the https://faucets.chain.link/arbitrum-sepolia
Create a new file inside the contracts
folder and name it XNFT.sol
Compile your contract by running:
Create a new file inside the src
folder and name it XNFT.sol
Create a new Solidity file by clicking on the "Create new file" button and name it XNFT.sol
Follow the steps to add the necessary environment variables for deploying these contracts and sending your first CCIP Message.
This contract expects at least 0.8.20 Solidity version. It is very important to understand that with Solc version 0.8.20, the default EVM version is set to "Shanghai". A new opcode, PUSH0, was added to the Ethereum Virtual Machine in the Shanghai upgrade.
However, besides Ethereum, the majority of blockchains haven't included PUSH0 opcode.
That means the PUSH0 opcode can now be part of the contract's bytecode and if the chain you are working on does not support it, it will error with the "Invalid opcode" error.
To understand more, we highly encourage you to check this StackOverflow answer:
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 Arbitrum Sepolia 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:
Create a new file and name it .env
. Fill in your wallet's PRIVATE_KEY and RPC URLs for at least two blockchains. For this example, we are going to use Arbitrum Sepolia and Ethereum Sepolia.
Once that is done, to load the variables in the .env
file, run the following command:
Finally, expand the foundry.toml
to support these two networks:
Navigate to the "Solidity compiler" tab
Toggle the "Advanced Configurations" dropdown
Toggle the "EVM VERSION" dropdown menu and select "paris" instead of "default"
Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu.
If you are using Metamask wallet, the Ethereum Sepolia network should already came preinstalled. Make sure you added the Arbitrum Sepolia network.
Go to Chainlist.org and search for "arbitrum sepolia". Once you see the network with Chain ID 421614, click the "Add to Metamask" button.
Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to the beginning of this page, at #ccip-config-details
Navigate to the scripts
folder and create new file named deployXNFT.ts
Run the deployment script:
Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to begging of this page, at #ccip-config-details
Option 1)
Deploy XNFT.sol
smart contract by running:
Create a new smart contract under the script
folder and name it XNFT.s.sol
Note that deployment of the XNFT
smart contract is hard coded to Ethereum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check CCIP Starter Kit (Foundry version) for reference.
Deploy XNFT.sol
smart contract by running:
Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to begging of this page, at #ccip-config-details
Open your Metamask wallet and switch to the Ethereum Sepolia network.
Open the XNFT.sol file.
Navigate to the "Solidity Compiler" tab and click the "Compile XNFT.sol" button.
Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that chainId
is switched to 11155111 (if not, you may need to refresh the Remix IDE page in your browser).
Under the "Contract" dropdown menu, make sure that the "XNFT - XNFT.sol" is selected.
Locate the orange "Deploy" button. Provide 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59
as the ccipRouterAddress
, 0x779877A7B0D9E8603169DdbD7836e478b4624789
as the linkTokenAddress
and 16015286601757825753
as the currentChainSelector
.
Click the orange "Deploy"/"Transact" button.
Metamask notification will pop up. Sign the transaction.
Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to beginning of this page, at #ccip-config-details
Navigate to the scripts
folder and create new file named deployXNFTArbitrum.ts
Run the deployment script:
Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to begging of this page, at #ccip-config-details
Option 1)
Deploy XNFT.sol
smart contract by running:
Create a new smart contract under the script
folder and name it XNFTArbitrum.s.sol
Note that deployment of the XNFT
smart contract is hard coded to Arbitrum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check CCIP Starter Kit (Foundry version) for reference.
Deploy XNFT.sol
smart contract by running:
Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to begging of this page, at #ccip-config-details
Open your Metamask wallet and switch to the Arbitrum Sepolia network.
Open the XNFT.sol file.
Navigate to the "Solidity Compiler" tab and click the "Compile XNFT.sol" button.
Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that chainId
is switched to 421614 (if not, you may need to refresh the Remix IDE page in your browser).
Under the "Contract" dropdown menu, make sure that the "XNFT - XNFT.sol" is selected.
Locate the orange "Deploy" button. Provide 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165
as the ccipRouterAddress
, 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E
as the linkTokenAddress
and 3478487238524512106
as the currentChainSelector
.
Click the orange "Deploy"/"Transact" button.
Metamask notification will pop up. Sign the transaction.
Prepare:
The address of the address of the XNFT.sol
smart contract you previously deployed to Ethereum Sepolia;
The address of the address of the XNFT.sol
smart contract you previously deployed to Arbitrum Sepolia;
3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the chainSelector
parameter;
0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs
parameter.
If you would like to calculate this value by yourself, you can reuse the following helper smart contract:
Create a new TypeScript file under the scripts
folder and name it enableChain.ts
Call the function by running the following command:
Prepare:
The address of the address of the XNFT.sol
smart contract you previously deployed to Ethereum Sepolia;
The address of the address of the XNFT.sol
smart contract you previously deployed to Arbitrum Sepolia;
3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the chainSelector
parameter;
0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs
parameter.
If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Inside the scripts folder, create EncodeExtraArgs.s.sol
and paste the following code:
Run:
Under the "Deployed Contracts" section, you should find the XNFT.sol
contract you previously deployed to Ethereum Sepolia. Find the enableChain
function and provide:
3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the chainSelector
parameter;
The address of the address of the XNFT.sol
smart contract you previously deployed to Arbitrum Sepolia, as xNftAddress
parameter;
0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs
parameter.
Hit the "Transact" orange button.
If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Create EncodeExtraArgs.sol
file and paste the following code:
Prepare:
The address of the XNFT.sol
smart contract you previously deployed to Arbitrum Sepolia;
The address of the address of the XNFT.sol
smart contract you previously deployed to Ethereum Sepolia, as xNftAddress
parameter;
16015286601757825753
, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the chainSelector
parameter;
0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs
parameter.
If you would like to calculate this value by yourself, you can reuse the following helper smart contract:
Create a new TypeScript file under the scripts
folder and name it enableChainArbitrum.ts
Call the function by running the following command:
Prepare:
The address of the address of the XNFT.sol
smart contract you previously deployed to Arbitrum Sepolia;
The address of the address of the XNFT.sol
smart contract you previously deployed to Ethereum Sepolia, as xNftAddress
parameter;
16015286601757825753
, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the chainSelector
parameter;
0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs
parameter.
If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Inside the scripts folder, create EncodeExtraArgs.s.sol
and paste the following code:
Run:
Under the "Deployed Contracts" section, you should find the XNFT.sol
contract you previously deployed to Arbitrum Sepolia. Find the enableChain
function and provide:
16015286601757825753
, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the chainSelector
parameter;
The address of the address of the XNFT.sol
smart contract you previously deployed to Ethereum Sepolia, as xNftAddress
parameter;
0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs
parameter.
Hit the "Transact" orange button.
If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Create EncodeExtraArgs.sol
file and paste the following code:
To cover for CCIP fees, fund XNFT.sol
with some amount of LINK, 3 should be more than enough for this demo. Obviously, for the sake of full functionality, you should fund XNFT.sol
smart contract on other blockchains as well, so you can perform cross-chain transfers between all of them.
Create a new TypeScript file under the scripts
folder and name it mint.ts
Call the function by running the following command:
Run:
Under the "Deployed Contracts" section, you should find the XNFT.sol
contract you previously deployed to Arbitrum Sepolia. Find the mint
function and hit the "Transact" orange button.
Prepare:
Your EOA address, as the from
parameter;
The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as to
parameter;
The ID of a xNFT you want to cross-transfer, as tokenId
parameter;
16015286601757825753
which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the destinationChainSelector
parameter;
1
which stands that we are paying for CCIP fees in LINK, as the payFeesIn
parameter.
Create a new TypeScript file under the scripts
folder and name it crossChainTransferFrom.ts
Call the function by running the following command:
Prepare:
Your EOA address, as the from
parameter;
The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as to
parameter;
The ID of a xNFT you want to cross-transfer, as tokenId
parameter;
16015286601757825753
which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the destinationChainSelector
parameter;
1
which stands that we are paying for CCIP fees in LINK, as the payFeesIn
parameter.
Run:
Under the "Deployed Contracts" section, you should find the XNFT.sol
contract you previously deployed to Arbitrum Sepolia. Find the crossChainTransferFrom
function and provide the following parameters:
Your EOA address, as the from
parameter;
The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as to
parameter;
The ID of a xNFT you want to cross-transfer, as tokenId
parameter;
16015286601757825753
which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the destinationChainSelector
parameter;
1
which stands that we are paying for CCIP fees in LINK, as the payFeesIn
parameter.
Hit the "Transact" orange button.
You can now monitor this cross-chain transfer on CCIP Explorer page.
Once cross-chain NFT arrives to Ethereum Sepolia, you can manually display it inside your Metamask wallet. Navigate to the "NFT" tab and hit the "Import NFT" button.
Then, fill in XNFT.sol
smart contract address on Ethereum Sepolia and token ID you received (0).
Finally, your NFT will appear inside Metamask wallet.
Chainlink is a blockchain-agnostic, decentralized computing platform that provides secure access to external data, off-chain computation, and cross-chain interoperability.
The Chainlink Cross-Chain Interoperability Protocol (CCIP) serves as an interoperability standard for transferring both tokens and/or data between any supported public or private blockchain network.
Level-5 cross-chain security achieves unprecedented levels of decentralization by utilizing multiple decentralized networks to secure a single cross-chain transaction, along with incorporating additional risk management systems to identify risks and take actions to prevent them, such as by implementing emergency shutdowns or imposing rate limits.
The fifth level of cross-chain security doesn’t just give you one independent network for your cross-chain data or messages; it gives you multiple networks, made up of independent nodes all working together to secure each bridge.
While many bridge solutions operate using a single node or multiple nodes under the control of one key holder (e.g., Multichain), the fifth level of security uses multiple independent nodes with their own independent key holders and even splits them up into two separate groups of nodes: the transactional DON nodes and the Risk Management Network nodes. One additional key feature of the separate networks in CCIP is the creation of two entirely separate implementations, with two independent code bases, so that CCIP features an unprecedented level of client diversity/decentralization for cross-chain interoperability.
Developed with security and reliability as the primary focus CCIP operates at the highest level of cross-chain security. CCIP’s defense-in-depth security and suitability can be broken down across four categories:
CCIP is underpinned by Chainlink’s proven decentralized oracle infrastructure. Rather than operating as a single monolithic network, CCIP is composed of multiple decentralized oracle networks (DONs) per chain lane, each consisting of a unique source chain and destination chain. This approach allows CCIP to be horizontally scalable, as additional DONs are added to CCIP for each additional blockchain network supported, versus funneling all cross-chain traffic through a single network.
The committing DON is a decentralized network of oracle nodes that monitor events on a given source chain, wait for source chain finality, bundle transactions to create a Merkle root, come to consensus on that Merkle root and finally commit that Merkle root to the destination chain. The executing DON is a decentralized network of oracle nodes that submit Merkle proofs on a destination chain, which is then verified onchain by ensuring the transactions were included in a previously committed Merkle root that has been validated by the Risk Management Network.
The Risk Management Network is a separate, independent network that continuously monitors and validates the behavior of CCIP, providing an additional layer of security by independently verifying cross-chain operations for anomalous activity. The Risk Management Network utilizes a separate, minimal implementation of the Chainlink node software, creating a form of client diversity for increased robustness while also minimizing external dependencies to prevent supply chain attacks.
More specifically, the Risk Management Network was written in a different programming language (Rust) than the primary CCIP system (Golang), developed by a different internal team, and uses a distinct non-overlapping set of node operators compared to the CCIP DONs. The Risk Management Network is a wholly unique concept in cross-chain interoperability that builds upon established engineering principles (N-version programming) seen in mission-critical systems in industries such as aviation, nuclear, and machine automation.
To increase the security and robustness of CCIP, the Risk Management Network engages in two types of activities:
Secondary Approval: The Risk Management Network independently recreates Merkle roots based on transactions from the source chain, which are then published on the destination chain and compared against the Merkle roots published by the Committing DON. Cross-chain transactions can only be executed if the Merkle roots from the two networks match.
Anomaly Detection: The Risk Management Network monitors for abnormal behavior from the CCIP network (e.g., committed transactions with no source chain equivalent) as well as the behavior of chains (e.g., deep block reorgs). If suspicious activity is detected, the Risk Management Network can trigger an emergency halt to pause all CCIP lanes and limit any losses.
Chainlink DONs are operated by a geographically distributed collection of Sybil-resistant, security-reviewed node operators with significant experience running mission-critical infrastructure across Web2 and Web3. Node operators in the Chainlink ecosystem include global enterprises (e.g., Deutsche Telekom MMS, Swisscom, Vodafone), leading Web3 DevOps teams (e.g. Infura, Coinbase Cloud), and experienced Chainlink ecosystem projects.
The Committing DONs and Executing DONs in CCIP are composed of 16 high-quality independent node operators, while the Risk Management Network is composed of 7 distinct node operators (resulting in a total of 23 node operators). Importantly, the Risk Management Network consists of a wholly separate and non-overlapping set of nodes compared to the primary CCIP networks, helping ensure independent secondary validation. As the value secured by CCIP expands over time, the number of node operators within each network can scale to meet the need for greater security.
As an additional layer of security for cross-chain token transfers, CCIP implements configurable rate limits, established on a per-token and per-lane basis, which are set up in alignment with the token contract owners like Lido. Furthermore, CCIP token transfers also benefit from the increased security provided by an aggregate rate limit (across token pools) on each lane, so even in a worst-case scenario, it would be impossible for every token’s limit to be maxed out before the aggregate rate limit on a lane is hit.
With CCIP you get:
Multiple independent nodes run by independent key holders.
Three decentralized networks all executing and verifying every bridge transaction.
Separation of responsibilities, with distinct sets of node operators, and with no nodes shared between the transactional DONs and the Risk Management Network.
Increased decentralization with two separate code bases across two different implementations, written in two different languages to create a previously unseen diversity of software clients in the bridging world.
Never-before-seen level of risk management that can be rapidly adapted to any new risks or attacks that appear for cross-chain bridging.
Brief overview of how one can approach debugging CCIP dApps
Welcome to bonus chapter about debugging Chainlink CCIP dApps and troubleshooting issues with sending and receiving cross-chain messages. Writing code is just the beginning, the true skill often lies in effectively troubleshooting and resolving issues that arise.
To understand where the issue is or may arise, you first need to have bookmarked the whole transfer of cross-chain message flow, which you can find in the Processing CCIP Messages subsection.
The second most important thing is to understand how ABI encoding/decoding works in EVM. When you compile Solidity code, the two primary outputs are Application Binary Interface (ABI) and EVM bytecode.
EVM bytecode is the machine-level code that the Ethereum Virtual Machine executes. It is what gets deployed to the Ethereum blockchain. It's a low-level, hexadecimal-encoded instruction set that the EVM interprets and runs. The bytecode represents the actual logic of the smart contract in an executable form.
The ABI is essentially a JSON-formatted text file that describes the functions and variables in your smart contract. When you want to interact with a deployed contract (e.g., calling a function from a web application), the ABI is used to encode the function call into a format that the EVM can understand. It serves as an interface between your high-level application (like a JavaScript front-end) and the low-level bytecode running on Ethereum. The ABI includes details about each function's name, return type, visibility (public, private, etc.), and the types of its parameters.
Anytime your message is not delivered due to error on the receiver side, CCIP Explorer will display the error message. However, if ABI is unknown to explorer (for example you haven't verified smart contract's source code on Block explorer) it will display its original content instead of human readable error message. However, if we know how ABI encoding/decoding works, that's still completely fine because there are plenty of tools that can help us.
So at first glance, when seeing this message, you can go into the panic mode and think how CCIP does not work or something similar. So let's decode the error message to see what went wrong.
So the error code is: 0x1c33fbee000000000000000000000000000000000000000000000000304611b6affba76a
This is esentially a hexadecimal value that mean a lot of stuff. But if we know the ABI of the Receiver smart contract it's simple to decode it.
Couple of tools that you can use:
Some online decoder, this one is pretty decent: https://bia.is/tools/abi-decoder/
Foundry's cast abi-decode
Let's use the https://bia.is/tools/abi-decoder/ online decoder for this example. We need to provide contract's ABI, the above error code and to hit decode to get a human readable error message.
The decoded output unequivocally tells us that the Solidity custom error ChainNotEnabled()
was thrown, which most likely means that we forgot to call enableChain()
function of the Receiver's smart contract in general case.
If you are a bit more technical, you can use a script present in the official CCIP GitHub repo https://github.com/smartcontractkit/ccip/blob/ccip-develop/core/scripts/ccip/ccip-revert-reason/main.go to accomplish the same thing when debugging, plus much more.
You will need to provide either an error code string or the chainId, txHash and txRequester alongside JSON RPC url in .env
file (archive node preffered).
And if you run go run main.go
the output with these predefined values should be: If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).
Which means that EVM Panic code 0x32 was thrown, which is actually really helpful error message.
Probably the most common error is the following one: ReceiverError. This may be due to an out of gas error on the destination chain. Error code: 0x
This most likely means that you've used extraArgs: ""
syntax which defaults to 200_000 for gasLimit
while your ccipReceive
function consumes more.
To solve this error, you will most likely just need to, connect your wallet to CCIP Explorer, set the "Gas limit override" field to appropriate value and trigger manual execution of this function by clicking the blue button.
In future, to prevent this issue, you can use the following syntax, if you want to set the gas limit to 500_000 gas for example:
Keep in mind that:
If you are transferring only tokens, gasLimit
should be set to 0 because there is no ccipReceive
function
The extraArgs
should mutable if you want to make your dApp compatible with future CCIP upgrades (https://docs.chain.link/ccip/best-practices#using-extraargs)
Oftentimes Tenderly is a good choice when you want to debug or simulate transactions against live test networks. Full trace feature allows you to see where the function actually reverts, which is much helpful than a below error message from at Block explorer.
One of the biggest problems when working with unverified smart contracts is function tracing. This smart contract wasn't verified, but the function selector was still known, and that's why Polygonscan was able to display it. This is sometimes very helpful.
Function selectors are the first four bytes of the Keccak-256 hash of the function's signature. The function signature includes the function name and the parenthesized list of parameter types. When a function call is made, these first four bytes are used by the EVM to determine which specific function in the contract should be executed.
So the exact function call that failed was triggered by the unverified smart contract to the CCIP Router smart contract, and its function selector is 0x96f4e9f9
. If we have the ABI of that smart contract finding the function selector will be extremely straightforward. But what if we don't? Well there is a https://openchain.xyz/signatures, website which has a database of all known function signatures so we can try searching for ours.
Looks like we found the function which reverted. Now we can take a look at its implementation and find out what went wrong. Although it was already clear from the Tenderly's UI that the issue is that CCIP Sender contract was not funded with tokens for covering CCIP fees - which is one of the most common User errors.
X (Twitter): https://x.com/sodofi_
💻 Minting Ethereum NFTs with Celo Gas Fees
Github repo: https://github.com/celo-org/celo-ccip-workshop
Getting started
You can use Chainlink CCIP with any blockchain development framework. For this Masterclass, we will use Remix IDE.
Let's create a new project by navigating to https://remix.ethereum.org/ and clicking the "Create new Workspace" button. Select "Blank" template and name the workspace as "CCIP Masterclass 4".
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, 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
and @openzeppelin/contracts
packages that we will use throughout this Masterclass as well.
This contract expects at least 0.8.20
version of a Solidity compiler. It is very important to understand that with the latest Remix IDE release, the default EVM version is set to "Cancun". A new opcode, PUSH0, was added to the Ethereum Virtual Machine in the Shanghai upgrade, which happened prior to the current, Cancun upgrade.
However, besides Ethereum, the majority of blockchains haven't included PUSH0 opcode.
That means the PUSH0 opcode can now be part of the contract's bytecode and if the chain you are working on does not support it, it will error with the "Invalid opcode" error.
What we want is to downgrade Ethereum Virtual Machine version to "Paris" instead.
To understand more, we highly encourage you to check this StackOverflow answer:
To set EVM version to "Paris", navigate to the "Solidity compiler" tab and then:
Set "COMPILER" version to 0.8.20+commit.a1b79de6
Toggle the "Advanced Configurations" dropdown
Toggle the "EVM VERSION" dropdown menu and select paris
instead of default
Now compile the smart contract by clicking the "Compile Empty.sol" button. If compiled successfully, go back to "File explorer" tab and if new .deps/npm/@chainlink/contracts-ccip
and .deps/npm/@openzeppelin/contracts
folders are generated, that means we imported all of the necessary packages into the Remix IDE Workspace successfully.
During this Masterclass, we will transfer USDC from Avalanche Fuji testnet to Ethereum Sepolia testnet. To get some amount of testnet USDC on Avalanche Fuji testnet, navigate to the https://faucet.circle.com/
To pay for CCIP Fees you can use either LINK token or native/wrapped native asset on a given blockchain. For this Masterclass we will need at least 3 LINK or Avalanche Fuji testnet. To get it, navigate to the https://faucets.chain.link/fuji
Create a new Solidity file by clicking on the "Create new file" button, name it TransferUSDC.sol
, and paste the following Solidity code.
Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu.
If you are using Metamask wallet, make sure you have added Avalanche Fuji C-Chain and Ethereum Sepolia (should already be added by default) networks.
Go to Chainlist.org and search for "avalanche fuji". Once you see the network with Chain ID 43113, click the "Add to Metamask" button.
Ethereum Sepolia should already be added by default to your Metamask wallet. However, if you need to manually add it, you can always repeat the same step we did for Avalanche Fuji C-Chain. Navigate to Chainlist.org and search for "sepolia". Once you see the network with Chain ID 11155111, click the "Add to Metamask" button.
Open your Metamask wallet and switch to the Avalanche Fuji network.
Open the TransferUSDC.sol
file.
Navigate to the "Solidity Compiler" tab and click the "Compile TransferUSDC.sol" button.
Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that chainId
is switched to 43113 (if not, you may need to refresh the Remix IDE page in your browser).
Under the "Contract" dropdown menu, make sure that the "TransferUSDC - TransferUSDC.sol" is selected.
Locate the orange "Deploy" button. Provide:
0xF694E193200268f9a4868e4Aa017A0118C9a8177
as the ccipRouter
,
0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846
as the linkToken
and
0x5425890298aed601595a70AB815c96711a31Bc65
as the usdcToken
.
Click the orange "Deploy"/"Transact" button.
Metamask notification will pop up. Sign the transaction.
Under the "Deployed Contracts" section, you should find the TransferUSDC.sol
contract you previously deployed to Avalanche Fuji. Find the allowlistDestinationChain
function and provide:
16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia test network, as the _destinationChainSelector
parameter,
true
as _allowed
parameter
Hit the "Transact" orange button.
To cover for CCIP fees, fund TransferUSDC.sol
with some amount of LINK, 3 should be enough for this demo.
Go to the Avalanche Fuji Snowtrace Explorer and search for USDC token. Locate the "Contract" tab, then click the "Write as Proxy" tab. Connect your wallet to the blockchain explorer. And finally find the "approve" function.
We want to approve 1 USDC to be spent by the TransferUSDC.sol
on our behalf. To do so we must provide:
The address of the TransferUSDC.sol
smart contract we previously deployed, as spender
parameter
1000000, as value
parameter.
Because USDC token has 6 decimals, 1000000 means that we will approve 1 USDC to be spent on our behalf.
Click the "Write" button. Metamask popup will show up. Sign the transaction.
Under the "Deployed Contracts" section, you should find the TransferUSDC.sol
contract you previously deployed to Avalanche Fuji. Find the transferUsdc
function and provide:
16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia test network, as the _destinationChainSelector
parameter,
Your wallet address, as the _receiver
parameter,
1000000, as the _amount
parameter
0, as the _gasLimit
parameter
0 is set as the _gasLimit
parameter because we are sending tokens to an EOA so there is no cost for executing the ccipReceive
function on the destination side.
Hit the "Transact" orange button.
You can now monitor the live status of your cross-chain message by copying the transaction hash into the search bar of a Chainlink CCIP Explorer.
16015286601757825753
CCIP Chain identifier of the destination blockchain (Ethereum Sepolia in this example). You can find each chain selector on the supported networks page.
Hello World!
Any string
0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4
The CCIP-BnM contract address at the source chain (Avalanche Fuji in this example). You can find all the addresses for each supported blockchain on the supported networks page.
1000000000000000
The token amount (0.001 CCIP-BnM).
Chainlink Local is so fast because it does not spin any type of offchain component needed for cross-chain transfers. That's why CCIP Local Simulator Fork (smart contract for Foundry, and typescript script for Hardhat) exposes functionality to switch between forks and route messages to the destination blockchain directly by developers - so don't forget this step .
👉
Chain Selector
CCIP Router Address
0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165
0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59
LINK Token Address
0xb1D4538B4571d411F07960EF2838Ce337FE1E80E
0x779877A7B0D9E8603169DdbD7836e478b4624789