arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 29

CCIP Bootcamp Aug 2024

Loading...

Day 1

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Day 2

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Day 3

Loading...

Loading...

Loading...

Loading...

SmartCon

hashtag
Where TradFi and DeFi converge

Chainlink SmartCon is the convergence point for the worlds of blockchain and finance, where digital asset leaders, DeFi pioneers, and onchain builders come together to invent the future of global markets and Web3.

👉 REGISTER NOWarrow-up-right

hashtag
Event Highlights

hashtag
Hacker House | October 28-29

Build alongside onchain innovators developing real-world applications that scale blockchain tech to billions of users worldwide.

hashtag
SmartCon Main Event | October 30-31

Get cutting-edge insights from global financial institutions, premier DeFi protocols, and game-changing startups.

hashtag
Past SmartCon Speakers

Eric Schmidt, former CEO, Google

Jonathan Ehrenfeld Solé, Strategy Director, SWIFT

Balaji Srinivasan, Angel Investor and Former CTO, Coinbase

Laurence Moroney, AI Lead, Google

Joseph Chan, Under Secretary for Financial Services and The Treasury, Hong Kong SAR

Oliver Roucloux, Business Analyst & Product Owner, Euroclear

Rashmi Misra, General Manager AI & Emerging Technologies Business Development, Microsoft

Setup Instructions

Please make sure you have completed these steps ahead of Day 1:

  • MetaMaskarrow-up-right

  • GitHub profilearrow-up-right (required for test tokens, and homework exercises)

    • if you have difficulties please use

    • password: BigMac777

If you have any questions, please ask them on discord:

Avalanche Fuji, Ethereum Sepolia, Arbitrum Sepolia testnets added arrow-up-right
Test LINK, AVAX, ETH, ARBarrow-up-right
Bootcamp Faucetarrow-up-right
Test USDC on Avalanche Fujiarrow-up-right
https://discord.gg/m9vSfK65arrow-up-right

Debugging Tips and Tricks

Brief overview of how one can approach debugging CCIP dApps

Welcome to bonus chapter about debugging Chainlink CCIP dApps and troubleshooting issues with sending and receiving cross-chain messages. Writing code is just the beginning, the true skill often lies in effectively troubleshooting and resolving issues that arise.

hashtag
Getting Started

To understand where the issue is or may arise, you first need to have bookmarked the whole transfer of cross-chain message flow, which you can find in the Processing CCIP Messages subsection.

The second most important thing is to understand how ABI encoding/decoding works in EVM. When you compile Solidity code, the two primary outputs are Application Binary Interface (ABI) and EVM bytecode.

EVM bytecode is the machine-level code that the Ethereum Virtual Machine executes. It is what gets deployed to the Ethereum blockchain. It's a low-level, hexadecimal-encoded instruction set that the EVM interprets and runs. The bytecode represents the actual logic of the smart contract in an executable form.

The ABI is essentially a JSON-formatted text file that describes the functions and variables in your smart contract. When you want to interact with a deployed contract (e.g., calling a function from a web application), the ABI is used to encode the function call into a format that the EVM can understand. It serves as an interface between your high-level application (like a JavaScript front-end) and the low-level bytecode running on Ethereum. The ABI includes details about each function's name, return type, visibility (public, private, etc.), and the types of its parameters.

hashtag
Decode error messages

Anytime your message is not delivered due to error on the receiver side, will display the error message. However, if ABI is unknown to explorer (for example you haven't verified smart contract's source code on Block explorer) it will display its original content instead of human readable error message. However, if we know how ABI encoding/decoding works, that's still completely fine because there are plenty of tools that can help us.

So at first glance, when seeing this message, you can go into the panic mode and think how CCIP does not work or something similar. So let's decode the error message to see what went wrong.

So the error code is: 0x1c33fbee000000000000000000000000000000000000000000000000304611b6affba76a

This is esentially a hexadecimal value that mean a lot of stuff. But if we know the ABI of the Receiver smart contract it's simple to decode it.

Couple of tools that you can use:

  • Some online decoder, this one is pretty decent:

  • Foundry's cast abi-decode

Let's use the online decoder for this example. We need to provide contract's ABI, the above error code and to hit decode to get a human readable error message.

The decoded output unequivocally tells us that the Solidity custom error ChainNotEnabled() was thrown, which most likely means that we forgot to call enableChain() function of the Receiver's smart contract in general case.

hashtag
Use CCIP Revert Reason script

If you are a bit more technical, you can use a script present in the official CCIP GitHub repo to accomplish the same thing when debugging, plus much more.

You will need to provide either an error code string or the chainId, txHash and txRequester alongside JSON RPC url in .env file (archive node preffered).

And if you run go run main.go the output with these predefined values should be: If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).

Which means that was thrown, which is actually really helpful error message.

hashtag
Measure gas costs

Probably the most common error is the following one: ReceiverError. This may be due to an out of gas error on the destination chain. Error code: 0x

This most likely means that you've used extraArgs: "" syntax which defaults to 200_000 for gasLimit while your ccipReceive function consumes more.

To solve this error, you will most likely just need to, connect your wallet to CCIP Explorer, set the "Gas limit override" field to appropriate value and trigger manual execution of this function by clicking the blue button.

In future, to prevent this issue, you can use the following syntax, if you want to set the gas limit to 500_000 gas for example:

Keep in mind that:

  • If you are transferring only tokens, gasLimit should be set to 0 because there is no ccipReceive function

  • The extraArgs should mutable if you want to make your dApp compatible with future CCIP upgrades ()

hashtag
Tenderly is your friend

Oftentimes Tenderly is a good choice when you want to debug or simulate transactions against live test networks. Full trace feature allows you to see where the function actually reverts, which is much helpful than a below error message from at Block explorer.

One of the biggest problems when working with unverified smart contracts is function tracing. This smart contract wasn't verified, but the function selector was still known, and that's why Polygonscan was able to display it. This is sometimes very helpful.

Function selectors are the first four bytes of the Keccak-256 hash of the function's signature. The function signature includes the function name and the parenthesized list of parameter types. When a function call is made, these first four bytes are used by the EVM to determine which specific function in the contract should be executed.

So the exact function call that failed was triggered by the unverified smart contract to the CCIP Router smart contract, and its function selector is 0x96f4e9f9. If we have the ABI of that smart contract finding the function selector will be extremely straightforward. But what if we don't? Well there is a , website which has a database of all known function signatures so we can try searching for ours.

Looks like we found the function which reverted. Now we can take a look at its implementation and find out what went wrong. Although it was already clear from the Tenderly's UI that the issue is that CCIP Sender contract was not funded with tokens for covering CCIP fees - which is one of the most common User errors.

CCIP Explorerarrow-up-right
https://bia.is/tools/abi-decoder/arrow-up-right
https://bia.is/tools/abi-decoder/arrow-up-right
https://github.com/smartcontractkit/ccip/blob/ccip-develop/core/scripts/ccip/ccip-revert-reason/main.goarrow-up-right
EVM Panic code 0x32arrow-up-right
https://docs.chain.link/ccip/best-practices#using-extraargsarrow-up-right
https://openchain.xyz/signaturesarrow-up-right
Solidity compiler outputs
Error message on the Receiver side
Ethereum ABI Decoder
Out of gas error
{
  "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})
)

Tokenizated RWA Bootcamp

Chainlink’s tokenization bootcamp offers an exciting journey for Web3 developers, elevating your skills in bringing real-world assets onchain using Chainlink's cutting-edge tokenization infrastructure.

Secure Your Spot Before Anyone Else. September 30 - October 2, 2024

👉 REGISTER NOWarrow-up-right

Join us for a three-day tokenization adventure, where you’ll tokenize a house onchain. You’ll learn to enhance it with diverse data points, securely transfer it across chains, and unlock its potential beyond the capabilities of traditional systems.

What You’ll Learn:

  • Steps to create and enrich tokenized assets

  • Key stages of onchain development for tokenized assets

  • Strategies to maximize the potential of tokenized assets

  • Powering a Unified Golden Record for tokenized assets

hashtag

Day 3 Homework

Please note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.

Submit your public GitHub repo for the exercise below via this formarrow-up-right

Following the https://docs.chain.link/ccip/tutorials/ccipreceive-gaslimitarrow-up-right guide measure the gas consumption of the ccipReceive function. Once you have the number, increase it by 10% and provide as gasLimit parameter of the transferUsdc function instead of the currently hard-coded 500.000

Use this Gitbook for guidancearrow-up-right

How CCIP Programmable Token Transfers Unlock Cross-Chain Innovation

The Chainlink Cross-Chain Interoperability Protocol (CCIP) is far more than a simple token bridging solution. It’s a generalized cross-chain messaging protocol for transferring tokens (value), messages (data), or both tokens and messages simultaneously within a single cross-chain transaction—referred to as Programmable Token Transfers.

In effect, CCIP Programmable Token Transfers enable smart contracts to transfer tokens cross-chain along with instructions on what the receiving smart contract should do with those tokens once they arrive on the destination chain. This revolutionary concept of wrapping value and instructions together allows tokenized value to interact automatically and dynamically once it arrives at a destination, opening up a world of new possibilities.

In decentralized finance (DeFi), CCIP Programmable Token Transfers enable the creation of cross-chain native dApps, such as a smart contract that can automatically transfer tokens cross-chain and deposit them into the highest yield lending markets. Within traditional finance (TradFi), CCIP Programmable Token Transfers enable advanced use cases, such as a cross-chain delivery-vs-payment (DvP) transaction where an institution holding stablecoins on its private blockchain can purchase a tokenized asset issued on a different private or public chain.

Importantly, CCIP Programmable Token Transfers enable institutions to interact with smart contractsarrow-up-right and tokenized assetsarrow-up-right on other blockchain networks without needing to integrate or directly interact with that blockchain. All they need to do is send instructions to CCIP on how to interact with that chain, greatly reducing their overhead and the risks associated with point-to-point integrations with each blockchain network.

hashtag
CCIP Supports Any Cross-Chain Transfer Involving Both Data and Value

Just as TCP/IP is a universal standard that underpins the Internet, Chainlink CCIP serves as a universal standard that underpins the . To support all the various cross-chain use cases that exist within DeFi and TradFi, CCIP allows for a variety of ways to transfer data and/or value across blockchains.

hashtag
Arbitrary Messaging

CCIP’s support for enables developers to transfer any arbitrary data (encoded as bytes) across blockchain networks. Developers utilize CCIP’s Arbitrary Messaging to make their smart contract applications cross-chain native.

With CCIP, smart contracts on a source chain can call any arbitrary function on any arbitrary smart contract on a destination chain to trigger any arbitrary action (and receive a callback on the source chain if needed). Developers can encode multiple instructions in a single message, empowering them to orchestrate complex, multi-step, multi-chain tasks.

hashtag
Token Transfers

CCIP Token Transfers enable the transfer of tokens between chains via highly audited and security-reviewed token pool contracts. Transactions can be initiated directly by externally owned accounts (EOAs), such as from user wallets via a bridging app like , or directly by a smart contract. Tokens can then be sent to an EOA or to a smart contract.

In order to ensure the highest level of security and a superior user experience, token issuers can use CCIP directly within their token’s smart contract to make it a native cross-chain token. As a result, any user or developer can use CCIP to transfer the official (canonical) version of that issuer’s token cross-chain. Various layer-1 blockchain and layer-2 rollups such as Wemix and Metis built upon this concept by integrating to power their canonical token bridges. Every token transferred onto those blockchain networks via CCIP is the canonical representation of that token on that chain.

There are three primary ways developers can integrate CCIP for token transfers:

  • Burn and mint—Tokens are burned on a source chain, and an equivalent amount is minted on a destination chain. This enables the creation of cross-chain native tokens with a dynamic, unified supply across chains. via the burn and mint token transfer method.

  • Lock and mint—Tokens are locked on the chain they were natively issued on, and fully collateralized “wrapped” tokens are minted on destination chains. These wrapped tokens can be transferred across other non-native destination chains via burn and mint or be burned to unlock tokens back on the original issuing source chain. TRUF token utilizes lock and mint for its token transfers on CCIP.

hashtag
Programmable Token Transfers

Programmable Token Transfers combine Token Transfers with Arbitrary Messaging. This enables developers to transfer both tokens (value) and instructions (data) about what to do with those tokens cross-chain within a single transaction. Importantly, Programmable Token Transfers are built natively into CCIP to give users the best possible security, reliability, UX (e.g., composability), and risk management.

Lock and unlock
—Tokens are locked on a source blockchain, and an equivalent amount of tokens are released on the destination blockchain. This enables the support of tokens without a burn/mint function or tokens that would introduce challenges if wrapped, such as native blockchain gas tokens. CCIP supports
via the lock and unlock token transfer method.
Internet of Contractsarrow-up-right
Arbitrary Messagingarrow-up-right
Transporterarrow-up-right
CCIP as their official cross-chain infrastructurearrow-up-right
CCIP supports Circle’s USDCarrow-up-right
Truflation’sarrow-up-right
CCIP is purpose-built to support cross-chain token transfers across a wide range of assets and applications.
Programmable Token Transfers enable tokens and messages to be sent cross-chain as part of a single transaction, improving the developer and user experience.
native ETH transfersarrow-up-right

Welcome to CCIP Bootcamp!

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.

Onchain Certificate of Completion will be issued to all qualifying participants - please make sure to complete your homework for each day!

Certificate of Completion

hashtag
Curriculum

hashtag
Day 1

hashtag
Theory

hashtag
Exercises

hashtag
Homework

👉

hashtag

hashtag
Day 2

hashtag
Theory

hashtag
Exercises

hashtag
Homework

👉

hashtag

hashtag
Day 3

hashtag
Homework

👉

hashtag

Introduction to Interoperability Problem and Chainlink CCIP

hashtag
Chainlink CCIP

General overview of the Chainlink CCIP

hashtag
The interoperability problem

Interoperability is the ability to exchange information between different systems or networks, even if they are incompatible. Shared concepts on different networks ensure that each party understands and trusts the exchanged information. It also considers the concept of finality to establish trust in the exchanged information by validating its accuracy and integrity.

The Web3 ecosystem has become multi-chain, with the rise of layer-1 blockchains and layer-2 scaling solutions like appchains, subnets, and more, where each network has its own approach to scalability, security, and trust.

However, blockchains are isolated networks that operate independently and cannot communicate natively. To create a truly interoperable Web3 ecosystem, data and value must move seamlessly between chains. This is where bridges come in.

Traditional bridges are one of the biggest problems in today's Web3 ecosystem because they are centralized. When you are transferring funds from one chain to another using bridge, you are essentially giving your funds to some centralized entity, and you are trusting them that the funds will appear on the other side. And surprise, surprise, numerous bridge hacks have happened so far.

hashtag
The solution - Chainlink CCIP

The Chainlink Cross-Chain Interoperability Protocol (CCIP) provides a single simple interface through which dApps and web3 entrepreneurs can securely meet all their cross-chain needs, including token transfers and arbitrary messaging.

Chainlink CCIP connects blockchain networks via lanes. The lane is a unique combination of source blockchain to destination blockchain path, e.g., from blockchain A to blockchain C. To transfer messages in reverse order, from blockchain C to blockchain A using Chianlink CCIP, you will need to use a different lane, the one which is unique to the C -> A path.

Chainlink CCIP will always support bi-directional lanes for each new chain added in the future. The logical question is how CCIP knows through which lane to transfer a CCIP cross-chain message. It's actually quite simple - each blockchain supported by CCIP has a unique chain selector.

We said multiple times that by using Chainlink CCIP, you can send cross-chain messages. But what can a cross-chain message consist of? With Chainlink CCIP, one can:

  • Transfer (supported) tokens

  • Send any kind of data

  • Send both tokens and data

CCIP sender can be:

  • EOA

  • Any smart contract

CCIP receiver can be:

  • EOA

  • Any smart contract that implements CCIPReceiver.sol

Note: If you send a message and token(s) to EOA, only tokens will arrive.

For now, you can consider CCIP as a "black-box" component and be aware of the Router contract only. We will explain the Chainlink CCIP architecture in the following chapters.

The Router is the primary contract CCIP users interface with. This contract is responsible for initiating cross-chain interactions. One router contact exists per chain. When transferring tokens, callers have to approve the router contract to be able to "spend" (transfer) the caller's tokens. When a message is received on the destination chain, the router is the contract that “delivers” tokens to the user's account or the message to the receiver's smart contract.

hashtag
Recap

The Chainlink Cross-Chain Interoperability Protocol provides a single interface to transfer tokens and data across multiple chains in a secure and decentralized manner.

New Terms
Meaning

Source blockchain

The blockchain the CCIP Message is sent from

Receiver

A smart contract or an EOA that receives the CCIP Message

Destination blockchain

The blockchain the CCIP Message is sent to

Interoperability

The ability to exchange information between different systems or networks, even if they are incompatible

Chainlink CCIP

The protocol that allows you to send tokens and arbitrary messages across different blockchains

Lane

The unique combination of source blockchain to destination blockchain path

Chain Selector

The unique identifier of a blockchain in Chainlink CCIP

CCIP Message

The message that you can send across blockchains through CCIP lane which can consist of tokens and arbitrary data

Sender

A smart contract (of the User) or an EOA that sends the CCIP Message

Example of a traditional bridge
Trust assumptions - Patrick bridging funds
Example of bi-directional CCIP lanes between two blockchains
CCIP lanes
Basic CCIP Architecture
CCIP Programmable Token Transfers in TradFi
  • CCIP Programmable Token Transfers in DeFi

  • How to Use Chainlink CCIP

  • Transporter, powered by CCIP
    Introduction to Interoperability Problem and Chainlink CCIP
    How CCIP Programmable Token Transfers Unlock Cross-Chain Innovation
    Exercise 1: Programmable Token Transfers using the Defensive Example Pattern
    Defensive Example Pattern
    Day 1 Homework
    A talk with the Interport team
    CCIP Architecture and Message Processing
    Building Cross-Chain NFTs
    Exercise 2: Build Your First Cross-Chain NFT
    Exercise 3: Testing Cross-Chain NFTs
    Debugging Tips and Tricks
    Day 2 Homework
    A talk with the Celo team
    5 Levels of Cross-Chain Security with Chainlink CCIP
    Exercise 4: Sending USDC Cross-Chainarrow-up-right
    Day 3 Homework
    Next Steps

    CCIP Programmable Token Transfers in DeFi

    hashtag
    Cross-Chain Swaps

    CCIP Programmable Token Transfers enable cross-chain swap use cases, where any token can be effectively bridged over CCIP by connecting to liquidity pools / DEXs on the source and destination chains.

    For example, a cross-chain swap app built on CCIP enables users holding Token A on Arbitrum to be swapped for Token B on Optimism by first swapping Token A on Arbitrum for USDC, bridging the USDC to Optimism along with data about the swap, and then automatically swapping the USDC to Token B and sending it to the user’s wallet. This is why CCIP’s support for native USDCarrow-up-right is so powerful; it doesn’t just support the cross-chain transfer of native USDC via burn and mint, but also the simultaneous transmission of data on what to do with the USDC once it arrives on the destination chain—a unique feature of CCIP Programmable Token Transfers.

    is a cross-chain swaps protocol and participant that uses CCIP for Programmable Token Transfers to enable cross-chain swaps between blockchain networks. USDC is used as the liquidity token for XSwap. Since its launch, XSwap users have initiated over $130M in CCIP Programmable Token Transfers.

    Other users of CCIP Programmable Token Transfers include , , , , and more.

    hashtag
    Cross-Chain Staking and Restaking

    CCIP Programmable Token Transfers unlock innovation for . End-users can stake/restake assets directly from a layer-2 network, where CCIP is used to transfer the native assets back to the layer-1 blockchain chain along with instructions to (re)stake the asset in a specified (re)staking protocol. This reduces gas costs for users and provides them the convenience to (re)stake from any chain.

    For example, to enable its users to deposit ETH directly into their layer-2 contracts in order to receive the corresponding LRT (egETH) without ever having to leave the chain. Once users deposit ETH into the layer-2 contract, CCIP’s Programmable Token Transfers will bridge the tokens to Ethereum with instructions to restake them into Eigenlayer. CCIP is then used to lock the minted egETH on Ethereum and bridge it back to the L2 where it is minted and sent to the end user’s wallet address.

    You can read more about Chainlink’s support for staking and restaking in the blog: . Learn how to implement Chainlink CCIP Programmable Token Transfers in the for a more technical deep dive.

    Meet Our Instructors

    hashtag
    Richard Gottleber, Developer Relations Engineer, Chainlink Labs

    X (Twitter): @thergdevarrow-up-right

    Email: richard.gottleber@smartcontract.comenvelope

    hashtag
    Andrej Rakic, Developer Relations Engineer, Chainlink Labs

    X (Twitter):

    Email:

    hashtag
    Darby Martinez, Developer Expert, Chainlink Community

    X (Twitter):

    LinkedIn:

    CCIP Programmable Token Transfers in TradFi

    CCIP Programmable Token Transfers are critical to enabling cross-chain Delivery vs. Payment (DvP) transactionsarrow-up-right. DvP in traditional finance refers to the requirement that the delivery of assets (e.g., securities) and the payment for those assets happen simultaneously (i.e., atomic settlement). DvP is an important feature in reducing the risk that a counterparty won’t deliver on its leg of the transaction despite the other leg being fulfilled.

    The Australia and New Zealand Banking Group Limited (ANZ)arrow-up-right demonstrated how CCIP Programmable Token Transfers can enable a cross-border, cross-chain, cross-currency DvP transaction. In a single cross-chain transaction, a stablecoin backed by a local currency (NZ$DC) was converted to another stablecoin in a different national currency (A$DC), transferred from the buyer’s source chain to the seller’s destination chain along with the instruction to purchase a tokenized asset (e.g., reef credits), which was subsequently sent back to the customer’s wallet on the source chain.

    To learn more, check out the case study Cross-Chain Settlement of Tokenized Assets Using CCIParrow-up-right written in collaboration with ANZ and the Sibos panel discussionarrow-up-right between Chainlink Co-Founder Sergey Nazarov and ANZ’s Banking Services Lead Nigel Dobson.

    ANZ used CCIP to showcase a cross-currency, cross-chain settlement of tokenized assets using stablecoins.

    Transporter, powered by CCIP

    hashtag
    Introduction to Transporter

    Transporter is a highly secure interface for bridging tokens across blockchains with total peace of mind. Transporter is built on top of , the industry-leading cross-chain protocol for defense-in-depth security. Transporter is the cross-chain bridging app for transferring digital assets across blockchains and experiencing the best that each chain has to offer. In just a few clicks, you can bridge your tokens and cross chains with confidence knowing that Transporter is underpinned by Chainlink CCIP’s Level-5 Security, offers users real-time transaction tracking, and is available to provide 24/7 support.

    hashtag
    Warm-up Exercise: Let's bridge some testnet tokens

    Video tutorial:

    Navigate to

    Connect your wallet by clicking the "Connect wallet button"

    Select your Source and Destination Network of choice. I am going to send tokens from Avalanche Fuji to Ethereum Sepolia.

    Then select Token you want to send (we can also send Messages). I am going to use USDC.

    If you need tesnet USDC, you can get 10 units from the

    Then select the amount of tokens you want to send. I am going to choose 1 USDC.

    And approve Transporter to transfer that amount of tokens on behalf of you, by clicking the "Approve USDC" button.

    The next step is to select token you would like to use to pay for CCIP fees. By default the native coin of the source blockchain is selected (AVAX), but I am going to use LINK because it is cheaper.

    Finally, initiate the transfer by clicking the "Send" button.

    You can now sit back, relax and monitor your first cross-chain transfer in real time!

    Once your transaction is completed on the source blockchain (Avalanche Fuji in this example) you can monitor it on Chainlink CCIP Block Explorer as well - at .

    Bookmark this URL, because we are going to use it a lot during this bootcamp.

    To navigate to the Chainlink CCIP Block Explorer click the "View transaction" button.

    Once the transaction on the source blockchain is finalized, Chainlink DONs can proceed with the cross-chain transfers. Finality is really important security concept in blockchain technology, so make sure you familiarize yourself with it by the end of this bootcamp.

    Finally, we just need to wait for a destination transaction to be included in the next Ethereum Sepolia block.

    And that's it! Transport is completed, simple as that!

    You can of course always come back to Chainlink CCIP Block Explorer to see more details about the transfer.

    Chainlink CCIParrow-up-right
    XSwaparrow-up-right
    BUILDarrow-up-right
    Transporterarrow-up-right
    ChainSwaparrow-up-right
    WEMIX PLAYarrow-up-right
    Amino Rewardsarrow-up-right
    cross-chain staking and restakingarrow-up-right
    EigenPie is integrating Chainlink CCIParrow-up-right
    How The Chainlink Platform Unlocks LST and LRT Adoption in DeFiarrow-up-right
    CCIP Masterclass: Cross-Chain Staking Editionarrow-up-right
    Chainlink CCIP enables Programmable Token Transfers across blockchains involving the USDC stablecoin.
    @andrej_dev arrow-up-right
    @darbeasearrow-up-right
    https://www.linkedin.com/in/darby-martinez/arrow-up-right
    https://www.youtube.com/watch?v=dCDQ-xetzpMarrow-up-right
    https://test.transporter.io/ arrow-up-right
    https://faucet.circle.com/arrow-up-right
    https://ccip.chain.linkarrow-up-right

    How to Use Chainlink CCIP

    The minimal code needed to send and receive CCIP Messages

    hashtag
    The minimal CCIP architecture

    To recap, with Chainlink CCIP, one can:

    • Transfer (supported) tokens

    • Send any kind of data

    • Send both tokens and data

    CCIP receiver can be:

    • EOA

    • Any smart contract that implements CCIPReceiver.sol

    Note: If you send a message and token(s) to EOA, only tokens will arrive.

    For now, you can consider CCIP as a "black-box" component and be aware of the Router contract only. We will explain the Chainlink CCIP architecture in the following chapters.

    hashtag
    Getting started

    You can use Chainlink CCIP with any blockchain development framework. For this Masterclass, we prepared the steps for Hardhat, Foundry, and Remix IDE.

    Let's create a new project

    Make sure you have and installed. To check, run the following command:

    Create a new folder and name it ccip-masterclass

    Navigate to it

    Create a hew Hardhat project by running:

    And then select either "Create a JavaScript project" or "Create a TypeScript project".

    Make sure you have installed. To check, run the following command:

    Alternatively, you can clone:

    hashtag
    The @chainlink/contracts-ccip NPM package

    To use Chainlink CCIP, you need to interact with Chainlink CCIP-specific contracts from the NPM package.

    To install it, follow steps specific to the development environment you will use for this Masterclass.

    Option 1)

    We cannot use git submodules to install @chainlink/contracts-ccip because the content of this package is not available as a separate GitHub repo. This essentially means that we cannot run a forge install command. Here's the workaround:

    Add the following line to the .gitignore file

    Then run the following command in your Terminal:

    Finally, add the following lines to the foundry.toml

    hashtag
    Basic interface

    Although, as being said, CCIP sender and receiver can be EOA and smart contract, and all combinations are possible, we are going to cover the most complex use-case where both CCIP sender and receiver are smart contracts on different blockchains.

    hashtag
    Source blockchain

    To send CCIP Messages, the smart contract on the source blockchain must call the ccipSend() function, which is defined the IRouterClient.sol interface.

    The CCIP Message which is being sent is a type of EVM2AnyMessage Solidity struct from the Client library.

    Let's now understand what each property of the EVM2AnyMessage struct we are sending represents and how to use it.

    receiver

    Receiver address. It can be a smart contract or an EOA. Use abi.encode(receiver) to encode the address to the bytes Solidity data-type.

    data

    Payload sent within the CCIP message. This is that "any type of data" one can send as a CCIP Message we are referring to from the start. It can be anything from simple text like "Hello, world!" to Solidity structs or function selectors.

    tokenAmounts

    Tokens and their amounts in the source chain representation. Here we are specifying which tokens (out of supported ones) we are sending and how much of it. This is the array of a EVMTokenAmount struct, which consists of two properties only:

    • token - Address of a token we are sending on the local (source) blockchain

    • amount The amount of tokens we are sending. The sender must approve the CCIP router to spend this amount on behalf of the sender, otherwise the call to the ccipSend function will revert.

    Currently, the maximum number of tokens one can send in a single CCIP send transaction is five.

    feeToken

    Address of feeToken. CCIP supports fee payments in LINK and in alternative assets, which currently include native blockchain gas coins and their ERC20 wrapped versions. For developers, this means you can simply pay on the source chain, and CCIP will take care of execution on the destination chain. Set address(0) to pay in native gas coins such as ETH on Ethereum or MATIC on Polygon. Keep in mind that even if you are paying for fees in the native asset, nodes in the Chainlink DON will be rewarded in LINK only.

    extraArgs

    Users fill in the EVMExtraArgsV1 struct and then encode it to bytes using the _argsToBytes function. The struct consists of two properties:

    • gasLimit - The maximum amount of gas CCIP can consume to execute ccipReceive() on the contract located on the destination blockchain. Unspent gas is not refunded. This means that if you are sending tokens to EOA, for example, you should put 0 as a gasLimit value because EOAs can't implement the ccipReceive() (or any other) function. To estimate the accurate gas limit for your destination contract, consider Leveraging Ethereum client RPC by applying eth_estimateGas on receiver.ccipReceive() function, or use the , or conduct .

    If extraArgs are left empty, a.k.a extraArgs: "", a default of 200_000 gasLimit will be set with no strict sequencing. For production deployments, make sure that extraArgs is mutable. This allows you to build it off-chain and pass it in a call to a function or store it in a variable that you can update on demand. This makes extraArgs compatible with future CCIP upgrades.

    hashtag
    Destination blockchain

    To receive CCIP Messages, the smart contract on the destination blockchain must implement the IAny2EVMMessageReceiver interface. The NPM package comes up with the contract which implements it in the right way, called CCIPReceiver.sol, but we are going to talk more about it in the next chapter. For now, let's understand which functions from the IAny2EVMMessageReceiver interface must be implemented in the general-case scenario.

    As you can see, the ccipReceive() function from the IAny2EVMMessageReceiver interface accepts object of the Any2EVMMessage struct from the Client library. This struct is the Solidity representation of the received CCIP Message. Please note that this struct, Any2EVMMessage is different than the one we used to send on the source blockchain - EVM2AnyMessage. They are not the same.

    Let's now understand what each property of the Any2EVMMessage struct we are receiving represents and how to use it.

    • messageId - CCIP Message Id, generated on the source chain.

    • sourceChainSelector - Source chain selector.

    • sender - Sender address. abi.decode(sender, (address))

    To recap, here's the diagram with the minimal architecture needed to send & receive the Chainlink CCIP Message:

    Defensive Example Pattern

    Another best practice that we will implement is so called Defensive Example, the pattern which allows us to reprocess failed messages without forcing the original transaction to fail. Let's explain how it works.

    Receiving and processing messages

    Upon receiving a message on the destination blockchain, the ccipReceive function is called by the CCIP Router. This function serves as the entry point to the contract for processing incoming CCIP messages, enforcing crucial security checks through the onlyRouter, and onlyAllowlisted modifiers.

    Here's the step-by-step breakdown of the process:

    1. Entrance through ccipReceive:

      • The ccipReceive function is invoked with an Any2EVMMessage struct containing the message to be processed.

      • Security checks ensure the call is from the authorized router, an allowlisted source chain, and an allowlisted sender.

    2. Processing Message:

      • ccipReceive calls the processMessage function, which is external to leverage Solidity's try/catch error handling mechanism. Note: The onlySelf modifier ensures that only the contract can call this function.

    3. Message Processing in _ccipReceive:

      • _ccipReceive extracts and stores various information from the message, such as the messageId, decoded sender address, token amounts, and data.

    4. Error Handling:

      • If an error occurs during the processing (or a simulated revert is triggered), the catch block within ccipReceive is executed.

      • The messageId of the failed message is added to s_failedMessages, and the message content is stored in

    Reprocessing of failed messages

    The retryFailedMessage function provides a mechanism to recover assets if a CCIP message processing fails. It's specifically designed to handle scenarios where message data issues prevent entire processing yet allow for token recovery:

    1. Initiation:

      • Only the contract owner can call this function, providing the messageId of the failed message and the tokenReceiver address for token recovery.

    2. Validation:

    This function showcases a graceful asset recovery solution, protecting user values even when message processing encounters issues.

    Building Cross-Chain NFTs

    How to design a cross-chain NFT smart contract

    hashtag
    What are Cross-Chain NFTs?

    A cross-chain NFT is a smart contract that can exist on any blockchain, abstracting away the need for users to understand which blockchain they’re using.

    Typically, NFT movements from one chain to another are eye-catching events, but this simple fact brings up a larger question of the reliability and security of Web3’s middleware infrastructure. In a seamless cross-chain world, moving digital assets from one chain to another should be as normal as submitting a transaction on the same blockchain.

    When an NFT moves from one blockchain to another, it becomes a cross-chain NFT.

    Inside 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.
  • If s_simRevert is false, processMessage calls the _ccipReceive function for further message processing.

  • It then emits a MessageReceived event, signaling the successful processing of the message.
    s_messageContents
    .
  • A MessageFailed event is emitted, which allows for later identification and reprocessing of failed messages.

  • It checks if the message has failed using s_failedMessages.get(messageId). If not, it reverts the transaction.

  • Status Update:

    • The error code for the message is updated to RESOLVED to prevent reentry and multiple retries.

  • Token Recovery:

    • Retrieves the failed message content using s_messageContents[messageId].

    • Transfers the locked tokens associated with the failed message to the specified tokenReceiver as an escape hatch without processing the entire message again.

  • Event Emission:

    • An event MessageRecovered is emitted to signal the successful recovery of the tokens.

  • Create a new folder and name it ccip-masterclass

    Navigate to it

    Create a hew Foundry project by running:

    Navigate to https://remix.ethereum.org/arrow-up-right and click the "Create new Workspace" button.

    file:

    Option 2) [October 2023 UPDATE]

    You can run

    And after that set remappings in your foundry.toml or remappings.txt files to

    Create a new Solidity file, and paste the following content. It is an empty contract that just imports one of the contracts from the @chainlink/contracts-ccip package.

    Compile it. If compiled successfully and new .deps/npm/@chainlink/contracts-ccip folders are generated, that means we imported the @chainlink/contracts-ccip package into the Remix IDE Workspace.

    Remix IDE compile
    strict - 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.
    if the source chain is an EVM chain.
  • data - Payload sent within the CCIP message. For example, "Hello, world!"

  • tokenAmounts - Received tokens and their amounts in their destination chain representation.

  • Node.jsarrow-up-right
    NPMarrow-up-right
    Foundryarrow-up-right
    CCIP Starter Kit (Hardhat version)arrow-up-right
    CCIP Starter Kit (Foundry version)arrow-up-right
    @chainlink/contracts-cciparrow-up-right
    Hardhat plugin for gas testsarrow-up-right
    Foundry gas testsarrow-up-right
    @chainlink/contracts-cciparrow-up-right
    Basic CCIP Architecture
    Developer Interfaces
    hashtag
    How Do Cross-Chain NFTs Work?

    At a high level, an NFT is a digital token on a blockchain with a unique identifier different from any other token on the chain.

    Any NFT is implemented by a smart contract that is intrinsically connected to a single blockchain. The smart contract is arguably the most important part of this equation because it controls the NFT implementation: How many are minted, when, what conditions need to be met to distribute them, and more. This means that any cross-chain NFT implementation requires at least two smart contracts on two blockchains and interconnection between them.

    This is what a cross-chain NFT looks like - equivalent NFTs that exist across multiple blockchains.

    With this in mind, cross-chain NFTs can be implemented in three ways:

    • Burn-and-mint: An NFT owner puts their NFT into a smart contract on the source chain and burns it, in effect removing it from that blockchain. Once this is done, an equivalent NFT is created on the destination blockchain from its corresponding smart contract. This process can occur in both directions.

    • Lock-and-mint: An NFT owner locks their NFT into a smart contract on the source chain, and an equivalent NFT is created on the destination blockchain. When the owner wants to move their NFT back, they burn the NFT and it unlocks the NFT on the original blockchain.

    • Lock and unlock: The same NFT collection is minted on multiple blockchains. An NFT owner can lock their NFT on a source blockchain to unlock the equivalent NFT on a destination blockchain. This means only a single NFT can actively be used at any point in time, even if there are multiple instances of that NFT across blockchains.

    CCIP Token Transfer Mechanisms

    At this Masterclass, we are going to implement the Burn-and-Mint mechanism

    In each scenario, a cross-chain messaging protocol in the middle is necessary to send data instructions from one blockchain to another.

    hashtag
    How to use Chainlink CCIP to create Cross-Chain NFTs?

    To build Cross-Chain NFTs with Chainlink CCIP, let's first understand what one can do with Chainlink CCIP. With Chainlink CCIP, one can:

    • Transfer (supported) tokens

    • Send any kind of data

    • Send both tokens and data

    CCIP sender can be:

    • EOA

    • Any smart contract

    CCIP receiver can be:

    • EOA

    • Any smart contract that implements CCIPReceiver.sol

    CCIP Transfer options

    hashtag
    Implementing Burn-and-Mint model

    To implement Burn-and-Mint model using Chainlink CCIP, we will on cross-chain transfer function burn an NFT on the source blockchain (Arbitrum Sepolia) and send the cross-chain message using Chainlink CCIP. We will need to encode to and from addresses, NFT's tokenId and tokenURI so we can mint exactly the same NFT on the destination blockchain once it receives a cross-chain message.

    The Arbitrum Sepolia side:

    The Ethereum Sepolia side:

    This design allows the cross-chain transfer and vice-versa, from Ethereum Sepolia back to Arbitrum Sepolia using the exact same codebase and Burn-and-Mint mechanism.

    hashtag
    NFT Metadata

    For this Cross-Chain NFT we will use Four Chainlink Warriors hosted on IPFS, as a Metadata.

    • Chainlink Elf, available at: https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1arrow-up-right

    Chainlink Elf
    • Chainlink Knight, available at: https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLHarrow-up-right

    Chainlink Knight
    • Chainlink Orc, available at: https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHParrow-up-right

    Chainlink Orc
    • Chainlink Witch, available at: https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpFarrow-up-right

    Chainlink Witch

    hashtag
    Development Best Practices

    For this exercise we will try to follow some of the CCIP Best Practices. For the full list you should always refer to the Chainlink Official Documentation.

    hashtag
    Verify source chain, destination chain, sender and receiver addresses

    It's crucial for this exercise that sending cross-chain messages is between Cross-Chain NFT smart contracts. To accomplish that, we need to track a record of these addresses on different blockchains using their CCIP chain selectors.

    hashtag
    Verify Router addresses

    When you implement the ccipReceive method in the contract residing on the destination chain, validate that the msg.sender is the correct Router address. This verification ensures that only the Router contract can call the ccipReceive function on the receiver contract and is for developers that want to restrict which accounts are allowed to call ccipReceive.

    hashtag
    Setting gasLimit

    The gasLimit specifies the maximum amount of gas CCIP can consume to execute ccipReceive() on the contract located on the destination blockchain. It is the main factor in determining the fee to send a message. Unspent gas is not refunded.

    hashtag
    Using extraArgs

    The purpose of extraArgs is to allow compatibility with future CCIP upgrades. To get this benefit, make sure that extraArgs is mutable in production deployments. This allows you to build it off-chain and pass it in a call to a function or store it in a variable that you can update on-demand.

    If extraArgs are left empty, a default of 200000 gasLimit will be set.

    To make extraArgs mutable, set them as described previously in the enableChain function. To calculate which bytes value to pass, you can create a helper script like this:

    Exercise 1: Programmable Token Transfers using the Defensive Example Pattern

    Transfer Tokens With Data - Defensive Example

    This tutorial extends the programmable token transfers examplearrow-up-right. It uses Chainlink CCIP to transfer tokens and arbitrary data between smart contracts on different blockchains, and focuses on defensive coding in the receiver contract. In the event of a specified error during the CCIP message reception, the contract locks the tokens. Locking the tokens allows the owner to recover and redirect them as needed. Defensive coding is crucial as it enables the recovery of locked tokens and ensures the protection of your users' assets.

    Before You Begin

    1. You should understand how to write, compile, deploy, and fund a smart contract. If you need to brush up on the basics, read this tutorialarrow-up-right, which will guide you through using the Solidity programming languagearrow-up-right, interacting with the and working within the .

    2. Your account must have some AVAX and LINK tokens on Avalanche Fuji and ETH tokens on Ethereum Sepolia. Learn how to .

    3. Check the to confirm that the tokens you will transfer are supported for your lane. In this example, you will transfer tokens from Avalanche Fuji to Ethereum Sepolia so check the list of supported tokens .

    4. Learn how to . Following this guide, you should have CCIP-BnM tokens, and CCIP-BnM should appear in the list of your tokens in MetaMask.

    5. Learn how to . This guide shows how to fund your contract in LINK, but you can use the same guide for funding your contract with any ERC20 tokens as long as they appear in the list of tokens in MetaMask.

    6. Follow the previous tutorial: to learn how to make programmable token transfers using CCIP.

    Coding time!

    In this exercise, we'll initiate a transaction from a smart contract on Avalanche Fuji, sending a string text and CCIP-BnM tokens to another smart contract on Ethereum Sepolia using CCIP. However, a deliberate failure in the processing logic will occur upon reaching the receiver contract. This tutorial will demonstrate a graceful error-handling approach, allowing the contract owner to recover the locked tokens.

    CORRECTLY ESTIMATE YOUR GAS LIMIT

    It is crucial to thoroughly test all scenarios to accurately estimate the required gas limit, including for failure scenarios. Be aware that the gas used to execute the error-handling logic for failure scenarios may be higher than that for successful scenarios.

    hashtag
    Deploy Your Contracts

    To use this contract:

    1. .

    2. Compile your contract.

    3. Deploy, fund your sender contract on Avalanche Fuji and enable sending messages to Ethereum Sepolia:

    At this point, you have one sender contract on Avalanche Fuji and one receiver contract on Ethereum Sepolia. As security measures, you enabled the sender contract to send CCIP messages to Ethereum Sepolia and the receiver contract to receive CCIP messages from the sender on Avalanche Fuji. The receiver contract cannot process the message, and therefore, instead of throwing an exception, it will lock the received tokens, enabling the owner to recover them.

    Note: Another security measure enforces that only the router can call the _ccipReceive function. Read the section for more details.

    hashtag
    Recover the locked tokens

    You will transfer 0.001 CCIP-BnM and a text. The CCIP fees for using CCIP will be paid in LINK.

    1. Open MetaMask and connect to Avalanche Fuji. Fund your contract with LINK tokens. You can transfer 0.5 LINK to your contract. In this example, LINK is used to pay the CCIP fees.

    2. Send a string data with tokens from Avalanche Fuji:

      1. Open MetaMask and select the network

    NOTE

    During gas price spikes, your transaction might fail, requiring more than 0.5 LINK to proceed. If your transaction fails, fund your contract with more LINK tokens and try again.

    1. Open the and search your cross-chain transaction using the transaction hash.

    1. The CCIP transaction is completed once the status is marked as "Success". In this example, the CCIP message ID is 0x120367995ef71f83d64a05bd7793862afda9d04049da4cb32851934490d03ae4.

    1. Check the receiver contract on the destination chain:

      1. Open MetaMask and select the network Ethereum Sepolia.

      2. In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Ethereum Sepolia.

    Argument
    Description

    1. After confirming the transaction, you can open it in a block explorer. Notice that the locked funds were transferred to the tokenReceiver address.

    2. Call again the getFailedMessages function with an offset of 0 and a limit of 1 to retrieve the first failed message. Notice that the error code is now 0

    Note: These example contracts are designed to work bi-directionally. As an exercise, you can use them to transfer tokens with data from Avalanche Fuji to Ethereum Sepolia and from Ethereum Sepolia back to Avalanche Fuji.

    hashtag
    Explanations

    The smart contract featured in this tutorial is designed to interact with CCIP to transfer and receive tokens and data. The contract code is similar to the tutorial. Hence, you can refer to its . We will only explain the main differences.

    hashtag
    Sending messages

    The sendMessagePayLINK function is similar to the sendMessagePayLINK function in the tutorial. The main difference is the increased gas limit to account for the additional gas required to process the error-handling logic.

    hashtag
    Receiving and processing messages

    Upon receiving a message on the destination blockchain, the ccipReceive function is called by the CCIP router. This function serves as the entry point to the contract for processing incoming CCIP messages, enforcing crucial security checks through the onlyRouter, and onlyAllowlisted modifiers.

    Here's the step-by-step breakdown of the process:

    1. Entrance through ccipReceive:

      • The ccipReceive function is invoked with an Any2EVMMessage containing the message to be processed.

    hashtag
    Reprocessing of failed messages

    The retryFailedMessage function provides a mechanism to recover assets if a CCIP message processing fails. It's specifically designed to handle scenarios where message data issues prevent entire processing yet allow for token recovery:

    1. Initiation:

      • Only the contract owner can call this function, providing the messageId of the failed message and the tokenReceiver address for token recovery.

    2. Validation:

    This function showcases a graceful asset recovery solution, protecting user values even when message processing encounters issues.

    Day 1 Homework

    Please note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.

    Please answer these 15 questions by the end of the bootcamp.

    Submit your answers via this formarrow-up-right

    Easy:

    1. What is CCIP Lane?

    2. What is CCIP Chain Selector? How does it differ from Chain ID?

    3. What is gasLimit in CCIP Messages used for?

    4. How can one monitor CCIP Messages in real time?

    5. What are the three main capabilities of Chainlink CCIP? Provide examples of potential use cases leveraging these capabilities.

    Medium:

    1. Detail the security best practices for verifying the integrity of incoming CCIP messages in your smart contracts. What specific checks should be implemented in the ccipReceive function, and why are these verifications crucial for the security of cross-chain dApps

    2. Which token transfer mechanisms are supported by Chainlink CCIP?

    3. Describe the role of the Risk Management Network in Chainlink CCIP and explain the process of "blessing" and "cursing".

    Hard:

    1. Explain the DefensiveExample Pattern and how to handle CCIP message failures gracefully.

    2. List and explain the scenarios that would trigger the need for manual execution using the Chainlink CCIP Explorer.

    3. Explain why it is a best practice to make the extraArgs mutable in Chainlink CCIP and describe how a developer can implement this in their smart contract.

    A talk with the Interport team

    hashtag
    Vladimir Makarov, co-founder of Interport

    Website: https://interport.fi/arrow-up-right

    X (Twitter): https://twitter.com/0xWheretoarrow-up-right

    CCIP Architecture and Message Processing

    CCIP Message Flow

    We will now see what the workflow of a CCIP Message looks like in a couple of steps.

    hashtag
    Step 1: Prepare

    1. The Sender prepares a CCIP Message (EVM2AnyMessage) for their cross-chain transaction to a destination blockchain (chainSelector) of choice. A CCIP message includes the following information:

      • Receiver

      • Data payload

      • Tokens and amounts

      • Fee token

    2. The Sender calls Router.getFee() to receive the total fees (gas + premium) to pay CCIP and approves the requested fee amount.

    3. The Sender calls Router.ccipSend(), providing the CCIP Message they want to send along with their desired destination chainSelector. In the case of token transfers, the amount to be transferred must be approved to the Router.

    hashtag
    Step 2: Send

    1. The Router validates the received Message (e.g., valid and supported destination chainId and supported tokens at the destination chain).

    2. The Router receives and transfers fees to the OnRamp.

    3. The Router receives and transfers tokens to its corresponding Token Pool. If the sender has not approved the tokens to the Router, this will fail.

    hashtag
    Step 3: Committing DON

    1. Nodes in the Committing DON listen for events of Messages that are ready to be sent

    2. Messages must be finalized to be considered secure against reorg attacks.

    3. Triggered by time or number of messages queued in the OnRamp, the Committing DON creates a Report with a commitment of all messages ready to be sent, in a batch. This commitment takes the form of a Merkle Root

    hashtag
    Merkle Root & Merkle Proof

    • Merkle Root: the root hash of the Merkle Tree. It is a commitment to all the leaves (Messages M1-M4) in the tree. Each node in the tree is the hash of the nodes below it.

    • Merkle Proof: to prove that Message M1 is included in the Merkle Root (commitment), a Prover provides the following elements as proof to a Verifier (who only has the root hash):

    hashtag
    Step 4: Executing DON

    1. Nodes in the Executing DON listen for events of Messages that are ready to be sent, similar to the Committing DON

    2. Messages must be finalized to be considered secure against reorg attacks.

    3. In addition to the time or number of messages queued in the OnRamp, the Executing DON also monitors the CommitStore to make sure the messages are ready to be executed at the destination chain, i.e., are the messages included in a blessed on-chain commitment?

    hashtag
    Step 5: Execute

    1. For each message in the batch received, the OffRamp verifies using the provided Merkle Proof whether the transaction is included in the blessed commitment in the CommitStore.

    2. If tokens are included in the transaction, the OffRamp validates the aggregate rate limit of the lane and identifies the matching Token Pool(s).

    hashtag
    CCIP as an International Flight

    Now, do you need to know in details how the processing of CCIP Message works? Absolutely not, you just need to send the cross-chain Message by interacting with the Router contract and Chainlink CCIP will deliver it, the same as when sending a package you just drop it at the Post Office, mark to whom you are sending it and pay for fees.

    Just as international travel involves various checkpoints and procedures to ensure a safe and verified journey from one country to another, Chainlink CCIP ensures secure and verified communication between different blockchain networks.

    Pre-Boarding at JFK Airport: The Committing Phase

    Before any plane takes off, a series of rigorous checks are in place—mirroring the initiating phase of CCIP. Here, the Router acts as our check-in desk, where users present their cross-chain requests, akin to passengers confirming their flight details.

    Travelers check in their luggage, which is then handled by the airport staff. As with any luggage on a long-haul flight, the cross-chain message undergoes a meticulous security protocol. Similarly, in CCIP, the OnRamp contract checks the validity of the destination blockchain address, message size, gas limits, and sequence numbers, ensuring that the 'luggage' (or data payload) is prepared correctly for its journey.

    Passport and Visa verification are much comparable to the Commit Store smart contract, which stores the Merkle root of finalized messages and ensures they are 'blessed' by the Risk Management Network for authenticity and security before being executed on the destination chain.

    In-Flight: The Cross-Chain Transit

    During the flight, passengers and luggage are in transit, similar to the cross-chain message being propagated across networks. The Executing DON works behind the scenes, analogous to the flight crew, ensuring the smooth passage of the cross-chain interaction.

    As we know, the duration of a flight can vary - flying east might chase the sun, shortening our perceived day, while westward travel extends it. Similarly, the delivery time of a cross-chain message depends on the source blockchain finality. We remember from the CCIP Masterclass #1 that finality refers to the state of irreversibility and permanent record of a transaction on the blockchain.

    Touchdown at Hong Kong Airport: The Executing Phase

    On arrival, the OffRamp contract ensures the message is authentic, verifying the proof against the committed and blessed Merkle root.

    Similarly to the final checks by the Risk Management Network, behind the scenes (unseen by the passenger) the suitcase or our cross-chain message undergoes various security checks, ensuring that the message adheres to the rules and regulations of the destination blockchain.

    Finally, the Router on the destination chain assumes the role of border control, delivering the message data and/or tokens to the receiver's address, akin to a traveler receiving their stamped passport and entering a new country.

    This entire process, while complex, remains largely invisible to the end-user, who simply experiences the seamless transfer of their digital assets from one blockchain to another. From the moment of check-in to the joy of retrieval, the journey of a cross-chain message through CCIP is a marvel of modern cryptography, ensuring that what we entrust to the network is what we receive.

    Exercise 2: Build Your First Cross-Chain NFT

    Coding Time 🎉

    hashtag
    CCIP Config Details

    Arbitrum Sepolia
    Ethereum Sepolia

    5 Levels of Cross-Chain Security with Chainlink CCIP

    💻

    hashtag
    Level 5: Defense-In-Depth

    Chainlink is a blockchain-agnostic, decentralized computing platform that provides secure access to external data, off-chain computation, and cross-chain interoperability.

    The Chainlink Cross-Chain Interoperability Protocol (CCIP) serves as an interoperability standard for transferring both tokens and/or data between any supported public or private blockchain network.

    Exercise 3: Testing Cross-Chain contracts using Chainlink Local

    You might noticed so far that building with CCIP directly on test networks is not ideal due to extra points of friction - like setting up a wallet, getting testnet tokens, waiting for cross-chain transactions to complete, etc. This is because test networks are for testing and local environments (like Foundry, Hardhat or Remix IDE) are for building and unit testing.

    hashtag
    Introducing Chainlink Local

    To address this issue, we created , and in this chapter you will learn how to simulate your cross-chain transactions locally and build with Chainlink CCIP 2000x quicker compared to working on test networks!

    A talk with the Celo team

    hashtag
    Sophia Dew, Developer Relations Engineering Lead

    X (Twitter):

    💻

    Github repo:

    // 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 -v
    npm -v
    mkdir ccip-masterclass
    cd ccip-masterclass
    npx hardhat@2.14.1 init
    forge --version
    npm 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-masterclass
    cd ccip-masterclass
    forge init
    libs = ['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/'
    ]
    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);
        }
    }
    Discuss the significance of the "finality" concept in the context of Chainlink CCIP. How does the finality of a source chain impact the end-to-end transaction time in CCIP?
  • Discuss the best practices for setting the gasLimit in CCIP messages. How can developers accurately estimate and optimize the gas limit to ensure reliable execution of cross-chain transactions?

  • What are CCIP Rate Limits? What considerations should developers keep in mind when designing applications to operate within these limits?
  • What is going to happen if you send arbitrary data alongside tokens to an Externally Owned Account using Chainlink CCIP?

  • https://celo.org/arrow-up-right

    Documentationarrow-up-right

    X (Twitter): @CeloDevsarrow-up-right and @Celoarrow-up-right

    Discordarrow-up-right

    https://x.com/sodofi_arrow-up-right
    Minting Ethereum NFTs with Celo Gas Feesarrow-up-right
    https://github.com/celo-org/celo-ccip-workshoparrow-up-right
  • Additional parameters (gasLimit)

  • The Router forwards the Message to the correct OnRamp (based on destination chainSelector) for processing:

    • Validate the message (number of tokens, gasLimit, data length …)

    • [For token transfers] Ensure that the transferred value does not hit the aggregate rate limit of the lane.

    • Sequence the message with a sequence number.

    • For each Token included in the Message: instruct the token pool to lock/burn the tokens. This will also validate the token pool rate limit for this lane.

  • The OnRamp emits an event containing the sequenced message. This triggers the DONs to process the message.

  • A messageId is generated and returned to the Sender.

  • Upon consensus in the Committing DON, the Report containing the Merkle Root is transmitted to the CommitStore contract on the destination chain

  • The Risk Management Network “blesses” the Merkle Root in the CommitStore, to make sure it is a correct representation of the queued messages at the OnRamp.

  • M1
  • H(M2)

  • H(H(M3),H(M4))

  • Using this Merkle Proof, a Verifier can easily verify that, indeed, M1 is included in the commitment (root hash) that it possesses.

  • If conditions are met, the Executing DON creates a Report with all messages ready to be sent, in a batch. It accounts for the gasLimit of each message in its batching logic. It also calculates a relevant Merkle Proof for each message to prove that the message is included in the Merkle Root submitted by the Committing DON in the CommitStore. Note that Executing DON batches can be any subset of a Committing DON batch.

  • Upon consensus, the Report is transmitted to the OffRamp contract on the destination chain.

  • OffRamp 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.
  • If the receiver is not an EOA and has the correct interface implemented, the OffRamp uses the Router to call the receiver’s ccipReceive() function

  • The receiver processes the message and is informed where the message comes from (blockchain + sender), the tokens transferred, and the data payload (with relevant instructions).

  • Based on the data payload, the receiver might transfer the tokens to the final recipient (end-user)

  • Chainlink Local is an installable dependency, like OpenZeppelin for example. It provides a tool (the Chainlink Local Simulator) that developers import into their Foundry or Hardhat or Remix IDE projects. This tool runs
    locally which means developers can rapidly explore, prototype and iterate CCIP dApps off-chain in a local environment, and move to testnet only when they're ready to test in a live environment.

    Most importantly, smart contracts tested with Chainlink Local can be deployed to test networks without any modifications (assuming network specific contract addresses such as Router contracts and LINK token addresses are passed in via a constructor).

    To view more detailed documentation and more examples, visit the Chainlink Local Documentationarrow-up-right and Chainlink Local YouTube Playlistarrow-up-right.

    hashtag
    Local Mode vs Forked Mode

    The simulator supports two modes:

    1. Local Mode - working with mock contracts on a locally running development blockchain node running on localhost, and

    2. Forked Mode - working with deployed Chainlink CCIP contracts using multiple forked networksarrow-up-right.

    circle-exclamation

    In this example we will use Forked Mode.

    For homework you must use Local Mode.

    hashtag
    Local Mode

    When working in local simulation mode, the simulator pre-deploys a set of smart contracts to a blank Hardhat/Anvil network EVM state and exposes their details via a call to the configuration() function. Even though there are two Router contracts exposed, sourceRouter and destinationRouter, to support the developer's mental model of routing cross-chain messages through two different Routers, both are actually the same contract running on the locally development blockchain node.

    hashtag
    Forked Mode

    When working in fork mode, you will need to create multiple locally running blockchain networks (you need an archive node that has historical network state in the pinned block from which you have forked for local development - see herearrow-up-right) and interact with the contract addresses provided in the Official Chainlink Documentationarrow-up-right.

    Chainlink Local is so fast because it does not spin any type of offchain component needed for cross-chain transfers. That's why CCIP Local Simulator Fork (smart contract for Foundry, and typescript script for Hardhat) exposes functionality to switch between forks and route messages to the destination blockchain directly by developers - so don't forget this step 😉.

    hashtag
    Coding time: Let's write a unit test for our XNFT.sol smart contract

    The full example is available at: https://github.com/smartcontractkit/ccip-cross-chain-nftarrow-up-right

    hashtag
    Creating a new Foundry project

    To get started let's first create a new Foundry project by running: arrow-up-right

    If this command fails, make sure you have Foundry installedarrow-up-right.

    hashtag
    Installing necessary dependencies

    After that we will need to install the following dependencies. If you don't want to commit changes after each installment, append the --no-commit flag to each of the following commands

    @chainlink/contracts

    @chainlink/contracts-ccip

    @openzeppelin/contracts

    @chainlink/local

    And then set the following remappings in your foundry.toml or remappings.txt file:

    Delete src/Counter.sol, test/Counter.t.sol and script/Counter.s.sol files. Create new XNFT.sol file in src folder and paste the content of the XNFT smart contract from the previous page/exercise. Run forge build to compile that contract. This should work if you installed dependencies properly.

    hashtag
    Getting Ethereum Sepolia and Arbitrum Sepolia RPC URLs

    Create a new file by copying the .env.example file, and name it .env. Fill in with Ethereum Sepolia and Arbitrum Sepolia RPC URLs using either local archive nodes or a service that provides archival data, like Infuraarrow-up-right or Alchemyarrow-up-right.

    Then add the rpc_endpoints section to your foundry.toml file. Its final version should look like this:

    hashtag
    Write your first test with Chainlink Local

    In the previous exercise, we performed 7 steps on two different test networks in order to deploy, mint and transfer XNFT from one account to another. Using Chainlink Local, you can write a test that will perform the same action, using exactly the same CCIP contracts from test networks, in your local environment, much faster.

    Create a new test/XNFT.t.sol file. Paste the following content:

    If you try to compile this contract you should encounter in an error because there is no EncodeExtraArgs.sol helper smart contract from the previous exercise. Fix that by creating a new file test/utils/EncodeExtraArgs.sol and paste the following code.

    Let's analyze the code so far. We've imported CCIPLocalSimulatorFork from the @chainlink/local package, meaning that we are using the Forked Mode. We created a new instance of it and made it persistent across all different forks so it can work properly.

    For your homework however, you will need to import CCIPLocalSimulator from the @chainlink/local package.

    We also created two forks (copies of the blockchain networks at the latest block - it is the best practice to pin this block number instead of always using the latest one) of Ethereum Sepolia and Arbitrum Sepolia test networks. We can now switch between multiple forks in Foundry, but we selected Ethereum Sepolia for start.

    Fork ID is Foundry's way to manage different forked networks in a same test and those are usually numbers from 1, 2, 3... etc. Fork ID is not the same thing as block.chainid - that's the actuall Chain ID of a blockchain network you can get in Solidity - 11155111 for Ethereum Sepolia, for example.

    CCIPLocalSimulatorFork in Foundry only, comes up with this Register helper smart contract that contains NetworkDetails struct which looks like this:

    It is pre-populated with these details for some test networks and NO MAIN NETWORKS (on purpose) so it is the best practice to always validate these information - otherwise simulations won't work:

    If there is no Network Details (for main networks for example) you must add those manually using the setNetworkDetailsarrow-up-right function.

    We are going to perform Steps 1) and 2) from previous exercise directly in the setup() function, like this:

    And the rest of the steps from the previous exercise directly in a new test function, like this:

    Full Final code:

    Chainlink Local - the Chainlink CCIP Local Simulatorarrow-up-right
    Chainlink CCIParrow-up-right
    forge init
    forge install smartcontractkit/chainlink-brownie-contracts
    forge install smartcontractkit/ccip@b06a3c2eecb9892ec6f76a015624413fffa1a122
    forge install OpenZeppelin/openzeppelin-contracts
    forge 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);
        }
    test/XNFT.t.sol
    // 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);
        }
    }
    Open MetaMask and select the network Avalanche Fuji.
  • In Remix IDE, click on Deploy & Run Transactions and select Injected Provider - MetaMask from the environment list. Remix will then interact with your MetaMask wallet to communicate with Avalanche Fuji.

  • Fill in your blockchain's router and LINK contract addresses. The router address can be found on the supported networks pagearrow-up-right and the LINK contract address on the LINK token contracts pagearrow-up-right. For Avalanche Fuji, the router address is 0xF694E193200268f9a4868e4Aa017A0118C9a8177 and the LINK contract address is 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846.

  • Click the transact button. After you confirm the transaction, the contract address appears on the Deployed Contracts list. Note your contract address.

  • Open MetaMask and fund your contract with CCIP-BnM tokens. You can transfer 0.002 CCIP-BnM to your contract.

  • Enable your contract to send CCIP messages to Ethereum Sepolia:

    1. In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Avalanche Fuji.

    2. Call the allowlistDestinationChain with 16015286601757825753 as the destination chain selector, and true as allowed. Each chain selector is found on the .

  • Deploy your receiver contract on Ethereum Sepolia and enable receiving messages from your sender contract:

    1. Open MetaMask and select the network Ethereum Sepolia.

    2. In Remix IDE, under Deploy & Run Transactions, make sure the environment is still Injected Provider - MetaMask.

    3. Fill in your blockchain's router and LINK contract addresses. The router address can be found on the and the LINK contract address on the . For Ethereum Sepolia, the router address is 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 and the LINK contract address is 0x779877A7B0D9E8603169DdbD7836e478b4624789.

    4. Click the transact button. After you confirm the transaction, the contract address appears on the Deployed Contracts list. Note your contract address.

    5. Enable your contract to receive CCIP messages from Avalanche Fuji:

      1. In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Ethereum Sepolia.

      2. Call the allowlistSourceChain with 14767482510784806043

    6. Enable your contract to receive CCIP messages from the contract that you deployed on Avalanche Fuji:

      1. In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Ethereum Sepolia.

      2. Call the allowlistSender with the contract address of the contract that you deployed on Avalanche Fuji, and true

    7. Call the setSimRevert function, passing true as a parameter, then wait for the transaction to confirm. Setting s_simRevert to true simulates a failure when processing the received message. Read the section for more details.

  • Avalanche Fuji
    .
  • In Remix IDE, under Deploy & Run Transactions, open the list of functions of your smart contract deployed on Avalanche Fuji.

  • Fill in the arguments of the sendMessagePayLINK function:

    Argument
    Value and Description

    _destinationChainSelector

    16015286601757825753 CCIP Chain identifier of the destination blockchain (Ethereum Sepolia in this example). You can find each chain selector on the .

    _receiver

    Your receiver contract address at Ethereum Sepolia. The destination contract address.

    _text

  • Click on transact and confirm the transaction on MetaMask.

  • After the transaction is successful, record the transaction hash. Here is an examplearrow-up-right of a transaction on Avalanche Fuji.

  • Call the getFailedMessages function with an offset of 0 and a limit of 1 to retrieve the first failed message.

  • Notice the returned values are: 0x120367995ef71f83d64a05bd7793862afda9d04049da4cb32851934490d03ae4 (the message ID) and 1 (the error code indicating failure).

  • To recover the locked tokens, call the retryFailedMessage function:

  • , indicating that the message was resolved.

    Security checks ensure the call is from the authorized router, an allowlisted source chain, and an allowlisted sender.
  • Processing Message:

    • ccipReceive calls the processMessage function, which is external to leverage Solidity's try/catch error handling mechanism. Note: The onlySelf modifier ensures that only the contract can call this function.

    • Inside 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.

    • If s_simRevert is false, processMessage calls the _ccipReceive function for further message processing.

  • Message Processing in _ccipReceive:

    • _ccipReceive extracts and stores various information from the message, such as the messageId, decoded sender address, token amounts, and data.

    • It then emits a MessageReceived event, signaling the successful processing of the message.

  • Error Handling:

    • If an error occurs during the processing (or a simulated revert is triggered), the catch block within ccipReceive is executed.

    • The messageId of the failed message is added to s_failedMessages, and the message content is stored in s_messageContents.

    • A MessageFailed event is emitted, which allows for later identification and reprocessing of failed messages.

  • It checks if the message has failed using s_failedMessages.get(messageId). If not, it reverts the transaction.

  • Status Update:

    • The error code for the message is updated to RESOLVED to prevent reentry and multiple retries.

  • Token Recovery:

    • Retrieves the failed message content using s_messageContents[messageId].

    • Transfers the locked tokens associated with the failed message to the specified tokenReceiver as an escape hatch without processing the entire message again.

  • Event Emission:

    • An event MessageRecovered is emitted to signal the successful recovery of the tokens.

  • messageId

    The unique identifier of the failed message.

    tokenReceiver

    The address to which the tokens will be sent.

    MetaMask walletarrow-up-right
    Remix Development Environmentarrow-up-right
    Acquire testnet LINKarrow-up-right
    Supported Networks pagearrow-up-right
    herearrow-up-right
    acquire CCIP test tokensarrow-up-right
    fund your contractarrow-up-right
    Transfer Tokens with Dataarrow-up-right
    Open the contract in Remixarrow-up-right
    explanationarrow-up-right
    CCIP explorerarrow-up-right
    Transfer Tokens with Dataarrow-up-right
    code explanationarrow-up-right
    Transfer Tokens with Dataarrow-up-right
    structarrow-up-right

    CCIP Router Address

    0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165

    0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59

    LINK Token Address

    0xb1D4538B4571d411F07960EF2838Ce337FE1E80E

    0x779877A7B0D9E8603169DdbD7836e478b4624789

    hashtag
    Getting started

    You can use Chainlink CCIP with any blockchain development framework. For this Masterclass, we prepared the steps for Hardhat, Foundry, and Remix IDE.

    Let's create a new project

    Make sure you have Node.jsarrow-up-right and NPMarrow-up-right installed. To check, run the following command:

    node -v
    npm -v

    Create a new folder and name it ccip-masterclass-3

    mkdir ccip-masterclass-3

    Navigate to it

    cd ccip-masterclass-3

    Create a hew Hardhat project by running:

    npx hardhat init

    And then select "Create a TypeScript project".

    Make sure you have Foundryarrow-up-right installed. To check, run the following command:

    forge --version

    Create a new folder and name it ccip-masterclass-3

    Navigate to it

    Create a hew Foundry project by running:

    Navigate to and click the "Create new Workspace" button. Select "Blank" template and name the workspace as "CCIP Masterclass 3".

    Alternatively, you can clone:

    • CCIP Starter Kit (Hardhat version)arrow-up-right

    • CCIP Starter Kit (Foundry version)arrow-up-right

    hashtag
    The @chainlink/contracts-ccip NPM package

    To use Chainlink CCIP, you need to interact with Chainlink CCIP-specific contracts from the @chainlink/contracts-cciparrow-up-right NPM package.

    To install it, follow steps specific to the development environment you will use for this Masterclass.

    We will need a standard @chainlink/contractsarrow-up-right NPM package for this Module as well, so let's install it too while we are here by running the following command:

    Finally, for this exercise we will need to install and the @openzeppelin/contractsarrow-up-right NPM package, as well. To do so, run:

    Since Foundry is designed to run with Solidity, NPM packages, though usable, ,can be replaced with directly installying Solidity contract packages from Github source repositories. We will install Chainlink CCIP contracts and then the contracts for other Chainlink Services.

    First the CCIP Contracts.

    And after that set remappings in your foundry.toml or remappings.txt files to

    Next we install the other Chainlink Services contracts by running the following command:

    And set remmapings to

    Finally, for this exercise we will need to install and the NPM package, as well. To do so, run:

    And set remappings to @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ in foundry.toml or remappings.txt files.

    Create a new Solidity file, and paste the following content. It is an empty contract that just imports one of the contracts from the @chainlink/contracts-ccip package.

    Compile it. If compiled successfully and new .deps/npm/@chainlink/contracts-ccip , .deps/npm/@chainlink/contracts and .deps/npm/@openzeppelin/contracts folders are generated, that means we imported all of the necessary packages into the Remix IDE Workspace.

    hashtag
    Faucet

    To pay for CCIP Fees you can use either LINK token or native/wrapped native asset on a given blockchain. For this exercise we will need at least 1 LINK or Arbitrum Sepolia testnet. To get it, navigate to the https://faucets.chain.link/arbitrum-sepoliaarrow-up-right

    Chainlink Faucets

    hashtag
    Develop xNFT smart contract

    Create a new file inside the contracts folder and name it XNFT.sol

    Compile your contract by running:

    Create a new file inside the src folder and name it XNFT.sol

    Create a new Solidity file by clicking on the "Create new file" button and name it XNFT.sol

    hashtag
    Prepare for deployment

    Follow the steps to add the necessary environment variables for deploying these contracts and sending your first CCIP Message.

    This contract expects at least 0.8.20 Solidity version. It is very important to understand that with Solc version 0.8.20, the default EVM version is set to "Shanghai". A new opcode, PUSH0, was added to the Ethereum Virtual Machine in the Shanghai upgrade.

    However, besides Ethereum, the majority of blockchains haven't included PUSH0 opcode.

    That means the PUSH0 opcode can now be part of the contract's bytecode and if the chain you are working on does not support it, it will error with the "Invalid opcode" error.

    To understand more, we highly encourage you to check this StackOverflow answer:

    We are going to use the @chainlink/env-encarrow-up-right package for extra security. It encrypts sensitive data instead of storing them as plain text in the .env file by creating a new .env.enc file. Although it's not recommended to push this file online, if that accidentally happens, your secrets will still be encrypted.

    Install the package by running the following command:

    Set a password for encrypting and decrypting the environment variable file. You can change it later by typing the same command.

    Now set the following environment variables: PRIVATE_KEY, Source Blockchain RPC URL, Destination Blockchain RPC URL. For this example, we are going to use Arbitrum Sepolia and Ethereum Sepolia.

    To set these variables, type the following command and follow the instructions in the terminal:

    After you are done, the .env.enc file will be automatically generated. If you want to validate your inputs, you can always run the next command:

    Finally, expand the hardhat.config to support these two networks:

    Create a new file and name it .env. Fill in your wallet's PRIVATE_KEY and RPC URLs for at least two blockchains. For this example, we are going to use Arbitrum Sepolia and Ethereum Sepolia.

    Once that is done, to load the variables in the .env file, run the following command:

    Finally, expand the foundry.toml to support these two networks:

    Navigate to the "Solidity compiler" tab

    • Toggle the "Advanced Configurations" dropdown

    • Toggle the "EVM VERSION" dropdown menu and select "paris" instead of "default"

    Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu.

    hashtag
    Step 1) Deploy XNFT.sol to Ethereum Sepolia

    Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to the beginning of this page, at CCIP Config Details

    Navigate to the scripts folder and create new file named deployXNFT.ts

    Run the deployment script:

    Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to begging of this page, at CCIP Config Details

    Option 1)

    Deploy XNFT.sol smart contract by running:

    hashtag
    Option 2)

    Create a new smart contract under the script folder and name it XNFT.s.sol

    Note that deployment of the XNFT smart contract is hard coded to Ethereum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check for reference.

    Deploy XNFT.sol smart contract by running:

    Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to begging of this page, at

    Open your Metamask wallet and switch to the Ethereum Sepolia network.

    Open the XNFT.sol file.

    Navigate to the "Solidity Compiler" tab and click the "Compile XNFT.sol" button.

    Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that chainId is switched to 11155111 (if not, you may need to refresh the Remix IDE page in your browser).

    Under the "Contract" dropdown menu, make sure that the "XNFT - XNFT.sol" is selected.

    hashtag
    Step 2) Deploy XNFT.sol to Arbitrum Sepolia

    Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to beginning of this page, at CCIP Config Details

    Navigate to the scripts folder and create new file named deployXNFTArbitrum.ts

    Run the deployment script:

    Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to begging of this page, at CCIP Config Details

    Option 1)

    Deploy XNFT.sol smart contract by running:

    hashtag
    Option 2)

    Create a new smart contract under the script folder and name it XNFTArbitrum.s.sol

    Note that deployment of the XNFT smart contract is hard coded to Arbitrum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check for reference.

    Deploy XNFT.sol smart contract by running:

    Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to begging of this page, at

    Open your Metamask wallet and switch to the Arbitrum Sepolia network.

    Open the XNFT.sol file.

    Navigate to the "Solidity Compiler" tab and click the "Compile XNFT.sol" button.

    Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that chainId is switched to 421614 (if not, you may need to refresh the Remix IDE page in your browser).

    Under the "Contract" dropdown menu, make sure that the "XNFT - XNFT.sol" is selected.

    hashtag
    Step 3) On Ethereum Sepolia, call enableChain function

    Prepare:

    • The address of the address of the XNFT.sol smart contract you previously deployed to Ethereum Sepolia;

    • The address of the address of the XNFT.sol smart contract you previously deployed to Arbitrum Sepolia;

    • 3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the chainSelector parameter;

    • 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs parameter.

    If you would like to calculate this value by yourself, you can reuse the following helper smart contract:

    Create a new TypeScript file under the scripts folder and name it enableChain.ts

    Call the function by running the following command:

    Prepare:

    • The address of the address of the XNFT.sol smart contract you previously deployed to Ethereum Sepolia;

    • The address of the address of the XNFT.sol smart contract you previously deployed to Arbitrum Sepolia;

    Under the "Deployed Contracts" section, you should find the XNFT.sol contract you previously deployed to Ethereum Sepolia. Find the enableChain function and provide:

    • 3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the chainSelector parameter;

    • The address of the address of the XNFT.sol

    hashtag
    Step 4) On Arbitrum Sepolia, call enableChain function

    Prepare:

    • The address of the XNFT.sol smart contract you previously deployed to Arbitrum Sepolia;

    • The address of the address of the XNFT.sol smart contract you previously deployed to Ethereum Sepolia, as xNftAddress parameter;

    • 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the chainSelector parameter;

    • 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs parameter.

    If you would like to calculate this value by yourself, you can reuse the following helper smart contract:

    Create a new TypeScript file under the scripts folder and name it enableChainArbitrum.ts

    Call the function by running the following command:

    Prepare:

    • The address of the address of the XNFT.sol smart contract you previously deployed to Arbitrum Sepolia;

    • The address of the address of the XNFT.sol smart contract you previously deployed to Ethereum Sepolia, as xNftAddress parameter;

    Under the "Deployed Contracts" section, you should find the XNFT.sol contract you previously deployed to Arbitrum Sepolia. Find the enableChain function and provide:

    • 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the chainSelector parameter;

    • The address of the address of the

    hashtag
    Step 5) On Arbitrum Sepolia, fund XNFT.sol with 3 LINK

    To cover for CCIP fees, fund XNFT.sol with some amount of LINK, 3 should be more than enough for this demo. Obviously, for the sake of full functionality, you should fund XNFT.sol smart contract on other blockchains as well, so you can perform cross-chain transfers between all of them.

    Fund your xNFT with LINK

    hashtag
    Step 6) On Arbitrum Sepolia, mint new xNFT

    Create a new TypeScript file under the scripts folder and name it mint.ts

    Call the function by running the following command:

    Run:

    Under the "Deployed Contracts" section, you should find the XNFT.sol contract you previously deployed to Arbitrum Sepolia. Find the mint function and hit the "Transact" orange button.

    hashtag
    Step 7) On Arbitrum Sepolia, crossTransferFrom xNFT

    Prepare:

    • Your EOA address, as the from parameter;

    • The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as to parameter;

    • The ID of a xNFT you want to cross-transfer, as tokenId parameter;

    • 16015286601757825753 which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the destinationChainSelector parameter;

    • 1 which stands that we are paying for CCIP fees in LINK, as the payFeesIn parameter.

    Create a new TypeScript file under the scripts folder and name it crossChainTransferFrom.ts

    Call the function by running the following command:

    Prepare:

    • Your EOA address, as the from parameter;

    • The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as to parameter;

    Under the "Deployed Contracts" section, you should find the XNFT.sol contract you previously deployed to Arbitrum Sepolia. Find the crossChainTransferFrom function and provide the following parameters:

    • Your EOA address, as the from parameter;

    • The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as to

    You can now monitor this cross-chain transfer on CCIP Explorer page.

    CCIP Explorer

    Once cross-chain NFT arrives to Ethereum Sepolia, you can manually display it inside your Metamask wallet. Navigate to the "NFT" tab and hit the "Import NFT" button.

    Import NFT

    Then, fill in XNFT.sol smart contract address on Ethereum Sepolia and token ID you received (0).

    Fill in NFT details

    Finally, your NFT will appear inside Metamask wallet.

    See your newly minted NFT

    Chain Selector

    Chainlink CCIP: The Level-5 Interoperability Standard for Web3 and Global Finance

    Level-5 cross-chain security achieves unprecedented levels of decentralization by utilizing multiple decentralized networks to secure a single cross-chain transaction, along with incorporating additional risk management systems to identify risks and take actions to prevent them, such as by implementing emergency shutdowns or imposing rate limits.

    The fifth level of cross-chain security doesn’t just give you one independent network for your cross-chain data or messages; it gives you multiple networks, made up of independent nodes all working together to secure each bridge.

    While many bridge solutions operate using a single node or multiple nodes under the control of one key holder (e.g., Multichain), the fifth level of security uses multiple independent nodes with their own independent key holders and even splits them up into two separate groups of nodes: the transactional DON nodes and the Risk Management Network nodes. One additional key feature of the separate networks in CCIP is the creation of two entirely separate implementations, with two independent code bases, so that CCIP features an unprecedented level of client diversity/decentralization for cross-chain interoperability.

    Developed with security and reliability as the primary focus CCIP operates at the highest level of cross-chain security. CCIP’s defense-in-depth security and suitability can be broken down across four categories:

    hashtag
    Multiple Layers of Decentralization

    CCIP is underpinned by Chainlink’s proven decentralized oracle infrastructure. Rather than operating as a single monolithic network, CCIP is composed of multiple decentralized oracle networks (DONs) per chain lane, each consisting of a unique source chain and destination chain. This approach allows CCIP to be horizontally scalable, as additional DONs are added to CCIP for each additional blockchain network supported, versus funneling all cross-chain traffic through a single network.

    The committing DON is a decentralized network of oracle nodes that monitor events on a given source chain, wait for source chain finality, bundle transactions to create a Merkle root, come to consensus on that Merkle root and finally commit that Merkle root to the destination chain. The executing DON is a decentralized network of oracle nodes that submit Merkle proofs on a destination chain, which is then verified onchain by ensuring the transactions were included in a previously committed Merkle root that has been validated by the Risk Management Network.

    A CCIP message is finalized with a Merkle tree on the source chain and the Merkle root is then published on the destination chain.

    hashtag
    Risk Management Network

    The Risk Management Network is a separate, independent network that continuously monitors and validates the behavior of CCIP, providing an additional layer of security by independently verifying cross-chain operations for anomalous activity. The Risk Management Network utilizes a separate, minimal implementation of the Chainlink node software, creating a form of client diversity for increased robustness while also minimizing external dependencies to prevent supply chain attacks.

    More specifically, the Risk Management Network was written in a different programming language (Rust) than the primary CCIP system (Golang), developed by a different internal team, and uses a distinct non-overlapping set of node operators compared to the CCIP DONs. The Risk Management Network is a wholly unique concept in cross-chain interoperability that builds upon established engineering principles (N-version programmingarrow-up-right) seen in mission-critical systems in industries such as aviation, nuclear, and machine automation.

    To increase the security and robustness of CCIP, the Risk Management Network engages in two types of activities:

    • Secondary Approval: The Risk Management Network independently recreates Merkle roots based on transactions from the source chain, which are then published on the destination chain and compared against the Merkle roots published by the Committing DON. Cross-chain transactions can only be executed if the Merkle roots from the two networks match.

    • Anomaly Detection: The Risk Management Network monitors for abnormal behavior from the CCIP network (e.g., committed transactions with no source chain equivalent) as well as the behavior of chains (e.g., deep block reorgs). If suspicious activity is detected, the Risk Management Network can trigger an emergency halt to pause all CCIP lanes and limit any losses.

    The Risk Management Network verifies CCIP messages.

    hashtag
    High-Quality Node Operators

    Chainlink DONs are operated by a geographically distributed collection of Sybil-resistant, security-reviewed node operators with significant experience running mission-critical infrastructure across Web2 and Web3. Node operators in the Chainlink ecosystem include global enterprises (e.g., Deutsche Telekom MMS, Swisscom, Vodafone), leading Web3 DevOps teams (e.g. Infura, Coinbase Cloud), and experienced Chainlink ecosystem projects.

    The Committing DONs and Executing DONs in CCIP are composed of 16 high-quality independent node operators, while the Risk Management Network is composed of 7 distinct node operators (resulting in a total of 23 node operators). Importantly, the Risk Management Network consists of a wholly separate and non-overlapping set of nodes compared to the primary CCIP networks, helping ensure independent secondary validation. As the value secured by CCIP expands over time, the number of node operators within each network can scale to meet the need for greater security.

    hashtag
    Configurable Rate Limits

    As an additional layer of security for cross-chain token transfers, CCIP implements configurable rate limits, established on a per-token and per-lane basis, which are set up in alignment with the token contract owners like Lido. Furthermore, CCIP token transfers also benefit from the increased security provided by an aggregate rate limit (across token pools) on each lane, so even in a worst-case scenario, it would be impossible for every token’s limit to be maxed out before the aggregate rate limit on a lane is hit.

    hashtag
    Conclusion

    With CCIP you get:

    • Multiple independent nodes run by independent key holders.

    • Three decentralized networks all executing and verifying every bridge transaction.

    • Separation of responsibilities, with distinct sets of node operators, and with no nodes shared between the transactional DONs and the Risk Management Network.

    • Increased decentralization with two separate code bases across two different implementations, written in two different languages to create a previously unseen diversity of software clients in the bridging world.

    • Never-before-seen level of risk management that can be rapidly adapted to any new risks or attacks that appear for cross-chain bridging.

    Darby's deckarrow-up-right

    Exercise 4: Sending USDC Cross-Chain

    Getting started

    You can use Chainlink CCIP with any blockchain development framework. For this Masterclass, we will use Remix IDE.

    Let's create a new project by navigating to https://remix.ethereum.org/arrow-up-right and clicking the "Create new Workspace" button. Select "Blank" template and name the workspace as "CCIP Masterclass 4".

    Alternatively, you can clone:

    • CCIP Starter Kit (Hardhat version)arrow-up-right

    hashtag
    The @chainlink/contracts-ccip NPM package

    To use Chainlink CCIP, you need to interact with Chainlink CCIP-specific contracts from the NPM package.

    To install it, create a new Solidity file, and paste the following content. It is an empty contract that just imports one of the contracts from the @chainlink/contracts-ccip and @openzeppelin/contracts packages that we will use throughout this Masterclass as well.

    This contract expects at least 0.8.20 version of a Solidity compiler. It is very important to understand that with the latest Remix IDE release, the default EVM version is set to "Cancun". A new opcode, PUSH0, was added to the Ethereum Virtual Machine in the Shanghai upgrade, which happened prior to the current, Cancun upgrade.

    However, besides Ethereum, the majority of blockchains haven't included PUSH0 opcode.

    That means the PUSH0 opcode can now be part of the contract's bytecode and if the chain you are working on does not support it, it will error with the "Invalid opcode" error.

    What we want is to downgrade Ethereum Virtual Machine version to "Paris" instead.

    To understand more, we highly encourage you to check this StackOverflow answer:

    To set EVM version to "Paris", navigate to the "Solidity compiler" tab and then:

    • Set "COMPILER" version to 0.8.20+commit.a1b79de6

    • Toggle the "Advanced Configurations" dropdown

    • Toggle the "EVM VERSION" dropdown menu and select paris instead of default

    Now compile the smart contract by clicking the "Compile Empty.sol" button. If compiled successfully, go back to "File explorer" tab and if new .deps/npm/@chainlink/contracts-ccip and .deps/npm/@openzeppelin/contracts folders are generated, that means we imported all of the necessary packages into the Remix IDE Workspace successfully.

    hashtag
    Faucet

    During this Masterclass, we will transfer USDC from Avalanche Fuji testnet to Ethereum Sepolia testnet. To get some amount of testnet USDC on Avalanche Fuji testnet, navigate to the

    To pay for CCIP Fees you can use either LINK token or native/wrapped native asset on a given blockchain. For this Masterclass we will need at least 3 LINK or Avalanche Fuji testnet. To get it, navigate to the

    hashtag
    Develop TransferUSDC smart contract

    Create a new Solidity file by clicking on the "Create new file" button, name it TransferUSDC.sol, and paste the following Solidity code.

    hashtag
    Prepare for deployment

    Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu.

    If you are using Metamask wallet, make sure you have added Avalanche Fuji C-Chain and Ethereum Sepolia (should already be added by default) networks.

    Go to and search for "avalanche fuji". Once you see the network with Chain ID 43113, click the "Add to Metamask" button.

    Ethereum Sepolia should already be added by default to your Metamask wallet. However, if you need to manually add it, you can always repeat the same step we did for Avalanche Fuji C-Chain. Navigate to and search for "sepolia". Once you see the network with Chain ID 11155111, click the "Add to Metamask" button.

    hashtag
    Step 1) Deploy TransferUSDC.sol to Avalanche Fuji

    Open your Metamask wallet and switch to the Avalanche Fuji network.

    Open the TransferUSDC.sol file.

    Navigate to the "Solidity Compiler" tab and click the "Compile TransferUSDC.sol" button.

    Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that chainId is switched to 43113 (if not, you may need to refresh the Remix IDE page in your browser).

    Under the "Contract" dropdown menu, make sure that the "TransferUSDC - TransferUSDC.sol" is selected.

    Locate the orange "Deploy" button. Provide:

    • 0xF694E193200268f9a4868e4Aa017A0118C9a8177 as the ccipRouter,

    • 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 as the linkToken and

    Click the orange "Deploy"/"Transact" button.

    Metamask notification will pop up. Sign the transaction.

    hashtag
    Step 2) On AvalancheFuji, call allowlistDestinationChain function

    Under the "Deployed Contracts" section, you should find the TransferUSDC.sol contract you previously deployed to Avalanche Fuji. Find the allowlistDestinationChain function and provide:

    • 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia test network, as the _destinationChainSelector parameter,

    • true as _allowed parameter

    Hit the "Transact" orange button.

    hashtag
    Step 3) On AvalancheFuji, fund TransferUSDC.sol with 3 LINK

    To cover for CCIP fees, fund TransferUSDC.sol with some amount of LINK, 3 should be enough for this demo.

    hashtag
    Step 4) On Avalanche Fuji, call approve function on USDC.sol

    Go to the and search for USDC token. Locate the "Contract" tab, then click the "Write as Proxy" tab. Connect your wallet to the blockchain explorer. And finally find the "approve" function.

    We want to approve 1 USDC to be spent by the TransferUSDC.sol on our behalf. To do so we must provide:

    • The address of the TransferUSDC.sol smart contract we previously deployed, as spender parameter

    • 1000000, as value parameter.

    Because USDC token has 6 decimals, 1000000 means that we will approve 1 USDC to be spent on our behalf.

    Click the "Write" button. Metamask popup will show up. Sign the transaction.

    hashtag
    Step 5) On AvalancheFuji, call transferUsdc function

    Under the "Deployed Contracts" section, you should find the TransferUSDC.sol contract you previously deployed to Avalanche Fuji. Find the transferUsdc function and provide:

    • 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia test network, as the _destinationChainSelector parameter,

    • Your wallet address, as the _receiver parameter,

    • 1000000, as the _amount parameter

    0 is set as the _gasLimit parameter because we are sending tokens to an EOA so there is no cost for executing the ccipReceive function on the destination side.

    Hit the "Transact" orange button.

    You can now monitor the live status of your cross-chain message by copying the transaction hash into the search bar of a .

    Next Steps

    hashtag
    1. Come join us at SmartCon - Chainlink's premier annual event on 28-31 October in Hong Kong.

    👉 Register nowarrow-up-right

    SmartCon Registration page

    hashtag
    2. Join Tokenized RWA Bootcamp to advance your skills in the most sought after niche in blockchain industry.

    Secure your spot before anyone else.

    👉

    hashtag
    3. Sign up for Chainlink developer newsletter to stay updated on our future technical workshops and bootcamps:

    hashtag
    4. Discover More in Our Dev Hub:

    hashtag
    5. Get in touch with our experts if you want to build with CCIP:

    hashtag
    6. CCIP Documentation:

    hashtag
    7. Learn more about Chainlink Platform:

    // 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);
        }
    }
    
    npm i @chainlink/contracts-ccip --save-dev
    npm i @chainlink/contracts --save-dev
    npm i @openzeppelin/contracts --save-dev
    forge 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-dev
    npx env-enc set-pw
    PRIVATE_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 arbitrumSepolia
    cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "mint()"
    Register nowarrow-up-right
    https://pages.chain.link/subscribearrow-up-right
    https://dev.chain.link/arrow-up-right
    https://chain.link/contactarrow-up-right
    https://docs.chain.link/cciparrow-up-right
    https://chain.link/arrow-up-right
    Tokenized RWA Bootcamp Registration page
    as the source chain selector, and true
    as allowed. Each chain selector is found on the
    .
    as allowed.

    Hello World! Any string

    _token

    0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 The CCIP-BnM contract address at the source chain (Avalanche Fuji in this example). You can find all the addresses for each supported blockchain on the supported networks pagearrow-up-right.

    _amount

    1000000000000000 The token amount (0.001 CCIP-BnM).

    supported networks pagearrow-up-right
    supported networks pagearrow-up-right
    LINK token contracts pagearrow-up-right
    explanationarrow-up-right
    supported networks pagearrow-up-right
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    supported networks pagearrow-up-right
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Connect your wallet to Remix IDE

    If you are using Metamask wallet, the Ethereum Sepolia network should already came preinstalled. Make sure you added the Arbitrum Sepolia network.

    Go to Chainlist.orgarrow-up-right and search for "arbitrum sepolia". Once you see the network with Chain ID 421614, click the "Add to Metamask" button.

    Add Arbitrum Sepolia network to Metamask
    Locate the orange "Deploy" button. Provide 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59 as the ccipRouterAddress , 0x779877A7B0D9E8603169DdbD7836e478b4624789 as the linkTokenAddress and 16015286601757825753 as the currentChainSelector.

    Click the orange "Deploy"/"Transact" button.

    Metamask notification will pop up. Sign the transaction.

    Locate the orange "Deploy" button. Provide 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165 as the ccipRouterAddress , 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E as the linkTokenAddress and 3478487238524512106 as the currentChainSelector.

    Click the orange "Deploy"/"Transact" button.

    Metamask notification will pop up. Sign the transaction.

    3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the chainSelector parameter;
  • 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs parameter.

  • If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Inside the scripts folder, create EncodeExtraArgs.s.sol and paste the following code:

    Run:

    smart contract you previously deployed to Arbitrum Sepolia, as
    xNftAddress
    parameter;
  • 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs parameter.

  • Hit the "Transact" orange button.

    If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Create EncodeExtraArgs.sol file and paste the following code:

    16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the chainSelector parameter;

  • 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs parameter.

  • If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Inside the scripts folder, create EncodeExtraArgs.s.sol and paste the following code:

    Run:

    XNFT.sol
    smart contract you previously deployed to Ethereum Sepolia, as
    xNftAddress
    parameter;
  • 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40, which is the bytes version of CCIP extraArgs' default value with 200_000 gas set for gasLimit, as ccipExtraArgs parameter.

  • Hit the "Transact" orange button.

    If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Create EncodeExtraArgs.sol file and paste the following code:

    The ID of a xNFT you want to cross-transfer, as tokenId parameter;
  • 16015286601757825753 which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the destinationChainSelector parameter;

  • 1 which stands that we are paying for CCIP fees in LINK, as the payFeesIn parameter.

  • Run:

    parameter;
  • The ID of a xNFT you want to cross-transfer, as tokenId parameter;

  • 16015286601757825753 which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the destinationChainSelector parameter;

  • 1 which stands that we are paying for CCIP fees in LINK, as the payFeesIn parameter.

  • Hit the "Transact" orange button.

    https://remix.ethereum.org/arrow-up-right
    @openzeppelin/contractsarrow-up-right
    CCIP Starter Kit (Foundry version)arrow-up-right
    CCIP Starter Kit (Foundry version)arrow-up-right
    CCIP Config Details
    CCIP Config Details
    mkdir ccip-masterclass-3
    cd ccip-masterclass-3
    forge init
    0x5425890298aed601595a70AB815c96711a31Bc65 as the usdcToken.
  • 0, as the _gasLimit parameter

  • CCIP Starter Kit (Foundry version)arrow-up-right
    @chainlink/contracts-cciparrow-up-right
    https://faucet.circle.com/arrow-up-right
    https://faucets.chain.link/fujiarrow-up-right
    Faucets | Chainlinkarrow-up-right
    Chainlist.orgarrow-up-right
    Chainlist.orgarrow-up-right
    Avalanche Fuji Snowtrace Explorerarrow-up-right
    Chainlink CCIP Explorerarrow-up-right
    CCIP Explorer | Chainlinkarrow-up-right
    arrow-up-right
    Circle Faucet
    Chainlink Faucet
    Connect your wallet to Remix IDE
    Add Avalanche Fuji network to Metamask
    Approve 1 USDC to be spent by TransferUSDC.sol
    Approve 1 USDC to be spent by TransferUSDC.sol
    Testnet Faucet | Circlefaucet.circle.comchevron-right
    Circle Faucet
    Testnet Faucet | Circlefaucet.circle.comchevron-right
    Circle Faucet

    Day 2 Homework

    Please note, only participants who finish their homework for all three days will be eligible for the Certificate of Completion.

    Submit your public GitHub repo for the exercise below via

    Using chainlink-local write a test for the project. You can use for help.

    circle-exclamation

    During the Exercise #3 we used Chainlink Local's Forked Mode with Foundry. For this homework you should use Chainlink Local's Local Mode with Hardhat

    # 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/'
    ]
    // 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 {}
    npx env-enc set
    npx env-enc view
    import * 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;
    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/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 ethereumSepolia
    forge 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
    3478487238524512106
    16015286601757825753
    // 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
    // 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);
        }
    }
    .

    In the test you must:

    • Create an instance of CCIPLocalSimulator.sol smart contract.

    • Call the configuration() function to get Router contract address.

    • Create instances of CrossChainNameServiceRegister.sol, CrossChainNameServiceReceiver.sol and CrossChainNameServiceLookup.sol smart contracts and call the enableChain() function where needed.

    • Call the setCrossChainNameServiceAddress function of the CrossChainNameServiceLookup.sol smart contract "source" instance and provide the address of the CrossChainNameServiceRegister.sol smart contract instance. Repeat the process for the CrossChainNameServiceLookup.sol smart contract "receiver" instance and provide the address of the CrossChainNameServiceReceiver.sol smart contract instance.

    • Call the register() function and provide “alice.ccns” and Alice’s EOA address as function arguments.

    • Call the lookup() function and provide “alice.ccns” as a function argument. Assert that the returned address is Alice’s EOA address.

    • Send a url pointing to the public Github repository.

    triangle-exclamation

    DO NOT PROVIDE PRIVATE KEY OR TESTNET RPC DETAILS, USE HARDHAT NETWORK ONLY! npx hardhat test --network hardhat

    this formarrow-up-right
    https://github.com/smartcontractkit/ccip-cross-chain-name-servicearrow-up-right
    https://cll-devrel.gitbook.io/chainlink-local-documentationarrow-up-right
    andrej.rakic@smartcontract.comenvelope
    https://www.npmjs.com/package/@chainlink/contracts-ccipwww.npmjs.comchevron-right
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    Copy to clipboard
    https://www.npmjs.com/package/@chainlink/contracts-ccipwww.npmjs.comchevron-right
    https://www.npmjs.com/package/@chainlink/contracts-ccipwww.npmjs.comchevron-right
    Logo
    Logo
    Transporter Testnets | Bridge Crypto Cross-ChainTransporterchevron-right
    test.transporter.io
    Logo
    Logo
    Arbitrum Sepolia Faucet - Get Testnet Tokensfaucets.chain.linkchevron-right
    Remix: Returned error: {"jsonrpc":"2.0","error":"invalid opcode: PUSH0", "id":2405507186007008}Stack Overflowchevron-right
    Setting solc EVM version in different environments
    Remix: Returned error: {"jsonrpc":"2.0","error":"invalid opcode: PUSH0", "id":2405507186007008}Stack Overflowchevron-right
    Setting solc EVM version in different environments
    Logo
    https://testnet.snowtrace.io/address/0x5425890298aed601595a70AB815c96711a31Bc65/contract/43113/writeProxyContract?chainId=43113testnet.snowtrace.iochevron-right
    Approve 1 USDC to be spent by TransferUSDC.sol
    Logo
    Logo