如何使用 Chainlink CCIP

发送和接收 CCIP 消息所需的最少代码

CCIP 最简架构

总结一下,通过 Chainlink CCIP,可以:

  • 转移(支持的)代币

  • 发送任何类型的数据

  • 发送代币和数据

CCIP 接收者可以是:

  • EOA(外部拥有的账户)

  • 任何实现了 CCIPReceiver.sol 的智能合约

注意:如果您向 EOA 发送消息和代币,只有代币会到达。

目前,您可以将 CCIP 视为一个“黑盒”组件,并且只需关注 Router 合约。我们将在后续章节中解释 Chainlink CCIP 的架构。

开始入门

您可以使用任何区块链开发框架来使用 Chainlink CCIP。对于本次大师班,我们准备了 Hardhat、Foundry 和 Remix IDE 的步骤。

让我们创建一个新项目。

确保您已安装 Node.jsNPM 。要检查,请运行以下命令:

node -v
npm -v

创建一个新文件夹并命名为 ccip-masterclass

mkdir ccip-masterclass

切换该文件夹:

cd ccip-masterclass

通过运行以下命令创建一个新的 Hardhat 项目:

npx hardhat@2.14.1 init

然后选择 “Create a JavaScript project” 或 “Create a TypeScript project”。

或者,您可以克隆以下项目:

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

要安装它,请按照针对您将用于本次大师班的开发环境的特定步骤进行操作。

npm i @chainlink/contracts-ccip --save-dev

基础接口

尽管如前所述,CCIP 的发送者和接收者可以是 EOA(外部拥有的账户)和智能合约,并且所有组合都是可能的,我们将要介绍最复杂的情况,即 CCIP 的发送者和接收者都是位于不同区块链上的智能合约。

源区块链

要发送 CCIP 消息,源区块链上的智能合约必须调用 ccipSend()函数,该函数定义在IRouterClient.sol接口中。

// SOURCE BLOCKCHAIN 

interface IRouterClient {
    /// @notice 请求发送 CCIP 消息到目标链
    /// @param destinationChainSelector 目标链选择器
    /// @param message 包含数据和/或代币的跨链 CCIP 消息
    /// @return messageId 消息 ID
    function ccipSend(
        uint64 destinationChainSelector,
        Client.EVM2AnyMessage calldata message
    ) external payable returns(bytes32 messageId);
}

正在发送的CCIP消息是来自Client库中的EVM2AnyMessage Solidity 结构体。

// SOURCE BLOCKCHAIN

library Client {
    struct EVM2AnyMessage {
        bytes receiver; // 目标 EVM 链上的接收者地址,使用 abi.encode(receiver address)
        bytes data; // 数据负载
        EVMTokenAmount[] tokenAmounts; // 转移的代币地址和数量
        address feeToken; // 费用代币地址;address(0) 表示你发送 msg.value
        bytes extraArgs; // 使用 _argsToBytes(EVMExtraArgsV1) 填充此字段
    }
    
    struct EVMTokenAmount {
        address token; // 本地区块链上的代币地址
        uint256 amount;
    }
    
    struct EVMExtraArgsV1 {
        uint256 gasLimit;
        bool strict;
    }
}

现在让我们理解一下我们正在发送的EVM2AnyMessage结构体的每个属性代表什么以及如何使用它。

receiver (接收者)

接收者合约的地址。它可以是智能合约或外部拥有的账户(EOA)。使用abi.encode(receiver)将地址编码为 Solidity 的字节数据类型。

data (数据)

通过 CCIP 消息发送的数据。这正是我们所说的 CCIP 消息可以携带的“任意类型的数据”。这些数据可以简单到像是“Hello, world!”这样的文本,也可以复杂到 Solidity 编写的结构体或者函数的签名。

tokenAmount (代币数量)

源链上表示的代币及其数量。这里我们指定了我们正在发送的代币(出自支持的代币)以及数量。这是一个EVMTokenAmount结构体数组,它只包含两个属性:

  • token(代币)- 我们在本地(源)区块链上发送的代币的地址

  • amount(数量) 我们正在发送的代币数量。发送者必须批准CCIP路由器代表发送者花费这个数量,否则对ccipSend函数的调用将会回滚。

目前,单一 CCIP 发送交易中可以发送的代币数量最多为5个。

feeToken (费用代币)

feeToken 是费用代币的地址。CCIP 支持使用 LINK 以及包括原生区块链原生代币(能支付gas fee的代币)和它们的 ERC20 包装版本在内的替代资产来支付费用。对开发者而言,这意味着你可以在源链上轻松支付费用,而 CCIP 将负责在目标链上执行操作。若要使用原生代币支付,例如在以太坊上的 ETH 或者在 Polygon 上的 MATIC,可以将 feeToken 设置为address(0)。即便使用原生资产支付费用,Chainlink 去中心化预言机网络(DON)中的节点只会得到 LINK 作为奖励

extraArgs (额外参数)

用户填写EVMExtraArgsV1结构体,然后使用_argsToBytes函数将其编码为字节。该结构体由两个属性组成:

  • gasLimit - CCIP 在目标区块链上的合约执行ccipReceive()可以消耗的最大 gas 数量。未用完的 gas 不会退还。这意味着,例如,如果你将代币发送到 EOA,你应该把gasLimit值设为0,因为EOA 无法实现ccipReceive()(或任何其他)函数。为了估计目标合约的准确 gas limit(要使用的 gas 数量),请考虑利用 Ethereum 客户端 RPC 通过在receiver.ccipReceive()函数上应用eth_estimateGas,或者使用 Hardhat插件进行gas测试 ,或者进行 Foundry gas测试

  • strict - 用于严格排序。你应该将其设置为false。CCIP将始终按发送顺序处理来自特定发送者到特定目标区块链的消息。如果你在消息的extraArgs部分设置strict: true,并且如果ccipReceive失败(回滚),它将阻止来自同一发送者的任何后续消息被处理,直到当前消息成功执行。使用此功能时应该非常小心,以避免无意中停止发送者的消息被处理。严格排序功能目前是实验性的,未来不保证其维护或进一步开发。

如果未指定 extraArgs,即设置为 extraArgs: "",默认情况下将应用 200_000gasLimit 并且不启用严格顺序。在生产环境中部署时,请确保 extraArgs 是可更改的。这允许您在链下构建此参数,并在需要时通过函数调用传递或存储在可更新的变量中。这样的设计使得 extraArgs 能够适应未来对CCIP的任何升级。

目标区块链

目标区块链上的智能合约要接收 CCIP 消息,必须实现IAny2EVMMessageReceiver接口。@chainlink/contracts-ccip NPM 包提供了正确实现此接口的合约,称为 CCIPReceiver.sol,但在接下来的章节中我们会更多地讨论它。现在,让我们理解在一般场景下必须实现的IAny2EVMMessageReceiver接口的哪些函数。

// DESTINATION BLOCKCHAIN

/// @notice 打算从路由器接收消息的应用合约应实现此接口。
interface IAny2EVMMessageReceiver {
    /// @notice 路由器调用此函数以传递消息
    /// @param message CCIP 消息
    /// @dev 注意确保你检查 `msg.sender` 是路由器
    function ccipReceive(Client.Any2EVMMessage calldata message) external;
}

如您所见,IAny2EVMMessageReceiver接口中的ccipReceive()函数接受来自 Client 库的Any2EVMMessage结构体对象。这个结构体是接收到的CCIP消息在Solidity中的表现形式。请注意,这个Any2EVMMessage结构体与我们在源区块链上用于发送的结构体——EVM2AnyMessage——是不同的。它们并不相同。

// DESTINATION BLOCKCHAIN

library Client {
    struct Any2EVMMessage {
        bytes32 messageId; // 源链上的 `ccipSend` 对应的消息 ID
        uint64 sourceChainSelector; // 源链选择器
        bytes sender; // 如果来自 EVM 链,使用 `abi.decode(sender)`
        bytes data; // 在原始消息中发送的负载
        EVMTokenAmount[] tokenAmounts; // 在目标链上的代币及其数量
    }
    
    struct EVMTokenAmount {
        address token; // 本地区块链上的代币地址
        uint256 amount;
    }
}

现在让我们理解一下我们接收到的Any2EVMMessage结构体的每个属性代表什么以及如何使用它:

  • messageId - CCIP消息ID,在源链生成。

  • sourceChainSelector - 源链选择器。

  • sender - 发送者地址。如果源链是EVM链,使用abi.decode(sender, (address))进行解码。

  • data - CCIP消息中发送的有效载荷。例如,"Hello, world!"。

  • tokenAmounts - 接收到的代币及其在目标链上的表示形式的金额。

回顾一下,下面是所需的最小架构图,用于发送和接收Chainlink CCIP消息:

Last updated