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

Warmest welcome to the first-ever CCIP Bootcamp! Designed for established Web3 developer, the CCIP Bootcamp is meant to elevate your cross-chain development skills to the next level.

















ccip-masterclassstrict - Used for strict sequencing. You should set it to false. CCIP will always process messages sent from a specific sender to a specific destination blockchain in the order they were sent. If you set strict: true in the extraArgs part of the message, and if the ccipReceive fails (reverts), it will prevent any following messages from the same sender from being processed until the current message is successfully executed. You should be very careful when using this feature to avoid unintentionally stopping messages from the sender from being processed. The strict sequencing feature is currently experimental, and there is no guarantee of its maintenance or further development in the future.















// 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-dev// SOURCE BLOCKCHAIN
interface IRouterClient {
/// @notice Request a CCIP message to be sent to the destination chain
/// @param destinationChainSelector The destination chain selector
/// @param message The cross-chain CCIP message including data and/or tokens
/// @return messageId The message ID
function ccipSend(
uint64 destinationChainSelector,
Client.EVM2AnyMessage calldata message
) external payable returns(bytes32 messageId);
}// SOURCE BLOCKCHAIN
library Client {
struct EVM2AnyMessage {
bytes receiver; // abi.encode(receiver address) for dest EVM chains
bytes data; // data payload
EVMTokenAmount[] tokenAmounts; // token transfers
address feeToken; // fee token address; address(0) means you are sending msg.value
bytes extraArgs; // populate this with _argsToBytes(EVMExtraArgsV1)
}
struct EVMTokenAmount {
address token; // token address on local blockchain
uint256 amount;
}
struct EVMExtraArgsV1 {
uint256 gasLimit;
bool strict;
}
}// DESTINATION BLOCKCHAIN
/// @notice Application contracts that intend to receive messages from
/// the router should implement this interface.
interface IAny2EVMMessageReceiver {
/// @notice Router calls this to deliver a message
/// @param message CCIP Message
/// @dev Note ensure you check that msg.sender is the Router
function ccipReceive(Client.Any2EVMMessage calldata message) external;
}// DESTINATION BLOCKCHAIN
library Client {
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[] tokenAmounts; // Tokens and their amounts at destination
}
struct EVMTokenAmount {
address token; // token address on local blockchain
uint256 amount;
}
}mkdir ccip-masterclasscd ccip-masterclassforge initlibs = ['node_modules', 'lib']
remappings = [
'@chainlink/contracts-ccip/=node_modules/@chainlink/contracts-ccip'
]forge install smartcontractkit/ccip@ccip-develop# foundry.toml
remappings = [
'@chainlink/contracts-ccip/=lib/ccip/contracts/'
]

Please note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.
How to design a cross-chain NFT smart contract
gasLimitextraArgsPlease note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.
Please note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.
// 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";
/**
* 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 transferring/receiving tokens and data across chains.
/// @dev - This example shows how to recover tokens in case of revert
contract ProgrammableDefensiveTokenTransfers is CCIPReceiver, OwnerIsCreator {
using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
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 to cover the fees.
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.
error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
error SourceChainNotAllowed(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
error SenderNotAllowed(address sender); // Used when the sender has not been allowlisted by the contract owner.
error InvalidReceiverAddress(); // Used when the receiver address is 0.
error OnlySelf(); // Used when a function is called outside of the contract itself.
error ErrorCase(); // Used when simulating a revert during message processing.
error MessageNotFailed(bytes32 messageId);
// Example error code, could have many different error codes.
enum ErrorCode {
// RESOLVED is first so that the default value is resolved.
RESOLVED,
// Could have any number of error codes here.
FAILED
}
struct FailedMessage {
bytes32 messageId;
ErrorCode errorCode;
}
// Event emitted when a message is sent to another chain.
event MessageSent(
bytes32 indexed messageId, // The unique ID of the CCIP message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
string text, // The text being sent.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
// Event emitted when a message is received from another chain.
event MessageReceived(
bytes32 indexed messageId, // The unique ID of the CCIP message.
uint64 indexed sourceChainSelector, // The chain selector of the source chain.
address sender, // The address of the sender from the source chain.
string text, // The text that was received.
address token, // The token address that was transferred.
uint256 tokenAmount // The token amount that was transferred.
);
event MessageFailed(bytes32 indexed messageId, bytes reason);
event MessageRecovered(bytes32 indexed messageId);
bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
address private s_lastReceivedTokenAddress; // Store the last received token address.
uint256 private s_lastReceivedTokenAmount; // Store the last received amount.
string private s_lastReceivedText; // Store the last received text.
// Mapping to keep track of allowlisted destination chains.
mapping(uint64 => bool) public allowlistedDestinationChains;
// Mapping to keep track of allowlisted source chains.
mapping(uint64 => bool) public allowlistedSourceChains;
// Mapping to keep track of allowlisted senders.
mapping(address => bool) public allowlistedSenders;
IERC20 private s_linkToken;
// The message contents of failed messages are stored here.
mapping(bytes32 messageId => Client.Any2EVMMessage contents)
public s_messageContents;
// Contains failed messages and their state.
EnumerableMap.Bytes32ToUintMap internal s_failedMessages;
// This is used to simulate a revert in the processMessage function.
bool internal s_simRevert = false;
/// @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) CCIPReceiver(_router) {
s_linkToken = IERC20(_link);
}
/// @dev Modifier that checks if the chain with the given destinationChainSelector is allowlisted.
/// @param _destinationChainSelector The selector of the destination chain.
modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {
if (!allowlistedDestinationChains[_destinationChainSelector])
revert DestinationChainNotAllowlisted(_destinationChainSelector);
_;
}
/// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted.
/// @param _sourceChainSelector The selector of the destination chain.
/// @param _sender The address of the sender.
modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {
if (!allowlistedSourceChains[_sourceChainSelector])
revert SourceChainNotAllowed(_sourceChainSelector);
if (!allowlistedSenders[_sender]) revert SenderNotAllowed(_sender);
_;
}
/// @dev Modifier that checks the receiver address is not 0.
/// @param _receiver The receiver address.
modifier validateReceiver(address _receiver) {
if (_receiver == address(0)) revert InvalidReceiverAddress();
_;
}
/// @dev Modifier to allow only the contract itself to execute a function.
/// Throws an exception if called by any account other than the contract itself.
modifier onlySelf() {
if (msg.sender != address(this)) revert OnlySelf();
_;
}
/// @dev Updates the allowlist status of a destination chain for transactions.
/// @notice This function can only be called by the owner.
/// @param _destinationChainSelector The selector of the destination chain to be updated.
/// @param allowed The allowlist status to be set for the destination chain.
function allowlistDestinationChain(
uint64 _destinationChainSelector,
bool allowed
) external onlyOwner {
allowlistedDestinationChains[_destinationChainSelector] = allowed;
}
/// @dev Updates the allowlist status of a source chain
/// @notice This function can only be called by the owner.
/// @param _sourceChainSelector The selector of the source chain to be updated.
/// @param allowed The allowlist status to be set for the source chain.
function allowlistSourceChain(
uint64 _sourceChainSelector,
bool allowed
) external onlyOwner {
allowlistedSourceChains[_sourceChainSelector] = allowed;
}
/// @dev Updates the allowlist status of a sender for transactions.
/// @notice This function can only be called by the owner.
/// @param _sender The address of the sender to be updated.
/// @param allowed The allowlist status to be set for the sender.
function allowlistSender(address _sender, bool allowed) external onlyOwner {
allowlistedSenders[_sender] = allowed;
}
/// @notice Sends data and transfer tokens to receiver on the destination chain.
/// @notice Pay for fees in LINK.
/// @dev Assumes your contract has sufficient LINK to pay for CCIP fees.
/// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
/// @param _receiver The address of the recipient on the destination blockchain.
/// @param _text The string data to be sent.
/// @param _token token address.
/// @param _amount token amount.
/// @return messageId The ID of the CCIP message that was sent.
function sendMessagePayLINK(
uint64 _destinationChainSelector,
address _receiver,
string calldata _text,
address _token,
uint256 _amount
)
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
// address(linkToken) means fees are paid in LINK
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_text,
_token,
_amount,
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);
// approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token
IERC20(_token).approve(address(router), _amount);
// Send the message through the router and store the returned message ID
messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
// Emit an event with message details
emit MessageSent(
messageId,
_destinationChainSelector,
_receiver,
_text,
_token,
_amount,
address(s_linkToken),
fees
);
// Return the message ID
return messageId;
}
/// @notice Sends data and transfer tokens to receiver on the destination chain.
/// @notice Pay for fees in native gas.
/// @dev Assumes your contract has sufficient native gas like ETH on Ethereum or MATIC on Polygon.
/// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
/// @param _receiver The address of the recipient on the destination blockchain.
/// @param _text The string data to be sent.
/// @param _token token address.
/// @param _amount token amount.
/// @return messageId The ID of the CCIP message that was sent.
function sendMessagePayNative(
uint64 _destinationChainSelector,
address _receiver,
string calldata _text,
address _token,
uint256 _amount
)
external
onlyOwner
onlyAllowlistedDestinationChain(_destinationChainSelector)
validateReceiver(_receiver)
returns (bytes32 messageId)
{
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
// address(0) means fees are paid in native gas
Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
_receiver,
_text,
_token,
_amount,
address(0)
);
// 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 > address(this).balance)
revert NotEnoughBalance(address(this).balance, fees);
// approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token
IERC20(_token).approve(address(router), _amount);
// Send the message through the router and store the returned message ID
messageId = router.ccipSend{value: fees}(
_destinationChainSelector,
evm2AnyMessage
);
// Emit an event with message details
emit MessageSent(
messageId,
_destinationChainSelector,
_receiver,
_text,
_token,
_amount,
address(0),
fees
);
// Return the message ID
return messageId;
}
/**
* @notice Returns the details of the last CCIP received message.
* @dev This function retrieves the ID, text, token address, and token amount of the last received CCIP message.
* @return messageId The ID of the last received CCIP message.
* @return text The text of the last received CCIP message.
* @return tokenAddress The address of the token in the last CCIP received message.
* @return tokenAmount The amount of the token in the last CCIP received message.
*/
function getLastReceivedMessageDetails()
public
view
returns (
bytes32 messageId,
string memory text,
address tokenAddress,
uint256 tokenAmount
)
{
return (
s_lastReceivedMessageId,
s_lastReceivedText,
s_lastReceivedTokenAddress,
s_lastReceivedTokenAmount
);
}
/**
* @notice Retrieves a paginated list of failed messages.
* @dev This function returns a subset of failed messages defined by `offset` and `limit` parameters. It ensures that the pagination parameters are within the bounds of the available data set.
* @param offset The index of the first failed message to return, enabling pagination by skipping a specified number of messages from the start of the dataset.
* @param limit The maximum number of failed messages to return, restricting the size of the returned array.
* @return failedMessages An array of `FailedMessage` struct, each containing a `messageId` and an `errorCode` (RESOLVED or FAILED), representing the requested subset of failed messages. The length of the returned array is determined by the `limit` and the total number of failed messages.
*/
function getFailedMessages(
uint256 offset,
uint256 limit
) external view returns (FailedMessage[] memory) {
uint256 length = s_failedMessages.length();
// Calculate the actual number of items to return (can't exceed total length or requested limit)
uint256 returnLength = (offset + limit > length)
? length - offset
: limit;
FailedMessage[] memory failedMessages = new FailedMessage[](
returnLength
);
// Adjust loop to respect pagination (start at offset, end at offset + limit or total length)
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 The entrypoint for the CCIP router to call. This function should
/// never revert, all errors should be handled internally in this contract.
/// @param any2EvmMessage The message to process.
/// @dev Extremely important to ensure only router calls this.
function ccipReceive(
Client.Any2EVMMessage calldata any2EvmMessage
)
external
override
onlyRouter
onlyAllowlisted(
any2EvmMessage.sourceChainSelector,
abi.decode(any2EvmMessage.sender, (address))
) // Make sure the source chain and sender are allowlisted
{
/* solhint-disable no-empty-blocks */
try this.processMessage(any2EvmMessage) {
// Intentionally empty in this example; no action needed if processMessage succeeds
} catch (bytes memory err) {
// Could set different error codes based on the caught error. Each could be
// handled differently.
s_failedMessages.set(
any2EvmMessage.messageId,
uint256(ErrorCode.FAILED)
);
s_messageContents[any2EvmMessage.messageId] = any2EvmMessage;
// Don't revert so CCIP doesn't revert. Emit event instead.
// The message can be retried later without having to do manual execution of CCIP.
emit MessageFailed(any2EvmMessage.messageId, err);
return;
}
}
/// @notice Serves as the entry point for this contract to process incoming messages.
/// @param any2EvmMessage Received CCIP message.
/// @dev Transfers specified token amounts to the owner of this contract. This function
/// must be external because of the try/catch for error handling.
/// It uses the `onlySelf`: can only be called from the contract.
function processMessage(
Client.Any2EVMMessage calldata any2EvmMessage
)
external
onlySelf
onlyAllowlisted(
any2EvmMessage.sourceChainSelector,
abi.decode(any2EvmMessage.sender, (address))
) // Make sure the source chain and sender are allowlisted
{
// Simulate a revert for testing purposes
if (s_simRevert) revert ErrorCase();
_ccipReceive(any2EvmMessage); // process the message - may revert as well
}
/// @notice Allows the owner to retry a failed message in order to unblock the associated tokens.
/// @param messageId The unique identifier of the failed message.
/// @param tokenReceiver The address to which the tokens will be sent.
/// @dev This function is only callable by the contract owner. It changes the status of the message
/// from 'failed' to 'resolved' to prevent reentry and multiple retries of the same message.
function retryFailedMessage(
bytes32 messageId,
address tokenReceiver
) external onlyOwner {
// Check if the message has failed; if not, revert the transaction.
if (s_failedMessages.get(messageId) != uint256(ErrorCode.FAILED))
revert MessageNotFailed(messageId);
// Set the error code to RESOLVED to disallow reentry and multiple retries of the same failed message.
s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED));
// Retrieve the content of the failed message.
Client.Any2EVMMessage memory message = s_messageContents[messageId];
// This example expects one token to have been sent, but you can handle multiple tokens.
// Transfer the associated tokens to the specified receiver as an escape hatch.
IERC20(message.destTokenAmounts[0].token).safeTransfer(
tokenReceiver,
message.destTokenAmounts[0].amount
);
// Emit an event indicating that the message has been recovered.
emit MessageRecovered(messageId);
}
/// @notice Allows the owner to toggle simulation of reversion for testing purposes.
/// @param simRevert If `true`, simulates a revert condition; if `false`, disables the simulation.
/// @dev This function is only callable by the contract owner.
function setSimRevert(bool simRevert) external onlyOwner {
s_simRevert = simRevert;
}
function _ccipReceive(
Client.Any2EVMMessage memory any2EvmMessage
) internal override {
s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text
// Expect one token to be transferred at once, but you can transfer several tokens.
s_lastReceivedTokenAddress = any2EvmMessage.destTokenAmounts[0].token;
s_lastReceivedTokenAmount = any2EvmMessage.destTokenAmounts[0].amount;
emit MessageReceived(
any2EvmMessage.messageId,
any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector)
abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address,
abi.decode(any2EvmMessage.data, (string)),
any2EvmMessage.destTokenAmounts[0].token,
any2EvmMessage.destTokenAmounts[0].amount
);
}
/// @notice Construct a CCIP message.
/// @dev This function will create an EVM2AnyMessage struct with all the necessary information for programmable tokens transfer.
/// @param _receiver The address of the receiver.
/// @param _text The string data to be sent.
/// @param _token The token to be transferred.
/// @param _amount The amount of the token to be transferred.
/// @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,
string calldata _text,
address _token,
uint256 _amount,
address _feeTokenAddress
) private pure returns (Client.EVM2AnyMessage memory) {
// Set the token amounts
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver), // ABI-encoded receiver address
data: abi.encode(_text), // ABI-encoded string
tokenAmounts: tokenAmounts, // The amount and type of token being transferred
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit
Client.EVMExtraArgsV1({gasLimit: 400_000})
),
// Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
feeToken: _feeTokenAddress
});
return evm2AnyMessage;
}
/// @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);
}
}
CrossChainNameServiceLookup.solenableChain()processMessage, a check is performed for a simulated revert condition using the s_simRevert state variable. This simulation is toggled by the setSimRevert function, callable only by the contract owner.MessageReceived event, signaling the successful processing of the message.s_messageContents



M1OffRamp calls the TokenPool’s unlock/mint function. This will validate the token pool rate limit, unlock or mint the token and transfer them to the specified receiver.
















{
"name": "ChainNotEnabled",
"params": [
{
"name": "chainSelector",
"value": "3478487238524512106",
"type": "uint64"
}
]
}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"
)
// How to use
// Set either an error code string OR set the chainId, txHash and txRequester.
// Setting an error code allows the script to run offline and doesn't require any RPC
// endpoint. Using the chainId, txHash and txRequester requires an RPC endpoint, and if
// the tx is old, the node needs to run in archive mode.
//
// Set the variable(s) and run main.go. The script will try to match the error code to the
// ABIs of various CCIP contracts. If it finds a match, it will check if it's a CCIP wrapped error
// like ExecutionError and TokenRateLimitError, and if so, it will decode the inner error.
//
// To configure an RPC endpoint, set the RPC_<chain_id> environment variable to the RPC endpoint.
// e.g. RPC_420=https://rpc.<chain_id>.com
const (
ErrorCodeString = "0x4e487b710000000000000000000000000000000000000000000000000000000000000032"
// The following inputs are only used if ERROR_CODE_STRING is empty
// Need a node URL
// NOTE: this node needs to run in archive mode if the tx is old
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 == "" {
// Try to load env vars from .env file
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})
)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 {
// Below is a simplistic example (same params for all messages) of using storage to allow for new options without
// upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
// and will always be backwards compatible i.e. upgrades are opt-in.
// Offchain we can compute the V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// Then later compute V2 extraArgs, for example if a refund feature was added:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// and update storage with the new args.
// If different options are required for different messages, for example different gas limits,
// one can simply key based on (chainSelector, messageType) instead of only 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));
// Step 1) Deploy XNFT.sol to Ethereum Sepolia
assertEq(vm.activeFork(), ethSepoliaFork);
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"
);
ethSepoliaXNFT = new XNFT(
ethSepoliaNetworkDetails.routerAddress,
ethSepoliaNetworkDetails.linkAddress,
ethSepoliaNetworkDetails.chainSelector
);
// Step 2) Deploy XNFT.sol to Arbitrum Sepolia
vm.selectFork(arbSepoliaFork);
assertEq(vm.activeFork(), arbSepoliaFork);
arbSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // we are currently on Arbitrum Sepolia Fork
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 {
// Step 3) On Ethereum Sepolia, call enableChain function
vm.selectFork(ethSepoliaFork);
assertEq(vm.activeFork(), ethSepoliaFork);
encodeExtraArgs = new EncodeExtraArgs();
uint256 gasLimit = 200_000;
bytes memory extraArgs = encodeExtraArgs.encode(gasLimit);
assertEq(extraArgs, hex"97a657c90000000000000000000000000000000000000000000000000000000000030d40"); // value taken from 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);
// Step 4) On Arbitrum Sepolia, call enableChain function
vm.selectFork(arbSepoliaFork);
assertEq(vm.activeFork(), arbSepoliaFork);
arbSepoliaXNFT.enableChain(ethSepoliaNetworkDetails.chainSelector, address(ethSepoliaXNFT), extraArgs);
// Step 5) On Arbitrum Sepolia, fund XNFT.sol with 3 LINK
assertEq(vm.activeFork(), arbSepoliaFork);
ccipLocalSimulatorFork.requestLinkFromFaucet(address(arbSepoliaXNFT), 3 ether);
// Step 6) On Arbitrum Sepolia, mint new xNFT
assertEq(vm.activeFork(), arbSepoliaFork);
vm.startPrank(alice);
arbSepoliaXNFT.mint();
uint256 tokenId = 0;
assertEq(arbSepoliaXNFT.balanceOf(alice), 1);
assertEq(arbSepoliaXNFT.ownerOf(tokenId), alice);
// Step 7) On Arbitrum Sepolia, crossTransferFrom xNFT
arbSepoliaXNFT.crossChainTransferFrom(
address(alice), address(bob), tokenId, ethSepoliaNetworkDetails.chainSelector, XNFT.PayFeesIn.LINK
);
vm.stopPrank();
assertEq(arbSepoliaXNFT.balanceOf(alice), 0);
// On Ethereum Sepolia, check if xNFT was succesfully transferred
ccipLocalSimulatorFork.switchChainAndRouteMessage(ethSepoliaFork); // THIS LINE REPLACES CHAINLINK CCIP DONs, DO NOT FORGET IT
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));
// Step 1) Deploy XNFT.sol to Ethereum Sepolia
assertEq(vm.activeFork(), ethSepoliaFork);
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"
);
ethSepoliaXNFT = new XNFT(
ethSepoliaNetworkDetails.routerAddress,
ethSepoliaNetworkDetails.linkAddress,
ethSepoliaNetworkDetails.chainSelector
);
// Step 2) Deploy XNFT.sol to Arbitrum Sepolia
vm.selectFork(arbSepoliaFork);
assertEq(vm.activeFork(), arbSepoliaFork);
arbSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // we are currently on Arbitrum Sepolia Fork
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 {
// Step 3) On Ethereum Sepolia, call enableChain function
vm.selectFork(ethSepoliaFork);
assertEq(vm.activeFork(), ethSepoliaFork);
encodeExtraArgs = new EncodeExtraArgs();
uint256 gasLimit = 200_000;
bytes memory extraArgs = encodeExtraArgs.encode(gasLimit);
assertEq(extraArgs, hex"97a657c90000000000000000000000000000000000000000000000000000000000030d40"); // value taken from 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);
// Step 4) On Arbitrum Sepolia, call enableChain function
vm.selectFork(arbSepoliaFork);
assertEq(vm.activeFork(), arbSepoliaFork);
arbSepoliaXNFT.enableChain(ethSepoliaNetworkDetails.chainSelector, address(ethSepoliaXNFT), extraArgs);
// Step 5) On Arbitrum Sepolia, fund XNFT.sol with 3 LINK
assertEq(vm.activeFork(), arbSepoliaFork);
ccipLocalSimulatorFork.requestLinkFromFaucet(address(arbSepoliaXNFT), 3 ether);
// Step 6) On Arbitrum Sepolia, mint new xNFT
assertEq(vm.activeFork(), arbSepoliaFork);
vm.startPrank(alice);
arbSepoliaXNFT.mint();
uint256 tokenId = 0;
assertEq(arbSepoliaXNFT.balanceOf(alice), 1);
assertEq(arbSepoliaXNFT.ownerOf(tokenId), alice);
// Step 7) On Arbitrum Sepolia, crossTransferFrom xNFT
arbSepoliaXNFT.crossChainTransferFrom(
address(alice), address(bob), tokenId, ethSepoliaNetworkDetails.chainSelector, XNFT.PayFeesIn.LINK
);
vm.stopPrank();
assertEq(arbSepoliaXNFT.balanceOf(alice), 0);
// On Ethereum Sepolia, check if xNFT was succesfully transferred
ccipLocalSimulatorFork.switchChainAndRouteMessage(ethSepoliaFork); // THIS LINE REPLACES CHAINLINK CCIP DONs, DO NOT FORGET IT
assertEq(vm.activeFork(), ethSepoliaFork);
assertEq(ethSepoliaXNFT.balanceOf(bob), 1);
assertEq(ethSepoliaXNFT.ownerOf(tokenId), bob);
}
}function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
// Burning token on source blockchain
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
// Encoding details for minting on the destination blockchain
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 {
// Below is a simplistic example (same params for all messages) of using storage to allow for new options without
// upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
// and will always be backwards compatible i.e. upgrades are opt-in.
// Offchain we can compute the V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// Then later compute V2 extraArgs, for example if a refund feature was added:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// and update storage with the new args.
// If different options are required for different messages, for example different gas limits,
// one can simply key based on (chainSelector, messageType) instead of only chainSelector.
function encode(
uint256 gasLimit
) external pure returns (bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({
gasLimit: gasLimit
});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}true


ccip-masterclass-3remappings.txtXNFT.sol smart contract by running:XNFT.sol smart contract by running:chainSelector parameter;tokenId parameter;





node -vnpm -vmkdir ccip-masterclass-3cd ccip-masterclass-3npx hardhat initforge --versionmkdir ccip-masterclass-3cd ccip-masterclass-3forge init0x5425890298aed601595a70AB815c96711a31Bc65 as the usdcToken.









// 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/ccip/contracts',
'@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/config347848723852451210616015286601757825753npm i @chainlink/contracts-ccip --save-devnpm i @chainlink/contracts --save-devnpm i @openzeppelin/contracts --save-devforge install smartcontractkit/ccip@ccip-develop// 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/security/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/security/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/security/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=""// 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()"# foundry.toml
remappings = [
'@chainlink/contracts-ccip/=lib/ccip/contracts/'
]forge install smartcontractkit/chainlink# foundry.toml
remappings = [
'@chainlink/contracts/=lib/chainlink/contracts/'
]forge install OpenZeppelin/openzeppelin-contracts# foundry.toml
remappings = [
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/'
]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;forge 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 {
// Below is a simplistic example (same params for all messages) of using storage to allow for new options without
// upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
// and will always be backwards compatible i.e. upgrades are opt-in.
// Offchain we can compute the V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// Then later compute V2 extraArgs, for example if a refund feature was added:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// and update storage with the new args.
// If different options are required for different messages, for example different gas limits,
// one can simply key based on (chainSelector, messageType) instead of only 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 {
// Below is a simplistic example (same params for all messages) of using storage to allow for new options without
// upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
// and will always be backwards compatible i.e. upgrades are opt-in.
// Offchain we can compute the V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// Then later compute V2 extraArgs, for example if a refund feature was added:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// and update storage with the new args.
// If different options are required for different messages, for example different gas limits,
// one can simply key based on (chainSelector, messageType) instead of only 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 arbitrumSepolia// 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);
}
}





// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// Below is a simplistic example (same params for all messages) of using storage to allow for new options without
// upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
// and will always be backwards compatible i.e. upgrades are opt-in.
// Offchain we can compute the V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// Then later compute V2 extraArgs, for example if a refund feature was added:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// and update storage with the new args.
// If different options are required for different messages, for example different gas limits,
// one can simply key based on (chainSelector, messageType) instead of only 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 {
// Below is a simplistic example (same params for all messages) of using storage to allow for new options without
// upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
// and will always be backwards compatible i.e. upgrades are opt-in.
// Offchain we can compute the V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// Then later compute V2 extraArgs, for example if a refund feature was added:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// and update storage with the new args.
// If different options are required for different messages, for example different gas limits,
// one can simply key based on (chainSelector, messageType) instead of only 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 {
// Below is a simplistic example (same params for all messages) of using storage to allow for new options without
// upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
// and will always be backwards compatible i.e. upgrades are opt-in.
// Offchain we can compute the V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// Then later compute V2 extraArgs, for example if a refund feature was added:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// and update storage with the new args.
// If different options are required for different messages, for example different gas limits,
// one can simply key based on (chainSelector, messageType) instead of only 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 {
// Below is a simplistic example (same params for all messages) of using storage to allow for new options without
// upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
// and will always be backwards compatible i.e. upgrades are opt-in.
// Offchain we can compute the V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// Then later compute V2 extraArgs, for example if a refund feature was added:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// and update storage with the new args.
// If different options are required for different messages, for example different gas limits,
// one can simply key based on (chainSelector, messageType) instead of only 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


