Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...






请注意,只有完成全部三天作业的参与者才有资格获得结业证书。













请注意,只有完成所有三天作业的参与者才有资格获得结业证书
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";
import {EnumerableMap} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableMap.sol";
/**
* 这是一个使用硬编码值来增加清晰度的示例合约。
* 这是一个使用未经审计代码的示例合约。
* 不要在生产环境中使用这段代码。
*/
/// @title - 一个简单的跨链传输/接收代币和数据的信使合约。
/// @dev - 这个示例展示了如何在撤销的情况下恢复代币
contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
using SafeERC20 for IERC20;
// 自定义错误,提供更具描述性的撤销消息。
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // 用于确保合约有足够的余额来支付费用。
error NothingToWithdraw(); // 当尝试提取以太币但没有可提取的以太币时使用。
error FailedToWithdrawEth(address owner, address target, uint256 value); // 当提取以太币失败时使用。
error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // 当目标链没有被合约所有者列入允许名单时使用。
error SourceChainNotAllowed(uint64 sourceChainSelector); // 当源链没有被列入允许名单时使用。
error SenderNotAllowed(address sender); // 当发件人没有被合约所有者列入允许名单时使用。
error InvalidReceiverAddress(); // 当接收地址为0时使用。
error OnlySelf(); // 当函数在合约外部被调用时使用。
error ErrorCase(); // 当模拟消息处理过程中的撤销时使用。
error MessageNotFailed(bytes32 messageId);
// 示例错误代码,可以有许多不同的错误代码。
enum ErrorCode {
// 首先是RESOLVED,以便默认值是已解决的。
RESOLVED,
// 此处可以有任意数量的错误代码。
FAILED
}
struct FailedMessage {
bytes32 messageId;
ErrorCode errorCode;
}
// 当消息发送到另一个链时发出的事件。
event MessageSent(
bytes32 indexed messageId, // CCIP消息的唯一ID。
uint64 indexed destinationChainSelector, // 目的链的链选择器。
address receiver, // 目的链上的接收地址。
string text, // 被发送的文本。
address token, // 被转移的代币地址。
uint256 tokenAmount, // 被转移的代币数量。
address feeToken, // 用于支付CCIP费用的代币地址。
uint256 fees // 为发送消息支付的费用。
);
// 当从另一个链收到消息时发出的事件。
event MessageReceived(
bytes32 indexed messageId, // CCIP消息的唯一ID。
uint64 indexed sourceChainSelector, // 来源链的链选择器。
address sender, // 来自源链的发件人地址。
string text, // 收到的文本。
address token, // 被转移的代币地址。
uint256 tokenAmount // 被转移的代币数量。
);
event MessageFailed(bytes32 indexed messageId, bytes reason);
event MessageRecovered(bytes32 indexed messageId);
bytes32 private s_lastReceivedMessageId; // 存储最后接收的消息ID。
address private s_lastReceivedTokenAddress; // 存储最后接收的代币地址。
uint256 private s_lastReceivedTokenAmount; // 存储最后接收的数量。
string private s_lastReceivedText; // 存储最后接收的文本。
// 映射,用于跟踪允许的目标链。
mapping(uint64 => bool) public allowlistedDestinationChains;
// 映射,用于跟踪允许的源链。
mapping(uint64 => bool) public allowlistedSourceChains;
// 映射,用于跟踪允许的发件人。
mapping(address => bool) public allowlistedSenders;
IERC20 private s_linkToken;
// 存储失败消息的内容。
mapping(bytes32 messageId => Client.Any2EVMMessage contents)
public s_messageContents;
// 包含失败消息及其状态。
EnumerableMap.Bytes32ToUintMap internal s_failedMessages;
// 用于模拟消息处理函数中的撤销。
bool internal s_simRevert = false;
/// @notice 构造函数,用于使用路由器地址初始化合约。
/// @param _router 路由器合约的地址。
/// @param _link LINK合约的地址。
constructor(address _router, address _link) CCIPReceiver(_router) {
s_linkToken = IERC20(_link);
}
/// @dev 修饰符,用于检查给定目标链选择器的链是否被列入允许名单。
/// @param _destinationChainSelector 目的链的选择器。
modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {
if (!allowlistedDestinationChains[_destinationChainSelector])
revert DestinationChainNotAllowlisted(_destinationChainSelector);
_;
}
/// @dev 修饰符,用于检查源链选择器的链和发件人是否被允许。
/// @param _sourceChainSelector 目的链的选择器。
/// @param _sender 发件人的地址。
modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {
if (!allowlistedSourceChains[_sourceChainSelector])
revert SourceChainNotAllowed(_sourceChainSelector);
if (!allowlistedSenders[_sender]) revert SenderNotAllowed(_sender);
_;
}
/// @dev 修饰符,检查接收地址不为0。
/// @param _receiver 接收地址。
modifier validateReceiver(address _receiver) {
if (_receiver == address(0)) revert InvalidReceiverAddress();
_;
}
/// @dev 修饰符,仅允许合同本身执行功能。
/// 如果由任何帐户调用,则抛出异常,而不是合同本身。
modifier onlySelf() {
if (msg.sender != address(this)) revert OnlySelf();
_;
}
/// @dev 更新目标链的交易允许状态。
/// @notice 只能由所有者调用此功能。
/// @param _destinationChainSelector 要更新的目的链选择器。
/// @param allowed 设置目的链的允许状态。
function allowlistDestinationChain(
uint64 _destinationChainSelector,
bool allowed
) external onlyOwner {
allowlistedDestinationChains[_destinationChainSelector] = allowed;
}
/// @dev 更新源链的允许状态
/// @notice 只能由所有者调用此功能。
/// @param _sourceChainSelector 要更新的源链选择器。
/// @param allowed 设置源链的允许状态。
function allowlistSourceChain(
uint64 _sourceChainSelector,
bool allowed
) external onlyOwner {
allowlistedSourceChains[_sourceChainSelector] = allowed;
}
/// @dev 更新发件人的交易允许状态。
/// @notice 只能由所有者调用此功能。
/// @param _sender 要更新的发件人地址。
/// @param allowed 设置发件人的允许状态。
function allowlistSender(address _sender, bool allowed) external onlyOwner {
allowlistedSenders[_sender] = allowed;
}
/// @notice 向目标链上的接收者发送数据和转移代币。
/// @notice 使用 LINK 支付费用。
/// @dev 假设你的合约有足够的LINK来支付 CCIP 费用。
/// @param _destinationChainSelector 目标区块链的标识符(即选择器)。
/// @param _receiver 目标区块链上的接收者地址。
/// @param _text 要发送的字符串数据。
/// @param _token 代币地址。
/// @param _amount 代币数量。
/// @return messageId 发送的 CCIP 消息的 ID。
function sendMessagePayLINK(
uint64 _destinationChainSelector,
address _receiver,
string calldata _text,
address _token,
uint256 _amount
)
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// 在内存中创建一个 EVM2AnyMessage 结构,其中包含发送跨链消息所需的信息
// address(linkToken) 表示费用以 LINK 支付
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_text,
_token,
_amount,
address(s_linkToken)
);
// 初始化一个路由器客户端实例,以与跨链路由器交互
IRouterClient router = IRouterClient(this.getRouter());
// 获取发送 CCIP 消息所需的费用
uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
if (fees > s_linkToken.balanceOf(address(this)))
revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
// 批准路由器代表合约转移 LINK 代币。它将花费费用中的 LINK
s_linkToken.approve(address(router), fees);
// 批准路由器代表合约花费给定数量的代币。它将花费给定数量的代币
IERC20(_token).approve(address(router), _amount);
// 通过路由器发送消息,并存储返回的消息 ID
messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
// 发出一个事件,其中包含消息详细信息
emit MessageSent(
messageId,
_destinationChainSelector,
_receiver,
_text,
_token,
_amount,
address(s_linkToken),
fees
);
// 返回消息 ID
return messageId;
}
/// @notice 向目标链上的接收者发送数据和转移代币。
/// @notice 使用原生gas支付费用。
/// @dev 假设你的合约有足够的原生gas,如以太坊上的ETH或Polygon上的MATIC。
/// @param _destinationChainSelector 目标区块链的标识符(即选择器)。
/// @param _receiver 目标区块链上的接收者地址。
/// @param _text 要发送的字符串数据。
/// @param _token 代币地址。
/// @param _amount 代币数量。
/// @return messageId 发送的CCIP消息的ID。
function sendMessagePayNative(
uint64 _destinationChainSelector,
address _receiver,
string calldata _text,
address _token,
uint256 _amount
)
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// 在内存中创建一个 EVM2AnyMessage 结构,其中包含发送跨链消息所需的信息
// address(0) 表示费用以原生气体支付
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_text,
_token,
_amount,
address(0)
);
// 初始化一个路由器客户端实例,以与跨链路由器交互
IRouterClient router = IRouterClient(this.getRouter());
// 获取发送 CCIP 消息所需的费用
uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
if (fees > address(this).balance)
revert NotEnoughBalance(address(this).balance, fees);
// 批准路由器代表合约花费给定数量的代币。它将花费给定数量的代币
IERC20(_token).approve(address(router), _amount);
// 通过路由器发送消息,并存储返回的消息 ID
messageId = router.ccipSend{value: fees}(
_destinationChainSelector,
evm2AnyMessage
);
// 发出一个事件,其中包含消息详细信息
emit MessageSent(
messageId,
_destinationChainSelector,
_receiver,
_text,
_token,
_amount,
address(0),
fees
);
// 返回消息 ID
return messageId;
}
/**
* @notice 返回最后接收到的CCIP消息的详细信息。
* @dev 此函数检索最后接收到的CCIP消息的ID、文本、代币地址和代币数量。
* @return messageId 最后接收到的CCIP消息的ID。
* @return text 最后接收到的CCIP消息的文本。
* @return tokenAddress 最后接收到的CCIP消息中的代币地址。
* @return tokenAmount 最后接收到的CCIP消息中的代币数量。
*/
function getLastReceivedMessageDetails()
public
view
returns (
bytes32 messageId,
string memory text,
address tokenAddress,
uint256 tokenAmount
)
{
return (
s_lastReceivedMessageId,
s_lastReceivedText,
s_lastReceivedTokenAddress,
s_lastReceivedTokenAmount
);
}
/**
* @notice 检索失败消息的分页列表。
* @dev 此功能返回由 `offset` 和 `limit` 参数定义的失败消息的子集。它确保分页参数在可用数据集的范围内。
* @param offset 要返回的第一个失败消息的索引,通过从数据集的开头跳过指定数量的消息来实现分页。
* @param limit 要返回的失败消息的最大数量,限制返回数组的大小。
* @return failedMessages `FailedMessage` 结构的数组,每个结构包含一个 `messageId` 和一个 `errorCode`(解决或失败),表示请求的失败消息的子集。返回数组的长度由 `limit` 和失败消息的总数决定。
*/
function getFailedMessages(
uint256 offset,
uint256 limit
) external view returns (FailedMessage[] memory) {
uint256 length = s_failedMessages.length();
// 计算实际返回的项目数量(不能超过总长度或请求的限制)
uint256 returnLength = (offset + limit > length)
? length - offset
: limit;
FailedMessage[] memory failedMessages = new FailedMessage[](
returnLength
);
// 调整循环以尊重分页(从 offset 开始,结束于 offset + limit 或总长度)
for (uint256 i = 0; i < returnLength; i++) {
(bytes32 messageId, uint256 errorCode) = s_failedMessages.at(
offset + i
);
failedMessages[i] = FailedMessage(messageId, ErrorCode(errorCode));
}
return failedMessages;
}
/// @notice CCIP 路由器调用的入口点。此功能不应撤销,所有错误应在合约内部处理。
/// @param any2EvmMessage 要处理的消息。
/// @dev 非常重要的是确保只有路由器调用此功能。
function ccipReceive(
Client.Any2EVMMessage calldata any2EvmMessage
)
external
override
onlyRouter
onlyAllowlisted(
any2EvmMessage.sourceChainSelector,
abi.decode(any2EvmMessage.sender, (address))
) // 确保源链和发件人被列入允许名单
{
/* solhint-disable no-empty-blocks */
try this.processMessage(any2EvmMessage) {
// 本示例中故意为空;如果 processMessage 成功,则无需采取任何行动
} catch (bytes memory err) {
// 可以根据捕获的错误设置不同的错误代码。每个都可以有不同的处理方式。
s_failedMessages.set(
any2EvmMessage.messageId,
uint256(ErrorCode.FAILED)
);
s_messageContents[any2EvmMessage.messageId] = any2EvmMessage;
// 不要撤销,以免 CCIP 撤销。改为发出事件。
// 可以稍后重试该消息,而无需手动执行 CCIP。
emit MessageFailed(any2EvmMessage.messageId, err);
return;
}
}
/// @notice 为此合约处理传入消息的入口点。
/// @param any2EvmMessage 接收到的 CCIP 消息。
/// @dev 将指定数量的代币转移到此合约的所有者。此功能必须是外部的,因为利用 Solidity 的 try/catch 错误处理机制。
/// 它使用 `onlySelf`:只能由合约调用。
function processMessage(
Client.Any2EVMMessage calldata any2EvmMessage
)
external
onlySelf
onlyAllowlisted(
any2EvmMessage.sourceChainSelector,
abi.decode(any2EvmMessage.sender, (address))
) // 确保源链和发件人被列入允许名单
{
// 为测试目的模拟撤销
if (s_simRevert) revert ErrorCase();
_ccipReceive(any2EvmMessage); // 处理消息 - 可能会撤销
}
/// @notice 允许所有者重试失败的消息,以解锁相关的代币。
/// @param messageId 失败消息的唯一标识符。
/// @param tokenReceiver 将代币发送到的地址。
/// @dev 该功能只能由合约所有者调用。它更改消息的状态从 'failed(失败)' 到 'resolved(已解决)',以防止重复条目和多次重试相同的消息
function retryFailedMessage(
bytes32 messageId,
address tokenReceiver
) external onlyOwner {
// 检查消息是否失败;如果没有,则撤销交易。
if (s_failedMessages.get(messageId) != uint256(ErrorCode.FAILED))
revert MessageNotFailed(messageId);
// 将错误代码设置为 RESOLVED,以禁止重新进入和多次重试同一失败消息。
s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED));
// 检索失败消息的内容。
Client.Any2EVMMessage memory message = s_messageContents[messageId];
// 本示例期望一次发送一枚代币,但你可以处理多个代币。
// 将关联的代币转移到指定的接收者作为紧急逃生方式。
IERC20(message.destTokenAmounts[0].token).safeTransfer(
tokenReceiver,
message.destTokenAmounts[0].amount
);
// 发出事件,表明消息已被恢复。
emit MessageRecovered(messageId);
}
/// @notice 允许所有者切换模拟撤销的测试。
/// @param simRevert 如果为 `true`,模拟撤销条件;如果为 `false`,禁用模拟。
/// @dev 该功能只能由合约所有者调用。
function setSimRevert(bool simRevert) external onlyOwner {
s_simRevert = simRevert;
}
function _ccipReceive(
Client.Any2EVMMessage memory any2EvmMessage
) internal override {
s_lastReceivedMessageId = any2EvmMessage.messageId; // 获取消息ID
s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // 解码发送的文本
// 预期一次转移一个代币,但可以转移多个代币。
s_lastReceivedTokenAddress = any2EvmMessage.destTokenAmounts[0].token;
s_lastReceivedTokenAmount = any2EvmMessage.destTokenAmounts[0].amount;
emit MessageReceived(
any2EvmMessage.messageId,
any2EvmMessage.sourceChainSelector, // 获取源链标识符(即选择器)
abi.decode(any2EvmMessage.sender, (address)), // 解码发送者地址,
abi.decode(any2EvmMessage.data, (string)),
any2EvmMessage.destTokenAmounts[0].token,
any2EvmMessage.destTokenAmounts[0].amount
);
}
/// @notice 构建CCIP消息。
/// @dev 此函数将创建一个EVM2AnyMessage结构体,包含所有必要的信息以实现可编程的代币转移。
/// @param _receiver 接收者的地址。
/// @param _text 要发送的字符串数据。
/// @param _token 要转移的代币。
/// @param _amount 要转移的代币数量。
/// @param _feeTokenAddress 用于费用的代币地址。将address(0)设置为使用原生gas。
/// @return Client.EVM2AnyMessage 返回一个EVM2AnyMessage结构体,其中包含发送CCIP消息所需的信息。
function _buildCCIPMessage(
address _receiver,
string calldata _text,
address _token,
uint256 _amount,
address _feeTokenAddress
) private pure returns (Client.EVM2AnyMessage memory) {
// 设置代币数量
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// 在内存中创建一个 EVM2AnyMessage 结构,包含发送跨链消息所需的信息
Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver), // ABI 编码的接收地址
data: abi.encode(_text), // ABI 编码的字符串
tokenAmounts: tokenAmounts, // 被转移的代币数量和类型
extraArgs: Client._argsToBytes(
// 额外参数,设置 gas 限制
Client.EVMExtraArgsV1({gasLimit: 400_000})
),
// 设置 feeToken 为 feeTokenAddress,表示将使用特定资产支付费用
feeToken: _feeTokenAddress
});
return evm2AnyMessage;
}
/// @notice 该合约接收以太的默认函数。
/// @dev 该函数没有函数体,使其成为接收以太的默认函数。
/// 当向合约发送以太时而没有任何数据时,会自动调用此函数。
receive() external payable {}
/// @notice 允许合约所有者从合约中提取全部以太余额。
/// @dev 如果没有资金可提取或转账失败,该函数将撤销。
/// 只能由合约所有者调用。
/// @param _beneficiary 将以太发送到的地址。
function withdraw(address _beneficiary) public onlyOwner {
// 检索此合约的余额
uint256 amount = address(this).balance;
// 如果没有可提取的内容,撤销
if (amount == 0) revert NothingToWithdraw();
// 尝试发送资金,捕获成功状态并丢弃任何返回数据
(bool sent, ) = _beneficiary.call{value: amount}("");
// 如果发送失败,带有尝试转账信息的撤销
if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
}
/// @notice 允许合约所有者提取特定ERC20代币的所有代币。
/// @dev 如果没有代币可提取,则此函数将回滚并显示'NothingToWithdraw'错误。
/// @param _beneficiary 应将代币发送到的地址。
/// @param _token 要提取的ERC20代币的合约地址。
function withdrawToken(
address _beneficiary,
address _token
) public onlyOwner {
// 检索此合约的代币余额
uint256 amount = IERC20(_token).balanceOf(address(this));
// 如果没有可提取的代币,撤销
if (amount == 0) revert NothingToWithdraw();
// 安全地将代币转移到指定受益人
IERC20(_token).safeTransfer(_beneficiary, amount);
}
}
ccipReceive进入:





请注意,只有完成前三天所有作业的参与者才有资格获得结业证书。

forge initforge install smartcontractkit/chainlink-brownie-contractsforge install smartcontractkit/ccip@b06a3c2eecb9892ec6f76a015624413fffa1a122forge install OpenZeppelin/openzeppelin-contractsforge install smartcontractkit/chainlink-local# foundry.toml
[profile.default]
src = "src"
out = "out"
test = "test"
libs = ["lib"]
solc = '0.8.24'
remappings = [
'@chainlink/contracts-ccip=lib/ccip/contracts',
'@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/',
'@chainlink/local/=lib/chainlink-local/',
]ETHEREUM_SEPOLIA_RPC_URL=""
ARBITRUM_SEPOLIA_RPC_URL=""# foundry.toml
[profile.default]
src = "src"
out = "out"
test = "test"
libs = ["lib"]
solc = '0.8.24'
remappings = [
'@chainlink/contracts-ccip=lib/ccip/contracts',
'@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/',
'@chainlink/local/=lib/chainlink-local/',
]
[rpc_endpoints]
ethereumSepolia = "${ETHEREUM_SEPOLIA_RPC_URL}"
arbitrumSepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {CCIPLocalSimulatorFork, Register} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol";
import {XNFT} from "../src/XNFT.sol";
import {EncodeExtraArgs} from "./utils/EncodeExtraArgs.sol";
contract XNFTTest is Test {
CCIPLocalSimulatorFork public ccipLocalSimulatorFork;
uint256 ethSepoliaFork;
uint256 arbSepoliaFork;
Register.NetworkDetails ethSepoliaNetworkDetails;
Register.NetworkDetails arbSepoliaNetworkDetails;
address alice;
address bob;
XNFT public ethSepoliaXNFT;
XNFT public arbSepoliaXNFT;
EncodeExtraArgs public encodeExtraArgs;
function setUp() public {
alice = makeAddr("alice");
bob = makeAddr("bob");
string memory ETHEREUM_SEPOLIA_RPC_URL = vm.envString("ETHEREUM_SEPOLIA_RPC_URL");
string memory ARBITRUM_SEPOLIA_RPC_URL = vm.envString("ARBITRUM_SEPOLIA_RPC_URL");
ethSepoliaFork = vm.createSelectFork(ETHEREUM_SEPOLIA_RPC_URL);
arbSepoliaFork = vm.createFork(ARBITRUM_SEPOLIA_RPC_URL);
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
vm.makePersistent(address(ccipLocalSimulatorFork));
}
// YOUR TEST GOES HERE...
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns (bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}struct NetworkDetails {
uint64 chainSelector;
address routerAddress;
address linkAddress;
address wrappedNativeAddress;
address ccipBnMAddress;
address ccipLnMAddress;
}ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // we are currently on Ethereum Sepolia Fork
assertEq(
ethSepoliaNetworkDetails.chainSelector,
16015286601757825753,
"Sanity check: Ethereum Sepolia chain selector should be 16015286601757825753"
); function setUp() public {
alice = makeAddr("alice");
bob = makeAddr("bob");
string memory ETHEREUM_SEPOLIA_RPC_URL = vm.envString("ETHEREUM_SEPOLIA_RPC_URL");
string memory ARBITRUM_SEPOLIA_RPC_URL = vm.envString("ARBITRUM_SEPOLIA_RPC_URL");
ethSepoliaFork = vm.createSelectFork(ETHEREUM_SEPOLIA_RPC_URL);
arbSepoliaFork = vm.createFork(ARBITRUM_SEPOLIA_RPC_URL);
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
vm.makePersistent(address(ccipLocalSimulatorFork));
// 步骤 1) 在Ethereum Sepolia网络中部署XNFT.sol
assertEq(vm.activeFork(), ethSepoliaFork);
ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // 目前我们处于Ethereum Sepolia的分叉网络中
assertEq(
ethSepoliaNetworkDetails.chainSelector,
16015286601757825753,
"Sanity check: Ethereum Sepolia chain selector should be 16015286601757825753"
);
ethSepoliaXNFT = new XNFT(
ethSepoliaNetworkDetails.routerAddress,
ethSepoliaNetworkDetails.linkAddress,
ethSepoliaNetworkDetails.chainSelector
);
// 步骤 2) 在Arbitrum Sepolia网络中部署XNFT.sol
vm.selectFork(arbSepoliaFork);
assertEq(vm.activeFork(), arbSepoliaFork);
arbSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // 目前我们处于Arbitrum Sepolia的分叉网络中
assertEq(
arbSepoliaNetworkDetails.chainSelector,
3478487238524512106,
"Sanity check: Arbitrum Sepolia chain selector should be 421614"
);
arbSepoliaXNFT = new XNFT(
arbSepoliaNetworkDetails.routerAddress,
arbSepoliaNetworkDetails.linkAddress,
arbSepoliaNetworkDetails.chainSelector
);
}function testShouldMintNftOnArbitrumSepoliaAndTransferItToEthereumSepolia() public {
// 步骤 3) 在Ethereum Sepolia网络中, 调用enableChain方法
vm.selectFork(ethSepoliaFork);
assertEq(vm.activeFork(), ethSepoliaFork);
encodeExtraArgs = new EncodeExtraArgs();
uint256 gasLimit = 200_000;
bytes memory extraArgs = encodeExtraArgs.encode(gasLimit);
assertEq(extraArgs, hex"97a657c90000000000000000000000000000000000000000000000000000000000030d40"); // 该值来源于 https://cll-devrel.gitbook.io/ccip-masterclass-3/ccip-masterclass/exercise-xnft#step-3-on-ethereum-sepolia-call-enablechain-function
ethSepoliaXNFT.enableChain(arbSepoliaNetworkDetails.chainSelector, address(arbSepoliaXNFT), extraArgs);
// 步骤 4) 在Arbitrum Sepolia网络中, 调用enableChain方法
vm.selectFork(arbSepoliaFork);
assertEq(vm.activeFork(), arbSepoliaFork);
arbSepoliaXNFT.enableChain(ethSepoliaNetworkDetails.chainSelector, address(ethSepoliaXNFT), extraArgs);
// 步骤 5) 在Arbitrum Sepolia网络中, 向XNFT.sol充值3 LINK
assertEq(vm.activeFork(), arbSepoliaFork);
ccipLocalSimulatorFork.requestLinkFromFaucet(address(arbSepoliaXNFT), 3 ether);
// 步骤 6) 在Arbitrum Sepolia网络中, 铸造新的xNFT
assertEq(vm.activeFork(), arbSepoliaFork);
vm.startPrank(alice);
arbSepoliaXNFT.mint();
uint256 tokenId = 0;
assertEq(arbSepoliaXNFT.balanceOf(alice), 1);
assertEq(arbSepoliaXNFT.ownerOf(tokenId), alice);
// 步骤 7) 在Arbitrum Sepolia网络中, 跨链转移xNFT
arbSepoliaXNFT.crossChainTransferFrom(
address(alice), address(bob), tokenId, ethSepoliaNetworkDetails.chainSelector, XNFT.PayFeesIn.LINK
);
vm.stopPrank();
assertEq(arbSepoliaXNFT.balanceOf(alice), 0);
// 在Ethereum Sepolia中验证xNFT已成功跨链转移
ccipLocalSimulatorFork.switchChainAndRouteMessage(ethSepoliaFork); // 这行代码将更换CHAINLINK CCIP DONs, 不要遗漏
assertEq(vm.activeFork(), ethSepoliaFork);
assertEq(ethSepoliaXNFT.balanceOf(bob), 1);
assertEq(ethSepoliaXNFT.ownerOf(tokenId), bob);
}// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {CCIPLocalSimulatorFork, Register} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol";
import {XNFT} from "../src/XNFT.sol";
import {EncodeExtraArgs} from "./utils/EncodeExtraArgs.sol";
contract XNFTTest is Test {
CCIPLocalSimulatorFork public ccipLocalSimulatorFork;
uint256 ethSepoliaFork;
uint256 arbSepoliaFork;
Register.NetworkDetails ethSepoliaNetworkDetails;
Register.NetworkDetails arbSepoliaNetworkDetails;
address alice;
address bob;
XNFT public ethSepoliaXNFT;
XNFT public arbSepoliaXNFT;
EncodeExtraArgs public encodeExtraArgs;
function setUp() public {
alice = makeAddr("alice");
bob = makeAddr("bob");
string memory ETHEREUM_SEPOLIA_RPC_URL = vm.envString("ETHEREUM_SEPOLIA_RPC_URL");
string memory ARBITRUM_SEPOLIA_RPC_URL = vm.envString("ARBITRUM_SEPOLIA_RPC_URL");
ethSepoliaFork = vm.createSelectFork(ETHEREUM_SEPOLIA_RPC_URL);
arbSepoliaFork = vm.createFork(ARBITRUM_SEPOLIA_RPC_URL);
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
vm.makePersistent(address(ccipLocalSimulatorFork));
// 步骤 1) 在Ethereum Sepolia网络中部署XNFT.sol
assertEq(vm.activeFork(), ethSepoliaFork);
ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // 目前我们处于Ethereum Sepolia的分叉网络中
assertEq(
ethSepoliaNetworkDetails.chainSelector,
16015286601757825753,
"Sanity check: Ethereum Sepolia chain selector should be 16015286601757825753"
);
ethSepoliaXNFT = new XNFT(
ethSepoliaNetworkDetails.routerAddress,
ethSepoliaNetworkDetails.linkAddress,
ethSepoliaNetworkDetails.chainSelector
);
// 步骤 2) 在Arbitrum Sepolia网络中部署XNFT.sol
vm.selectFork(arbSepoliaFork);
assertEq(vm.activeFork(), arbSepoliaFork);
arbSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // 目前我们处于Arbitrum Sepolia的分叉网络中
assertEq(
arbSepoliaNetworkDetails.chainSelector,
3478487238524512106,
"Sanity check: Arbitrum Sepolia chain selector should be 421614"
);
arbSepoliaXNFT = new XNFT(
arbSepoliaNetworkDetails.routerAddress,
arbSepoliaNetworkDetails.linkAddress,
arbSepoliaNetworkDetails.chainSelector
);
}
function testShouldMintNftOnArbitrumSepoliaAndTransferItToEthereumSepolia() public {
// 步骤 3) 在Ethereum Sepolia网络中, 调用enableChain方法
vm.selectFork(ethSepoliaFork);
assertEq(vm.activeFork(), ethSepoliaFork);
encodeExtraArgs = new EncodeExtraArgs();
uint256 gasLimit = 200_000;
bytes memory extraArgs = encodeExtraArgs.encode(gasLimit);
assertEq(extraArgs, hex"97a657c90000000000000000000000000000000000000000000000000000000000030d40"); // 该值来源于 https://cll-devrel.gitbook.io/ccip-masterclass-3/ccip-masterclass/exercise-xnft#step-3-on-ethereum-sepolia-call-enablechain-function
ethSepoliaXNFT.enableChain(arbSepoliaNetworkDetails.chainSelector, address(arbSepoliaXNFT), extraArgs);
// 步骤 4) 在Arbitrum Sepolia网络中, 调用enableChain方法
vm.selectFork(arbSepoliaFork);
assertEq(vm.activeFork(), arbSepoliaFork);
arbSepoliaXNFT.enableChain(ethSepoliaNetworkDetails.chainSelector, address(ethSepoliaXNFT), extraArgs);
// 步骤 5) 在Arbitrum Sepolia网络中, 向XNFT.sol充值3 LINK
assertEq(vm.activeFork(), arbSepoliaFork);
ccipLocalSimulatorFork.requestLinkFromFaucet(address(arbSepoliaXNFT), 3 ether);
// 步骤 6) 在Arbitrum Sepolia网络中, 增发新的xNFT
assertEq(vm.activeFork(), arbSepoliaFork);
vm.startPrank(alice);
arbSepoliaXNFT.mint();
uint256 tokenId = 0;
assertEq(arbSepoliaXNFT.balanceOf(alice), 1);
assertEq(arbSepoliaXNFT.ownerOf(tokenId), alice);
// 步骤 7) 在Arbitrum Sepolia网络中, 跨链转移xNFT
arbSepoliaXNFT.crossChainTransferFrom(
address(alice), address(bob), tokenId, ethSepoliaNetworkDetails.chainSelector, XNFT.PayFeesIn.LINK
);
vm.stopPrank();
assertEq(arbSepoliaXNFT.balanceOf(alice), 0);
// 在Ethereum Sepolia中验证xNFT已成功跨链转移
ccipLocalSimulatorFork.switchChainAndRouteMessage(ethSepoliaFork); // 这行代码将更换CHAINLINK CCIP DONs, 不要遗漏
assertEq(vm.activeFork(), ethSepoliaFork);
assertEq(ethSepoliaXNFT.balanceOf(bob), 1);
assertEq(ethSepoliaXNFT.ownerOf(tokenId), bob);
}
}0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59,LINK 合约地址是 0x779877A7B0D9E8603169DdbD7836e478b4624789。setSimRevert

falseextraArgsstrict: trueccipReceive

node -vnpm -vmkdir ccip-masterclass-3cd ccip-masterclass-3npx hardhat initforge --version

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract Empty {}node -vnpm -vmkdir ccip-masterclasscd ccip-masterclassnpx hardhat@2.14.1 initforge --versionnpm i @chainlink/contracts-ccip --save-dev# Node modules
node_modules/npm i @chainlink/contracts-ccip --save-devlibs = ['node_modules', 'lib']
remappings = [
'@chainlink/contracts-ccip/=node_modules/@chainlink/contracts-ccip'
]// SOURCE BLOCKCHAIN
interface IRouterClient {
/// @notice 请求发送 CCIP 消息到目标链
/// @param destinationChainSelector 目标链选择器
/// @param message 包含数据和/或代币的跨链 CCIP 消息
/// @return messageId 消息 ID
function ccipSend(
uint64 destinationChainSelector,
Client.EVM2AnyMessage calldata message
) external payable returns(bytes32 messageId);
}// SOURCE BLOCKCHAIN
library Client {
struct EVM2AnyMessage {
bytes receiver; // 目标 EVM 链上的接收者地址,使用 abi.encode(receiver address)
bytes data; // 数据负载
EVMTokenAmount[] tokenAmounts; // 转移的代币地址和数量
address feeToken; // 费用代币地址;address(0) 表示你发送 msg.value
bytes extraArgs; // 使用 _argsToBytes(EVMExtraArgsV1) 填充此字段
}
struct EVMTokenAmount {
address token; // 本地区块链上的代币地址
uint256 amount;
}
struct EVMExtraArgsV1 {
uint256 gasLimit;
bool strict;
}
}// DESTINATION BLOCKCHAIN
/// @notice 打算从路由器接收消息的应用合约应实现此接口。
interface IAny2EVMMessageReceiver {
/// @notice 路由器调用此函数以传递消息
/// @param message CCIP 消息
/// @dev 注意确保你检查 `msg.sender` 是路由器
function ccipReceive(Client.Any2EVMMessage calldata message) external;
}// DESTINATION BLOCKCHAIN
library Client {
struct Any2EVMMessage {
bytes32 messageId; // 源链上的 `ccipSend` 对应的消息 ID
uint64 sourceChainSelector; // 源链选择器
bytes sender; // 如果来自 EVM 链,使用 `abi.decode(sender)`
bytes data; // 在原始消息中发送的负载
EVMTokenAmount[] tokenAmounts; // 在目标链上的代币及其数量
}
struct EVMTokenAmount {
address token; // 本地区块链上的代币地址
uint256 amount;
}
}mkdir ccip-masterclasscd ccip-masterclassforge initforge install smartcontractkit/ccip@ccip-develop# foundry.toml
remappings = [
'@chainlink/contracts-ccip/=lib/ccip/contracts/'
]function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
// 在源区块链上销毁token
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
// 为了在目标区块链上铸造而编码一些细节参数
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK
? address(i_linkToken)
: address(0)
});
}function ccipReceive(
Client.Any2EVMMessage calldata message
)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(
message.sourceChainSelector,
abi.decode(message.sender, (address))
)
{
(
address from,
address to,
uint256 tokenId,
string memory tokenUri
) = abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
}struct XNftDetails {
address xNftAddress;
bytes ccipExtraArgsBytes;
}
mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain)
public s_chains;
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].xNftAddress == address(0))
revert ChainNotEnabled(_chainSelector);
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].xNftAddress != _sender)
revert SenderNotEnabled(_sender);
_;
}
function enableChain(
uint64 chainSelector,
address xNftAddress,
bytes memory ccipExtraArgs
) external onlyOwner onlyOtherChains(chainSelector) {
s_chains[chainSelector] = XNftDetails({
xNftAddress: xNftAddress,
ccipExtraArgsBytes: ccipExtraArgs
});
emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
}
function disableChain(
uint64 chainSelector
) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}// EncodeExtraArgs.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(
uint256 gasLimit
) external pure returns (bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({
gasLimit: gasLimit
});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}{
"name": "SenderNotWhitelisted",
"params": [
{
"name": "sender",
"value": "0xcd936a39336a2e2c5a011137e46c8120dcae0d65",
"type": "address"
}
]
}package main
import (
"fmt"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/joho/godotenv"
"github.com/smartcontractkit/chainlink/core/scripts/ccip/revert-reason/handler"
"github.com/smartcontractkit/chainlink/core/scripts/ccip/secrets"
)
// 如何使用
// 设置一个错误代码字符串或设置chainId、txHash和txRequester。
// 设置错误代码允许脚本离线运行且不需要任何RPC终端。
// 使用chainId、txHash和txRequester则需要一个RPC终端,如果交易较旧,节点需要运行在归档模式下。
// 设置变量并运行 main.go。脚本将尝试将错误代码与各种CCIP合约的ABI匹配。如果找到匹配项,它将检查是否是CCIP包装的错误,
// 如ExecutionError和TokenRateLimitError,如果是,将解码内部错误。
// 要配置RPC终端,请将RPC_<chain_id>环境变量设置为RPC终端。例如:RPC_420=https://rpc.<chain_id>.com
const (
ErrorCodeString = "0x4e487b710000000000000000000000000000000000000000000000000000000000000032"
// 以下输入仅在ERROR_CODE_STRING为空时使用
// 需要一个节点的URL
// 注意:如果交易较旧,该节点需要运行在归档模式下
ChainId = uint64(420)
TxHash = "0x97be8559164442595aba46b5f849c23257905b78e72ee43d9b998b28eee78b84"
TxRequester = "0xe88ff73814fb891bb0e149f5578796fa41f20242"
EnvFileName = ".env"
)
func main() {
errorString, err := getErrorString()
if err != nil {
panic(err)
}
decodedError, err := handler.DecodeErrorStringFromABI(errorString)
if err != nil {
panic(err)
}
fmt.Println(decodedError)
}
func getErrorString() (string, error) {
errorCodeString := ErrorCodeString
if errorCodeString == "" {
// 尝试从.env文件中载入环境变量
err := godotenv.Load(EnvFileName)
if err != nil {
fmt.Println("No .env file found, using env vars from shell")
}
ec, err := ethclient.Dial(secrets.GetRPC(ChainId))
if err != nil {
return "", err
}
errorCodeString, err = handler.GetErrorForTx(ec, TxHash, TxRequester)
if err != nil {
return "", err
}
}
return errorCodeString, nil
}extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 500_000, strict: false})
)npm i @chainlink/contracts-ccip --save-devnpm i @chainlink/contracts --save-devnpm i @openzeppelin/contracts --save-devforge install smartcontractkit/ccip@ccip-develop# foundry.toml
remappings = [
'@chainlink/contracts-ccip/=lib/ccip/contracts/'
]forge install smartcontractkit/chainlink# foundry.toml
remappings = [
'@chainlink/contracts/=lib/chainlink/contracts/'
]// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/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.
*/
contract XNFT is
ERC721,
ERC721URIStorage,
ERC721Burnable,
IAny2EVMMessageReceiver,
ReentrancyGuard,
OwnerIsCreator
{
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnArbitrumSepolia();
error NotEnoughBalanceForFees(
uint256 currentBalance,
uint256 calculatedFees
);
error NothingToWithdraw();
error FailedToWithdrawEth(address owner, address target, uint256 value);
error ChainNotEnabled(uint64 chainSelector);
error SenderNotEnabled(address sender);
error OperationNotAllowedOnCurrentChain(uint64 chainSelector);
struct XNftDetails {
address xNftAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ARBITRUM_SEPOLIA_CHAIN_ID = 421614;
string[] characters = [
"https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png",
"https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH?filename=Chainlink_Knight.png",
"https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP?filename=Chainlink_Orc.png",
"https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF?filename=Chainlink_Witch.png"
];
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain)
public s_chains;
event ChainEnabled(
uint64 chainSelector,
address xNftAddress,
bytes ccipExtraArgs
);
event ChainDisabled(uint64 chainSelector);
event CrossChainSent(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
event CrossChainReceived(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
modifier onlyRouter() {
if (msg.sender != address(i_ccipRouter))
revert InvalidRouter(msg.sender);
_;
}
modifier onlyOnArbitrumSepolia() {
if (block.chainid != ARBITRUM_SEPOLIA_CHAIN_ID)
revert OnlyOnArbitrumSepolia();
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].xNftAddress == address(0))
revert ChainNotEnabled(_chainSelector);
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].xNftAddress != _sender)
revert SenderNotEnabled(_sender);
_;
}
modifier onlyOtherChains(uint64 _chainSelector) {
if (_chainSelector == i_currentChainSelector)
revert OperationNotAllowedOnCurrentChain(_chainSelector);
_;
}
constructor(
address ccipRouterAddress,
address linkTokenAddress,
uint64 currentChainSelector
) ERC721("Cross Chain NFT", "XNFT") {
if (ccipRouterAddress == address(0)) revert InvalidRouter(address(0));
i_ccipRouter = IRouterClient(ccipRouterAddress);
i_linkToken = LinkTokenInterface(linkTokenAddress);
i_currentChainSelector = currentChainSelector;
}
function mint() external onlyOnArbitrumSepolia {
uint256 tokenId = _nextTokenId++;
string memory uri = characters[tokenId % characters.length];
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
function enableChain(
uint64 chainSelector,
address xNftAddress,
bytes memory ccipExtraArgs
) external onlyOwner onlyOtherChains(chainSelector) {
s_chains[chainSelector] = XNftDetails({
xNftAddress: xNftAddress,
ccipExtraArgsBytes: ccipExtraArgs
});
emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
}
function disableChain(
uint64 chainSelector
) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}
function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK
? address(i_linkToken)
: address(0)
});
// Get the fee required to send the CCIP message
uint256 fees = i_ccipRouter.getFee(destinationChainSelector, message);
if (payFeesIn == PayFeesIn.LINK) {
if (fees > i_linkToken.balanceOf(address(this)))
revert NotEnoughBalanceForFees(
i_linkToken.balanceOf(address(this)),
fees
);
// Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
i_linkToken.approve(address(i_ccipRouter), fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend(
destinationChainSelector,
message
);
} else {
if (fees > address(this).balance)
revert NotEnoughBalanceForFees(address(this).balance, fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend{value: fees}(
destinationChainSelector,
message
);
}
emit CrossChainSent(
from,
to,
tokenId,
i_currentChainSelector,
destinationChainSelector
);
}
/// @inheritdoc IAny2EVMMessageReceiver
function ccipReceive(
Client.Any2EVMMessage calldata message
)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(
message.sourceChainSelector,
abi.decode(message.sender, (address))
)
{
uint64 sourceChainSelector = message.sourceChainSelector;
(
address from,
address to,
uint256 tokenId,
string memory tokenUri
) = abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
emit CrossChainReceived(
from,
to,
tokenId,
sourceChainSelector,
i_currentChainSelector
);
}
function withdraw(address _beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
(bool sent, ) = _beneficiary.call{value: amount}("");
if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
}
function withdrawToken(
address _beneficiary,
address _token
) public onlyOwner {
uint256 amount = IERC20(_token).balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).safeTransfer(_beneficiary, amount);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function getCCIPRouter() public view returns (address) {
return address(i_ccipRouter);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return
interfaceId == type(IAny2EVMMessageReceiver).interfaceId ||
super.supportsInterface(interfaceId);
}
}npx hardhat compile// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/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.
*/
contract XNFT is
ERC721,
ERC721URIStorage,
ERC721Burnable,
IAny2EVMMessageReceiver,
ReentrancyGuard,
OwnerIsCreator
{
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnArbitrumSepolia();
error NotEnoughBalanceForFees(
uint256 currentBalance,
uint256 calculatedFees
);
error NothingToWithdraw();
error FailedToWithdrawEth(address owner, address target, uint256 value);
error ChainNotEnabled(uint64 chainSelector);
error SenderNotEnabled(address sender);
error OperationNotAllowedOnCurrentChain(uint64 chainSelector);
struct XNftDetails {
address xNftAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ARBITRUM_SEPOLIA_CHAIN_ID = 421614;
string[] characters = [
"https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png",
"https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH?filename=Chainlink_Knight.png",
"https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP?filename=Chainlink_Orc.png",
"https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF?filename=Chainlink_Witch.png"
];
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain)
public s_chains;
event ChainEnabled(
uint64 chainSelector,
address xNftAddress,
bytes ccipExtraArgs
);
event ChainDisabled(uint64 chainSelector);
event CrossChainSent(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
event CrossChainReceived(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
modifier onlyRouter() {
if (msg.sender != address(i_ccipRouter))
revert InvalidRouter(msg.sender);
_;
}
modifier onlyOnArbitrumSepolia() {
if (block.chainid != ARBITRUM_SEPOLIA_CHAIN_ID)
revert OnlyOnArbitrumSepolia();
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].xNftAddress == address(0))
revert ChainNotEnabled(_chainSelector);
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].xNftAddress != _sender)
revert SenderNotEnabled(_sender);
_;
}
modifier onlyOtherChains(uint64 _chainSelector) {
if (_chainSelector == i_currentChainSelector)
revert OperationNotAllowedOnCurrentChain(_chainSelector);
_;
}
constructor(
address ccipRouterAddress,
address linkTokenAddress,
uint64 currentChainSelector
) ERC721("Cross Chain NFT", "XNFT") {
if (ccipRouterAddress == address(0)) revert InvalidRouter(address(0));
i_ccipRouter = IRouterClient(ccipRouterAddress);
i_linkToken = LinkTokenInterface(linkTokenAddress);
i_currentChainSelector = currentChainSelector;
}
function mint() external onlyOnArbitrumSepolia {
uint256 tokenId = _nextTokenId++;
string memory uri = characters[tokenId % characters.length];
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
function enableChain(
uint64 chainSelector,
address xNftAddress,
bytes memory ccipExtraArgs
) external onlyOwner onlyOtherChains(chainSelector) {
s_chains[chainSelector] = XNftDetails({
xNftAddress: xNftAddress,
ccipExtraArgsBytes: ccipExtraArgs
});
emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
}
function disableChain(
uint64 chainSelector
) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}
function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK
? address(i_linkToken)
: address(0)
});
// Get the fee required to send the CCIP message
uint256 fees = i_ccipRouter.getFee(destinationChainSelector, message);
if (payFeesIn == PayFeesIn.LINK) {
if (fees > i_linkToken.balanceOf(address(this)))
revert NotEnoughBalanceForFees(
i_linkToken.balanceOf(address(this)),
fees
);
// Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
i_linkToken.approve(address(i_ccipRouter), fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend(
destinationChainSelector,
message
);
} else {
if (fees > address(this).balance)
revert NotEnoughBalanceForFees(address(this).balance, fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend{value: fees}(
destinationChainSelector,
message
);
}
emit CrossChainSent(
from,
to,
tokenId,
i_currentChainSelector,
destinationChainSelector
);
}
/// @inheritdoc IAny2EVMMessageReceiver
function ccipReceive(
Client.Any2EVMMessage calldata message
)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(
message.sourceChainSelector,
abi.decode(message.sender, (address))
)
{
uint64 sourceChainSelector = message.sourceChainSelector;
(
address from,
address to,
uint256 tokenId,
string memory tokenUri
) = abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
emit CrossChainReceived(
from,
to,
tokenId,
sourceChainSelector,
i_currentChainSelector
);
}
function withdraw(address _beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
(bool sent, ) = _beneficiary.call{value: amount}("");
if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
}
function withdrawToken(
address _beneficiary,
address _token
) public onlyOwner {
uint256 amount = IERC20(_token).balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).safeTransfer(_beneficiary, amount);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function getCCIPRouter() public view returns (address) {
return address(i_ccipRouter);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return
interfaceId == type(IAny2EVMMessageReceiver).interfaceId ||
super.supportsInterface(interfaceId);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/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.
*/
contract XNFT is
ERC721,
ERC721URIStorage,
ERC721Burnable,
IAny2EVMMessageReceiver,
ReentrancyGuard,
OwnerIsCreator
{
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnArbitrumSepolia();
error NotEnoughBalanceForFees(
uint256 currentBalance,
uint256 calculatedFees
);
error NothingToWithdraw();
error FailedToWithdrawEth(address owner, address target, uint256 value);
error ChainNotEnabled(uint64 chainSelector);
error SenderNotEnabled(address sender);
error OperationNotAllowedOnCurrentChain(uint64 chainSelector);
struct XNftDetails {
address xNftAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ARBITRUM_SEPOLIA_CHAIN_ID = 421614;
string[] characters = [
"https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png",
"https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH?filename=Chainlink_Knight.png",
"https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP?filename=Chainlink_Orc.png",
"https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF?filename=Chainlink_Witch.png"
];
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain)
public s_chains;
event ChainEnabled(
uint64 chainSelector,
address xNftAddress,
bytes ccipExtraArgs
);
event ChainDisabled(uint64 chainSelector);
event CrossChainSent(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
event CrossChainReceived(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
modifier onlyRouter() {
if (msg.sender != address(i_ccipRouter))
revert InvalidRouter(msg.sender);
_;
}
modifier onlyOnArbitrumSepolia() {
if (block.chainid != ARBITRUM_SEPOLIA_CHAIN_ID)
revert OnlyOnArbitrumSepolia();
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].xNftAddress == address(0))
revert ChainNotEnabled(_chainSelector);
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].xNftAddress != _sender)
revert SenderNotEnabled(_sender);
_;
}
modifier onlyOtherChains(uint64 _chainSelector) {
if (_chainSelector == i_currentChainSelector)
revert OperationNotAllowedOnCurrentChain(_chainSelector);
_;
}
constructor(
address ccipRouterAddress,
address linkTokenAddress,
uint64 currentChainSelector
) ERC721("Cross Chain NFT", "XNFT") {
if (ccipRouterAddress == address(0)) revert InvalidRouter(address(0));
i_ccipRouter = IRouterClient(ccipRouterAddress);
i_linkToken = LinkTokenInterface(linkTokenAddress);
i_currentChainSelector = currentChainSelector;
}
function mint() external onlyOnArbitrumSepolia {
uint256 tokenId = _nextTokenId++;
string memory uri = characters[tokenId % characters.length];
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
function enableChain(
uint64 chainSelector,
address xNftAddress,
bytes memory ccipExtraArgs
) external onlyOwner onlyOtherChains(chainSelector) {
s_chains[chainSelector] = XNftDetails({
xNftAddress: xNftAddress,
ccipExtraArgsBytes: ccipExtraArgs
});
emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
}
function disableChain(
uint64 chainSelector
) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}
function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK
? address(i_linkToken)
: address(0)
});
// Get the fee required to send the CCIP message
uint256 fees = i_ccipRouter.getFee(destinationChainSelector, message);
if (payFeesIn == PayFeesIn.LINK) {
if (fees > i_linkToken.balanceOf(address(this)))
revert NotEnoughBalanceForFees(
i_linkToken.balanceOf(address(this)),
fees
);
// Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
i_linkToken.approve(address(i_ccipRouter), fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend(
destinationChainSelector,
message
);
} else {
if (fees > address(this).balance)
revert NotEnoughBalanceForFees(address(this).balance, fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend{value: fees}(
destinationChainSelector,
message
);
}
emit CrossChainSent(
from,
to,
tokenId,
i_currentChainSelector,
destinationChainSelector
);
}
/// @inheritdoc IAny2EVMMessageReceiver
function ccipReceive(
Client.Any2EVMMessage calldata message
)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(
message.sourceChainSelector,
abi.decode(message.sender, (address))
)
{
uint64 sourceChainSelector = message.sourceChainSelector;
(
address from,
address to,
uint256 tokenId,
string memory tokenUri
) = abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
emit CrossChainReceived(
from,
to,
tokenId,
sourceChainSelector,
i_currentChainSelector
);
}
function withdraw(address _beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
(bool sent, ) = _beneficiary.call{value: amount}("");
if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
}
function withdrawToken(
address _beneficiary,
address _token
) public onlyOwner {
uint256 amount = IERC20(_token).balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).safeTransfer(_beneficiary, amount);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function getCCIPRouter() public view returns (address) {
return address(i_ccipRouter);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return
interfaceId == type(IAny2EVMMessageReceiver).interfaceId ||
super.supportsInterface(interfaceId);
}
}npm i @chainlink/env-enc --save-devnpx env-enc set-pwPRIVATE_KEY=""
ARBITRUM_SEPOLIA_RPC_URL=""
ETHEREUM_SEPOLIA_RPC_URL=""npx env-enc setnpx env-enc viewimport * as dotenvenc from '@chainlink/env-enc'
dotenvenc.config();
import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const ARBITRUM_SEPOLIA_RPC_URL = process.env.ARBITRUM_SEPOLIA_RPC_URL;
const ETHEREUM_SEPOLIA_RPC_URL = process.env.ETHEREUM_SEPOLIA_RPC_URL;
const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: '0.8.20',
settings: {
evmVersion: 'paris'
}
}
]
},
networks: {
hardhat: {
chainId: 31337
},
arbitrumSepolia: {
url: ARBITRUM_SEPOLIA_RPC_URL !== undefined ? ARBITRUM_SEPOLIA_RPC_URL : '',
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
chainId: 421614
},
ethereumSepolia: {
url: ETHEREUM_SEPOLIA_RPC_URL !== undefined ? ETHEREUM_SEPOLIA_RPC_URL : '',
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
chainId: 11155111
},
}
};
export default config;// deployXNFT.ts
import { ethers, network } from "hardhat";
async function main() {
const ccipRouterAddressEthereumSepolia = `0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59`;
const linkTokenAddressEthereumSepolia = `0x779877A7B0D9E8603169DdbD7836e478b4624789`;
const chainIdEthereumSepolia = `16015286601757825753`;
const xNft = await ethers.deployContract("XNFT", [
ccipRouterAddressEthereumSepolia,
linkTokenAddressEthereumSepolia,
chainIdEthereumSepolia
]);
await xNft.waitForDeployment();
console.log(`XNFT deployed on ${network.name} with address ${xNft.target}`);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});npx hardhat run ./scripts/deployXNFT.ts --network ethereumSepolia// deployXNFTArbitrum.ts
import { ethers, network } from "hardhat";
async function main() {
const ccipRouterAddressArbitrumSepolia = `0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165`;
const linkTokenAddressArbitrumSepolia = `0xb1D4538B4571d411F07960EF2838Ce337FE1E80E`;
const chainIdArbitrumSepolia = `3478487238524512106`;
const xNft = await ethers.deployContract("XNFT", [
ccipRouterAddressArbitrumSepolia,
linkTokenAddressArbitrumSepolia,
chainIdArbitrumSepolia
]);
await xNft.waitForDeployment();
console.log(`XNFT deployed on ${network.name} with address ${xNft.target}`);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});npx hardhat run ./scripts/deployXNFTArbitrum.ts --network arbitrumSepolia// scripts/mint.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.mint();
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});npx hardhat run ./scripts/mint.ts --network arbitrumSepoliacast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "mint()"
























linkTokenAddress0x779877A7B0D9E8603169DdbD7836e478b4624789currentChainSelector16015286601757825753ccipExtraArgs 参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。16015286601757825753tokenId 参数:您要跨链转移的 xNFT 的 ID;




mkdir ccip-masterclass-3cd ccip-masterclass-3forge init
unlockmint














forge install OpenZeppelin/openzeppelin-contracts# foundry.toml
remappings = [
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/'
]// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Empty {}PRIVATE_KEY=""
ARBITRUM_SEPOLIA_RPC_URL=""
ETHEREUM_SEPOLIA_RPC_URL=""source .env[profile.default]
src = 'src'
out = 'out'
remappings = [
'@chainlink/contracts/=lib/chainlink/contracts',
'@chainlink/contracts-ccip/=lib/chainlink/contracts-ccip',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/'
]
solc = '0.8.20'
evm_version = 'paris'
[rpc_endpoints]
arbitrumSepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
ethereumSepolia = "${ETHEREUM_SEPOLIA_RPC_URL}"
# See more config options https://github.com/foundry-rs/foundry/tree/master/configforge create --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY src/XNFT.sol:XNFT --constructor-args 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59 0x779877A7B0D9E8603169DdbD7836e478b4624789 16015286601757825753// script/XNFT.s.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "forge-std/Script.sol";
import {XNFT} from "../src/XNFT.sol";
contract DeployXNFT is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
address ccipRouterAddressEthereumSepolia = 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59;
address linkTokenAddressEthereumSepolia = 0x779877A7B0D9E8603169DdbD7836e478b4624789;
uint64 chainSelectorEthereumSepolia = 16015286601757825753;
XNFT xNft = new XNFT(
ccipRouterAddressEthereumSepolia,
linkTokenAddressEthereumSepolia,
chainSelectorEthereumSepolia
);
console.log(
"XNFT deployed to ",
address(xNft)
);
vm.stopBroadcast();
}
}forge script ./script/XNFT.s.sol:XNFT -vvv --broadcast --rpc-url ethereumSepoliaforge create --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY src/XNFT.sol:XNFT --constructor-args 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E 3478487238524512106// script/XNFTArbitrum.s.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "forge-std/Script.sol";
import {XNFT} from "../src/XNFT.sol";
contract DeployXNFTArbitrum is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
address ccipRouterAddressArbitrumSepolia = 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165;
address linkTokenAddressArbitrumSepolia = 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E;
uint64 chainSelectorArbitrumSepolia = 3478487238524512106;
XNFT xNft = new XNFT(
ccipRouterAddressArbitrumSepolia,
linkTokenAddressArbitrumSepolia,
chainSelectorArbitrumSepolia
);
console.log(
"XNFT deployed to ",
address(xNft)
);
vm.stopBroadcast();
}
}forge script ./script/XNFTArbitrum.s.sol:XNFT -vvv --broadcast --rpc-url arbitrumSepolia// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}// scripts/enableChain.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `ethereumSepolia`) {
console.error(`Must be called from Ethereum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressEthereumSepolia = `PUT XNFT ADDRESS ON ETHEREUM SEPOLIA HERE`;
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const chainSelectorArbitrumSepolia = `3478487238524512106`;
const ccipExtraArgs = `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressEthereumSepolia, signer);
const tx = await xNft.enableChain(
chainSelectorArbitrumSepolia,
xNftAddressArbitrumSepolia,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});npx hardhat run ./scripts/enableChain.ts --network ethereumSepolia// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}// scripts/enableChainArbitrum.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const xNftAddressEthereumSepolia = `PUT XNFT ADDRESS ON ETHEREUM SEPOLIA HERE`;
const chainSelectorEthereumSepolia = `16015286601757825753`;
const ccipExtraArgs = `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.enableChain(
chainSelectorEthereumSepolia,
xNftAddressEthereumSepolia,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});npx hardhat run ./scripts/enableChainArbitrum.ts --network arbitrumSepolia// scripts/crossChainTransfer.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const from = `PUT YOUR EOA ADDRESS HERE`;
const to = `PUT RECEIVER's ADDRESS HERE`;
const tokenId = 0; // put NFT token id here
const destinationChainSelector = `16015286601757825753`;
const payFeesIn = 1; // 0 - Native, 1 - LINK
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.crossChainTransferFrom(
from,
to,
tokenId,
destinationChainSelector,
payFeesIn
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});npx hardhat run ./scripts/crossChainTransfer.ts --network arbitrumSepolia347848723852451210616015286601757825753// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}cast send <XNFT_ADDRESS_ON_ETHEREUM_SEPOLIA> --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY "enableChain(uint64,address,bytes)" 3478487238524512106 <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "enableChain(uint64,address,bytes)" 16015286601757825753 <XNFT_ADDRESS_ON_ETHEREUM_SEPOLIA> 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "crossChainTransferFrom(address,address,uint256,uint64,uint8)" <YOUR_EOA_ADDRESS> <RECEIVER_ADDRESS> 0 16015286601757825753 1// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Empty {}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/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.
*/
contract TransferUSDC is OwnerIsCreator {
using SafeERC20 for IERC20;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
error DestinationChainNotAllowlisted(uint64 destinationChainSelector);
error NothingToWithdraw();
IRouterClient private immutable i_ccipRouter;
IERC20 private immutable i_linkToken;
IERC20 private immutable i_usdcToken;
mapping(uint64 => bool) public allowlistedChains;
modifier onlyAllowlistedChain(uint64 _destinationChainSelector) {
if (!allowlistedChains[_destinationChainSelector])
revert DestinationChainNotAllowlisted(_destinationChainSelector);
_;
}
event UsdcTransferred(
bytes32 messageId,
uint64 destinationChainSelector,
address receiver,
uint256 amount,
uint256 ccipFee
);
constructor(address ccipRouter, address linkToken, address usdcToken) {
i_ccipRouter = IRouterClient(ccipRouter);
i_linkToken = IERC20(linkToken);
i_usdcToken = IERC20(usdcToken);
}
function allowlistDestinationChain(
uint64 _destinationChainSelector,
bool _allowed
) external onlyOwner {
allowlistedChains[_destinationChainSelector] = _allowed;
}
function transferUsdc(
uint64 _destinationChainSelector,
address _receiver,
uint256 _amount,
uint64 _gasLimit
)
external
onlyOwner
onlyAllowlistedChain(_destinationChainSelector)
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: address(i_usdcToken),
amount: _amount
});
tokenAmounts[0] = tokenAmount;
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: "",
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: _gasLimit})
),
feeToken: address(i_linkToken)
});
uint256 ccipFee = i_ccipRouter.getFee(
_destinationChainSelector,
message
);
if (ccipFee > i_linkToken.balanceOf(address(this)))
revert NotEnoughBalance(
i_linkToken.balanceOf(address(this)),
ccipFee
);
i_linkToken.approve(address(i_ccipRouter), ccipFee);
i_usdcToken.safeTransferFrom(msg.sender, address(this), _amount);
i_usdcToken.approve(address(i_ccipRouter), _amount);
// Send CCIP Message
messageId = i_ccipRouter.ccipSend(_destinationChainSelector, message);
emit UsdcTransferred(
messageId,
_destinationChainSelector,
_receiver,
_amount,
ccipFee
);
}
function withdrawToken(
address _beneficiary,
address _token
) public onlyOwner {
uint256 amount = IERC20(_token).balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).transfer(_beneficiary, amount);
}
}



