跳转到内容

跨链消息 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链专用的发送/接收基础设施

安装依赖

Terminal window
npm install @chainlink/contracts-ccip
# 或 Foundry
forge install smartcontractkit/ccip

发送跨链消息

// SPDX-License-Identifier: MIT
pragma 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
});
// 先授权代币给 Router
IERC20(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 One4949039107694359620
Optimism3734403246176062136
Base15971525489660198786
Sepolia16015286601757825753
Arbitrum Sepolia3478487238524512106

CCIP Explorer

追踪跨链消息状态:ccip.chain.link

安全最佳实践

  1. 只接受可信 Router 的消息:校验 msg.sender == router
  2. 幂等处理:记录已处理的 messageId,防止重复执行
  3. 验证源链和发送者:校验 sourceChainSelector 和 sender 地址
  4. 代币白名单:只处理已知代币,防止恶意代币注入

深入阅读