链锁 CCIP

导言
Chainlink Cross-Chain Interoperability Protocol (CCIP) 为开发者和去中心化应用程序(dApps)提供了一种安全、高效的跨区块链交互方式。 利用 CCIP,您可以发送代币和任意消息,以触发目标合约上的操作,如铸造 NFT、重新平衡指数或调用自定义函数。
在本教程中,您将学习如何使用 Chainlink CCIP 从 Kaia 智能合约向另一条链上的合约发送信息和代币,以及如何接收这些信息和代币。
先决条件
- 代工厂安装
- 使用
curl -L https://foundry.paradigm.xyz | bash安装,然后运行foundryup。 - 使用
forge --version,cast --version和anvil --version进行验证。
- 使用
- MetaMask 钱包
- 设置开发钱包
- 在 MetaMask 中添加 Kaia Kairos 测试网络和以太坊 Sepolia 网络。
- 从水龙头测试代币
- KAIA:用于从 Kaia 部署和发送的气体。
- LINK (testnet):用 LINK 支付 CCIP 费用。
- 目标链上的本地代币(例如,Sepolia ETH:用于部署,如果选择,用于支付本地 CCIP 费用)。
入门
在本指南中,您将使用 Chainlink CCIP 在 Kaia(Kairos Testnet)和以太坊 Sepolia 之间收发跨链消息。
到最后,你会
- 初始化为 Kairos 和 Sepolia 配置的 Foundry 项目
- 将 Chainlink CCIP 合同和接口添加为依赖项
- 执行信使合约,跨链收发信息
- 部署到两个网络并验证往返信息
创建项目
在本节中,您将使用 Foundry 设置开发环境。 要创建一个新的 Foundry 项目,首先要创建一个新目录:
mkdir kaia-foundry-ccip-example
那就跑吧
cd kaia-foundry-ccip-exampleforge init
这将创建一个具有以下基本布局的 Foundry 项目:
├── foundry.toml├── script├── src└── test
安装 Chainlink 智能合约
要在 Foundry 项目中使用 Chainlink CCIP,需要使用 forge install 将 Chainlink CCIP 智能合约安装为项目依赖项。
要安装 Chainlink CCIP 智能合约,请运行
forge install smartcontractkit/chainlink-ccip@2114b90f39c82c052e05af7c33d42c1ae98f4180forge install smartcontractkit/chainlink-evm@ff814eb0a01f89d9a215f825d243bf421e6434a9
安装完成后,创建一个 remapping.txt 文件:
forge remappings > remappings.txt
然后将以下内容粘贴到新创建的文件中:
@chainlink/contracts/=lib/chainlink-evm/contracts/@chainlink/contracts-ccip/=lib/chainlink-ccip/chains/evm/contracts/
编写智能合约
在本节中,您将使用下面的代码跨链发送和接收信息。
在项目的 src 目录下创建一个名为 Messenger.sol 的新文件,并将下面的代码复制到该文件中:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import { IRouterClient } from "@chainlink/contracts-ccip/interfaces/IRouterClient.sol";import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";import { Client } from "@chainlink/contracts-ccip/libraries/Client.sol";import { CCIPReceiver } from "@chainlink/contracts-ccip/applications/CCIPReceiver.sol";import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";import {SafeERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";/** * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. * DO NOT USE THIS CODE IN PRODUCTION. *//// @title - A simple messenger contract for sending/receiving string data across chains.contract Messenger is CCIPReceiver, OwnerIsCreator { using SafeERC20 for IERC20; // Custom errors to provide more descriptive revert messages. error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance. error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw. error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails. error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner. error SourceChainNotAllowlisted(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner. error SenderNotAllowlisted(address sender); // Used when the sender has not been allowlisted by the contract owner. error InvalidReceiverAddress(); // Used when the receiver address is 0. // Event emitted when a message is sent to another chain. event MessageSent( bytes32 indexed messageId, // The unique ID of the CCIP message. uint64 indexed destinationChainSelector, // The chain selector of the destination chain. address receiver, // The address of the receiver on the destination chain. string text, // The text being sent. address feeToken, // the token address used to pay CCIP fees. uint256 fees // The fees paid for sending the CCIP message. ); // Event emitted when a message is received from another chain. event MessageReceived( bytes32 indexed messageId, // The unique ID of the CCIP message. uint64 indexed sourceChainSelector, // The chain selector of the source chain. address sender, // The address of the sender from the source chain. string text // The text that was received. ); bytes32 private s_lastReceivedMessageId; // Store the last received messageId. string private s_lastReceivedText; // Store the last received text. // Mapping to keep track of allowlisted destination chains. mapping(uint64 => bool) public allowlistedDestinationChains; // Mapping to keep track of allowlisted source chains. mapping(uint64 => bool) public allowlistedSourceChains; // Mapping to keep track of allowlisted senders. mapping(address => bool) public allowlistedSenders; IERC20 private s_linkToken; /// @notice Constructor initializes the contract with the router address. /// @param _router The address of the router contract. /// @param _link The address of the link contract. constructor(address _router, address _link) CCIPReceiver(_router) { s_linkToken = IERC20(_link); } /// @dev Modifier that checks if the chain with the given destinationChainSelector is allowlisted. /// @param _destinationChainSelector The selector of the destination chain. modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) { if (!allowlistedDestinationChains[_destinationChainSelector]) revert DestinationChainNotAllowlisted(_destinationChainSelector); _; } /// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted. /// @param _sourceChainSelector The selector of the destination chain. /// @param _sender The address of the sender. modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) { if (!allowlistedSourceChains[_sourceChainSelector]) revert SourceChainNotAllowlisted(_sourceChainSelector); if (!allowlistedSenders[_sender]) revert SenderNotAllowlisted(_sender); _; } /// @dev Modifier that checks the receiver address is not 0. /// @param _receiver The receiver address. modifier validateReceiver(address _receiver) { if (_receiver == address(0)) revert InvalidReceiverAddress(); _; } /// @dev Updates the allowlist status of a destination chain for transactions. function allowlistDestinationChain( uint64 _destinationChainSelector, bool allowed ) external onlyOwner { allowlistedDestinationChains[_destinationChainSelector] = allowed; } /// @dev Updates the allowlist status of a source chain for transactions. function allowlistSourceChain( uint64 _sourceChainSelector, bool allowed ) external onlyOwner { allowlistedSourceChains[_sourceChainSelector] = allowed; } /// @dev Updates the allowlist status of a sender for transactions. function allowlistSender(address _sender, bool allowed) external onlyOwner { allowlistedSenders[_sender] = allowed; } /// @notice Sends data to receiver on the destination chain. /// @notice Pay for fees in LINK. /// @dev Assumes your contract has sufficient LINK. /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain. /// @param _receiver The address of the recipient on the destination blockchain. /// @param _text The text to be sent. /// @return messageId The ID of the CCIP message that was sent. function sendMessagePayLINK( uint64 _destinationChainSelector, address _receiver, string calldata _text ) external onlyOwner onlyAllowlistedDestinationChain(_destinationChainSelector) validateReceiver(_receiver) returns (bytes32 messageId) { // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( _receiver, _text, address(s_linkToken) ); // Initialize a router client instance to interact with cross-chain router IRouterClient router = IRouterClient(this.getRouter()); // Get the fee required to send the CCIP message uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); if (fees > s_linkToken.balanceOf(address(this))) revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees); // Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK s_linkToken.approve(address(router), fees); // Send the CCIP message through the router and store the returned CCIP message ID messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); // Emit an event with message details emit MessageSent( messageId, _destinationChainSelector, _receiver, _text, address(s_linkToken), fees ); // Return the CCIP message ID return messageId; } /// @notice Sends data to receiver on the destination chain. /// @notice Pay for fees in native gas. /// @dev Assumes your contract has sufficient native gas tokens. /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain. /// @param _receiver The address of the recipient on the destination blockchain. /// @param _text The text to be sent. /// @return messageId The ID of the CCIP message that was sent. function sendMessagePayNative( uint64 _destinationChainSelector, address _receiver, string calldata _text ) external onlyOwner onlyAllowlistedDestinationChain(_destinationChainSelector) validateReceiver(_receiver) returns (bytes32 messageId) { // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( _receiver, _text, address(0) ); // Initialize a router client instance to interact with cross-chain router IRouterClient router = IRouterClient(this.getRouter()); // Get the fee required to send the CCIP message uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); if (fees > address(this).balance) revert NotEnoughBalance(address(this).balance, fees); // Send the CCIP message through the router and store the returned CCIP message ID messageId = router.ccipSend{value: fees}( _destinationChainSelector, evm2AnyMessage ); // Emit an event with message details emit MessageSent( messageId, _destinationChainSelector, _receiver, _text, address(0), fees ); // Return the CCIP message ID return messageId; } /// handle a received message function _ccipReceive( Client.Any2EVMMessage memory any2EvmMessage ) internal override onlyAllowlisted( any2EvmMessage.sourceChainSelector, abi.decode(any2EvmMessage.sender, (address)) ) // Make sure source chain and sender are allowlisted { s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text emit MessageReceived( any2EvmMessage.messageId, any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector) abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address, abi.decode(any2EvmMessage.data, (string)) ); } /// @notice Construct a CCIP message. /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for sending a text. /// @param _receiver The address of the receiver. /// @param _text The string data to be sent. /// @param _feeTokenAddress The address of the token used for fees. Set address(0) for native gas. /// @return Client.EVM2AnyMessage Returns an EVM2AnyMessage struct which contains information for sending a CCIP message. function _buildCCIPMessage( address _receiver, string calldata _text, address _feeTokenAddress ) private pure returns (Client.EVM2AnyMessage memory) { // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message return Client.EVM2AnyMessage({ receiver: abi.encode(_receiver), // ABI-encoded receiver address data: abi.encode(_text), // ABI-encoded string tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array as no tokens are transferred extraArgs: Client._argsToBytes( // Additional arguments, setting gas limit and allowing out-of-order execution. // Best Practice: For simplicity, the values are hardcoded. It is advisable to use a more dynamic approach // where you set the extra arguments off-chain. This allows adaptation depending on the lanes, messages, // and ensures compatibility with future CCIP upgrades. Read more about it here: https://docs.chain.link/ccip/concepts/best-practices/evm#using-extraargs Client.GenericExtraArgsV2({ gasLimit: 200_000, // Gas limit for the callback on the destination chain allowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages from the same sender }) ), // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees feeToken: _feeTokenAddress }); } /// @notice Fetches the details of the last received message. /// @return messageId The ID of the last received message. /// @return text The last received text. function getLastReceivedMessageDetails() external view returns (bytes32 messageId, string memory text) { return (s_lastReceivedMessageId, s_lastReceivedText); } /// @notice Fallback function to allow the contract to receive Ether. /// @dev This function has no function body, making it a default function for receiving Ether. /// It is automatically called when Ether is sent to the contract without any data. receive() external payable {} /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract. /// @dev This function reverts if there are no funds to withdraw or if the transfer fails. /// It should only be callable by the owner of the contract. /// @param _beneficiary The address to which the Ether should be sent. function withdraw(address _beneficiary) public onlyOwner { // Retrieve the balance of this contract uint256 amount = address(this).balance; // Revert if there is nothing to withdraw if (amount == 0) revert NothingToWithdraw(); // Attempt to send the funds, capturing the success status and discarding any return data (bool sent, ) = _beneficiary.call{value: amount}(""); // Revert if the send failed, with information about the attempted transfer if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount); } /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token. /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw. /// @param _beneficiary The address to which the tokens will be sent. /// @param _token The contract address of the ERC20 token to be withdrawn. function withdrawToken( address _beneficiary, address _token ) public onlyOwner { // Retrieve the balance of this contract uint256 amount = IERC20(_token).balanceOf(address(this)); // Revert if there is nothing to withdraw if (amount == 0) revert NothingToWithdraw(); IERC20(_token).safeTransfer(_beneficiary, amount); }}
上面的代码是一个双向 CCIP 合约,可在允许列表的链上发送和接收字符串信息,并带有所有者控制、LINK 或本地费用支付功能。 让我们来看看本合同中要用到的主要功能:
1. 所有列表
- allowlistSourceChain(selector, allowed):控制允许哪些源链向此合约传递消息。
- allowlistDestinationChain(selector, allowed):控制允许向哪些目标链发送此合约。
- allowlistedSenders[address](通过 allowlistSender(addr, allowed)):当信息到达时,限制信任源链上的哪些发件人地址。
测试前在两端都设置好。 信息源必须信任发送方和链。 目的地也必须列入允许发送列表。
2. 发送信息
sendMessagePayLINK(selector, receiver, text):发送信息并在 LINK 中支付 CCIP 费用。 这将生成一条信息,报出费用,检查 LINK 余额,批准路由器,然后执行 ccipSend。 完成后,它会返回一个与发送信息相关的唯一 ID。
发送消息支付本地(选择器、接收器、文本):发送信息并以本地令牌支付 CCIP 费用。 这将生成一条信息,报出费用,检查本地余额,然后执行 ccipSend(值:费用)。 完成后,它会返回一个与发送信息相关的唯一 ID。
3. 建立信息
_buildCCIPMessage(receiver, text, feeTokenAddress) -> EVM2AnyMessage
- 对接收器和文本进行编码
- 不发送令牌(令牌数量为空)
- 使用带有可配置 gasLimit 的 GenericExtraArgsV2 封装 extraArgs
- 将 feeToken 设置为 LINK 或本地 address(0)。
4. 接收信息
CCIP 调用 _ccipReceive(...) 在目标链上。 合同:
- 根据允许列表验证源链和发件人
- 解码字符串
- 将其存储为最后收到的有效载荷
- 发射 MessageReceived
- 通过以下方式读回最后一次接收的有效载荷: getLastReceivedMessageDetails() -> (messageId, text)
编译智能合约
要编译智能合约,请运行
forge build