代码详情:https://github.com/langjiyunmie/Chainlink-CCIP.git chainlink学习的官方视频链接:https://www.bilibili.com/video/BV1RFsfe5Ek5/
源链 (Source Chain)
├── Sender (发送方合约)
├── Router (路由合约)
└── Token Pool (代币池)
目标链 (Destination Chain)
├── Receiver (接收方合约)
├── Router (路由合约)
└── Token Pool (代币池)
链下部分 (Offchain)
├── Committing DON (提交DON网络)
├── Executing DON (执行DON网络)
└── RMN (风险管理网络)
概括CCIP的三个部分的完整流程:
源链(Source Chain)
用户 → Sender合约 → Router → OnRamp → Token Pool
具体流程:
链下(Off-chain)
Committing DON → RMN → Executing DON
具体流程:
目标链(Destination Chain)
OffRamp → Token Pool → Router → 接收方合约
具体流程:
发起跨链请求:
用户使用sender合约,指定目标链,指定目标链,指定接收地址,准备要发送的代币,附带自定义消息数据
// 1. Sender发起跨链请求
contract Sender {
function sendMessage(
uint64 destinationChainSelector,
address receiver,
bytes memory data,
TokenTransfer[] memory tokens
) external {
// 构造CCIP消息
Message memory message = Message({
sender: msg.sender,
receiver: receiver,
data: data,
tokens: tokens
});
// 计算费用
uint256 fee = router.getFee(destinationChainSelector, message);
// 调用Router
router.ccipSend{value: fee}(destinationChainSelector, message);
}
}
Router合约源链路由器,验证消息格式,计算费用,处理代币锁定
// 2. Router验证并转发到OnRamp
contract Router {
function ccipSend(uint64 chainSelector, Message memory message) external {
// 验证目标链
validateDestination(chainSelector);
// 获取对应的OnRamp
OnRamp onRamp = getOnRamp(chainSelector);
// 转发到OnRamp
onRamp.forwardMessage(message);
}
}
OnRamp合约处理(源链):接收Router的请求,验证消息格式和费用,与Token Pool交互锁定代币,生成跨链消息的证明
// 3. OnRamp处理并与Token Pool交互
contract OnRamp {
function forwardMessage(Message memory message) external {
// 验证消息格式
validateMessage(message);
// 处理代币锁定
for (TokenTransfer token : message.tokens) {
tokenPool.lockTokens(
token.token,
token.amount,
message.sender
);
}
// 生成merkle叶子
bytes32 leaf = generateLeaf(message);
// 提交到Commit Store
commitStore.addMessage(leaf);
// 触发事件供DON监听
emit MessageSent(message);
}
}
Token Pool: 代币池锁定源链代币,管理流动性
详细交互流程:
Committing DON 监听和处理
class CommittingDON {
// 监听源链事件
async listenToSourceChain() {
sourceChain.on('MessageSent', async (event) => {
const message = event.args.message;
await this.processMessage(message);
});
}
// 处理跨链消息
async processMessage(message) {
// 验证消息
await this.validateMessage(message);
// 构建 merkle 树
const leaf = this.generateLeaf(message);
const merkleTree = this.buildMerkleTree([leaf]);
// 收集 DON 签名
const signatures = await this.collectSignatures(merkleTree.root);
// 提交到 RMN
await this.submitToRMN(message, merkleTree, signatures);
}
// 生成 merkle 叶子
generateLeaf(message) {
return ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
['address', 'address', 'bytes', 'uint256'],
[message.sender, message.receiver, message.data, message.nonce]
)
);
}
}
监听和收集:
验证流程:
数据整理:
Lane Manager(通道管理)
class LaneManager {
constructor() {
this.lanes = new Map();
this.limits = new Map();
}
// 检查通道状态
async checkLane(sourceChain, destChain) {
const lane = this.getLane(sourceChain, destChain);
// 检查通道容量
if (lane.messageCount >= lane.maxCapacity) {
throw new Error('Lane capacity exceeded');
}
// 检查限额
if (lane.totalValue >= this.limits.get(lane.id)) {
throw new Error('Lane value limit exceeded');
}
// 更新统计
await this.updateLaneStats(lane);
}
// 更新通道统计
async updateLaneStats(lane) {
lane.messageCount++;
lane.lastUpdated = Date.now();
await this.persistLaneData(lane);
}
}
通道状态管理:
流量控制:
Price Feed(价格预言机):
class PriceFeed {
// 获取实时价格
async getPrice(token) {
// 从多个源获取价格
const prices = await Promise.all([
this.getPriceFromSource1(token),
this.getPriceFromSource2(token),
this.getPriceFromSource3(token)
]);
// 过滤和计算加权价格
return this.calculateWeightedPrice(prices);
}
// 价格偏差检查
async checkPriceDeviation(token, price) {
const historicalPrice = await this.getHistoricalPrice(token);
const deviation = Math.abs(price - historicalPrice) / historicalPrice;
if (deviation > this.maxDeviation) {
await this.triggerPriceProtection(token, price);
}
}
}
价格数据服务:
价格保护机制:
Risk Management Network (RMN):
class RiskManagementNetwork {
// 风险评估
async assessRisk(message, context) {
const riskScore = await this.calculateRiskScore({
// 交易相关风险
transactionRisk: await this.assessTransactionRisk(message),
// 地址风险
addressRisk: await this.assessAddressRisk(message.sender),
// 网络风险
networkRisk: await this.assessNetworkRisk(context),
// 代币风险
tokenRisk: await this.assessTokenRisk(message.tokens)
});
return this.evaluateRiskScore(riskScore);
}
// 流动性检查
async checkLiquidity(message) {
const liquidityData = await this.getLiquidityData(message.destChain);
return {
isLiquidityOk: liquidityData.available >= message.value,
liquidityRatio: liquidityData.available / liquidityData.total
};
}
}
风险评估:
安全检查:
流动性管理:
Executing DON:
class ExecutingDON {
// 准备执行
async prepareExecution(message, proof) {
// 验证所有条件
await this.validateExecutionConditions(message);
// 准备执行数据
const executionData = await this.prepareExecutionData(message);
// 收集执行签名
const signatures = await this.collectExecutionSignatures(executionData);
return { executionData, signatures };
}
// 执行共识
async reachConsensus(executionData) {
const nodes = await this.getActiveNodes();
const votes = await this.collectNodeVotes(nodes, executionData);
if (this.hasConsensus(votes)) {
return this.prepareConsensusProof(votes);
}
throw new Error('Consensus not reached');
}
}
执行准备:
共识过程:
执行触发:
跨链消息传递流程:
消息封装:
验证层级:
基础验证
共识验证
风险验证
监控和优化系统:
class MonitoringSystem {
// 性能监控
async monitorPerformance() {
const metrics = {
nodeLatency: await this.measureNodeLatency(),
messageQueueSize: await this.getQueueSize(),
processingTime: await this.getAverageProcessingTime(),
resourceUsage: await this.getResourceMetrics()
};
await this.analyzeMetrics(metrics);
}
// 异常检测
async detectAnomalies() {
const patterns = await this.analyzePatterns();
if (patterns.hasAnomaly) {
await this.triggerAlert(patterns.anomalyType);
}
}
}
性能监控:
数据分析:
系统调优:
应急响应机制:
class EmergencyHandler {
// 处理异常
async handleEmergency(error) {
// 记录错误
await this.logError(error);
// 执行应急预案
const plan = await this.selectEmergencyPlan(error);
await this.executeEmergencyPlan(plan);
// 通知相关方
await this.notifyStakeholders(error, plan);
}
// 恢复服务
async recoverService() {
// 检查系统状态
const status = await this.checkSystemStatus();
// 执行恢复步骤
if (status.needsRecovery) {
await this.executeRecoverySteps(status);
}
// 验证恢复结果
await this.verifyRecovery();
}
}
异常处理:
故障恢复:
OffRamp接收和验证:
这是目标链上的入口合约,负责接收和处理来自链下DON网络的跨链消息。它会验证消息的有效性,包括检查消息证明和DON签名。一旦验证通过,它会协调TokenPool进行代币释放,并通过Router将消息转发给最终的接收方。可以把它理解为跨链消息在目标链上的"报关处",负责验证和清关。
class OffRamp {
async processIncomingMessage(message, proof) {
// 1. 验证消息和证明
await this.validateMessage(message, proof);
// 2. 检查执行条件
const executionContext = {
message,
proof,
timestamp: await this.getBlockTimestamp(),
gasPrice: await this.getGasPrice()
};
// 3. 准备执行
await this.prepareExecution(executionContext);
}
async validateMessage(message, proof) {
// 验证merkle证明
const isValidProof = await this.verifyMerkleProof(
message.leaf,
proof.root,
proof.path
);
// 验证DON签名
const isValidSignature = await this.verifyDONSignatures(
message,
proof.signatures
);
if (!isValidProof || !isValidSignature) {
throw new Error('Invalid message or proof');
}
}
}
TokenPool合约:
这是代币管理合约,管理着目标链上用于跨链的代币流动性池。当OffRamp确认跨链消息有效后,TokenPool负责将对应数量的代币释放给接收方。它还管理流动性提供者的存款和取款,确保池中始终有足够的代币来满足跨链需求。这就像是一个银行金库,负责资金的安全存管和分发。
contract TokenPool {
// 代币余额映射
mapping(address => uint256) public poolBalance;
mapping(address => uint256) public lockedAmount;
// 释放代币给接收方
function releaseTokens(
address token,
address receiver,
uint256 amount
) external onlyOffRamp {
require(
poolBalance[token] >= amount,
"Insufficient liquidity"
);
poolBalance[token] -= amount;
IERC20(token).transfer(receiver, amount);
emit TokensReleased(token, receiver, amount);
}
// 添加流动性
function addLiquidity(
address token,
uint256 amount
) external {
IERC20(token).transferFrom(
msg.sender,
address(this),
amount
);
poolBalance[token] += amount;
emit LiquidityAdded(token, amount);
}
}
Router合约
Router是消息路由合约,负责将验证过的跨链消息传递给正确的接收方合约。它会检查接收方是否是有效的合约地址,并调用接收方的ccipReceive函数。如果消息执行失败,Router还负责处理失败情况。它就像是一个邮递员,确保消息准确送达指定接收方。
contract Router {
// 路由表
mapping(address => bool) public whitelistedOffRamps;
// 执行消息
function routeMessage(
OffRamp.CCIPMessage memory message
) external onlyOffRamp {
// 验证接收方合约
require(
_isContract(message.receiver),
"Receiver must be a contract"
);
// 调用接收方的ccipReceive函数
try ICCIPReceiver(message.receiver).ccipReceive(
message.sourceChainSelector,
message.sender,
message.data
) {
emit MessageRouted(message.messageId);
} catch Error(string memory reason) {
emit MessageFailed(message.messageId, reason);
_handleFailure(message);
}
}
}
CommitStore合约:
这是消息存储和验证合约,存储了所有经过DON网络确认的消息根。当OffRamp收到跨链消息时,会向CommitStore验证该消息是否已经得到了DON网络的确认。它维护着一个可信消息的数据库,确保只有经过验证的消息才能被执行。这像是一个公证处,负责验证消息的真实性
contract CommitStore {
// 存储已确认的消息根
mapping(bytes32 => bool) public committedRoots;
// DON签名者
mapping(address => bool) public allowedSigners;
// 验证并存储消息证明
function verifyMessage(
OffRamp.CCIPMessage memory message,
bytes memory proof
) external returns (bool) {
bytes32 root = _computeRoot(message);
require(
committedRoots[root],
"Unknown message root"
);
require(
_verifyProof(message, proof),
"Invalid proof"
);
return true;
}
}
接收方合约接口:
这是最终接收和处理跨链消息的合约,需要实现ccipReceive接口。当Router转发消息时,接收方合约会被调用,然后根据收到的消息执行相应的业务逻辑。这可以是任何需要接收跨链消息的智能合约,比如跨链桥、跨链交易所等。
interface ICCIPReceiver {
function ccipReceive(
uint64 sourceChainSelector,
address sender,
bytes calldata data
) external;
}
// 示例实现
contract ExampleReceiver is ICCIPReceiver {
// 只允许Router调用
modifier onlyRouter() {
require(
msg.sender == address(router),
"Only router can call"
);
_;
}
function ccipReceive(
uint64 sourceChainSelector,
address sender,
bytes calldata data
) external override onlyRouter {
// 解码数据
(uint256 amount, bytes memory payload) = abi.decode(
data,
(uint256, bytes)
);
// 处理业务逻辑
_handleBusinessLogic(sender, amount, payload);
}
}
权限控制合约:
contract AccessControl {
// 角色定义
bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
// 角色管理
mapping(bytes32 => mapping(address => bool)) public roles;
modifier onlyRole(bytes32 role) {
require(
roles[role][msg.sender],
"Caller is not authorized"
);
_;
}
}
目标链流程
消息。验证通过后,如果消息包含代币转移,OffRamp会通知TokenPool释放相应的代币。然后OffRamp将消息交给Router,Router负责将消息路由到正确的接收方合约。在这个过程中,CommitStore提供消息验证服务,确保只有经过DON网络确认的消息才会被处理。
原始模型
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {IRouterClient} from "@chainlink/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "node_modules/@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "node_modules/@chainlink/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {CCIPReceiver} from "@chainlink/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {IERC20} from "node_modules/@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "node_modules/@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/receving 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.
// 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.
bytes 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.
);
bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
string private s_lastReceivedText; // Store the last received text.
IERC20 private s_linkToken;
// remember to add visibility for the variable
MyToken public nft;
struct RequestData{
uint256 tokenId;
address newOwner;
}
/// @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, address nftAddr) CCIPReceiver(_router) {
s_linkToken = IERC20(_link);
nft = MyToken(nftAddr);
}
/// @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 _payload The data to be sent.
/// @return messageId The ID of the CCIP message that was sent.
function sendMessagePayLINK(
uint64 _destinationChainSelector,
address _receiver,
bytes memory _payload
)
internal
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_payload,
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,
_payload,
address(s_linkToken),
fees
);
// Return the CCIP message ID
return messageId;
}
/// handle a received message
function _ccipReceive(
Client.Any2EVMMessage memory any2EvmMessage
)
internal
override
{
s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
RequestData memory requestData = abi.decode(any2EvmMessage.data, (RequestData));
uint256 tokenId = requestData.tokenId;
address newOwner = requestData.newOwner;
require(tokenLocked[tokenId], "the NFT is not locked");
nft.transferFrom(address(this), newOwner, tokenId);
emit TokenUnlocked(tokenId, newOwner);
}
/// @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 _payload 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,
bytes memory _payload,
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: _payload, // ABI-encoded string
tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array aas no tokens are transferred
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit
Client.EVMExtraArgsV1({gasLimit: 200_000})
),
// 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);
}
}
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, ERC721Enumerable, ERC721URIStorage, ERC721Burnable, Ownable {
string constant public METADATA_URI = "ipfs://QmXw7TEAJWKjKifvLE25Z9yjvowWk2NWY3WgnZPUto9XoA";
uint256 private _nextTokenId;
constructor(string memory tokenName, string memory tokenSymbol)
ERC721(tokenName, tokenSymbol)
Ownable(msg.sender)
{}
function safeMint(address to)
public
{
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, METADATA_URI);
}
// The following functions are overrides required by Solidity.
function _update(address to, uint256 tokenId, address auth)
internal
override(ERC721, ERC721Enumerable)
returns (address)
{
return super._update(to, tokenId, auth);
}
function _increaseBalance(address account, uint128 value)
internal
override(ERC721, ERC721Enumerable)
{
super._increaseBalance(account, value);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory )
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {IRouterClient} from "@chainlink/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "node_modules/@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "node_modules/@chainlink/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {CCIPReceiver} from "@chainlink/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {IERC20} from "node_modules/@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "node_modules/@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";
import {MyNFT} from "./MyNFT.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/receving string data across chains.
contract NFTPoolLockAndRelease 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.
// 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.
bytes 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.
);
bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
string private s_lastReceivedText; // Store the last received text.
IERC20 private s_linkToken;
MyNFT public nft;//第一步先实例化一个nft对象,同时需要在构造函数中初始化
// remember to add visibility for the variable
MyToken public nft;
struct RequestData{
uint256 tokenId;
address newOwner;
}
/// @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, address nftAddr) CCIPReceiver(_router) {
s_linkToken = IERC20(_link);
nft = MyToken(nftAddr);
}
function lockAndSendNFT(
uint256 tokenId,
address newOwner,
uint64 chainSelector,
address receiver) public returns(bytes32 messageId){
//transfer
nft.transferFrom(msg.sender,address(this),tokenId);//从持有人地址转入当前地址
//发送跨链消息:需要传入receiver地址和tokenid给到链下的ccip组件
//通过加密,打包两个参数
bytes memory payload = abi.encode(tokenId,newOwner);
//发送消息,使用link支付
bytes32 messageId = sendMessagePayLINK(chainSelector,receiver,payload);
}
/// @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 _payload The data to be sent.
/// @return messageId The ID of the CCIP message that was sent.
function sendMessagePayLINK(
uint64 _destinationChainSelector,
address _receiver,
bytes memory _payload
)
internal
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_payload,
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,
_payload,
address(s_linkToken),
fees
);
// Return the CCIP message ID
return messageId;
}
/// handle a received message
function _ccipReceive(
Client.Any2EVMMessage memory any2EvmMessage
)
internal
override
{
s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
RequestData memory requestData = abi.decode(any2EvmMessage.data, (RequestData));
uint256 tokenId = requestData.tokenId;
address newOwner = requestData.newOwner;
require(tokenLocked[tokenId], "the NFT is not locked");
nft.transferFrom(address(this), newOwner, tokenId);
emit TokenUnlocked(tokenId, newOwner);
}
/// @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 _payload 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,
bytes memory _payload,
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: _payload, // ABI-encoded string
tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array aas no tokens are transferred
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit
Client.EVMExtraArgsV1({gasLimit: 200_000})
),
// 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);
}
}
传入参数
function lockAndSendNFT(
uint256 tokenId,
address newOwner,
uint64 chainSelector,
address receiver) public{
//transfer
}
先将NFT锁入当前合约并检查
先创建一个nft的实例,前面我们已经创建了好了一个MyNFT的合约
import {MyNFT} from "./MyNFT.sol";
MyNFT public nft;//第一步先实例化一个nft对象,同时需要在构造函数中初始化
之后将自己拥有的NFT转移到当前的合约中
nft.transferFrom(msg.sender,address(this),tokenId);//从持有人地址转入当前地址
发送跨链消息,首先两个主要函数起到主要作用
/// @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 _payload The data to be sent.
/// @return messageId The ID of the CCIP message that was sent.
function sendMessagePayLINK(
uint64 _destinationChainSelector,
address _receiver,
bytes memory _payload
)
internal
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
// evm to AnyMessage, 这个消息时从evm链上发送到链下的
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_payload,
address(s_linkToken)
);
// Initialize a router client instance to interact with cross-chain router
//Router验证请求并计算gas费用
IRouterClient router = IRouterClient(this.getRouter());
// Get the fee required to send the CCIP message
//计算发送消息的gas费
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
//授权link给router合约 to 发送消息
s_linkToken.approve(address(router), fees);
// Send the CCIP message through the router and store the returned CCIP message ID
//通过router合约发送ccip消息,并将CCIP message ID返回
messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
// Emit an event with message details
// 释放事件
emit MessageSent(
messageId,
_destinationChainSelector,
_receiver,
_payload,
address(s_linkToken),
fees
);
// Return the CCIP message ID
return messageId;
}
该函数的主要目的是构建一个用于跨链消息传递的 EVM2AnyMessage 结构体。这个结构体包含了发送跨链消息所需的所有信息
/// @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 _payload 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,
bytes memory _payload,
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: _payload, // ABI-encoded string
tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array aas no tokens are transferred
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit
Client.EVMExtraArgsV1({gasLimit: 200_000})
),
// Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
feeToken: _feeTokenAddress
});
}
参数:
构建 EVM2AnyMessage 结构体:
return
Client.EVM2AnyMessage({
receiver: abi.encode(_receiver), // ABI-encoded receiver address
data: _payload, // ABI-encoded string
tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array as no tokens are transferred
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit
Client.EVMExtraArgsV1({gasLimit: 200_000})
),
// Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
feeToken: _feeTokenAddress
});
并将构建好的消息返回给 sendMessagePayLINK 函数
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_payload,
address(s_linkToken)
);
这里重新写了函数 lockAndSendNFT ,将 tokenId ,newOwner 编码打包
function lockAndSendNFT(
uint256 tokenId,
address newOwner,
uint64 chainSelector,
address receiver) public returns(bytes32 messageId){
//transfer
nft.transferFrom(msg.sender,address(this),tokenId);//从持有人地址转入当前地址
//发送跨链消息:需要传入receiver地址和tokenid给到链下的ccip组件
//通过加密,打包两个参数
bytes memory payload = abi.encode(tokenId,newOwner);
//发送消息,使用link支付
bytes32 messageId = sendMessagePayLINK(chainSelector,receiver,payload);
}
其实对于结构体的参数结构,Client 库里面定义了
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// End consumer library.
library Client {
/// @dev RMN depends on this struct, if changing, please notify the RMN maintainers.
struct EVMTokenAmount {
address token; // token address on the local chain.
uint256 amount; // Amount of tokens.
}
struct Any2EVMMessage {
bytes32 messageId; // MessageId corresponding to ccipSend on source.
uint64 sourceChainSelector; // Source chain selector.
bytes sender; // abi.decode(sender) if coming from an EVM chain.
bytes data; // payload sent in original message.
EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation.
}
// If extraArgs is empty bytes, the default is 200k gas limit.
struct EVM2AnyMessage {
bytes receiver; // abi.encode(receiver address) for dest EVM chains
bytes data; // Data payload
EVMTokenAmount[] tokenAmounts; // Token transfers
address feeToken; // Address of feeToken. address(0) means you will send msg.value.
bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV2)
}
// bytes4(keccak256("CCIP EVMExtraArgsV1"));
bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9;
struct EVMExtraArgsV1 {
uint256 gasLimit;
}
function _argsToBytes(
EVMExtraArgsV1 memory extraArgs
) internal pure returns (bytes memory bts) {
return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs);
}
// bytes4(keccak256("CCIP EVMExtraArgsV2"));
bytes4 public constant EVM_EXTRA_ARGS_V2_TAG = 0x181dcf10;
/// @param gasLimit: gas limit for the callback on the destination chain.
/// @param allowOutOfOrderExecution: if true, it indicates that the message can be executed in any order relative to other messages from the same sender.
/// This value's default varies by chain. On some chains, a particular value is enforced, meaning if the expected value
/// is not set, the message request will revert.
struct EVMExtraArgsV2 {
uint256 gasLimit;
bool allowOutOfOrderExecution;
}
function _argsToBytes(
EVMExtraArgsV2 memory extraArgs
) internal pure returns (bytes memory bts) {
return abi.encodeWithSelector(EVM_EXTRA_ARGS_V2_TAG, extraArgs);
}
}
来自目标链上的合约接收链下的ccip的组件的消息
首先接收的到消息需要先进行decode解码 any2EvmMessage,获取需要的信息,信息结构需要进行实例化
RequestData memory message = abi.decode(any2EvmMessage.data,(RequestData));
uint256 tokenId = message.tokenId;
address newOwner = message.newOwner;
将 wnft 转给新的owner地址
//mint the NFT ,注意这里是mint一个nft,而不是直接进行transferFrom
wnft.ResetTokenId(newOwner,tokenId);
补充
接收的这个信息结构由Client库提供
/// handle a received message
function _ccipReceive(
Client.Any2EVMMessage memory any2EvmMessage
)
internal
override
{
RequestData memory message = abi.decode(any2EvmMessage.data,(RequestData));
uint256 tokenId = message.tokenId;
address newOwner = message.newOwner;
//mint the NFT
wnft.ResetTokenId(newOwner,tokenId);
emit MessageReceived(
any2EvmMessage.messageId,
any2EvmMessage.sourceChainSelector,
abi.decode(any2EvmMessage.sender, (address)),
tokenId,
newOwner
);
}
首先需要的参数与 lockAndSendNFT的函数是一样的,因为需要使用 sendMessagePayLINK 函数去发送消息
function BurnAndReturn(
uint256 _tokenId,
address newOwner,
uint64 destChainSelector,
address receiver) public {}
将 wnft 从owner地址转移到pool地址,用burn函数烧毁
// transfer NFT to the pool
wnft.transferFrom(msg.sender, address(this), _tokenId);
// burn the NFT
wnft.burn(_tokenId);
使用encode打包消息,提供 payload 给 sendMessagePayLINK函数
// send transaction to the destination chain
bytes memory payload = abi.encode(_tokenId, newOwner);
sendMessagePayLINK(destChainSelector, receiver, payload);
BurnAndReturn函数
function BurnAndReturn(
uint256 _tokenId,
address newOwner,
uint64 destChainSelector,
address receiver) public {
// verify if the sender is the owner of NFT
// comment this because the check is already performed by ERC721
// require(wnft.ownerOf(_tokenId) == msg.sender, "you are not the owner of the NFT");
// transfer NFT to the pool
wnft.transferFrom(msg.sender, address(this), _tokenId);
// burn the NFT
wnft.burn(_tokenId);
// send transaction to the destination chain
bytes memory payload = abi.encode(_tokenId, newOwner);
sendMessagePayLINK(destChainSelector, receiver, payload);
}
对于hardhat框架来说,部署的时候,主要用到两个工具,getNamedAccounts 和 deployments
getNamedAccounts
const {getNamedAccounts,deployments} = require("hardhat");
module.exports = async({getNamedAccounts,deployments}) => {
const {firstAccount} = await getNamedAccounts();
const {deploy,log} = deployments;
log("Deploying CCIP Simulator...");
await deploy("CCIPLocalSimulator",{
contract: "CCIPLocalSimulator",
from: firstAccount,
log:true,
args:[]
});
log("CCIPSimulator contract deployed successfully");
}
module.exports.tags = ["testlocal","all"];//输出标签
使用异步函数,原因是需要先等待关键参数的获取,区别于按顺序执行命令,没有等待时间
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-ethers");
require("hardhat-deploy");
require("hardhat-deploy-ethers");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: {
compilers: [
{
version: "0.8.28",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
]
},
namedAccounts: {
firstAccount: {
default: 0,
}
}
};
await deploy("CCIPLocalSimulator",{
contract: "CCIPLocalSimulator",
from: firstAccount,
log:true,
args:[] //构造函数需要传入的参数
});
log("CCIPSimulator contract deployed successfully");
const {getNamedAccounts,deployments, ethers} = require("hardhat");
module.exports = async({getNamedAccounts,deployments}) => {
const {firstAccount} = await getNamedAccounts();
const {deploy,log} = deployments;
log("Deploying NFTPoolLockAndRelease contract...");
// 1. 先获取部署信息
const ccipSimulatorDeployment = await deployments.get("CCIPLocalSimulator");
// 2. 获取合约实例
const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address);
// 3. 调用configuration函数
const ccipConfig = await ccipSimulator.configuration();
// 4. 获取router,link的地址,nft的地址
const sourceChainRouter = ccipConfig.sourceRouter_;
const sourceChainlink = ccipConfig.linkToken_;
const nftAddrDeployment = await deployments.get("MyNFT");
const nftAddr = await nftAddrDeployment.address;
// 5. 部署NFTPoolLockAndRelease合约
await deploy("NFTPoolLockAndRelease",{
contract: "NFTPoolLockAndRelease",
from: firstAccount,
log:true,
//需要传入的参数: address _router, address _link, address nftAddr
args:[sourceChainRouter,sourceChainlink,nftAddr]
});
log("NFTPoolLockAndRelease contract deployed successfully");
}
module.exports.tags = ["SourceChain","all"];
deployment.get 方法 -- 获取部署的信息 -- 查找合约在哪
ethers.getContractAt -- 创建合约实例
const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address);
传入合约名字 和 地址信息,创建合约实例 -- ccipSimulator
调用configuration函数 --- 这个是 ccip-local 的函数
返回参数
/**
* @dev Returns the configuration of the CCIP simulator
*/
function configuration()
public
view
returns (
uint64 chainSelector,
IRouterClient sourceRouter,
IRouterClient destinationRouter,
WETH9 wrappedNative,
LinkToken linkToken,
BurnMintERC677Helper ccipBnM,
BurnMintERC677Helper ccipLnM
)
{
// 返回所有配置参数
return (
chainSelector,
sourceRouter,
destinationRouter,
wrappedNative,
linkToken,
ccipBnM,
ccipLnM
);
}
//源链 --> 目标链
//mint 一个 nft 到源链
//将nft 锁定在源链, 发送跨链消息
//在目标链得到 mint的 wnft
//目标链 --> 源链
//将目标链的 wnft烧掉,发送跨链消息
//将源链的nft解锁,得到nft
//验证nft是否正确
ethers.getContractAt 与 ethers.getContract 方法
ethers.getContractAt 用于任何地址的合约,包括其他项目的合约。需要传入合约地址进行调用
// 需要指定具体的合约地址
const myNFT = await ethers.getContractAt(
"MyNFT",
"0x1234..." // 具体的合约地址
);
// 适用场景:
- 与已经部署的合约交互
- 与其他项目的合约交互
- 需要指定特定版本的合约
ethers.getContract 适用于同一个项目
// 直接用合约名获取最新部署的合约
const myNFT = await ethers.getContract("MyNFT");
// 多传入一个signer参数,带 signer 的用法:
// 需要发送交易的场景
const contract = await ethers.getContract("ContractName", signer);
await contract.mint(tokenId); // 铸造 NFT
await contract.transfer(to, amount); // 转账
await contract.approve(spender, id); // 授权
await contract.setBaseURI(uri); // 设置 URI
// 适用场景:
- 在同一个项目中
- 合约刚刚部署完
- 想要获取最新部署的合约实例
Chai工具
Chai 是一个用于测试的断言库,它让我们可以写出更易读的测试代码
const { expect } = require("chai");
describe("NFT Contract", function() {
it("Should mint NFT correctly", async function() {
const nft = await ethers.getContract("MyNFT");
const [owner] = await ethers.getSigners();
// Chai 的断言方法
expect(await nft.balanceOf(owner.address)).to.equal(0); // 检查初始余额
await nft.mint(owner.address, 1);
expect(await nft.balanceOf(owner.address)).to.equal(1); // 检查铸造后余额
expect(await nft.ownerOf(1)).to.equal(owner.address); // 检查所有权
});
});
常用方法
// 相等判断
expect(value).to.equal(expectedValue);
expect(value).to.be.equal(expectedValue);
// 大小比较
expect(value).to.be.gt(5); // 大于
expect(value).to.be.gte(5); // 大于等于
expect(value).to.be.lt(10); // 小于
expect(value).to.be.lte(10); // 小于等于
// 包含判断
expect(array).to.include(item);
expect(string).to.contain("text");
// 事件测试
await expect(contract.function())
.to.emit(contract, "EventName")
.withArgs(arg1, arg2);
// 错误测试
await expect(contract.function())
.to.be.revertedWith("error message");
Mocha
是一个功能强大的 JavaScript 测试框架,主要用于 Node.js 应用程序的单元测试和集成测试。它提供了一个灵活的测试环境,支持异步测试,并且可以与其他断言库(如 Chai)结合使用。
describe 块:
describe("Mint NFT,source chain --> destination chain",async function(){//一个注释,一个函数参数,JavaScript 的语法需要一个函数来包含代码块,函数参数就是来形成闭包的
it("Mint NFT",async function(){
await nft.mint(firstAccount.user1.address,1);
})
})
//变量准备
const {getNamedAccounts,deployments, ethers} = require("hardhat");
const {expect} = require("chai");
let firstAccount;
let ccipSimulator;
let nft;
let wnft;
let NFTPoolLockAndRelease;
let NFTPoolBurnAndMint;
let chainSelector;
before(async function () {
firstAccount = (await getNamedAccounts()).firstAccount;
// 部署所有带 "all" 标签的合约并创建快照
await deployments.fixture(["all"]);
ccipSimulator = await ethers.getContract("CCIPLocalSimulator",firstAccount);
nft = await ethers.getContract("MyNFT",firstAccount);
wnft = await ethers.getContract("WrappedNFT",firstAccount);
NFTPoolLockAndRelease = await ethers.getContract("NFTPoolLockAndRelease",firstAccount);
NFTPoolBurnAndMint = await ethers.getContract("NFTPoolBurnAndMint",firstAccount);
chainSelector = (await ccipSimulator.configuration()).chainSelector_;
})
describe("source chain --> dest chain",
async function () {
it("mint nft and test the owner is minter",
async function () {
// get nft
await nft.safeMint(firstAccount);
const ownerOfNft = await nft.ownerOf(0);
expect(ownerOfNft).to.equal(firstAccount);
console.log("owner address is",firstAccount);
}
)
it("transfer NFT from source chain to dest chain, check if the nft is locked",
async function() {
await ccipSimulator.requestLinkFromFaucet(NFTPoolLockAndRelease.target, ethers.parseEther("10"))
// lock and send with CCIP
await nft.approve(NFTPoolLockAndRelease.target, 0)
await NFTPoolLockAndRelease.lockAndSendNFT(0, firstAccount, chainSelector, NFTPoolBurnAndMint.target)
// check if owner of nft is pool's address
const newOwner = await nft.ownerOf(0)
console.log("test")
expect(newOwner).to.equal(NFTPoolLockAndRelease.target)
// check if the nft is locked
const isLocked = await NFTPoolLockAndRelease.tokenLocked(0)
expect(isLocked).to.equal(true)
}
)
it("check if the nft is minted on dest chain",
async function() {
const ownerOfNft = await wnft.ownerOf(0)
expect(ownerOfNft).to.equal(firstAccount)
}
)
})
describe("dest chain --> source chain",
async function () {
it("burn nft and check the nft owner is firstAccount",
async function() {
await wnft.approve(NFTPoolBurnAndMint.target,0)
await NFTPoolBurnAndMint.BurnAndReturn(0, firstAccount, chainSelector, NFTPoolLockAndRelease.target)
const ownerOfNft = await nft.ownerOf(0)
expect(ownerOfNft).to.equal(firstAccount)
}
)
}
)
网络配置文件
developmentChains = ["hardhat", "localhost"]
const networkConfig = {
11155111: {
name: "sepolia",
router: "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59",
linkToken: "0x779877A7B0D9E8603169DdbD7836e478b4624789",
companionChainSelector: "16281711391670634445"
},
80002: {
name: "amoy",
router: "0x9C32fCB86BF0f4a1A8921a9Fe46de3198bb884B2",
linkToken: "0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904",
companionChainSelector: "16015286601757825753"
}
}
module.exports ={
developmentChains,
networkConfig
}
deploy--ccipsimulator
如果network.name是 hardhat 或者 localhost,就运行该脚本
const {getNamedAccounts,deployments, network} = require("hardhat");
const {ethers} = require("hardhat");
const {developmentChains} = require("helper-hardhat-config.js")
module.exports = async({getNamedAccounts,deployments}) => {
if(developmentChains.includes(network.name)){
const {firstAccount} = await getNamedAccounts();
const {deploy,log} = deployments;
log("Deploying CCIP Simulator...");
const ccipSimulator = await deploy("CCIPLocalSimulator",{
contract: "CCIPLocalSimulator",
from: firstAccount,
log:true,
args:[]
});
}
}
module.exports.tags = ["testlocal","all"];
developmentChains.includes(network.name)
在 Hardhat 部署脚本中,network.name 会返回当前运行网络的名称。
比如:
当你运行 hardhat deploy 时,network.name 会是 "hardhat"
当你运行 hardhat deploy --network localhost 时,会是 "localhost"
deploy--NFTPoolLockAndRelease
const {getNamedAccounts,deployments, ethers, network} = require("hardhat");
const {developmentChains,networkConfig} = require("helper-hardhat-config.js")
module.exports = async({getNamedAccounts,deployments}) => {
const {firstAccount} = await getNamedAccounts();
const {deploy,log} = deployments;
let sourceChainRouter
let linkTokenAddr
if(developmentChains.includes(network.name)){
// 1. 先获取部署信息
const ccipSimulatorDeployment = await deployments.get("CCIPLocalSimulator");
// 2. 获取合约实例
const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address);
// 3. 调用configuration函数
const ccipConfig = await ccipSimulator.configuration();
// 4. 获取router,link的地址,nft的地址
sourceChainRouter = ccipConfig.sourceRouter_;
linkTokenAddr = ccipConfig.linkToken_;
}
else{
//network.config 是 Hardhat 提供的,用来获取当前运行网络的配置
//network.config 从 hardhat.config.js 获取网络配置(比如 chainId)
//用这个 chainId 去 helper-hardhat-config.js 中查找对应的合约配置
sourceChainRouter = networkConfig[network.config.chainId].router
linkTokenAddr = networkConfig[network.config.chainId].linkToken
}
log("Deploying NFTPoolLockAndRelease contract...");
const nftAddrDeployment = await deployments.get("MyNFT");
const nftAddr = await nftAddrDeployment.address;
// 5. 部署NFTPoolLockAndRelease合约
await deploy("NFTPoolLockAndRelease",{
contract: "NFTPoolLockAndRelease",
from: firstAccount,
log:true,
//需要传入的参数: address _router, address _link, address nftAddr
args:[sourceChainRouter,sourceChainlink,nftAddr]
});
log("NFTPoolLockAndRelease contract deployed successfully");
}
module.exports.tags = ["SourceChain","all"];
sourceChainRouter = networkConfig[network.config.chainId].router
这种方法的使用例子
// 2. 使用数字作为键的对象
const networkConfig = {
11155111: {
name: "sepolia",
router: "0x0BF3..."
},
80002: {
name: "amoy",
router: "0x9C32..."
}
}
// 假设现在 network.config.chainId 是 11155111
// 这三种写法是等价的:
console.log(networkConfig[11155111].router) // "0x0BF3..."
console.log(networkConfig["11155111"].router) // "0x0BF3..."
console.log(networkConfig[network.config.chainId].router) // "0x0BF3..."
而 network.config.chainId 是从 hardhat.config.js 的配置中去获取的
deploy--NFTPoolBurnAndMint
const {getNamedAccounts,deployments, ethers, network} = require("hardhat");
const {developmentChains,networkConfig} = require("../helper-hardhat-config.js")
module.exports = async({getNamedAccounts,deployments}) => {
const {firstAccount} = await getNamedAccounts();
const {deploy,log} = deployments;
let destChainRouter;
let linkTokenAddr;
if(developmentChains.includes(network.name)){
// 1. 获取部署信息
const ccipSimulatorDeployment = await deployments.get("CCIPLocalSimulator");
// 2. 获取合约实例
const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address);
// 3. 获取router,link,wnft的地址
const ccipConfig = await ccipSimulator.configuration();
destChainRouter = ccipConfig.destinationRouter_;
linkTokenAddr = ccipConfig.linkToken_;
}
else{
destChainRouter = networkConfig[network.config.chainId].router
linkTokenAddr = networkConfig[network.config.chainId].linkToken
}
log("Deploying NFTPoolBurnAndMint contract...");
const wnftAddrDeployment = await deployments.get("WrappedNFT");
const wnftAddr = wnftAddrDeployment.address;
// 4. 部署NFTPoolBurnAndMint合约
await deploy("NFTPoolBurnAndMint",{
contract:"NFTPoolBurnAndMint",
from:firstAccount,
log:true,
args:[destChainRouter,linkTokenAddr,wnftAddr]
})
}
module.exports.tags = ["destChain","all"];
// 定义一个名为 "check-nft" 的任务
task("check-nft")
// 添加参数(可选)
.addParam("address", "NFT contract address")
// 添加可选参数(可选)
.addOptionalParam("tokenId", "Token ID to check")
// 设置任务描述(可选)
.setDescription("Check NFT information")
// 设置任务执行的操作
.setAction(async (taskArgs, hre) => {
// taskArgs: taskArgs 就是用来接收通过 addParam 和 addOptionalParam 定义的参数
// hre: Hardhat Runtime Environment,包含 ethers, network 等工具
// 任务逻辑
const nftContract = await hre.ethers.getContractAt("YourNFT", taskArgs.address);
console.log("Checking NFT...");
});
使用实例
task("check-nft")
.addParam("address", "NFT contract address")
.addOptionalParam("tokenId", "Token ID to check", "0")
.setAction(async (taskArgs, hre) => {
// 获取合约实例
const nftContract = await hre.ethers.getContractAt("YourNFT", taskArgs.address);
// 获取 NFT 信息
const owner = await nftContract.ownerOf(taskArgs.tokenId);
const uri = await nftContract.tokenURI(taskArgs.tokenId);
console.log(`Token ${taskArgs.tokenId}:`);
console.log(`Owner: ${owner}`);
console.log(`URI: ${uri}`);
});
任务运行
# 基本使用
npx hardhat check-nft --address 0x123... --network sepolia
# 带可选参数
npx hardhat check-nft --address 0x123... --token-id 1 --network sepolia
其他用法
task("task-name")
// 添加必需参数
.addParam("param1", "描述")
// 添加可选参数
.addOptionalParam("param2", "描述", "默认值")
// 添加标志参数
.addFlag("flag", "描述")
// 添加位置参数
.addPositionalParam("pos", "描述")
// 设置描述
.setDescription("任务描述")
// 设置执行操作
.setAction(async (taskArgs, hre) => {
// 任务逻辑
});
文件组织结构
在 Hardhat 中,任务(Task)系统的文件组织采用了模块化的结构:每个具体任务都在独立的文件中定义(如 check-nft.js),然后通过一个中心化的 index.js 文件统一导出所有任务,最后在 hardhat.config.js 中只需要一行代码就能导入所有任务。这种结构使得代码更容易维护和扩展,同时保持了项目结构的清晰性。当需要添加新任务时,只需创建新的任务文件并在 index.js 中添加导出即可,而不需要修改配置文件。
如果你后来添加了新任务:
task("mint-nft").setAction(async(taskArgs,hre)=>{
// 铸造 NFT 的逻辑
})
module.exports = {}
只需要在 index.js 中添加:
exports.checkNft = require("./check-nft")
exports.mintNft = require("./mint-nft") // 添加新任务
hardhat.config.js 不需要改变:
require("./task") // 自动包含所有任务
mint-nft
const {task} = require("hardhat/config")
task("mint-nft").setAction(async(taskArgs,hre)=>{
try {
// 1. 检查网络
const network = await hre.ethers.provider.getNetwork();
console.log("Current network:", network.name, network.chainId);
// 2. 检查账户
const {firstAccount} = await hre.getNamedAccounts();
console.log("Account:", firstAccount);
// 3. 检查部署
const deployments = await hre.deployments.all();
console.log("Available deployments:", Object.keys(deployments));
// 4. 获取合约
console.log("Getting contract...");
const MyNFT = await hre.deployments.get("MyNFT");
console.log("Contract address:", MyNFT.address);
// 5. 创建合约实例
const nft = await hre.ethers.getContractAt(
"MyNFT",
MyNFT.address,
await hre.ethers.getSigner(firstAccount)
);
// 6. 铸造 NFT
console.log("Minting NFT...");
const mintTx = await nft.safeMint(firstAccount);
console.log("Waiting for confirmation...");
await mintTx.wait(6);
const tokenAmount = await nft.totalSupply();
const tokenId = tokenAmount - 1n;
console.log(`Mint successful! TokenId:${tokenId}, Amount:${tokenAmount}, Owner:${firstAccount}`);
} catch (error) {
console.error("Detailed error:");
console.error(error);
// 检查特定错误
if (error.code === 'INVALID_ARGUMENT') {
console.error("Contract deployment not found. Please ensure the contract is deployed to Sepolia.");
}
}
})
module.exports = {}
为什么safeMint函数没有返回值,却可以赋值给 mintTx ?
在以太坊智能合约中,当你调用一个写入函数(比如 safeMint)时,它会返回一个 Transaction 对象,即使函数本身没有返回值。这是因为所有改变状态的操作都需要发送交易。
// 1. 调用 safeMint 函数会返回一个待处理的交易对象
const mintTx = await nft.safeMint(firstAccount)
// mintTx 包含了交易的信息,例如:
// {
// hash: "0x...", // 交易哈希
// from: "0x...", // 发送者地址
// to: "0x...", // 合约地址
// nonce: 1, // 交易序号
// gasLimit: BigNumber, // gas 限制
// data: "0x...", // 调用数据
// value: BigNumber, // 发送的以太币数量
// ...
// }
// 2. wait(6) 等待 6 个区块确认
await mintTx.wait(6) // 返回交易收据
// 交易收据包含:
// {
// transactionHash: "0x...",
// blockNumber: 123,
// blockHash: "0x...",
// status: 1, // 1 表示成功
// events: [...], // 包含事件日志
// ...
// }
ethers.js 仍然会返回一个交易对象,这让我们可以:
这是以太坊交易机制的一部分,所有状态改变都通过交易完成
为什么需要这样子写 const tokenId = tokenAmount - BigInt(1) ?
1n 是 JavaScript 中的 BigInt 字面量表示法。在以太坊开发中,我们经常需要处理大数字,特别是当与智能合约交互时。
合约返回的数字通常是 BigNumber 或 BigInt,BigInt 的范围是无限的,只受限于系统的内存
JavaScript 的普通数字(Number)只能安全表示到 2^53 - 1
与 BigInt 类型的数字运算时,必须使用 BigInt 类型
例子
// ❌ 错误:不能混合 BigInt 和 Number
const tokenId = tokenAmount - 1 // TypeError
// ✅ 正确:使用 BigInt
const tokenId = tokenAmount - 1n // 正确
const tokenId = tokenAmount - BigInt(1) // 也正确
// 其他 BigInt 字面量例子
const a = 1n
const b = 100n
const c = 1000000000000000000n // 1 ETH 的 wei 值
check-nft
//引入task工具
const {task} = require("hardhat/config")
task("check-nft").setAction(async(taskArgs,hre)=>{
const {firstAccount } = (await getNamedAccounts()).firstAccount;
const nft = await hre.ethers.getContract("MyNFT",firstAccount)
const totalSupply = await nft.totalSupply()
console.log("check-nft status:")
for(let tokenId=0; tokenId< totalSupply; tokenId++){
const owner = await nft.ownerOf(tokenId)
console.log(`tokenId:${tokenId},owner:${owner}`)
}
})
module.exports = {}
lock-and-cross
const {task} = require("hardhat/config");
const { networkConfig } = require("../helper-hardhat-config");
const { networks } = require("../hardhat.config");
task("lock-and-cross")
.addParam("tokenid", "tokenid to lock and cross")
.addOptionalParam("chainselector", "chainSelector of destination chain")
.addOptionalParam("receiver", "receiver in destination chain")
.setAction(async(taskArgs, hre) => {
//get tokenid
const tokenId = taskArgs.tokenid
//get deployer
const {firstAccount} = await hre.getNamedAccounts();
console.log("deployer is:", firstAccount)
//get chainSelector
let destChainSelector
if(taskArgs.chainselector){
destChainSelector = taskArgs.chainselector
}else{
destChainSelector = networkConfig[hre.network.config.chainId].companionChainSelector
}
console.log("destination chainSelector is:", destChainSelector)
//get receiver
let destReceiver
if(taskArgs.receiver){
destReceiver = taskArgs.receiver
}else{
const nftBurnAndMint = await hre.companionNetworks["destChain"].deployments.get("NFTPoolBurnAndMint")
destReceiver = nftBurnAndMint.address
}
console.log("destination receiver is:", destReceiver)
//get link token
const linkTokenAddr = networkConfig[hre.network.config.chainId].linkToken
const linkToken = await hre.ethers.getContractAt("LinkToken", linkTokenAddr)
console.log("link token is:", linkTokenAddr)
//get nft pool
const nftPoolLockAndRelease = await hre.ethers.getContract("NFTPoolLockAndRelease", firstAccount)
console.log("nft pool is:", nftPoolLockAndRelease.target)
//Transfer link token to nft pool
const balanceBefore = await linkToken.balanceOf(nftPoolLockAndRelease.target)
console.log("balance before is:", balanceBefore)
const transferLinkTx = await linkToken.transfer(nftPoolLockAndRelease.target, hre.ethers.parseEther("0"))
await transferLinkTx.wait(6)
const balanceAfter = await linkToken.balanceOf(nftPoolLockAndRelease.target)
console.log("balance after is:", balanceAfter)
//get nft and approve
const nft = await hre.ethers.getContract("MyNFT", firstAccount)
await nft.approve(nftPoolLockAndRelease.target, tokenId)
console.log("nft approved successfully")
//lock nft
console.log("locking nft...")
console.log(`tokenId: ${tokenId}`, `owner: ${firstAccount}`, `destChainSelector: ${destChainSelector}`, `destReceiver: ${destReceiver}`)
const lockAndCrossTx = await nftPoolLockAndRelease.lockAndSendNFT(tokenId, firstAccount , destChainSelector, destReceiver)
await lockAndCrossTx.wait(6)
console.log("nft locked and sent successfully")
// provide the transaction hash
console.log(`NFT locked and crossed, transaction hash is ${lockAndCrossTx.hash}`)
//messageId
console.log(`messageId is ${lockAndCrossTx.value}`)
})
注意这里要使用 addOptionalParam,保证参数是可选项,而不是 addParam
companionNetworks["destChain"].deployments.get("NFTPoolBurnAndMint"),我们执行命令的时候会使用 network --sepolia 这样的参数,这个参数可以让 hardhat 识别我们config文件里面的网络配置
不管你的函数是否有返回值,根据以太坊的规则,都会有交易对象返回,比如这里
const lockAndCrossTx = await nftPoolLockAndRelease.lockAndSendNFT(tokenId, firstAccount , destChainSelector, destReceiver)
await lockAndCrossTx.wait(6)
console.log("nft locked and sent successfully")
// provide the transaction hash
console.log(`NFT locked and crossed, transaction hash is ${lockAndCrossTx.hash}`)
//messageId
console.log(`messageId is ${lockAndCrossTx.value}`)
lockAndSendNFT 是会返回一个 bytes32 类型的数据的,但是 lockAndCrossTx 并不是 bytes32 类型的数据,而是一个交易对象,交易对象类似json数据
const transferTx = await linkToken.transfer(...)
// transferTx = {
// hash: "0x...", // 交易哈希
// from: "0x...", // 发送者地址
// to: "0x...", // 接收者地址
// data: "0x...", // 交易数据
// ...
// }
还有一点,这里是先返回对象再进行 wait ,wait() 需要交易对象才能监听确认
必须先有交易才能等待它的确认
check-wnft
const { task } = require("hardhat/config")
task("check-wrapped-nft")
.addParam("tokenid", "tokenid to check")
.setAction(async(taskArgs, hre) => {
const tokenId = taskArgs.tokenid
const {firstAccount} = await getNamedAccounts()
const wnft = await ethers.getContract("WrappedNFT", firstAccount)
console.log("checking status of ERC-721")
const totalSupply = await wnft.totalSupply()
console.log(`there are ${totalSupply} tokens under the collection`)
const owner = await wnft.ownerOf(tokenId)
console.log(`TokenId: ${tokenId}, Owner is ${owner}`)
})
module.exports = {}
burn-and-cross
const { task } = require("hardhat/config")
const { networkConfig } = require("../helper-hardhat-config")
task("burn-and-cross")
.addParam("tokenid", "token id to be burned and crossed")
.addOptionalParam("chainselector", "chain selector of destination chain")
.addOptionalParam("receiver", "receiver in the destination chain")
.setAction(async(taskArgs, hre) => {
const { firstAccount } = await getNamedAccounts()
// get token id from parameter
const tokenId = taskArgs.tokenid
const wnft = await ethers.getContract("WrappedNFT", firstAccount)
const nftPoolBurnAndMint = await ethers.getContract("NFTPoolBurnAndMint", firstAccount)
// approve the pool have the permision to transfer deployer's token
const approveTx = await wnft.approve(nftPoolBurnAndMint.target, tokenId)
await approveTx.wait(6)
// transfer 10 LINK token from deployer to pool
console.log("transfering 10 LINK token to NFTPoolBurnAndMint contract")
const linkAddr = networkConfig[network.config.chainId].linkToken
const linkToken = await ethers.getContractAt("LinkToken", linkAddr)
const transferTx = await linkToken.transfer(nftPoolBurnAndMint.target, ethers.parseEther("0"))
await transferTx.wait(6)
// get chain selector
let chainSelector
if(taskArgs.chainselector) {
chainSelector = taskArgs.chainselector
} else {
chainSelector = networkConfig[network.config.chainId].companionChainSelector
}
// get receiver
let receiver
if(taskArgs.receiver) {
receiver = taskArgs.receiver
} else {
receiver = (await hre.companionNetworks["destChain"].deployments.get("NFTPoolLockAndRelease")).address
}
// burn and cross
const burnAndCrossTx = await nftPoolBurnAndMint.BurnAndReturn(tokenId, firstAccount, chainSelector, receiver)
console.log(`NFT burned and crossed with txhash ${burnAndCrossTx.hash}`)
})
module.exports = {}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!