Exercise 4: Sending USDC Cross-Chain

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.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Empty {}

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:

Setting solc EVM version in different environments

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.

Faucet

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/

Circle Faucet

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

Develop TransferUSDC smart contract

Create a new Solidity file by clicking on the "Create new file" button, name it TransferUSDC.sol, and paste the following Solidity code.

// SPDX-License-Identifier: MIT
pragma solidity ^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 {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";

/**
 * 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.
 */
contract TransferUSDC is OwnerIsCreator {
    using SafeERC20 for IERC20;

    error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
    error DestinationChainNotAllowlisted(uint64 destinationChainSelector);
    error NothingToWithdraw();

    IRouterClient private immutable i_ccipRouter;
    IERC20 private immutable i_linkToken;
    IERC20 private immutable i_usdcToken;

    mapping(uint64 => bool) public allowlistedChains;

    modifier onlyAllowlistedChain(uint64 _destinationChainSelector) {
        if (!allowlistedChains[_destinationChainSelector])
            revert DestinationChainNotAllowlisted(_destinationChainSelector);
        _;
    }

    event UsdcTransferred(
        bytes32 messageId,
        uint64 destinationChainSelector,
        address receiver,
        uint256 amount,
        uint256 ccipFee
    );

    constructor(address ccipRouter, address linkToken, address usdcToken) {
        i_ccipRouter = IRouterClient(ccipRouter);
        i_linkToken = IERC20(linkToken);
        i_usdcToken = IERC20(usdcToken);
    }

    function allowlistDestinationChain(
        uint64 _destinationChainSelector,
        bool _allowed
    ) external onlyOwner {
        allowlistedChains[_destinationChainSelector] = _allowed;
    }

    function transferUsdc(
        uint64 _destinationChainSelector,
        address _receiver,
        uint256 _amount,
        uint64 _gasLimit
    )
        external
        onlyOwner
        onlyAllowlistedChain(_destinationChainSelector)
        returns (bytes32 messageId)
    {
        Client.EVMTokenAmount[]
            memory tokenAmounts = new Client.EVMTokenAmount[](1);
        Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
            token: address(i_usdcToken),
            amount: _amount
        });
        tokenAmounts[0] = tokenAmount;

        Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
            receiver: abi.encode(_receiver),
            data: "",
            tokenAmounts: tokenAmounts,
            extraArgs: Client._argsToBytes(
                Client.EVMExtraArgsV1({gasLimit: _gasLimit})
            ),
            feeToken: address(i_linkToken)
        });

        uint256 ccipFee = i_ccipRouter.getFee(
            _destinationChainSelector,
            message
        );

        if (ccipFee > i_linkToken.balanceOf(address(this)))
            revert NotEnoughBalance(
                i_linkToken.balanceOf(address(this)),
                ccipFee
            );

        i_linkToken.approve(address(i_ccipRouter), ccipFee);

        i_usdcToken.safeTransferFrom(msg.sender, address(this), _amount);
        i_usdcToken.approve(address(i_ccipRouter), _amount);

        // Send CCIP Message
        messageId = i_ccipRouter.ccipSend(_destinationChainSelector, message);

        emit UsdcTransferred(
            messageId,
            _destinationChainSelector,
            _receiver,
            _amount,
            ccipFee
        );
    }

    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        uint256 amount = IERC20(_token).balanceOf(address(this));

        if (amount == 0) revert NothingToWithdraw();

        IERC20(_token).transfer(_beneficiary, amount);
    }
}

Prepare for deployment

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.

Step 1) Deploy TransferUSDC.sol to Avalanche Fuji

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.

Step 2) On AvalancheFuji, call allowlistDestinationChain function

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.

Step 4) 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.

Approve 1 USDC to be spent by TransferUSDC.sol

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 5) 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,

  • 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.

CCIP Explorer | Chainlink

Last updated