练习4:跨链发送USDC

入门

您可以使用任何区块链开发框架与Chainlink CCIP进行交互。在本次大师班中,我们将使用Remix IDE。

首先,访问https://remix.ethereum.org/ 并点击“Create new Workspace”按钮来创建一个新项目。选择“Blank”模板,并将工作区命名为“CCIP Masterclass 4”。

或者,您可以克隆:

@chainlink/contracts-ccip NPM包

要使用Chainlink CCIP,您需要与@chainlink/contracts-ccip NPM 钱包中的Chainlink CCIP专用合约进行交互。

要安装它,请创建一个新的Solidity文件,并粘贴以下内容。这是一个空合约,仅导入了@chainlink/contracts-ccip和@openzeppelin/contracts包中的合约,我们将在整个大师班中使用这些合约。

// 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 {}

该合约要求至少使用0.8.20版本的Solidity编译器。需要特别注意的是,在最新的Remix IDE版本中,默认的EVM版本设置为“Cancun”。在上海升级(发生在当前的Cancun升级之前)中,向以太坊虚拟机中添加了一个新的操作码PUSH0。

然而,除了以太坊,大多数区块链尚未包含PUSH0操作码。这意味着PUSH0操作码现在可以成为合约字节码的一部分,如果您正在使用的链不支持它,它将会出现“无效操作码”错误。我们希望将以太坊虚拟机版本降级到“Paris”。

要了解更多信息,我们强烈建议您查看这个StackOverflow回答:

在不同环境中设置solc EVM版本

要将EVM版本设置为“Paris”,请导航到“Solidity compiler”标签,然后按照以下步骤操作:

  • 将“COMPILER”版本设置为0.8.20+commit.a1b79de6

  • 切换“Advanced Configurations”下拉菜单

  • 切换“EVM VERSION”下拉菜单,并选择“paris”而不是默认值

现在通过点击“Compile Empty.sol”按钮来编译智能合约。如果编译成功,返回“File explorer”标签,如果生成了新的.deps/npm/@chainlink/contracts-ccip和.deps/npm/@openzeppelin/contracts文件夹,这意味着我们已经成功地将所有必要的包导入到了Remix IDE工作区。

水龙头

在本次大师班中,我们将把USDC从Avalanche Fuji测试网转移到以太坊Sepolia测试网。要在Avalanche Fuji测试网上获取一些测试网USDC,请导航到以下网站:https://faucet.circle.com/

Circle Faucet

要支付CCIP费用,您可以使用LINK代币或给定区块链上的原生/包装原生资产。在本次大师班中,我们需要至少3个LINK或Avalanche Fuji测试网的资产。要获取它,请导航到以下网站: https://faucets.chain.link/fuji

开发TransferUSDC智能合约

点击“Create new file”按钮创建一个新的Solidity文件,将其命名为TransferUSDC.sol,并粘贴以下Solidity代码。

// 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);
    }
}

准备部署

导航到“Deploy & run transactions”标签,从“Environment”下拉菜单中选择“Injected Provider - Metamask”选项。

如果您使用的是Metamask钱包,请确保已添加Avalanche Fuji C-Chain和Ethereum Sepolia(默认情况下应已添加)网络。

访问 Chainlist.org 并搜索“avalanche fuji”。当您看到链ID为43113的网络时,点击“Add to Metamask”按钮。

以太坊Sepolia网络应默认已添加到您的Metamask钱包中。然而,如果您需要手动添加,您可以重复我们为Avalanche Fuji C-Chain所做的步骤。导航到 Chainlist.org 并搜索“sepolia”。当您看到链ID为11155111的网络时,点击“Add to Metamask”按钮。

步骤1)将TransferUSDC.sol部署到Avalanche Fuji

打开您的Metamask钱包并切换到Avalanche Fuji网络。 打开TransferUSDC.sol文件。 导航到“Solidity Compiler”标签,并点击“Compile TransferUSDC.sol”按钮。 导航到“Deploy & run transactions”标签,从“Environment”下拉菜单中选择“Injected Provider - Metamask”选项。确保链ID已切换到43113(如果没有,您可能需要刷新浏览器中的Remix IDE页面)。 在“Contract”下拉菜单下,确保选择了“TransferUSDC - TransferUSDC.sol”

找到橙色的“Deploy”按钮。提供以下信息:

  • 0xF694E193200268f9a4868e4Aa017A0118C9a8177 作为ccipRoute

  • 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846作为linkToken

  • 0x5425890298aed601595a70AB815c96711a31Bc65作为usdcToken。

点击橙色的“Deploy”/“Transact”按钮。

Metamask通知将弹出。签署交易。

步骤2)在Avalanche Fuji上调用allowlistDestinationChain函数

在“Deployed Contracts”部分,您应该能找到之前部署到Avalanche Fuji的TransferUSDC.sol合约。找到allowlistDestinationChain函数并提供以下信息:

  • 16015286601757825753,这是以太坊Sepolia测试网络的CCIP链选择器,作为_destinationChainSelector参数。

  • true 作为_allowed参数

点击橙色的“Transact”按钮。

为了支付CCIP费用,请向TransferUSDC.sol合约充值一定数量的LINK,3个LINK对于本次演示来说应该足够。

步骤4)在Avalanche Fuji上,调用USDC.sol合约的approve函数。

访问 Avalanche Fuji Snowtrace Explorer并搜索USDC代币。找到“Contract”标签,然后点击“Write as Proxy”标签。将您的钱包连接到区块链浏览器。最后,找到“approve”函数。

用LINK为TransferUSDC合约充值

我们希望批准TransferUSDC.sol代表我们支出1个USDC。为此,我们必须提供以下信息:

  • 之前部署的TransferUSDC.sol智能合约地址,作为spender参数

  • 1000000,作为value参数。

因为USDC代币有6位小数,1000000意味着我们将批准支出1个USDC。

点击“Write”按钮。Metamask弹窗将出现。签署交易。

步骤5)在Avalanche Fuji上调用transferUsdc函数。

在“Deployed Contracts”部分,您应该能找到之前部署到Avalanche Fuji的TransferUSDC.sol合约。找到transferUsdc函数并提供以下信息:

  • 16015286601757825753,这是以太坊Sepolia测试网络的CCIP链选择器,作为_destinationChainSelector参数。

  • 您的钱包地址,作为_receiver参数,

  • 1000000,作为_amount参数

  • 0,作为_gasLimit参数

将_gasLimit参数设置为0,因为我们将代币发送到一个EOA(外部拥有账户),因此在目标端执行ccipReceive函数没有费用。

点击橙色的“Transact”按钮。

现在,您可以将交易哈希复制到 Chainlink CCIP Explorer的搜索栏中,以监控跨链消息的实时状态。

CCIP Explorer | Chainlink

Last updated