跨链消息 CCIP
跨链消息 CCIP
本页内容正在整理中,欢迎贡献。
概述
Chainlink CCIP(Cross-Chain Interoperability Protocol,跨链互操作协议)是一个安全的跨链消息传递和代币转移标准。本教程介绍如何使用 CCIP 在以太坊主网和 Arbitrum 之间发送消息和转移 Token。
主要内容
CCIP 核心概念
CCIP 架构: 发送链(Source Chain) ↓ 调用 Router.ccipSend() CCIP 网络(链下验证) ↓ 达成共识 接收链(Destination Chain) ↓ Router 调用接收合约的 ccipReceive() 接收合约核心组件:
| 组件 | 说明 |
|---|---|
| Router | 每条链上的 CCIP 入口,用于发送和接收 |
| LINK Token | 支付跨链手续费(也可用原生代币) |
| ARM(Active Risk Management) | 安全监控网络 |
| OnRamp/OffRamp | 链专用的发送/接收基础设施 |
安装依赖
npm install @chainlink/contracts-ccip# 或 Foundryforge install smartcontractkit/ccip发送跨链消息
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";import "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract CrossChainSender { IRouterClient public immutable router; IERC20 public immutable linkToken;
event MessageSent( bytes32 indexed messageId, uint64 indexed destinationChainSelector, address receiver, string message, uint256 fees );
// Sepolia Router: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 // Arbitrum Sepolia Router: 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165 constructor(address _router, address _link) { router = IRouterClient(_router); linkToken = IERC20(_link); }
// 发送跨链消息(用 LINK 付费) function sendMessage( uint64 destinationChainSelector, // 目标链的 Chain Selector address receiver, // 目标链上的接收合约地址 string calldata message ) external returns (bytes32 messageId) { // 构建 CCIP 消息 Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ receiver: abi.encode(receiver), data: abi.encode(message), tokenAmounts: new Client.EVMTokenAmount[](0), // 无代币转移 extraArgs: Client._argsToBytes( Client.EVMExtraArgsV1({ gasLimit: 200_000 }) ), feeToken: address(linkToken) // 用 LINK 付手续费 });
// 估算手续费 uint256 fees = router.getFee(destinationChainSelector, ccipMessage);
// 授权并发送 linkToken.approve(address(router), fees); messageId = router.ccipSend(destinationChainSelector, ccipMessage);
emit MessageSent(messageId, destinationChainSelector, receiver, message, fees); }
// 用原生代币(ETH)付手续费 function sendMessagePayNative( uint64 destinationChainSelector, address receiver, string calldata message ) external payable returns (bytes32 messageId) { Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ receiver: abi.encode(receiver), data: abi.encode(message), tokenAmounts: new Client.EVMTokenAmount[](0), extraArgs: Client._argsToBytes( Client.EVMExtraArgsV1({ gasLimit: 200_000 }) ), feeToken: address(0) // 用原生代币 });
uint256 fees = router.getFee(destinationChainSelector, ccipMessage); require(msg.value >= fees, "Insufficient ETH for fees");
messageId = router.ccipSend{value: fees}(destinationChainSelector, ccipMessage);
// 退还多余 ETH if (msg.value > fees) { payable(msg.sender).transfer(msg.value - fees); }
emit MessageSent(messageId, destinationChainSelector, receiver, message, fees); }}接收跨链消息
import "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";import "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract CrossChainReceiver is IAny2EVMMessageReceiver { address public immutable router;
mapping(bytes32 => bool) public processedMessages;
event MessageReceived( bytes32 indexed messageId, uint64 indexed sourceChainSelector, address sender, string message );
modifier onlyRouter() { require(msg.sender == router, "Only router can call"); _; }
constructor(address _router) { router = _router; }
// CCIP Router 调用此函数传递消息 function ccipReceive(Client.Any2EVMMessage calldata message) external override onlyRouter { bytes32 messageId = message.messageId; require(!processedMessages[messageId], "Already processed"); processedMessages[messageId] = true;
address sender = abi.decode(message.sender, (address)); string memory text = abi.decode(message.data, (string));
emit MessageReceived( messageId, message.sourceChainSelector, sender, text );
// 在这里执行你的业务逻辑 _handleMessage(message.sourceChainSelector, sender, text); }
function _handleMessage( uint64 sourceChain, address sender, string memory message ) internal virtual { // 子类实现具体逻辑 }}跨链代币转移
// 在 ccipMessage 中添加代币转移Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);tokenAmounts[0] = Client.EVMTokenAmount({ token: address(USDC), // 支持 CCIP 的代币 amount: 100e6 // 100 USDC});
// 先授权代币给 RouterIERC20(USDC).approve(address(router), 100e6);
Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ receiver: abi.encode(receiverAddress), data: "", tokenAmounts: tokenAmounts, // 携带代币 extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({ gasLimit: 0 })), feeToken: address(linkToken)});重要 Chain Selector
| 网络 | Chain Selector |
|---|---|
| 以太坊主网 | 5009297550715157269 |
| Arbitrum One | 4949039107694359620 |
| Optimism | 3734403246176062136 |
| Base | 15971525489660198786 |
| Sepolia | 16015286601757825753 |
| Arbitrum Sepolia | 3478487238524512106 |
CCIP Explorer
追踪跨链消息状态:ccip.chain.link
安全最佳实践
- 只接受可信 Router 的消息:校验
msg.sender == router - 幂等处理:记录已处理的
messageId,防止重复执行 - 验证源链和发送者:校验
sourceChainSelector和 sender 地址 - 代币白名单:只处理已知代币,防止恶意代币注入