arrow-left

All pages
gitbookPowered by GitBook
1 of 2

Loading...

Loading...

练习 1:使用防御性示例模式进行可编程代币转移

转移代币与数据 - 防御性示例

本教程扩展了可编程代币传输的示例arrow-up-right。它使用 Chainlink CCIP 在不同区块链上的智能合约之间传输代币和任意数据,专注于接收合约中的防御性编码。如果在 CCIP 消息接收期间发生指定错误,合约将锁定代币。锁定代币允许所有者根据需要恢复和重定向它们。防御性编码至关重要,因为它使得恢复锁定的代币成为可能,并确保了用户资产的保护。

开始之前

  1. 您应该了解如何编写、编译、部署和资助智能合约。如果您需要复习基础知识,请阅读本教程arrow-up-right,它将指导您使用 Solidity 编程语言arrow-up-right,在 MetaMask 钱包arrow-up-right中交互以及在 Remix 开发环境arrow-up-right中工作。

  2. 您的账户必须在 Avalanche Fuji 上有一些 AVAX 和 LINK 代币,在以太坊 Sepolia 上有一些 ETH 代币。了解如何 。

  3. 查看,确认您要传输的代币是否支持您的通道。在这个例子中,您将从 Avalanche Fuji 传输代币到以太坊 Sepolia,所以请在检查支持的代币列表。

  4. 了解如何 。按照本指南操作后,您应该拥有 CCIP-BnM 代币,并且 CCIP-BnM 应该出现在 MetaMask 中您的代币列表中。

  5. 了解如何 。本指南展示了如何使用 LINK 资助您的合约,但您可以使用相同的指南为合约资助任何 ERC20 代币,只要它们出现在 MetaMask 的代币列表中。

  6. 按照前一个教程:使用 来学习如何使用 CCIP 进行可编程代币传输。

编码时间!

在这个练习中,我们将从 Avalanche Fuji 上的智能合约启动交易,向 以太坊 Sepolia 上的另一个智能合约发送字符串文本和 CCIP-BnM 代币,使用 CCIP。然而,在到达接收合约时,处理逻辑将故意失败。本教程将展示一种优雅的错误处理方法,允许合约所有者恢复被锁定的代币。

正确估计您的 gas 限制

彻底测试所有场景以准确估计所需的 gas 限制至关重要,包括失败场景。请注意,用于执行失败场景的错误处理逻辑的 gas 可能高于成功场景。

hashtag
部署您的合约

要使用此合约:

  1. 。

  2. 编译您的合约。

  3. 部署并在 Avalanche Fuji 上资助您的发送者合约,并启用向以太坊 Sepolia 发送消息的功能:

此时,您在 Avalanche Fuji 上有一个发送者合约,在 以太坊 Sepolia 上有一个接收者合约。作为安全措施,您已启用发送者合约向 以太坊 Sepolia 发送 CCIP 消息,以及接收者合约接收来自 Avalanche Fuji 上发送者的 CCIP 消息。接收者合约无法处理消息,因此,它不会抛出异常,而是会锁定接收到的代币,使所有者能够恢复它们。

注意:另一项安全措施强制只有路由器可以调用 _ccipReceive 函数。更多详情请参阅。

hashtag
恢复被锁定的代币

转移 0.001 CCIP-BnM 和一些文本。使用 CCIP 的 CCIP 费用将以 LINK 支付。

  1. 打开 MetaMask 并连接到 Avalanche Fuji。用 LINK 代币为您的合约注资。您可以向您的合约转账 0.5 LINK。在此示例中,LINK 用于支付 CCIP 费用。

  2. 从 Avalanche Fuji 发送带有代币的字符串数据::

    1. 打开 MetaMask 并选择网络 Avalanche Fuji.

注意

在gas价格飙升期间,您的交易可能会失败,需要超过 0.5 LINK 的gas费才能继续。如果您的交易失败,请向您的合约中注入更多的 LINK 代币,并尝试重新提交交易。

  1. 打开,通过交易哈希搜索你的跨链交易。

  1. CCIP交易在状态被标记为“Success”时完成。在这个例子中,CCIP消息ID是0x120367995ef71f83d64a05bd7793862afda9d04049da4cb32851934490d03ae4。

  1. 检查目标链上的接收方合约:

    1. 打开 MetaMask 并选择 Ethereum Sepolia 网络。

    2. 在 Remix IDE 中,在“部署与运行交易”(Deploy & Run Transactions)部分,打开您在以太坊 Sepolia 上部署的智能合约的函数列表。

参数
描述

  1. 确认交易后,您可以在区块链浏览器中打开它。请注意,被锁定的资金已转移到 tokenReceiver 地址。

  2. 再次调用 getFailedMessages 函数,使用偏移量(offset)为 0 和限制(limit)为 1,以检索第一个失败的消息。请注意,现在错误代码是 0,表示该消息已经解决。

注意:这些示例合约被设计为可以双向工作。作为练习,您可以使用它们将带有数据的代币从Avalanche Fuji转移到以太坊Sepolia,也可以从以太坊Sepolia转回Avalanche Fuji。

hashtag
说明

本教程中展示的智能合约旨在与CCIP交互,以传输和接收代币和数据。该合约代码与 中的代码相似,因此,您可以参阅 。我们只将解释主要的不同之处。

hashtag
发送消息

sendMessagePayLINK 函数与 教程中的 sendMessagePayLINK 函数类似。主要的区别在于增加了gas限制,以适应处理错误逻辑所需的额外gas。

hashtag
接收和处理消息

当目标区块链接收到消息时,CCIP路由器会调用ccipReceive函数。此函数作为合约处理传入CCIP消息的入口点,通过onlyRouter和onlyAllowlisted修饰符实施关键的安全检查。

以下是该过程的逐步分解:

  1. 通过ccipReceive进入:

    • ccipReceive函数被调用,传入一个包含待处理消息的Any2EVMMessage结构体。

    • 安全检查确保调用来自授权的路由器、已允许的源链和已允许的发送者。

hashtag
重新处理失败的消息

retryFailedMessage 函数提供了一种机制,用于在 CCIP 消息处理失败时恢复资产。它专门设计用于处理那些消息数据问题导致整个处理过程受阻,但仍然允许代币恢复的场景:

  1. 初始化:

    • 只有合约所有者可以调用此函数,提供失败消息的 messageId 和用于代币恢复的 tokenReceiver 地址。

  2. 验证:

这个函数展示了一个优雅的资产恢复解决方案,即使在消息处理遇到问题时也能保护用户的价值。

防御性示例模式

我们将要实现的另一个最佳实践称为防御性示例(Defensive Example),这是一种模式,它允许我们在不强制原始交易失败的情况下重新处理失败的消息。让我们解释一下它的工作原理。

接收和处理消息

当目标区块链接收到消息时,CCIP路由器会调用ccipReceive函数。此函数作为合约处理传入CCIP消息的入口点,通过onlyRouter和onlyAllowlisted修饰符实施关键的安全检查。

以下是该过程的逐步分解:

通过ccipReceive进入:
  • ccipReceive函数被调用,传入一个包含待处理消息的Any2EVMMessage结构体。

  • 安全检查确保调用来自授权的路由器、已允许的源链和已允许的发送者。

  • 处理消息:

    • ccipReceive调用processMessage函数,该函数是外部的,以利用Solidity的try/catch错误处理机制。注意:onlySelf修饰符确保只有合约本身可以调用此函数。

    • 在processMessage内部,使用s_simRevert状态变量检查模拟回滚条件。这个模拟由只有合约所有者可调用的setSimRevert函数来切换。

    • 如果s_simRevert为假,processMessage调用_ccipReceive函数进行进一步的消息处理。

  • _ccipReceive中的消息处理:

    • _ccipReceive从消息中提取并存储各种信息,如messageId、解码后的sender地址、代币数量和数据。

    • 然后发出一个MessageReceived事件,表示消息处理成功。

  • 错误处理:

    • 如果在处理过程中发生错误(或触发了模拟回滚),ccipReceive内的catch块将被执行。

    • 失败消息的messageId被添加到s_failedMessages中,消息内容被存储在s_messageContents中。

    • 发出一个MessageFailed事件,允许以后识别和重处理失败的消息。

  • 失败消息的重新处理

    retryFailedMessage函数提供了一种机制,如果CCIP消息处理失败,可以用来恢复资产。它专门设计用来处理消息数据问题导致整个处理无法进行,但允许代币恢复的场景:

    1. 初始化:

      • 只有合约所有者可以调用此函数,提供失败消息的messageId和代币恢复用的tokenReceiver地址。

    2. 验证:

      • 使用s_failedMessages.get(messageId)检查消息是否已失败。如果没有,就回滚交易。

    3. 状态更新:

      • 将消息的错误代码更新为RESOLVED,以防止重复进入和多次重试。

    4. 代币恢复:

      • 使用s_messageContents[messageId]检索失败的消息内容。

      • 将与失败消息关联的锁定代币转移到指定的tokenReceiver,作为一个逃生舱口,而无需再次处理整个消息。

    5. 事件发出:

      • 发出一个MessageRecovered事件,以信号代币已成功恢复。

    这个函数展示了一个优雅的资产恢复解决方案,即使在消息处理遇到问题时也能保护用户的价值。

    打开 MetaMask 并选择网络 Avalanche Fuji。
  • 在 Remix IDE 中,点击 Deploy & Run Transactions,从环境列表中选择 Injected Provider - MetaMask。然后 Remix 将与您的 MetaMask 钱包交互,以与 Avalanche Fuji 通信。

  • 填写您的区块链路由器和 LINK 合约地址。可以在 支持的网络页面arrow-up-right 上找到路由器地址,在 LINK 代币合约arrow-up-right页面 arrow-up-right上找到 LINK 合约地址。对于 Avalanche Fuji,路由器地址是 0xF694E193200268f9a4868e4Aa017A0118C9a8177,LINK 合约地址是 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846。

  • 点击 transact 按钮。确认交易后,合约地址将出现在 已部署合约 列表中。记下您的合约地址。

  • 打开 MetaMask 并用 CCIP-BnM 代币为您的合约注资。您可以向您的合约转账 0.002 CCIP-BnM。

  • 启用您的合约以向 以太坊 Sepolia 发送 CCIP 消息:

    1. 在 Remix IDE 中,转到 Deploy & Run Transactions,在您部署在 Avalanche Fuji 上的智能合约的函数列表中。

    2. 调用 allowlistDestinationChain,传入 16015286601757825753 作为目标链选择器,以及 true 设置为允许。每个链选择器都可以在 上找到。

  • 在 以太坊 Sepolia 上部署您的接收者合约,并启用从您的发送者合约接收消息:

    1. 打开 MetaMask 并选择网络 以太坊 Sepolia。

    2. 在 Remix IDE 中,转到 Deploy & Run Transactions,确保环境仍然是 Injected Provider - MetaMask。

    3. 填写您的区块链路由器和 LINK 合约地址。可以在 上找到路由器地址,在 上找到 LINK 合约地址。对于以太坊 Sepolia,路由器地址是 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59,LINK 合约地址是 0x779877A7B0D9E8603169DdbD7836e478b4624789。

    4. 点击 transact 按钮。确认交易后,合约地址将出现在 已部署合约列表 中。记下您的合约地址。

    5. 启用您的合约以接收来自 Avalanche Fuji 的 CCIP 消息::

      1. 在 Remix IDE 中,转到 Deploy & Run Transactions,打开您部署在 以太坊 Sepolia 上的智能合约的函数列表。

      2. 调用 allowlistSourceChain,传入 14767482510784806043 作为源链选择器,以及 true

    6. 启用您的合约以接收来自您部署在 Avalanche Fuji 上的合约的 CCIP 消息:

      1. 在 Remix IDE 中,转到 Deploy & Run Transactions,打开您部署在 以太坊 Sepolia 上的智能合约的函数列表。

      2. 调用 allowlistSender,传入您部署在 Avalanche Fuji 上的合约地址,并设置为 true

    7. 调用 setSimRevert 函数,传入 true 作为参数,然后等待交易确认。将 s_simRevert 设置为 true 会在处理接收到的消息时模拟失败。更多详情请参阅。

  • 在 Remix IDE 中,转到 Deploy & Run Transactions,打开您部署在 Avalanche Fuji 上的智能合约的函数列表。

  • 填写 sendMessagePayLINK 函数的参数:

    参数
    值和描述

    _destinationChainSelector

    16015286601757825753 这是目标区块链的CCIP链标识符(在这个例子中是 以太坊 Sepolia)。您可以在 上找到每个链选择器。

    _receiver

    您在 以太坊 Sepolia 上的接收者合约地址。 目标合约地址。

    _text

  • 点击 transact 并在 MetaMask 上确认交易。

  • 交易成功后,请记录交易哈希。以下是在Avalanche Fuji上进行交易的一个示例arrow-up-right。

  • 调用 getFailedMessages 函数,使用偏移量(offset)为 0 和限制(limit)为 1,以检索第一个失败的消息。
  • 请注意返回的值有:0x120367995ef71f83d64a05bd7793862afda9d04049da4cb32851934490d03ae4(消息ID)和 1(表示失败的错误代码)。

  • 为了恢复被锁定的代币,请调用 retryFailedMessage 函数:

  • 处理消息:

    • ccipReceive调用processMessage函数,该函数是外部的,以利用Solidity的try/catch错误处理机制。注意:onlySelf修饰符确保只有合约本身可以调用此函数。

    • 在processMessage内部,使用s_simRevert状态变量检查是否模拟回滚条件。这个模拟由合约所有者调用的setSimRevert函数来切换。 如

    • 果s_simRevert为false,processMessage调用_ccipReceive函数进行进一步的消息处理。

  • 在_ccipReceive中处理消息:

    • _ccipReceive从消息中提取并存储各种信息,如messageId、解码后的发送者地址、代币数量和数据。

    • 然后发出一个MessageReceived事件,表示消息处理成功。

  • 错误处理:

    • 如果处理过程中发生错误(或触发模拟回滚),ccipReceive内的catch块将被执行。

    • 失败消息的messageId被添加到s_failedMessages中,消息内容被存储在s_messageContents中。

    • 发出一个MessageFailed事件,允许以后识别和重处理失败的消息。

  • 它使用 s_failedMessages.get(messageId) 检查消息是否已失败。如果没有,它将回滚交易。

  • 状态更新:

    • 将消息的错误代码更新为 RESOLVED,以防止重复条目和多次重试。

  • 代币恢复:

    • 使用 s_messageContents[messageId] 检索失败的消息内容。

    • 将与失败消息关联的锁定代币转移到指定的 tokenReceiver,作为一个逃生舱口,而无需再次处理整个消息。

  • 事件发出:

    • 发出一个 MessageRecovered 事件,用以标示代币恢复操作已成功完成。

  • messageId

    失败消息的唯一标识符。

    tokenReceiver

    代币将被发送到的地址。

    获取测试网 LINKarrow-up-right
    支持的网络页面arrow-up-right
    这里arrow-up-right
    获取 CCIP 测试代币arrow-up-right
    资助您的合约arrow-up-right
    数据传输代币arrow-up-right
    在 Remix 中打开合约arrow-up-right
    解释部分arrow-up-right
    CCIP浏览器arrow-up-right
    传输代币及数据教程arrow-up-right
    其代码解释arrow-up-right
    传输代币及数据arrow-up-right
    // 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 {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";
    
    /**
     * 这是一个使用硬编码值来增加清晰度的示例合约。
     * 这是一个使用未经审计代码的示例合约。
     * 不要在生产环境中使用这段代码。
     */
    
    /// @title - 一个简单的跨链传输/接收代币和数据的信使合约。
    /// @dev - 这个示例展示了如何在撤销的情况下恢复代币
    contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
        using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
        using SafeERC20 for IERC20;
    
        // 自定义错误,提供更具描述性的撤销消息。
        error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // 用于确保合约有足够的余额来支付费用。
        error NothingToWithdraw(); // 当尝试提取以太币但没有可提取的以太币时使用。
        error FailedToWithdrawEth(address owner, address target, uint256 value); // 当提取以太币失败时使用。
        error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // 当目标链没有被合约所有者列入允许名单时使用。
        error SourceChainNotAllowed(uint64 sourceChainSelector); // 当源链没有被列入允许名单时使用。
        error SenderNotAllowed(address sender); // 当发件人没有被合约所有者列入允许名单时使用。
        error InvalidReceiverAddress(); // 当接收地址为0时使用。
        error OnlySelf(); // 当函数在合约外部被调用时使用。
        error ErrorCase(); // 当模拟消息处理过程中的撤销时使用。
        error MessageNotFailed(bytes32 messageId);
    
        // 示例错误代码,可以有许多不同的错误代码。
        enum ErrorCode {
            // 首先是RESOLVED,以便默认值是已解决的。
            RESOLVED,
            // 此处可以有任意数量的错误代码。
            FAILED
        }
    
        struct FailedMessage {
            bytes32 messageId;
            ErrorCode errorCode;
        }
    
        // 当消息发送到另一个链时发出的事件。
        event MessageSent(
            bytes32 indexed messageId, // CCIP消息的唯一ID。
            uint64 indexed destinationChainSelector, // 目的链的链选择器。
            address receiver, // 目的链上的接收地址。
            string text, // 被发送的文本。
            address token, // 被转移的代币地址。
            uint256 tokenAmount, // 被转移的代币数量。
            address feeToken, // 用于支付CCIP费用的代币地址。
            uint256 fees // 为发送消息支付的费用。
        );
    
        // 当从另一个链收到消息时发出的事件。
        event MessageReceived(
            bytes32 indexed messageId, // CCIP消息的唯一ID。
            uint64 indexed sourceChainSelector, // 来源链的链选择器。
            address sender, // 来自源链的发件人地址。
            string text, // 收到的文本。
            address token, // 被转移的代币地址。
            uint256 tokenAmount // 被转移的代币数量。
        );
    
        event MessageFailed(bytes32 indexed messageId, bytes reason);
        event MessageRecovered(bytes32 indexed messageId);
    
        bytes32 private s_lastReceivedMessageId; // 存储最后接收的消息ID。
        address private s_lastReceivedTokenAddress; // 存储最后接收的代币地址。
        uint256 private s_lastReceivedTokenAmount; // 存储最后接收的数量。
        string private s_lastReceivedText; // 存储最后接收的文本。
    
        // 映射,用于跟踪允许的目标链。
        mapping(uint64 => bool) public allowlistedDestinationChains;
    
        // 映射,用于跟踪允许的源链。
        mapping(uint64 => bool) public allowlistedSourceChains;
    
        // 映射,用于跟踪允许的发件人。
        mapping(address => bool) public allowlistedSenders;
    
        IERC20 private s_linkToken;
    
        // 存储失败消息的内容。
        mapping(bytes32 messageId => Client.Any2EVMMessage contents)
            public s_messageContents;
    
        // 包含失败消息及其状态。
        EnumerableMap.Bytes32ToUintMap internal s_failedMessages;
    
        // 用于模拟消息处理函数中的撤销。
        bool internal s_simRevert = false;
    
        /// @notice 构造函数,用于使用路由器地址初始化合约。
        /// @param _router 路由器合约的地址。
        /// @param _link LINK合约的地址。
        constructor(address _router, address _link) CCIPReceiver(_router) {
            s_linkToken = IERC20(_link);
        }
    
        /// @dev 修饰符,用于检查给定目标链选择器的链是否被列入允许名单。
        /// @param _destinationChainSelector 目的链的选择器。
        modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {
            if (!allowlistedDestinationChains[_destinationChainSelector])
                revert DestinationChainNotAllowlisted(_destinationChainSelector);
            _;
        }
    
        /// @dev 修饰符,用于检查源链选择器的链和发件人是否被允许。
        /// @param _sourceChainSelector 目的链的选择器。
        /// @param _sender 发件人的地址。
        modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {
            if (!allowlistedSourceChains[_sourceChainSelector])
                revert SourceChainNotAllowed(_sourceChainSelector);
            if (!allowlistedSenders[_sender]) revert SenderNotAllowed(_sender);
            _;
        }
    
        /// @dev 修饰符,检查接收地址不为0。
        /// @param _receiver 接收地址。
        modifier validateReceiver(address _receiver) {
            if (_receiver == address(0)) revert InvalidReceiverAddress();
            _;
        }
    
        /// @dev 修饰符,仅允许合同本身执行功能。
        /// 如果由任何帐户调用,则抛出异常,而不是合同本身。
        modifier onlySelf() {
            if (msg.sender != address(this)) revert OnlySelf();
            _;
        }
    
        /// @dev 更新目标链的交易允许状态。
        /// @notice 只能由所有者调用此功能。
        /// @param _destinationChainSelector 要更新的目的链选择器。
        /// @param allowed 设置目的链的允许状态。
        function allowlistDestinationChain(
            uint64 _destinationChainSelector,
            bool allowed
        ) external onlyOwner {
            allowlistedDestinationChains[_destinationChainSelector] = allowed;
        }
    
        /// @dev 更新源链的允许状态
        /// @notice 只能由所有者调用此功能。
        /// @param _sourceChainSelector 要更新的源链选择器。
        /// @param allowed 设置源链的允许状态。
        function allowlistSourceChain(
            uint64 _sourceChainSelector,
            bool allowed
        ) external onlyOwner {
            allowlistedSourceChains[_sourceChainSelector] = allowed;
        }
    
        /// @dev 更新发件人的交易允许状态。
        /// @notice 只能由所有者调用此功能。
        /// @param _sender 要更新的发件人地址。
        /// @param allowed 设置发件人的允许状态。
        function allowlistSender(address _sender, bool allowed) external onlyOwner {
            allowlistedSenders[_sender] = allowed;
        }
    
        /// @notice 向目标链上的接收者发送数据和转移代币。
        /// @notice 使用 LINK 支付费用。
        /// @dev 假设你的合约有足够的LINK来支付 CCIP 费用。
        /// @param _destinationChainSelector 目标区块链的标识符(即选择器)。
        /// @param _receiver 目标区块链上的接收者地址。
        /// @param _text 要发送的字符串数据。
        /// @param _token 代币地址。
        /// @param _amount 代币数量。
        /// @return messageId 发送的 CCIP 消息的 ID。
        function sendMessagePayLINK(
            uint64 _destinationChainSelector,
            address _receiver,
            string calldata _text,
            address _token,
            uint256 _amount
        )
            external
            onlyOwner
            onlyAllowlistedDestinationChain(_destinationChainSelector)
            validateReceiver(_receiver)
            returns (bytes32 messageId)
        {
            // 在内存中创建一个 EVM2AnyMessage 结构,其中包含发送跨链消息所需的信息
            // address(linkToken) 表示费用以 LINK 支付
            Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
                _receiver,
                _text,
                _token,
                _amount,
                address(s_linkToken)
            );
    
            // 初始化一个路由器客户端实例,以与跨链路由器交互
            IRouterClient router = IRouterClient(this.getRouter());
    
            // 获取发送 CCIP 消息所需的费用
            uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
    
            if (fees > s_linkToken.balanceOf(address(this)))
                revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
    
            // 批准路由器代表合约转移 LINK 代币。它将花费费用中的 LINK
            s_linkToken.approve(address(router), fees);
    
            // 批准路由器代表合约花费给定数量的代币。它将花费给定数量的代币
            IERC20(_token).approve(address(router), _amount);
    
            // 通过路由器发送消息,并存储返回的消息 ID
            messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
    
            // 发出一个事件,其中包含消息详细信息
            emit MessageSent(
                messageId,
                _destinationChainSelector,
                _receiver,
                _text,
                _token,
                _amount,
                address(s_linkToken),
                fees
            );
    
            // 返回消息 ID
            return messageId;
        }
    
        /// @notice 向目标链上的接收者发送数据和转移代币。
        /// @notice 使用原生gas支付费用。
        /// @dev 假设你的合约有足够的原生gas,如以太坊上的ETH或Polygon上的MATIC。
        /// @param _destinationChainSelector 目标区块链的标识符(即选择器)。
        /// @param _receiver 目标区块链上的接收者地址。
        /// @param _text 要发送的字符串数据。
        /// @param _token 代币地址。
        /// @param _amount 代币数量。
        /// @return messageId 发送的CCIP消息的ID。
        function sendMessagePayNative(
            uint64 _destinationChainSelector,
            address _receiver,
            string calldata _text,
            address _token,
            uint256 _amount
        )
            external
            onlyOwner
            onlyAllowlistedDestinationChain(_destinationChainSelector)
            validateReceiver(_receiver)
            returns (bytes32 messageId)
        {
            // 在内存中创建一个 EVM2AnyMessage 结构,其中包含发送跨链消息所需的信息
            // address(0) 表示费用以原生气体支付
            Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
                _receiver,
                _text,
                _token,
                _amount,
                address(0)
            );
    
            // 初始化一个路由器客户端实例,以与跨链路由器交互
            IRouterClient router = IRouterClient(this.getRouter());
    
            // 获取发送 CCIP 消息所需的费用
            uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
    
            if (fees > address(this).balance)
                revert NotEnoughBalance(address(this).balance, fees);
    
            // 批准路由器代表合约花费给定数量的代币。它将花费给定数量的代币
            IERC20(_token).approve(address(router), _amount);
    
            // 通过路由器发送消息,并存储返回的消息 ID
            messageId = router.ccipSend{value: fees}(
                _destinationChainSelector,
                evm2AnyMessage
            );
    
            // 发出一个事件,其中包含消息详细信息
            emit MessageSent(
                messageId,
                _destinationChainSelector,
                _receiver,
                _text,
                _token,
                _amount,
                address(0),
                fees
            );
    
            // 返回消息 ID
            return messageId;
        }
        
        /**
         * @notice 返回最后接收到的CCIP消息的详细信息。
         * @dev 此函数检索最后接收到的CCIP消息的ID、文本、代币地址和代币数量。
         * @return messageId 最后接收到的CCIP消息的ID。
         * @return text 最后接收到的CCIP消息的文本。
         * @return tokenAddress 最后接收到的CCIP消息中的代币地址。
         * @return tokenAmount 最后接收到的CCIP消息中的代币数量。
         */
        function getLastReceivedMessageDetails()
            public
            view
            returns (
                bytes32 messageId,
                string memory text,
                address tokenAddress,
                uint256 tokenAmount
            )
        {
            return (
                s_lastReceivedMessageId,
                s_lastReceivedText,
                s_lastReceivedTokenAddress,
                s_lastReceivedTokenAmount
            );
        }
    
        /**
         * @notice 检索失败消息的分页列表。
         * @dev 此功能返回由 `offset` 和 `limit` 参数定义的失败消息的子集。它确保分页参数在可用数据集的范围内。
         * @param offset 要返回的第一个失败消息的索引,通过从数据集的开头跳过指定数量的消息来实现分页。
         * @param limit 要返回的失败消息的最大数量,限制返回数组的大小。
         * @return failedMessages `FailedMessage` 结构的数组,每个结构包含一个 `messageId` 和一个 `errorCode`(解决或失败),表示请求的失败消息的子集。返回数组的长度由 `limit` 和失败消息的总数决定。
         */
        function getFailedMessages(
            uint256 offset,
            uint256 limit
        ) external view returns (FailedMessage[] memory) {
            uint256 length = s_failedMessages.length();
    
            // 计算实际返回的项目数量(不能超过总长度或请求的限制)
            uint256 returnLength = (offset + limit > length)
                ? length - offset
                : limit;
            FailedMessage[] memory failedMessages = new FailedMessage[](
                returnLength
            );
    
            // 调整循环以尊重分页(从 offset 开始,结束于 offset + limit 或总长度)
            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 CCIP 路由器调用的入口点。此功能不应撤销,所有错误应在合约内部处理。
        /// @param any2EvmMessage 要处理的消息。
        /// @dev 非常重要的是确保只有路由器调用此功能。
        function ccipReceive(
            Client.Any2EVMMessage calldata any2EvmMessage
        )
            external
            override
            onlyRouter
            onlyAllowlisted(
                any2EvmMessage.sourceChainSelector,
                abi.decode(any2EvmMessage.sender, (address))
            ) // 确保源链和发件人被列入允许名单
        {
            /* solhint-disable no-empty-blocks */
            try this.processMessage(any2EvmMessage) {
                // 本示例中故意为空;如果 processMessage 成功,则无需采取任何行动
            } catch (bytes memory err) {
                // 可以根据捕获的错误设置不同的错误代码。每个都可以有不同的处理方式。
                s_failedMessages.set(
                    any2EvmMessage.messageId,
                    uint256(ErrorCode.FAILED)
                );
                s_messageContents[any2EvmMessage.messageId] = any2EvmMessage;
                // 不要撤销,以免 CCIP 撤销。改为发出事件。
                // 可以稍后重试该消息,而无需手动执行 CCIP。
                emit MessageFailed(any2EvmMessage.messageId, err);
                return;
            }
        }
    
        /// @notice 为此合约处理传入消息的入口点。
        /// @param any2EvmMessage 接收到的 CCIP 消息。
        /// @dev 将指定数量的代币转移到此合约的所有者。此功能必须是外部的,因为利用 Solidity 的 try/catch 错误处理机制。
        /// 它使用 `onlySelf`:只能由合约调用。
        function processMessage(
            Client.Any2EVMMessage calldata any2EvmMessage
        )
            external
            onlySelf
            onlyAllowlisted(
                any2EvmMessage.sourceChainSelector,
                abi.decode(any2EvmMessage.sender, (address))
            ) // 确保源链和发件人被列入允许名单
        {
            // 为测试目的模拟撤销
            if (s_simRevert) revert ErrorCase();
    
            _ccipReceive(any2EvmMessage); // 处理消息 - 可能会撤销
        }
    
        /// @notice 允许所有者重试失败的消息,以解锁相关的代币。
        /// @param messageId 失败消息的唯一标识符。
        /// @param tokenReceiver 将代币发送到的地址。
        /// @dev 该功能只能由合约所有者调用。它更改消息的状态从 'failed(失败)' 到 'resolved(已解决)',以防止重复条目和多次重试相同的消息
        function retryFailedMessage(
            bytes32 messageId,
            address tokenReceiver
        ) external onlyOwner {
            // 检查消息是否失败;如果没有,则撤销交易。
            if (s_failedMessages.get(messageId) != uint256(ErrorCode.FAILED))
                revert MessageNotFailed(messageId);
    
            // 将错误代码设置为 RESOLVED,以禁止重新进入和多次重试同一失败消息。
            s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED));
    
            // 检索失败消息的内容。
            Client.Any2EVMMessage memory message = s_messageContents[messageId];
    
            // 本示例期望一次发送一枚代币,但你可以处理多个代币。
            // 将关联的代币转移到指定的接收者作为紧急逃生方式。
            IERC20(message.destTokenAmounts[0].token).safeTransfer(
                tokenReceiver,
                message.destTokenAmounts[0].amount
            );
    
            // 发出事件,表明消息已被恢复。
            emit MessageRecovered(messageId);
        }
    
        /// @notice 允许所有者切换模拟撤销的测试。
        /// @param simRevert 如果为 `true`,模拟撤销条件;如果为 `false`,禁用模拟。
        /// @dev 该功能只能由合约所有者调用。
        function setSimRevert(bool simRevert) external onlyOwner {
            s_simRevert = simRevert;
        }
    
        function _ccipReceive(
            Client.Any2EVMMessage memory any2EvmMessage
        ) internal override {
            s_lastReceivedMessageId = any2EvmMessage.messageId; // 获取消息ID
            s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // 解码发送的文本
            // 预期一次转移一个代币,但可以转移多个代币。
            s_lastReceivedTokenAddress = any2EvmMessage.destTokenAmounts[0].token;
            s_lastReceivedTokenAmount = any2EvmMessage.destTokenAmounts[0].amount;
            emit MessageReceived(
                any2EvmMessage.messageId,
                any2EvmMessage.sourceChainSelector, // 获取源链标识符(即选择器)
                abi.decode(any2EvmMessage.sender, (address)), // 解码发送者地址,
                abi.decode(any2EvmMessage.data, (string)),
                any2EvmMessage.destTokenAmounts[0].token,
                any2EvmMessage.destTokenAmounts[0].amount
            );
        }
        
        /// @notice 构建CCIP消息。
        /// @dev 此函数将创建一个EVM2AnyMessage结构体,包含所有必要的信息以实现可编程的代币转移。
        /// @param _receiver 接收者的地址。
        /// @param _text 要发送的字符串数据。
        /// @param _token 要转移的代币。
        /// @param _amount 要转移的代币数量。
        /// @param _feeTokenAddress 用于费用的代币地址。将address(0)设置为使用原生gas。
        /// @return Client.EVM2AnyMessage 返回一个EVM2AnyMessage结构体,其中包含发送CCIP消息所需的信息。
        function _buildCCIPMessage(
            address _receiver,
            string calldata _text,
            address _token,
            uint256 _amount,
            address _feeTokenAddress
        ) private pure returns (Client.EVM2AnyMessage memory) {
            // 设置代币数量
            Client.EVMTokenAmount[]
                memory tokenAmounts = new Client.EVMTokenAmount[](1);
            Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
                token: _token,
                amount: _amount
            });
            tokenAmounts[0] = tokenAmount;
            // 在内存中创建一个 EVM2AnyMessage 结构,包含发送跨链消息所需的信息
            Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
                receiver: abi.encode(_receiver), // ABI 编码的接收地址
                data: abi.encode(_text), // ABI 编码的字符串
                tokenAmounts: tokenAmounts, // 被转移的代币数量和类型
                extraArgs: Client._argsToBytes(
                    // 额外参数,设置 gas 限制
                    Client.EVMExtraArgsV1({gasLimit: 400_000})
                ),
                // 设置 feeToken 为 feeTokenAddress,表示将使用特定资产支付费用
                feeToken: _feeTokenAddress
            });
            return evm2AnyMessage;
        }
    
        /// @notice 该合约接收以太的默认函数。
        /// @dev 该函数没有函数体,使其成为接收以太的默认函数。
        /// 当向合约发送以太时而没有任何数据时,会自动调用此函数。
        receive() external payable {}
    
        /// @notice 允许合约所有者从合约中提取全部以太余额。
        /// @dev 如果没有资金可提取或转账失败,该函数将撤销。
        /// 只能由合约所有者调用。
        /// @param _beneficiary 将以太发送到的地址。
        function withdraw(address _beneficiary) public onlyOwner {
            // 检索此合约的余额
            uint256 amount = address(this).balance;
    
            // 如果没有可提取的内容,撤销
            if (amount == 0) revert NothingToWithdraw();
    
            // 尝试发送资金,捕获成功状态并丢弃任何返回数据
            (bool sent, ) = _beneficiary.call{value: amount}("");
    
            // 如果发送失败,带有尝试转账信息的撤销
            if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
        }
    
        /// @notice 允许合约所有者提取特定ERC20代币的所有代币。
        /// @dev 如果没有代币可提取,则此函数将回滚并显示'NothingToWithdraw'错误。
        /// @param _beneficiary 应将代币发送到的地址。
        /// @param _token 要提取的ERC20代币的合约地址。
        function withdrawToken(
            address _beneficiary,
            address _token
        ) public onlyOwner {
            // 检索此合约的代币余额
            uint256 amount = IERC20(_token).balanceOf(address(this));
    
            // 如果没有可提取的代币,撤销
            if (amount == 0) revert NothingToWithdraw();
    
            // 安全地将代币转移到指定受益人
            IERC20(_token).safeTransfer(_beneficiary, amount);
        }
    }
    
    设置为允许。每个链选择器都可以在
    上找到。
    。

    Hello World! 任何 string

    _token

    0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 这是源链(本例中为 Avalanche Fuji)上的 CCIP-BnM 合约地址。您可以在 上找到每个支持的区块链的所有地址。

    _amount

    1000000000000000 代币数量(对应0.001 CCIP-BnM)。

    支持的网络页面arrow-up-right
    支持的网络页面arrow-up-right
    LINK 代币合约arrow-up-right
    页面arrow-up-right
    解释部分arrow-up-right
    支持的网络页面arrow-up-right
    支持的网络页面arrow-up-right
    支持的网络页面arrow-up-right
    Copy to clipboard
    Copy to clipboard