Exercise #2: Deposit transferred USDC to Compound V3
Expand the previous exercise
For this exercise, we will need exercise 1 completed. If you haven't done it already, navigate back to Exercise #1: Cross-Chain Transfer USDCand follow steps.
We will use the already deployed TransferUSDC.sol contract to transfer USDC from Avalanche Fuji to Ethereum Sepolia in this example as well.
Many DeFi protocols, including Compound V3 use their own mock ERC20 tokens on testnets, for easier testing purposes. So we will create this simple smart contract that will swap received, actual USDC, to Compound's mock USDC that we can deposit into Compound V3 on Ethereum Sepolia.
Keep in mind, that this extra step is necessary on test networks only! In production, you will just use the one and only USDC token.
Create a new Solidity file by clicking on the "Create new file" button, name it SwapTestnetUSDC.sol and copy the following codebase into it.
// SPDX-License-Identifier: MITpragmasolidity ^0.8.19;import {IERC20} from"@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from"@openzeppelin/contracts@4.8.0/security/ReentrancyGuard.sol";interface IFauceteer {functiondrip(address token) external;}/** * 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. */contractSwapTestnetUSDCisReentrancyGuard {usingSafeERC20forIERC20;addressprivateimmutable i_usdcToken;addressprivateimmutable i_compoundUsdcToken;eventSwap(address tokenIn, address tokenOut, uint256 amount, address trader);constructor(address usdcToken,address compoundUsdcToken,address fauceteer) { i_usdcToken = usdcToken; i_compoundUsdcToken = compoundUsdcToken;IFauceteer(fauceteer).drip(compoundUsdcToken); }functionswap(address tokenIn,address tokenOut,uint256 amount) externalnonReentrant {require(tokenIn == i_usdcToken || tokenIn == i_compoundUsdcToken);require(tokenOut == i_usdcToken || tokenOut == i_compoundUsdcToken);IERC20(tokenIn).safeTransferFrom(msg.sender,address(this), amount);IERC20(tokenOut).transfer(msg.sender, amount);emitSwap(tokenIn, tokenOut, amount, msg.sender); }functiongetSupportedTokens() externalviewreturns(address usdcToken,address compoundUsdcToken) {return(i_usdcToken, i_compoundUsdcToken); }}
Now open up your Metamask wallet and switch to the Ethereum Sepolia network.
Open the SwapTestnetUSDC.sol file.
Navigate to the "Solidity Compiler" tab and click the "Compile SwapTestnetUSDC.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 "SwapTestnetUSDC - SwapTestnetUSDC.sol" is selected.
Locate the orange "Deploy" button. Provide:
0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238, as the usdcToken parameter;
0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238, as the compoundUsdcToken parameter;
0x68793eA49297eB75DFB4610B68e076D2A5c7646C, as the fauceteer parameter.
Click the orange "Deploy"/"Transact" button.
Metamask notification will pop up. Sign the transaction.
Step 2) On Ethereum Sepolia, develop the CrossChainReceiver smart contract
Create a new Solidity file by clicking on the "Create new file" button, name itCrossChainReceiver.sol and paste the following content.
// SPDX-License-Identifier: MITpragmasolidity ^0.8.19;import {CCIPReceiver} from"@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";import {Client} from"@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";import {OwnerIsCreator} from"@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";import {IERC20} from"@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {EnumerableMap} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/utils/structs/EnumerableMap.sol";
interface CometMainInterface {functionsupply(address asset,uint amount) external;}interface ISwapTestnetUSDC {functionswap(address tokenIn,address tokenOut,uint256 amount) external;functiongetSupportedTokens()externalviewreturns (address usdcToken,address compoundUsdcToken);}/** * 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. */contractCrossChainReceiverisCCIPReceiver, OwnerIsCreator {usingEnumerableMapforEnumerableMap.Bytes32ToUintMap;usingSafeERC20forIERC20;// 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. BASIC } 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.errorOnlySelf(); // Used when a function is called outside of the contract itself.errorErrorCase(); // Used when simulating a revert during message processing.errorMessageNotFailed(bytes32 messageId); CometMainInterface internalimmutable i_comet; ISwapTestnetUSDC internalimmutable i_swapTestnetUsdc;// This is used to simulate a revert in the processMessage function.boolinternal s_simRevert =false;// Contains failed messages and their state. EnumerableMap.Bytes32ToUintMap internal s_failedMessages;// Mapping to keep track of allowlisted source chains.mapping(uint64 chainSelecotor =>bool isAllowlisted)public allowlistedSourceChains;// Mapping to keep track of allowlisted senders.mapping(address sender =>bool isAllowlisted) public allowlistedSenders;// Mapping to keep track of the message contents of failed messages.mapping(bytes32 messageId => Client.Any2EVMMessage contents)public s_messageContents;eventMessageFailed(bytes32indexed messageId, bytes reason);eventMessageRecovered(bytes32indexed messageId);constructor(address ccipRouterAddress,address cometAddress,address swapTestnetUsdcAddress ) CCIPReceiver(ccipRouterAddress) { i_comet =CometMainInterface(cometAddress); i_swapTestnetUsdc =ISwapTestnetUSDC(swapTestnetUsdcAddress); } /// @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 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 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 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.BASIC) ); 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.BASIC))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 {address usdcToken = any2EvmMessage.destTokenAmounts[0].token; (,address compoundUsdcToken) = i_swapTestnetUsdc.getSupportedTokens();uint256 amount = any2EvmMessage.destTokenAmounts[0].amount;IERC20(usdcToken).approve(address(i_swapTestnetUsdc), amount);// Swap actual testnet USDC for Compound V3's version of USDC test token.// This step is neccessary on testnets only! i_swapTestnetUsdc.swap(usdcToken, compoundUsdcToken, amount);IERC20(compoundUsdcToken).approve(address(i_comet), amount); i_comet.supply(compoundUsdcToken, amount); }/** * @notice Retrieves the IDs of failed messages from the `s_failedMessages` map. * @dev Iterates over the `s_failedMessages` map, collecting all keys. * @return ids An array of bytes32 containing the IDs of failed messages from the `s_failedMessages` map. */functiongetFailedMessagesIds()externalviewreturns (bytes32[] memory ids) {uint256 length = s_failedMessages.length();bytes32[] memory allKeys =newbytes32[](length);for (uint256 i =0; i < length; i++) { (bytes32 key, ) = s_failedMessages.at(i); allKeys[i] = key; }return allKeys; }}
Step 3) On Ethereum Sepolia, deploy the CrossChainReceiver smart contract
Open up your Metamask wallet and make sure you are connected to the Ethereum Sepolia network.
Open the CrossChainReceiver.sol file.
Navigate to the "Solidity Compiler" tab and click the "Compile CrossChainReceiver.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 "CrossChainReceiver - CrossChainReceiver.sol" is selected.
Locate the orange "Deploy" button. Provide:
0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59, as the ccipRouterAddress parameter;
0xAec1F48e02Cfb822Be958B68C7957156EB3F0b6e, as the cometAddress parameter;
The address of a previously deployed SwapTestnetUsdc.sol smart contract, as the swapTestnetUsdcAddress parameter.
Click the orange "Deploy"/"Transact" button.
Metamask notification will pop up. Sign the transaction.
Step 4) On Ethereum Sepolia, call allowlistSourceChain function
Under the "Deployed Contracts" section, you should find the CrossChainReceiver.sol contract you previously deployed to Ethereum Sepolia. Find the allowlistSourceChain function and provide:
14767482510784806043, which is the CCIP Chain Selector for the Avalanche Fuji network, as the _sourceChainSelector parameter.
true as _allowed parameter
Hit the "Transact" orange button.
Step 5) On Ethereum Sepolia, call allowlistSender function
Under the "Deployed Contracts" section, you should find the CrossChainReceiver.sol contract you previously deployed to Ethereum Sepolia. Find the allowlistSender function and provide:
The address of the TransferUSDC.sol smart contract you deployed in Exercise #1, as the _sender parameter
true as _allowed parameter
Hit the "Transact" orange button.
Step 6) On Avalanche Fuji, call approve function on USDC.sol
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.
Step 7) On AvalancheFuji, call transferUsdc function
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,
The address of a CrossChainReceiver.sol smart contract you previously deployed to Ethereum Sepolia, as the _receiver parameter,
1000000, as the _amount parameter
500000, as the _gasLimit parameter
Now we are setting the _gasLimit parameter because we are sending tokens to a smart contract so there is a cost for executing the ccipReceive function on the destination side. In the bonus section of this Masterclass we are going to learn how to actually calculate that exact value. For now, we are setting 500000 gas.
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.
Once you receive a cross-chain message, the CrossChainReceiver.sol smart contract will swap received USDC tokens for Compound's mock USDC tokens, deposit those mock tokens into the Compound V3 by calling the Comet.sol smart contract's supply function and CrossChainReceiver.sol smart contract should get COMP tokens in return, which we can see on the Etherscan Blockchain Explorer.