发送和接收 CCIP 消息所需的最少代码
CCIP 最简架构
总结一下,通过 Chainlink CCIP,可以:
转移(支持的)代币
发送任何类型的数据
发送代币和数据
CCIP 接收者可以是:
EOA(外部拥有的账户)
任何实现了 CCIPReceiver.sol 的智能合约
注意:如果您向 EOA 发送消息和代币,只有代币会到达。
目前,您可以将 CCIP 视为一个“黑盒”组件,并且只需关注 Router 合约。我们将在后续章节中解释 Chainlink CCIP 的架构。
您可以使用任何区块链开发框架来使用 Chainlink CCIP。对于本次大师班,我们准备了 Hardhat、Foundry 和 Remix IDE 的步骤。
让我们创建一个新项目。
确保您已安装 Foundry。要检查,请运行以下命令:
创建一个新文件夹并命名为 ccip-masterclass
:
切换该文件夹:
通过运行以下命令创建一个新的 Foundry 项目:
导航到 https://remix.ethereum.org/ 并点击“Create new Workspace”按钮
或者,您可以克隆以下项目:
要使用 Chainlink CCIP,你需要与 @chainlink/contracts-ccip NPM 包中的 Chainlink CCIP 专用合约进行交互。
要安装它,请按照针对您将用于本次大师班的开发环境的特定步骤进行操作。
选项 1)
我们不能使用 git 子模块来安装 @chainlink/contracts-ccip
,因为这个包的内容并没有作为一个单独的 GitHub 仓库提供。这意味着我们不能运行 forge install
命令。
以下是解决方法:
在 .gitignore
文件中添加以下行
然后在终端中运行以下命令:
最后,在 foundry.toml
文件中添加以下几行:
选项 2) [2023年10月更新]
你可以运行
之后在 foundry.toml
或 remappings.txt
文件中设置 remappings:
创建一个新的 Solidity 文件,并粘贴以下内容。这是一个空合约,只是导入了 @chainlink/contracts-ccip
包中的一个合约。
编译它。如果编译成功并生成了新的 .deps/npm/@chainlink/contracts-ccip
文件夹,这意味着我们已将 @chainlink/contracts-ccip
包导入到 Remix IDE 工作区。
尽管如前所述,CCIP 的发送者和接收者可以是 EOA(外部拥有的账户)和智能合约,并且所有组合都是可能的,我们将要介绍最复杂的情况,即 CCIP 的发送者和接收者都是位于不同区块链上的智能合约。
要发送 CCIP 消息,源区块链上的智能合约必须调用 ccipSend()
函数,该函数定义在IRouterClient.sol
接口中。
正在发送的CCIP消息是来自Client
库中的EVM2AnyMessage
Solidity 结构体。
现在让我们理解一下我们正在发送的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_000 的 gasLimit
并且不启用严格顺序。在生产环境中部署时,请确保 extraArgs
是可更改的。这允许您在链下构建此参数,并在需要时通过函数调用传递或存储在可更新的变量中。这样的设计使得 extraArgs
能够适应未来对CCIP的任何升级。
目标区块链上的智能合约要接收 CCIP 消息,必须实现IAny2EVMMessageReceiver
接口。@chainlink/contracts-ccip NPM 包提供了正确实现此接口的合约,称为 CCIPReceiver.sol
,但在接下来的章节中我们会更多地讨论它。现在,让我们理解在一般场景下必须实现的IAny2EVMMessageReceiver
接口的哪些函数。
如您所见,IAny2EVMMessageReceiver
接口中的ccipReceive()
函数接受来自 Client 库的Any2EVMMessage
结构体对象。这个结构体是接收到的CCIP消息在Solidity中的表现形式。请注意,这个Any2EVMMessage
结构体与我们在源区块链上用于发送的结构体——EVM2AnyMessage
——是不同的。它们并不相同。
现在让我们理解一下我们接收到的Any2EVMMessage
结构体的每个属性代表什么以及如何使用它:
messageId
- CCIP消息ID,在源链生成。
sourceChainSelector
- 源链选择器。
sender
- 发送者地址。如果源链是EVM链,使用abi.decode(sender, (address))
进行解码。
data
- CCIP消息中发送的有效载荷。例如,"Hello, world!"。
tokenAmounts
- 接收到的代币及其在目标链上的表示形式的金额。
回顾一下,下面是所需的最小架构图,用于发送和接收Chainlink CCIP消息: