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.
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.
messageId
The unique identifier of the failed message.
tokenReceiver
The address to which the tokens will be sent.
_destinationChainSelector
_receiver
Your receiver contract address at Ethereum Sepolia. The destination contract address.
_text
_token
_amount
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).