CCIP 简介(超简)
- CCIP(Cross-Chain Interoperability Protocol):Chainlink 推出的跨链消息与资产传输协议。
- 本示例:在 Ethereum Sepolia 上发送一条文本消息,经 CCIP 路由传到 BSC Testnet 并被目标链合约接收与解码。
架构与流程
- 源链:Sepolia;合约:
Sender
- 目标链:BSC Testnet;合约:
Receiver
- 流程:
Sender.sendMessage(...)
→ CCIP 路由跨链 →Receiver._ccipReceive(...)
解码与记录
快速上手步骤(概览)
- 在 BSC Testnet 部署
Receiver
(构造写死 Router 地址) - 在 Sepolia 部署
Sender
(构造写死 Router 与 LINK 地址) - 向
Sender
合约转入足量 LINK → 调sendMessage(chainSelector, receiver, "hello")
- 在 BSC Testnet 监听
MessageReceived
或读getLastReceivedMessageDetails()
源码:Receiver(BSC Testnet 接收)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;import {Client} from "@chainlink/contracts-ccip@1.6.0/contracts/libraries/Client.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip@1.6.0/contracts/applications/CCIPReceiver.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.*/
/**
bsc测试网接收消息
1. 部署合约
**//// @title - A simple contract for receiving string data across chains.
contract Receiver is CCIPReceiver {// Event emitted when a message is received from another chain.event MessageReceived(bytes32 indexed messageId, // The unique ID of the 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./// @notice Constructor initializes the contract with the router address./// 写死bsc testnet的router:0xE1053aE1857476f36A3C62580FF9b016E8EE8F6fconstructor() CCIPReceiver(0xE1053aE1857476f36A3C62580FF9b016E8EE8F6f) {}/// handle a received messagefunction _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override {s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageIds_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent textemit 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 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()externalviewreturns (bytes32 messageId, string memory text){return (s_lastReceivedMessageId, s_lastReceivedText);}
}
源码:Sender(Sepolia 发送)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;import {IRouterClient} from "@chainlink/contracts-ccip@1.6.0/contracts/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "@chainlink/contracts@1.4.0/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "@chainlink/contracts-ccip@1.6.0/contracts/libraries/Client.sol";
import {LinkTokenInterface} from "@chainlink/contracts@1.4.0/src/v0.8/shared/interfaces/LinkTokenInterface.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.*//**
从sepolia发送消息给bsc测试网
1. 部署合约
2. 给合约转账70link
**//// @title - A simple contract for sending string data across chains.
contract Sender is OwnerIsCreator {// Custom errors to provide more descriptive revert messages.error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance.// 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.);IRouterClient private s_router;LinkTokenInterface private s_linkToken;/// @notice Constructor initializes the contract with the router address.constructor() {// 写死sepolia的router和link地址address _router =0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59;address _link = 0x779877A7B0D9E8603169DdbD7836e478b4624789;s_router = IRouterClient(_router);s_linkToken = LinkTokenInterface(_link);}/// @notice Sends data to receiver on the destination chain./// @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 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) {// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain messageClient.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({receiver: abi.encode(receiver), // ABI-encoded receiver addressdata: abi.encode(text), // ABI-encoded stringtokenAmounts: new Client.EVMTokenAmount[](0), // Empty array indicating no tokens are being sentextraArgs: 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-extraargsClient.GenericExtraArgsV2({gasLimit: 200_000, // Gas limit for the callback on the destination chainallowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages from the same sender})),// Set the feeToken address, indicating LINK will be used for feesfeeToken: address(s_linkToken)});// Get the fee required to send the messageuint256 fees = s_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 LINKs_linkToken.approve(address(s_router), fees);// Send the message through the router and store the returned message IDmessageId = s_router.ccipSend(destinationChainSelector, evm2AnyMessage);// Emit an event with message detailsemit MessageSent(messageId,destinationChainSelector,receiver,text,address(s_linkToken),fees);// Return the message IDreturn messageId;}
}
实战提示(简版)
- 确认两链的 CCIP Router 地址与 Chain Selector 为官方最新值。
feeToken
使用 LINK 时,需要先将 LINK 转入Sender
合约,并approve
费用。gasLimit
需与接收逻辑复杂度匹配;开启allowOutOfOrderExecution
时注意幂等与去重(用messageId
)。
如果需要,我可以补充一份一键部署与调用的脚本(Hardhat/Foundry),并附上常用测试网的 chain selector
与 Router 地址清单,帮助你从部署到演示全流程跑通。