Exercise 1: Programmable Token Transfers using the Defensive Example Pattern
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.
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.
// SPDX-License-Identifier: MITpragmasolidity 0.8.19;import {IRouterClient} from"@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";import {OwnerIsCreator} from"@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";import {Client} from"@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";import {CCIPReceiver} from"@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";
import {EnumerableMap} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableMap.sol";
/** * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. * DO NOT USE THIS CODE IN PRODUCTION. *//// @title - A simple messenger contract for transferring/receiving tokens and data across chains./// @dev - This example shows how to recover tokens in case of revertcontractProgrammableDefensiveTokenTransfersisCCIPReceiver, OwnerIsCreator {usingEnumerableMapforEnumerableMap.Bytes32ToUintMap;usingSafeERC20forIERC20;// Custom errors to provide more descriptive revert messages. error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance to cover the fees.
errorNothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw. error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.
error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
error SourceChainNotAllowed(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
errorSenderNotAllowed(address sender); // Used when the sender has not been allowlisted by the contract owner.errorInvalidReceiverAddress(); // Used when the receiver address is 0.errorOnlySelf(); // Used when a function is called outside of the contract itself.errorErrorCase(); // Used when simulating a revert during message processing.errorMessageNotFailed(bytes32 messageId);// Example error code, could have many different error codes.enumErrorCode {// RESOLVED is first so that the default value is resolved. RESOLVED,// Could have any number of error codes here. FAILED }structFailedMessage {bytes32 messageId; ErrorCode errorCode; }// Event emitted when a message is sent to another chain.eventMessageSent(bytes32indexed messageId, // The unique ID of the CCIP message.uint64indexed destinationChainSelector, // The chain selector of the destination chain.address receiver, // The address of the receiver on the destination chain.string text, // The text being sent.address token, // The token address that was transferred.uint256 tokenAmount, // The token amount that was transferred.address feeToken, // the token address used to pay CCIP fees.uint256 fees // The fees paid for sending the message. );// Event emitted when a message is received from another chain.eventMessageReceived(bytes32indexed messageId, // The unique ID of the CCIP message.uint64indexed sourceChainSelector, // The chain selector of the source chain.address sender, // The address of the sender from the source chain.string text, // The text that was received.address token, // The token address that was transferred.uint256 tokenAmount // The token amount that was transferred. );eventMessageFailed(bytes32indexed messageId, bytes reason);eventMessageRecovered(bytes32indexed messageId);bytes32private s_lastReceivedMessageId; // Store the last received messageId.addressprivate s_lastReceivedTokenAddress; // Store the last received token address.uint256private s_lastReceivedTokenAmount; // Store the last received amount.stringprivate s_lastReceivedText; // Store the last received text.// Mapping to keep track of allowlisted destination chains.mapping(uint64=>bool) public allowlistedDestinationChains;// Mapping to keep track of allowlisted source chains.mapping(uint64=>bool) public allowlistedSourceChains;// Mapping to keep track of allowlisted senders.mapping(address=>bool) public allowlistedSenders; IERC20 private s_linkToken;// The message contents of failed messages are stored here.mapping(bytes32 messageId => Client.Any2EVMMessage contents)public s_messageContents;// Contains failed messages and their state. EnumerableMap.Bytes32ToUintMap internal s_failedMessages;// This is used to simulate a revert in the processMessage function.boolinternal s_simRevert =false;/// @notice Constructor initializes the contract with the router address./// @param _router The address of the router contract./// @param _link The address of the link contract.constructor(address_router,address_link) CCIPReceiver(_router) { s_linkToken =IERC20(_link); }/// @dev Modifier that checks if the chain with the given destinationChainSelector is allowlisted./// @param _destinationChainSelector The selector of the destination chain.modifieronlyAllowlistedDestinationChain(uint64_destinationChainSelector) {if (!allowlistedDestinationChains[_destinationChainSelector])revertDestinationChainNotAllowlisted(_destinationChainSelector); _; } /// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted.
/// @param _sourceChainSelector The selector of the destination chain./// @param _sender The address of the sender.modifieronlyAllowlisted(uint64_sourceChainSelector,address_sender) {if (!allowlistedSourceChains[_sourceChainSelector])revertSourceChainNotAllowed(_sourceChainSelector);if (!allowlistedSenders[_sender]) revertSenderNotAllowed(_sender); _; }/// @dev Modifier that checks the receiver address is not 0./// @param _receiver The receiver address.modifiervalidateReceiver(address_receiver) {if (_receiver ==address(0)) revertInvalidReceiverAddress(); _; }/// @dev Modifier to allow only the contract itself to execute a function./// Throws an exception if called by any account other than the contract itself.modifieronlySelf() {if (msg.sender !=address(this)) revertOnlySelf(); _; }/// @dev Updates the allowlist status of a destination chain for transactions./// @notice This function can only be called by the owner./// @param _destinationChainSelector The selector of the destination chain to be updated./// @param allowed The allowlist status to be set for the destination chain.functionallowlistDestinationChain(uint64_destinationChainSelector,bool allowed ) externalonlyOwner { allowlistedDestinationChains[_destinationChainSelector] = allowed; }/// @dev Updates the allowlist status of a source chain/// @notice This function can only be called by the owner./// @param _sourceChainSelector The selector of the source chain to be updated./// @param allowed The allowlist status to be set for the source chain.functionallowlistSourceChain(uint64_sourceChainSelector,bool allowed ) externalonlyOwner { allowlistedSourceChains[_sourceChainSelector] = allowed; }/// @dev Updates the allowlist status of a sender for transactions./// @notice This function can only be called by the owner./// @param _sender The address of the sender to be updated./// @param allowed The allowlist status to be set for the sender.functionallowlistSender(address_sender,bool allowed) externalonlyOwner { allowlistedSenders[_sender] = allowed; }/// @notice Sends data and transfer tokens to receiver on the destination chain./// @notice Pay for fees in LINK./// @dev Assumes your contract has sufficient LINK to pay for CCIP fees./// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain./// @param _receiver The address of the recipient on the destination blockchain./// @param _text The string data to be sent./// @param _token token address./// @param _amount token amount./// @return messageId The ID of the CCIP message that was sent.functionsendMessagePayLINK(uint64_destinationChainSelector,address_receiver,stringcalldata_text,address_token,uint256_amount )externalonlyOwneronlyAllowlistedDestinationChain(_destinationChainSelector)validateReceiver(_receiver)returns (bytes32 messageId) {// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message// address(linkToken) means fees are paid in LINK Client.EVM2AnyMessage memory evm2AnyMessage =_buildCCIPMessage( _receiver, _text, _token, _amount,address(s_linkToken) );// Initialize a router client instance to interact with cross-chain router IRouterClient router =IRouterClient(this.getRouter());// Get the fee required to send the CCIP messageuint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);if (fees > s_linkToken.balanceOf(address(this)))revertNotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);// approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK s_linkToken.approve(address(router), fees);// approve the Router to spend tokens on contract's behalf. It will spend the amount of the given tokenIERC20(_token).approve(address(router), _amount);// Send the message through the router and store the returned message ID messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);// Emit an event with message detailsemitMessageSent( messageId, _destinationChainSelector, _receiver, _text, _token, _amount,address(s_linkToken), fees );// Return the message IDreturn messageId; }/// @notice Sends data and transfer tokens to receiver on the destination chain./// @notice Pay for fees in native gas./// @dev Assumes your contract has sufficient native gas like ETH on Ethereum or MATIC on Polygon./// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain./// @param _receiver The address of the recipient on the destination blockchain./// @param _text The string data to be sent./// @param _token token address./// @param _amount token amount./// @return messageId The ID of the CCIP message that was sent.functionsendMessagePayNative(uint64_destinationChainSelector,address_receiver,stringcalldata_text,address_token,uint256_amount )externalonlyOwneronlyAllowlistedDestinationChain(_destinationChainSelector)validateReceiver(_receiver)returns (bytes32 messageId) {// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message// address(0) means fees are paid in native gas Client.EVM2AnyMessage memory evm2AnyMessage =_buildCCIPMessage( _receiver, _text, _token, _amount,address(0) );// Initialize a router client instance to interact with cross-chain router IRouterClient router =IRouterClient(this.getRouter());// Get the fee required to send the CCIP messageuint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);if (fees >address(this).balance)revertNotEnoughBalance(address(this).balance, fees);// approve the Router to spend tokens on contract's behalf. It will spend the amount of the given tokenIERC20(_token).approve(address(router), _amount);// Send the message through the router and store the returned message ID messageId = router.ccipSend{value: fees}( _destinationChainSelector, evm2AnyMessage );// Emit an event with message detailsemitMessageSent( messageId, _destinationChainSelector, _receiver, _text, _token, _amount,address(0), fees );// Return the message IDreturn messageId; }/** * @notice Returns the details of the last CCIP received message. * @dev This function retrieves the ID, text, token address, and token amount of the last received CCIP message. * @return messageId The ID of the last received CCIP message. * @return text The text of the last received CCIP message. * @return tokenAddress The address of the token in the last CCIP received message. * @return tokenAmount The amount of the token in the last CCIP received message. */functiongetLastReceivedMessageDetails()publicviewreturns (bytes32 messageId,stringmemory text,address tokenAddress,uint256 tokenAmount ) {return ( s_lastReceivedMessageId, s_lastReceivedText, s_lastReceivedTokenAddress, s_lastReceivedTokenAmount ); }/** * @notice Retrieves a paginated list of failed messages. * @dev This function returns a subset of failed messages defined by `offset` and `limit` parameters. It ensures that the pagination parameters are within the bounds of the available data set.
* @param offset The index of the first failed message to return, enabling pagination by skipping a specified number of messages from the start of the dataset.
* @param limit The maximum number of failed messages to return, restricting the size of the returned array. * @return failedMessages An array of `FailedMessage` struct, each containing a `messageId` and an `errorCode` (RESOLVED or FAILED), representing the requested subset of failed messages. The length of the returned array is determined by the `limit` and the total number of failed messages.
*/functiongetFailedMessages(uint256 offset,uint256 limit ) externalviewreturns (FailedMessage[] memory) {uint256 length = s_failedMessages.length();// Calculate the actual number of items to return (can't exceed total length or requested limit)uint256 returnLength = (offset + limit > length)? length - offset: limit; FailedMessage[] memory failedMessages =new FailedMessage[]( returnLength );// Adjust loop to respect pagination (start at offset, end at offset + limit or total length)for (uint256 i =0; i < returnLength; i++) { (bytes32 messageId,uint256 errorCode) = s_failedMessages.at( offset + i ); failedMessages[i] =FailedMessage(messageId,ErrorCode(errorCode)); }return failedMessages; }/// @notice The entrypoint for the CCIP router to call. This function should/// never revert, all errors should be handled internally in this contract./// @param any2EvmMessage The message to process./// @dev Extremely important to ensure only router calls this.functionccipReceive(Client.Any2EVMMessagecalldata any2EvmMessage )externaloverrideonlyRouteronlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address)) ) // Makesurethesourcechainandsenderareallowlisted {/* solhint-disable no-empty-blocks */trythis.processMessage(any2EvmMessage) {// Intentionally empty in this example; no action needed if processMessage succeeds } catch (bytesmemory err) {// Could set different error codes based on the caught error. Each could be// handled differently. s_failedMessages.set( any2EvmMessage.messageId,uint256(ErrorCode.FAILED) ); s_messageContents[any2EvmMessage.messageId] = any2EvmMessage;// Don't revert so CCIP doesn't revert. Emit event instead.// The message can be retried later without having to do manual execution of CCIP.emitMessageFailed(any2EvmMessage.messageId, err);return; } }/// @notice Serves as the entry point for this contract to process incoming messages./// @param any2EvmMessage Received CCIP message./// @dev Transfers specified token amounts to the owner of this contract. This function/// must be external because of the try/catch for error handling./// It uses the `onlySelf`: can only be called from the contract.functionprocessMessage(Client.Any2EVMMessagecalldata any2EvmMessage )externalonlySelfonlyAllowlisted(any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address)) ) // Makesurethesourcechainandsenderareallowlisted {// Simulate a revert for testing purposesif (s_simRevert) revertErrorCase();_ccipReceive(any2EvmMessage); // process the message - may revert as well }/// @notice Allows the owner to retry a failed message in order to unblock the associated tokens./// @param messageId The unique identifier of the failed message./// @param tokenReceiver The address to which the tokens will be sent./// @dev This function is only callable by the contract owner. It changes the status of the message/// from 'failed' to 'resolved' to prevent reentry and multiple retries of the same message.functionretryFailedMessage(bytes32 messageId,address tokenReceiver ) externalonlyOwner {// Check if the message has failed; if not, revert the transaction.if (s_failedMessages.get(messageId) !=uint256(ErrorCode.FAILED))revertMessageNotFailed(messageId);// Set the error code to RESOLVED to disallow reentry and multiple retries of the same failed message. s_failedMessages.set(messageId,uint256(ErrorCode.RESOLVED));// Retrieve the content of the failed message. Client.Any2EVMMessage memory message = s_messageContents[messageId];// This example expects one token to have been sent, but you can handle multiple tokens.// Transfer the associated tokens to the specified receiver as an escape hatch.IERC20(message.destTokenAmounts[0].token).safeTransfer( tokenReceiver, message.destTokenAmounts[0].amount );// Emit an event indicating that the message has been recovered.emitMessageRecovered(messageId); }/// @notice Allows the owner to toggle simulation of reversion for testing purposes./// @param simRevert If `true`, simulates a revert condition; if `false`, disables the simulation./// @dev This function is only callable by the contract owner.functionsetSimRevert(bool simRevert) externalonlyOwner { s_simRevert = simRevert; }function_ccipReceive(Client.Any2EVMMessagememory any2EvmMessage ) internaloverride { s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text// Expect one token to be transferred at once, but you can transfer several tokens. s_lastReceivedTokenAddress = any2EvmMessage.destTokenAmounts[0].token; s_lastReceivedTokenAmount = any2EvmMessage.destTokenAmounts[0].amount;emitMessageReceived( any2EvmMessage.messageId, any2EvmMessage.sourceChainSelector,// fetch the source chain identifier (aka selector) abi.decode(any2EvmMessage.sender, (address)),// abi-decoding of the sender address, abi.decode(any2EvmMessage.data, (string)), any2EvmMessage.destTokenAmounts[0].token, any2EvmMessage.destTokenAmounts[0].amount ); }/// @notice Construct a CCIP message. /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for programmable tokens transfer.
/// @param _receiver The address of the receiver./// @param _text The string data to be sent./// @param _token The token to be transferred./// @param _amount The amount of the token to be transferred./// @param _feeTokenAddress The address of the token used for fees. Set address(0) for native gas. /// @return Client.EVM2AnyMessage Returns an EVM2AnyMessage struct which contains information for sending a CCIP message.
function_buildCCIPMessage(address_receiver,stringcalldata_text,address_token,uint256_amount,address_feeTokenAddress ) privatepurereturns (Client.EVM2AnyMessagememory) {// Set the token amounts Client.EVMTokenAmount[]memory tokenAmounts =new Client.EVMTokenAmount[](1); Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({ token: _token, amount: _amount }); tokenAmounts[0] = tokenAmount;// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ receiver: abi.encode(_receiver),// ABI-encoded receiver address data: abi.encode(_text),// ABI-encoded string tokenAmounts: tokenAmounts,// The amount and type of token being transferred extraArgs: Client._argsToBytes(// Additional arguments, setting gas limit Client.EVMExtraArgsV1({gasLimit:400_000}) ),// Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees feeToken: _feeTokenAddress });return evm2AnyMessage; }/// @notice Fallback function to allow the contract to receive Ether./// @dev This function has no function body, making it a default function for receiving Ether./// It is automatically called when Ether is sent to the contract without any data.receive() externalpayable {}/// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract./// @dev This function reverts if there are no funds to withdraw or if the transfer fails./// It should only be callable by the owner of the contract./// @param _beneficiary The address to which the Ether should be sent.functionwithdraw(address_beneficiary) publiconlyOwner {// Retrieve the balance of this contractuint256 amount =address(this).balance;// Revert if there is nothing to withdrawif (amount ==0) revertNothingToWithdraw();// Attempt to send the funds, capturing the success status and discarding any return data (bool sent, ) = _beneficiary.call{value: amount}("");// Revert if the send failed, with information about the attempted transferif (!sent) revertFailedToWithdrawEth(msg.sender, _beneficiary, amount); }/// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token./// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw./// @param _beneficiary The address to which the tokens will be sent./// @param _token The contract address of the ERC20 token to be withdrawn.functionwithdrawToken(address_beneficiary,address_token ) publiconlyOwner {// Retrieve the balance of this contractuint256 amount =IERC20(_token).balanceOf(address(this));// Revert if there is nothing to withdrawif (amount ==0) revertNothingToWithdraw();IERC20(_token).safeTransfer(_beneficiary, amount); }}
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.
Recover the locked tokens
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.
Explanations
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.
Sending messages
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.
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 Any2EVMMessagestruct 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.
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.002CCIP-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.5LINK 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).