> For the complete documentation index, see [llms.txt](https://cll-devrel.gitbook.io/ccip-bootcamp/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://cll-devrel.gitbook.io/ccip-bootcamp/mandarin-ccip-bootcamp/di-1-tian/lian-xi-1-shi-yong-fang-yu-xing-shi-li-mo-shi-jin-xing-ke-bian-cheng-dai-bi-zhuan-yi.md).

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

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

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

**开始之前**

1. 您应该了解如何编写、编译、部署和资助智能合约。如果您需要复习基础知识，请阅读[本教程](https://docs.chain.link/quickstarts/deploy-your-first-contract)，它将指导您使用 [Solidity 编程语言](https://soliditylang.org/)，在 [MetaMask 钱包](https://metamask.io/)中交互以及在 [Remix 开发环境](https://remix.ethereum.org/)中工作。
2. 您的账户必须在 *Avalanche Fuji* 上有一些 AVAX 和 LINK 代币，在以太坊 Sepolia 上有一些 ETH 代币。了解如何 [获取测试网 LINK](https://docs.chain.link/resources/acquire-link)。
3. 查看[支持的网络页面](https://docs.chain.link/ccip/supported-networks)，确认您要传输的代币是否支持您的通道。在这个例子中，您将从 Avalanche Fuji 传输代币到以太坊 Sepolia，所以请在[这里](https://docs.chain.link/ccip/supported-networks/v1_2_0/testnet#avalanche-fuji-ethereum-sepolia)检查支持的代币列表。
4. 了解如何 [获取 CCIP 测试代币](https://docs.chain.link/ccip/test-tokens#mint-test-tokens)。按照本指南操作后，您应该拥有 CCIP-BnM 代币，并且 CCIP-BnM 应该出现在 MetaMask 中您的代币列表中。
5. 了解如何 [资助您的合约](https://docs.chain.link/resources/fund-your-contract)。本指南展示了如何使用 LINK 资助您的合约，但您可以使用相同的指南为合约资助任何 ERC20 代币，只要它们出现在 MetaMask 的代币列表中。
6. 按照前一个教程：使用 [数据传输代币](https://docs.chain.link/ccip/tutorials/programmable-token-transfers) 来学习如何使用 CCIP 进行可编程代币传输。

**编码时间！**

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

*正确估计您的 gas 限制*

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

<pre class="language-solidity"><code class="lang-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 {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 &#x3C; 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消息所需的信息。
<strong>    function _buildCCIPMessage(
</strong>        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);
    }
}

</code></pre>

## 部署您的合约

要使用此合约：

1. [在 Remix 中打开合约](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/ProgrammableDefensiveTokenTransfers.sol)。
2. 编译您的合约。
3. 部署并在 *Avalanche Fuji* 上资助您的发送者合约，并启用向*以太坊 Sepolia* 发送消息的功能：
   1. 打开 MetaMask 并选择网络 *Avalanche Fuj*i。
   2. 在 Remix IDE 中，点击 *Deploy & Run Transactions*，从环境列表中选择 *Injected Provider - MetaMask*。然后 Remix 将与您的 MetaMask 钱包交互，以与 *Avalanche Fuji* 通信。
   3. 填写您的区块链路由器和 LINK 合约地址。可以在 [支持的网络页面](https://docs.chain.link/ccip/supported-networks#overview) 上找到路由器地址，在 [LINK 代币合约](https://docs.chain.link/ccip/supported-networks)[页面 ](https://docs.chain.link/resources/link-token-contracts)上找到 LINK 合约地址。对于 Avalanche Fuji，路由器地址是 `0xF694E193200268f9a4868e4Aa017A0118C9a8177`，LINK 合约地址是 `0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846`。
   4. 点击 `transact` 按钮。确认交易后，合约地址将出现在 *已部署合约* 列表中。记下您的合约地址。&#x20;
   5. 打开 MetaMask 并用 CCIP-BnM 代币为您的合约注资。您可以向您的合约转账 `0.002` *CCIP-BnM*。
   6. 启用您的合约以向 *以太坊 Sepolia* 发送 CCIP 消息：
      1. 在 Remix IDE 中，转到 *Deploy & Run Transactions*，在您部署在 *Avalanche Fuji* 上的智能合约的函数列表中。
      2. 调用 `allowlistDestinationChain`，传入 `16015286601757825753` 作为目标链选择器，以及 `true` 设置为允许。每个链选择器都可以在 [支持的网络页面](https://docs.chain.link/ccip/supported-networks) 上找到。
4. 在 *以太坊 Sepolia* 上部署您的接收者合约，并启用从您的发送者合约接收消息：
   1. 打开 MetaMask 并选择网络 *以太坊 Sepolia*。
   2. 在 Remix IDE 中，转到 *Deploy & Run Transactions*，确保环境仍然是 *Injected Provider - MetaMask*。
   3. 填写您的区块链路由器和 LINK 合约地址。可以在 [支持的网络页面](https://docs.chain.link/ccip/supported-networks#overview) 上找到路由器地址，在 [LINK 代币合约](https://docs.chain.link/ccip/supported-networks)[页面](https://docs.chain.link/resources/link-token-contracts) 上找到 LINK 合约地址。对于以太坊 Sepolia，路由器地址是 `0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59`，LINK 合约地址是 `0x779877A7B0D9E8603169DdbD7836e478b4624789`。
   4. 点击 `transact` 按钮。确认交易后，合约地址将出现在 *已部署合约列表* 中。记下您的合约地址。
   5. 启用您的合约以接收来自 *Avalanche Fuji* 的 CCIP 消息：:
      1. 在 Remix IDE 中，转到 *Deploy & Run Transactions*，打开您部署在 *以太坊 Sepolia* 上的智能合约的函数列表。
      2. 调用 `allowlistSourceChain`，传入 `14767482510784806043` 作为源链选择器，以及 `true` 设置为允许。每个链选择器都可以在 [支持的网络页面](https://docs.chain.link/ccip/supported-networks) 上找到。
   6. 启用您的合约以接收来自您部署在 *Avalanche Fuji* 上的合约的 CCIP 消息：
      1. 在 Remix IDE 中，转到 *Deploy & Run Transactions*，打开您部署在 *以太坊 Sepolia* 上的智能合约的函数列表。
      2. 调用 `allowlistSender`，传入您部署在 *Avalanche Fuji* 上的合约地址，并设置为 `true`。
   7. 调用 `setSimRevert` 函数，传入 `true` 作为参数，然后等待交易确认。将 `s_simRever`t 设置为 `true` 会在处理接收到的消息时模拟失败。更多详情请参阅[解释部分](https://docs.chain.link/ccip/tutorials/programmable-token-transfers-defensive#explanation)。

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

**注意**：另一项安全措施强制只有路由器可以调用 \_ccipReceive 函数。更多详情请参阅[解释部分](https://docs.chain.link/ccip/tutorials/programmable-token-transfers-defensive#explanation)。

## 恢复被锁定的代币

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

1. 打开 MetaMask 并连接到 *Avalanche Fuji*。用 LINK 代币为您的合约注资。您可以向您的合约转账 `0.5` LINK。在此示例中，LINK 用于支付 CCIP 费用。
2. 从 *Avalanche Fuji* 发送带有代币的字符串数据：:
   1. 打开 MetaMask 并选择网络 *Avalanche Fuji*.
   2. 在 Remix IDE 中，转到 *Deploy & Run Transactions*，打开您部署在 *Avalanche Fuji* 上的智能合约的函数列表。
   3. 填写 ***sendMessagePayLINK*** 函数的参数:

      <br>

      <table><thead><tr><th width="265">参数</th><th>值和描述</th></tr></thead><tbody><tr><td>_destinationChainSelector</td><td><code>16015286601757825753</code> 这是目标区块链的CCIP链标识符（在这个例子中是 <em>以太坊 Sepolia</em>）。您可以在 <a href="https://docs.chain.link/ccip/supported-networks">支持的网络页面</a> 上找到每个链选择器。</td></tr><tr><td>_receiver</td><td>您在 <em>以太坊 Sepolia</em> 上的接收者合约地址。 目标合约地址。</td></tr><tr><td>_text</td><td><code>Hello World!</code><img src="https://docs.chain.link/assets/icons/copyIcon.svg" alt="Copy to clipboard"><br>任何 <code>string</code></td></tr><tr><td>_token</td><td><code>0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4</code> 这是源链（本例中为 <em>Avalanche Fuji</em>）上的 <em>CCIP-BnM</em> 合约地址。您可以在 <a href="https://docs.chain.link/ccip/supported-networks">支持的网络页面</a> 上找到每个支持的区块链的所有地址。</td></tr><tr><td>_amount</td><td><code>1000000000000000</code><img src="https://docs.chain.link/assets/icons/copyIcon.svg" alt="Copy to clipboard"><br>代币数量（对应0.001 CCIP-BnM）。</td></tr></tbody></table>
   4. 点击 `transact` 并在 MetaMask 上确认交易。
   5. 交易成功后，请记录交易哈希。以下是在`Avalanche Fuji`上进行交易的一个[示例](https://testnet.snowtrace.io/tx/0x4c7a192fa5636557569d076c06633c4f06140f117a44b49f21628eedd72b8423)。

注意

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

3. 打开[CCIP浏览器](https://ccip.chain.link/)，通过交易哈希搜索你的跨链交易。<br>

<figure><img src="https://docs.chain.link/images/ccip/tutorials/ccip-explorer-send-tokens-message-pay-link-tx-details-defensive.webp" alt=""><figcaption></figcaption></figure>

4. CCIP交易在状态被标记为“Success”时完成。在这个例子中，CCIP消息ID是*0x120367995ef71f83d64a05bd7793862afda9d04049da4cb32851934490d03ae4*。<br>

<figure><img src="https://docs.chain.link/images/ccip/tutorials/ccip-explorer-send-tokens-message-pay-link-tx-details-success-defensive.webp" alt=""><figcaption></figcaption></figure>

5. 检查目标链上的接收方合约：
   1. 打开 MetaMask 并选择 *Ethereum Sepolia* 网络。
   2. 在 Remix IDE 中，在“部署与运行交易”（Deploy & Run Transactions）部分，打开您在*以太坊 Sepolia* 上部署的智能合约的函数列表。
   3. 调用 `getFailedMessages` 函数，使用偏移量（offset）为 `0` 和限制（limit）为 `1`，以检索第一个失败的消息。<br>

      <figure><img src="https://docs.chain.link/images/ccip/tutorials/ccip-explorer-send-tokens-lastfailedmessageids-defensive.webp" alt=""><figcaption></figcaption></figure>
   4. 请注意返回的值有：*0x120367995ef71f83d64a05bd7793862afda9d04049da4cb32851934490d03ae4*（消息ID）和 *1*（表示失败的错误代码）。
6. 为了恢复被锁定的代币，请调用 `retryFailedMessage` 函数：

| 参数              | 描述          |
| --------------- | ----------- |
| `messageId`     | 失败消息的唯一标识符。 |
| `tokenReceiver` | 代币将被发送到的地址。 |

<figure><img src="https://docs.chain.link/images/ccip/tutorials/ccip-explorer-ccip-retry-failed-defensive.webp" alt=""><figcaption></figcaption></figure>

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

   <figure><img src="https://docs.chain.link/images/ccip/tutorials/ccip-explorer-ccip-retry-failed-tokens-transferred-defensive.webp" alt=""><figcaption></figcaption></figure>
8. 再次调用 `getFailedMessages` 函数，使用偏移量（offset）为 `0` 和限制（limit）为 `1`，以检索第一个失败的消息。请注意，现在错误代码是 *0*，表示该消息已经解决。

   <br>

   <figure><img src="https://docs.chain.link/images/ccip/tutorials/ccip-explorer-send-tokens-lastfailedmessageids-defensive-recovered.webp" alt=""><figcaption></figcaption></figure>

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

## 说明

本教程中展示的智能合约旨在与CCIP交互，以传输和接收代币和数据。该合约代码与 [*传输代币及数据教程*](https://docs.chain.link/ccip/tutorials/programmable-token-transfers#overview) 中的代码相似，因此，您可以参阅 [其代码解释](https://docs.chain.link/ccip/tutorials/programmable-token-transfers#explanation)。我们只将解释主要的不同之处。

### 发送消息

`sendMessagePayLINK` 函数与 [*传输代币及数据*](https://docs.chain.link/ccip/tutorials/programmable-token-transfers) 教程中的 `sendMessagePayLINK` 函数类似。主要的区别在于增加了gas限制，以适应处理错误逻辑所需的额外gas。

### 接收和处理消息

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

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

1. 通过`ccipReceive`进入：
   * `ccipReceive`函数被调用，传入一个包含待处理消息的Any2EVMMessage结构体。
   * 安全检查确保调用来自授权的路由器、已允许的源链和已允许的发送者。
2. 处理消息：
   * `ccipReceive`调用`processMessage`函数，该函数是外部的，以利用Solidity的try/catch错误处理机制。注意：`onlySelf`修饰符确保只有合约本身可以调用此函数。
   * 在`processMessage`内部，使用`s_simRevert`状态变量检查是否模拟回滚条件。这个模拟由合约所有者调用的`setSimRevert`函数来切换。 如
   * 果`s_simRevert`为false，`processMessage`调用`_ccipReceive`函数进行进一步的消息处理。
3. 在`_ccipReceive`中处理消息：
   * `_ccipReceive`从消息中提取并存储各种信息，如messageId、解码后的发送者地址、代币数量和数据。
   * 然后发出一个`MessageReceived`事件，表示消息处理成功。
4. 错误处理：
   * 如果处理过程中发生错误（或触发模拟回滚），`ccipReceive`内的catch块将被执行。
   * 失败消息的messageId被添加到`s_failedMessages`中，消息内容被存储在`s_messageContents`中。
   * 发出一个`MessageFailed`事件，允许以后识别和重处理失败的消息。

### 重新处理失败的消息

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

1. 初始化:
   * 只有合约所有者可以调用此函数，提供失败消息的 `messageId` 和用于代币恢复的 `tokenReceiver` 地址。
2. 验证:
   * &#x20;它使用 `s_failedMessages.get(messageId)` 检查消息是否已失败。如果没有，它将回滚交易。
3. 状态更新:
   * 将消息的错误代码更新为 `RESOLVED`，以防止重复条目和多次重试。
4. 代币恢复:
   * 使用 `s_messageContents[messageId]` 检索失败的消息内容。
   * &#x20;将与失败消息关联的锁定代币转移到指定的 `tokenReceiver`，作为一个逃生舱口，而无需再次处理整个消息。
5. 事件发出:
   * 发出一个 `MessageRecovered` 事件，用以标示代币恢复操作已成功完成。

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