基于CCIP的Base跨链桥开发
本文介绍如何使用 Chainlink 的跨链互操作性协议 (CCIP) 将消息和代币从Base链的智能合约发送到不同链上的另一个智能合约。
一键发币: SOL | BNB | ETH | BASE | Blast | ARB | OP | POLYGON | AVAX | FTM | OK
本教程介绍如何使用 Chainlink 的跨链互操作性协议 (CCIP) 将消息和代币从Base链的智能合约发送到不同链上的另一个智能合约。
在本教程结束时,你应该能够执行以下操作:
- 使用 Foundry 为 Base 设置智能合约项目
- 安装 Chainlink CCIP 作为依赖项
- 在你的智能合约中使用 Chainlink CCIP 向其他不同链上的合约发送消息和/或代币
- 在 Base 测试网上部署和测试你的智能合约
Chainlink CCIP 处于“早期访问”开发阶段,这意味着本教程中描述的一些功能正在开发中,并且可能会在后续版本中发生变化
1、先决条件
本教程要求你安装 Foundry。
- 从命令行(终端)运行:
curl -L https://foundry.paradigm.xyz | bash
- 然后运行
foundryup
,安装最新的(夜间)Foundry 版本
有关更多信息,请参阅 Foundry Book 安装指南。
为了部署智能合约,你首先需要一个钱包。可以通过下载 Coinbase Wallet 浏览器扩展来创建钱包。
对于本教程,你需要在 Base Goerli 和 Optimism Goerli 上使用 ETH 和 LINK 为你的钱包提供资金。
ETH 是支付与将智能合约部署到区块链相关的 gas 费用所必需的,LINK 代币是支付使用 CCIP 时的相关费用所必需的。
- 要在 Base Goerli 上使为你的钱包充值ETH,请访问 Base Faucets 页面上列出的水龙头。
- 要在 Optimism Goerli 上为你的钱包充值ETH,请访问 Optimism Faucets 页面上列出的水龙头。
- 要为你的钱包充值LINK,请访问 Chainlink Faucet。
如果有兴趣在主网上构建,你需要申请 Chainlink CCIP 主网访问权限。
2、什么是 Chainlink CCIP?
Chainlink CCIP(跨链互操作性协议)提供了一种跨不同链发送消息数据和转移代币的解决方案。
用户与 Chainlink CCIP 交互的主要方式是通过称为路由器的智能合约。路由器合约负责启动跨链交互。用户可以与路由器交互以执行以下跨链功能:
功能 | 描述 | 支持的接收器 |
---|---|---|
任意消息传递 | 将任意(编码)数据从一个链发送到另一个链。 | 仅限智能合约 |
代币转移 | 将代币从一个链发送到另一个链。 | 智能合约或 EOA |
可编程代币转移 | 在单个交易中将代币和任意(编码)数据从一个链发送到另一个链。 | 仅限智能合约 |
危险!:EVM 区块链上的外部拥有账户 (EOA) 无法接收消息数据,因此,在发送任意消息或可编程代币转移时,仅支持智能合约作为接收方。任何试图向 EOA 发送可编程代币转移(数据和代币)的行为,都会导致只接收代币。
3、Chainlink CCIP高级概念
虽然路由器是用户使用 CCIP 时与之交互的主要接口,但本节将介绍跨链交互指令发送到路由器后发生的情况。
- OnRamps
一旦路由器收到跨链交互指令,它就会将其传递给另一个称为 OnRamp 的合约。
OnRamps 负责各种任务,包括:验证消息大小和 gas 限制、保留消息的顺序、管理任何费用支付以及与代币池交互以在进行代币转移时锁定或销毁代币。
- OffRamps
目标链将有一个称为 OffRamp 的合约。 OffRamps 负责各种任务,包括:确保消息的真实性、确保每个交易只执行一次以及将收到的消息传输到目标链上的路由器合约。
- 代币池
代币池(token pool)是 ERC-20 代币上的抽象层,可促进 OnRamp 和 OffRamp 代币相关操作。它们配置为使用锁定和解锁或销毁和铸造机制,具体取决于代币的类型。
例如,由于区块链原生 gas 代币(即 ETH、MATIC、AVAX)只能在其原生链上铸造,因此必须使用锁定和铸造机制。此机制将代币锁定在源链上,并在目标链上铸造合成资产。
相比之下,可以在多个链上铸造的代币(即 USDC、USDT、FRAX 等)代币池可以使用销毁和铸造机制,其中代币在源链上被销毁并在目标链上铸造。
- 风险管理网络
在跨链交互的指令从源链上的 OnRamp 到目标链上的 OffRamp 之间,它将通过风险管理网络(risk management network)。风险管理网络是使用各种链下和链上组件构建的二级验证服务,负责监控所有链上的异常活动。
本教程无法深入介绍每个组件的技术细节,但如果有兴趣,你可以通过访问 Chainlink 文档了解更多信息。
4、创建项目
开始之前,你需要设置智能合约开发环境。你可以使用 Hardhat 或 Foundry 等工具设置开发环境。在本教程中,你将使用 Foundry。
要创建新的 Foundry 项目,首先创建一个新目录:
mkdir myproject
然后运行:
cd myproject
forge init
这将创建一个具有以下基本布局的 Foundry 项目:
.
├── foundry.toml
├── script
├── src
└── test
你可以删除项目生成的 src/Counter.sol、test/Counter.t.sol 和 script/Counter.s.sol 样板文件,因为不再需要它们。
5、安装 Chainlink 智能合约
要在Foundry 项目中使用 Chainlink CCIP,你需要使用 forge install
安装 Chainlink CCIP 智能合约作为项目依赖项。
要安装 Chainlink CCIP 智能合约,请运行:
forge install smartcontractkit/ccip --no-commit
安装后,通过添加以下行来更新你的 foundry.toml 文件:
remappings = ['@chainlink/contracts-ccip/=lib/ccip/contracts']
6、编写智能合约
Chainlink CCIP 最基本的用例是在不同区块链上的智能合约之间发送数据和/或代币。为了实现这一点,在本教程中,你需要创建两个单独的智能合约:
Sender
合约:与 CCIP 交互以发送数据和代币的智能合约。Receiver
合约:与 CCIP 交互以接收数据和代币的智能合约。
6.1 创建Sender合约
以下代码片段是使用 CCIP 发送数据的基本智能合约:
pragma solidity ^0.8.0;
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.3/contracts/token/ERC20/IERC20.sol";
contract Sender is OwnerIsCreator {
IRouterClient private router;
IERC20 private linkToken;
/// @notice Initializes the contract with the router and LINK token address.
/// @param _router The address of the router contract.
/// @param _link The address of the link contract.
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = IERC20(_link);
}
/// @notice Sends data to receiver on the destination chain.
/// @param destinationChainSelector The identifier (aka selector) for the destination blockchain.
/// @param receiver The address of the recipient on the destination blockchain.
/// @param text The string text to be sent.
/// @return messageId The ID of the message that was sent.
function sendMessage(
uint64 destinationChainSelector,
address receiver,
string calldata text
) external onlyOwner returns (bytes32 messageId) {
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(receiver), // Encode receiver address
data: abi.encode(text), // Encode text to send
tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array indicating no tokens are being sent
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 200_000}) // Set gas limit
),
feeToken: address(linkToken) // Set the LINK as the feeToken address
});
// Get the fee required to send the message
uint256 fees = router.getFee(
destinationChainSelector,
message
);
// Revert if contract does not have enough LINK tokens for sending a message
require(linkToken.balanceOf(address(this)) > fees, "Not enough LINK balance");
// Approve the Router to transfer LINK tokens on contract's behalf in order to pay for fees in LINK
linkToken.approve(address(router), fees);
// Send the message through the router
messageId = router.ccipSend(destinationChainSelector, message);
// Return the messageId
return messageId;
}
}
在项目的 src/ 目录下创建一个名为 Sender.sol
的新文件,并将上述代码复制到文件中。
以下部分详细说明了上述 Sender
合约的代码。
初始化合约
为了使用 CCIP 发送数据,发送方合约需要访问以下依赖项:
- 路由器合约:此合约是使用 CCIP 发送和接收消息和代币时的主要接口。
- 费用代币合约:此合约是用于在发送消息和代币时支付费用的代币的合约。在本教程中,使用 LINK 代币的合约地址。
路由器合约地址和 LINK 代币地址作为参数传递给合约的构造函数,并作为成员变量存储,以便稍后发送消息和支付任何相关费用。
contract Sender is OwnerIsCreator {
IRouterClient private router;
IERC20 private linkToken;
/// @notice Initializes the contract with the router and LINK token address.
/// @param _router The address of the router contract.
/// @param _link The address of the link contract.
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = IERC20(_link);
}
Router 合约提供了两种重要的方法,可以在使用 CCIP 发送消息时使用:
getFee
:给定一个链选择器和消息,返回发送消息所需的费用金额。ccipSend
:给定一个链选择器和消息,通过路由器发送消息并返回关联的消息 ID。
下一节介绍如何使用这些方法将消息发送到另一个链。
发送消息
Sender 合约定义了一个名为 sendMessage
的自定义方法,该方法首先使用客户端 CCIP 库提供的 EVM2AnyMessage
方法构造消息,使用以下数据:
- receiver:接收方合约地址(编码)。
- data:要随消息发送的文本数据(编码)。
- tokenAmounts:要随消息发送的代币数量。对于仅发送任意消息,此字段定义为空数组(new Client.EVMTokenAmount),表示不会发送任何代币。
- extraArgs:与消息相关的额外参数,例如 gasLimit。
- feeToken:用于支付费用的代币地址。
接下来该方法使用 Router 合约提供的 getFee
方法获取发送消息所需的费用,然后检查合约是否持有足够数量的代币来支付费用。如果没有,则撤销交易。如果有则批准 Router 合约代表发送方合约转移代币以支付费用,然后使用 Router 合约的 ccipSend
方法将消息发送到指定链,最后返回与发送消息关联的唯一 ID。
/// @param receiver The address of the recipient on the destination blockchain.
/// @param text The string text to be sent.
/// @return messageId The ID of the message that was sent.
function sendMessage(
uint64 destinationChainSelector,
address receiver,
string calldata text
) external onlyOwner returns (bytes32 messageId) {
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(receiver), // Encode receiver address
data: abi.encode(text), // Encode text to send
tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array indicating no tokens are being sent
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 200_000}) // Set gas limit
),
feeToken: address(linkToken) // Set the LINK as the feeToken address
});
// Get the fee required to send the message
uint256 fees = router.getFee(
destinationChainSelector,
message
);
// Revert if contract does not have enough LINK tokens for sending a message
require(linkToken.balanceOf(address(this)) > fees, "Not enough LINK balance");
// Approve the Router to transfer LINK tokens on contract's behalf in order to pay for fees in LINK
linkToken.approve(address(router), fees);
// Send the message through the router
messageId = router.ccipSend(destinationChainSelector, message);
// Return the messageId
return messageId;
}
6.2 创建Receiver合约
以下代码片段是使用 CCIP 接收数据的基本智能合约:
pragma solidity ^0.8.0;
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";
contract Receiver is CCIPReceiver {
bytes32 private _messageId;
string private _text;
/// @notice Constructor - Initializes the contract with the router address.
/// @param router The address of the router contract.
constructor(address router) CCIPReceiver(router) {}
/// @notice Handle a received message
/// @param message The cross-chain message being received.
function _ccipReceive(
Client.Any2EVMMessage memory message
) internal override {
_messageId = message.messageId; // Store the messageId
_text = abi.decode(message.data, (string)); // Decode and store the message text
}
/// @notice Gets the last received message.
/// @return messageId The ID of the last received message.
/// @return text The last received text.
function getMessage()
external
view
returns (bytes32 messageId, string memory text)
{
return (_messageId, _text);
}
}
在项目的 src/ 目录下创建一个名为 Receiver.sol
的新文件,并将上述代码复制到该文件中。
以下部分详细说明了上述 Receiver 合约的代码。
初始化合约
为了使用 CCIP 接收数据,Receiver 合约需要扩展到 CCIPReceiver
接口。扩展此接口允许 Receiver 合约使用构造函数中的路由器地址初始化合约,如下所示:
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
contract Receiver is CCIPReceiver {
/// @notice Constructor - Initializes the contract with the router address.
/// @param router The address of the router contract.
constructor(address router) CCIPReceiver(router) {}
}
接收消息
扩展 CCIPReceiver
接口还允许接收方合约在收到消息时覆盖 _ccipReceive
处理程序方法并定义自定义逻辑。
/// @notice Handle a received message
/// @param message The cross-chain message being received.
function _ccipReceive(
Client.Any2EVMMessage memory message
) internal override {
// Add custom logic here
}
本教程中的 Receiver
合约提供了自定义逻辑,将消息 ID 和文本(已解码)存储为成员变量。
contract Receiver is CCIPReceiver {
bytes32 private _messageId;
string private _text;
/// @notice Constructor - Initializes the contract with the router address.
/// @param router The address of the router contract.
constructor(address router) CCIPReceiver(router) {}
/// @notice Handle a received message
/// @param message The cross-chain message being received.
function _ccipReceive(
Client.Any2EVMMessage memory message
) internal override {
_messageId = message.messageId; // Store the messageId
_text = abi.decode(message.data, (string)); // Decode and store the message text
}
}
检索消息
接收方合约定义了一个名为 getMessage
的自定义方法,该方法返回最后收到的消息 _messagId
和 _text
的详细信息。 _ccipReceive
收到新消息后,可以调用此方法来获取消息数据详细信息。
/// @notice Gets the last received message.
/// @return messageId The ID of the last received message.
/// @return text The last received text.
function getMessage()
external
view
returns (bytes32 messageId, string memory text)
{
return (_messageId, _text);
}
7、编译智能合约
要编译智能合约,请运行:
forge build
8、部署智能合约
在将你的智能合约部署到 Base 网络之前,你需要设置一个钱包作为部署者。为此,可以使用 cast wallet import
命令将钱包的私钥导入 Foundry 的安全加密密钥库:
cast wallet import deployer --interactive
运行上述命令后,系统将提示你输入私钥以及用于签署交易的密码。
注意
有关如何从 Coinbase Wallet 获取私钥的说明,请访问 Coinbase Wallet 文档。切勿将其提交到公共存储库,这一点至关重要。
要确认钱包已作为部署者帐户导入到 Foundry 项目中,请运行:
cast wallet list
8.1 设置环境变量
要设置你的环境,请在项目的主目录中创建一个 .env
文件,并为 Base Goerli 和 Optimism Goerli 测试网添加 RPC URL、CCIP 链选择器、CCIP 路由器地址和 LINK 代币地址:
BASE_GOERLI_RPC="https://goerli.base.org"
OPTIMISM_GOERLI_RPC="https://goerli.optimism.io"
BASE_GOERLI_CHAIN_SELECTOR=5790810961207155433
OPTIMISM_GOERLI_CHAIN_SELECTOR=2664363617261496610
BASE_GOERLI_ROUTER_ADDRESS="0x80AF2F44ed0469018922c9F483dc5A909862fdc2"
OPTIMISM_GOERLI_ROUTER_ADDRESS="0xcc5a0B910D9E9504A7561934bed294c51285a78D"
BASE_GOERLI_LINK_ADDRESS="0x6D0F8D488B669aa9BA2D0f0b7B75a88bf5051CD3"
OPTIMISM_GOERLI_LINK_ADDRESS="0xdc2CC710e42857672E7907CF474a69B63B93089f"
创建 .env
文件后,运行以下命令在当前命令行会话中加载环境变量:
source .env
8.2 部署智能合约
在编译合约并设置环境后,就可以部署智能合约了。要使用 Foundry 部署智能合约,你可以使用 forge create
命令。该命令要求你指定要部署的智能合约、要部署到的网络的 RPC URL 以及要用于部署的帐户。
你的钱包必须在 Base Goerli 和 Optimism Goerli 上存入 ETH,以支付与智能合约部署相关的 gas 费用。否则,部署将失败。要获取 Base Goerli 和 Optimism Goerli 的测试网 ETH,请参阅先决条件部分的说明。
要将发送方智能合约部署到 Base Goerli 测试网,请运行以下命令:
forge create ./src/Sender.sol:Sender --rpc-url $BASE_GOERLI_RPC --constructor-args $BASE_GOERLI_ROUTER_ADDRESS $BASE_GOERLI_LINK_ADDRESS --account deployer
出现提示时,输入你之前导入钱包私钥时设置的密码。
运行上述命令后,合约将部署在 Base Goerli 测试网络上。你可以使用区块浏览器查看部署状态和合约。
要将 Receiver 智能合约部署到 Optimism Goerli 测试网,请运行以下命令:
forge create ./src/Receiver.sol:Receiver --rpc-url $OPTIMISM_GOERLI_RPC --constructor-args $OPTIMISM_GOERLI_ROUTER_ADDRESS --account deployer
出现提示时,输入你之前导入钱包私钥时设置的密码。
运行上述命令后,合约将部署在 Optimism Goerli 测试网络上。你可以使用 OP Goerli 区块浏览器查看部署状态和合约。
8.3 为你的智能合约提供资金
为了支付与发送消息相关的费用,发送方合约需要持有 LINK 代币余额。直接从你的钱包为你的合约提供资金,或通过运行以下 cast 命令:
cast send $BASE_GOERLI_LINK_ADDRESS --rpc-url $BASE_GOERLI_RPC "transfer(address,uint256)" <SENDER_CONTRACT_ADDRESS> 5 --account deployer
上述命令将 Base Goerli 测试网上的 5 个 LINK 代币发送到发送方合约。 INFO
在运行提供的 cast 命令之前,请将 <SENDER_CONTRACT_ADDRESS>
替换为你部署的发送方合约的合约地址。
9、与智能合约交互
Foundry 提供了 cast 命令行工具,可用于与已部署的智能合约交互并调用其函数
9.1 发送数据
cast 命令可用于调用部署到 Base Goerli 的 Sender 合约上的 sendMessage(uint64, address, string)
函数,以便将消息数据发送到 Optimism Goerli 上的 Receiver 合约。
要调用 Sender 智能合约的 sendMessage(uint64, address, string)
函数,请运行:
cast send <SENDER_CONTRACT_ADDRESS> --rpc-url $BASE_GOERLI_RPC "sendMessage(uint64, address, string)" $OPTIMISM_GOERLI_CHAIN_SELECTOR <RECEIVER_CONTRACT_ADDRESS> "Based" --account deployer
上面的命令调用了 sendMessage(uint64, address, string)
来发送消息,传入方法的参数包括:目标链的链选择器(Optimism Goerli)、接收方合约地址、消息中需要包含的文本数据(Based)。
在运行提供的转换命令之前,分别将 <SENDER_CONTRACT_ADDRESS>
和 RECEIVER_CONTRACT_ADDRESS>
替换为你部署的发送方和接收方合约的合约地址。
运行命令后,应返回一个唯一的 messageId。
交易完成后,CCIP 需要几分钟时间将数据传送给 Optimism Goerli 并调用接收方合约上的 ccipReceive
函数。
你可以使用 CCIP 资源管理器查看 CCIP 交易的状态。
9.2 接收数据
cast 命令还可用于调用部署到 Optimism Goerli 的 Receiver 合约上的 getMessage()
函数,以读取接收到的消息数据。
要调用 Receiver 智能合约的 getMessage()
函数,请运行:
cast send <RECEIVER_CONTRACT_ADDRESS> --rpc-url $OPTIMISM_GOERLI_RPC "getMessage()" --account deployer
在运行提供的 cast 命令之前,将 <RECEIVER_CONTRACT_ADDRESS>
替换为您部署的 Receiver 合约的合约地址。
运行该命令后,应返回最后收到的消息的 messageId 和文本。
如果交易失败,请确保你的 ccipSend 交易的状态已完成。你可以使用 CCIP 浏览器。
10、结束语
恭喜!你已成功了解如何使用 Chainlink CCIP 在 Base 上执行跨链消息传递。
原文链接:Sending messages and tokens from Base to other chains using Chainlink CCIP
DefiPlot翻译整理,转载请标明出处