---
description: How to design a cross-chain NFT smart contract
---

# Building Cross-Chain NFTs

### What are Cross-Chain NFTs? <a href="#what-are-cross-chain-nfts" id="what-are-cross-chain-nfts"></a>

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.

### How Do Cross-Chain NFTs Work? <a href="#how-do-cross-chain-nfts-work" id="how-do-cross-chain-nfts-work"></a>

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.

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FMdtL9vkWnielJxxXjVO5%2F11.jpeg?alt=media&#x26;token=27b16e0a-8686-419c-9f2e-6a809064fb87" alt=""><figcaption><p>CCIP Token Transfer Mechanisms</p></figcaption></figure>

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.

### How to use Chainlink CCIP to create Cross-Chain NFTs? <a href="#how-to-use-chainlink-ccip-to-create-cross-chain-nfts" id="how-to-use-chainlink-ccip-to-create-cross-chain-nfts"></a>

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`

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2F1YTdUI7pr6Rie6nFAOmx%2F22.avif?alt=media&#x26;token=e9838003-d2a7-4d6a-b3da-7684b76bb687" alt=""><figcaption><p>CCIP Transfer options</p></figcaption></figure>

## 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:

```solidity
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)
    });
}
```

The Ethereum Sepolia side:

```solidity
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);
}
```

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.

### NFT Metadata <a href="#nft-metadata" id="nft-metadata"></a>

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/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1](https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1)

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FafVJCoqQ3mMRGfzN64nr%2F33.webp?alt=media&#x26;token=c15c8cc4-e416-41b4-b755-00c58baa112a" alt="" width="188"><figcaption><p>Chainlink Elf</p></figcaption></figure>

* **Chainlink Knight**, available at: [https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH](https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH)

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FfYy3XacmPCvghPW8btwR%2F44.webp?alt=media&#x26;token=28ffcc98-6844-4000-b6a9-874ac09d70fb" alt="" width="188"><figcaption><p>Chainlink Knight</p></figcaption></figure>

* **Chainlink Orc**, available at: [https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP](https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP)

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2F7K2I1OCIPlRMs5YTdl8g%2F55.webp?alt=media&#x26;token=cadabb1e-0c41-4371-b4e0-a01da64ae859" alt="" width="188"><figcaption><p>Chainlink Orc</p></figcaption></figure>

* **Chainlink Witch**, available at: [https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF](https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF)

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FE4Ac4zO4Fz8yiJZgyQjX%2F66.avif?alt=media&#x26;token=9fba3507-3722-492c-a70d-f19bf6bc3d3b" alt="" width="188"><figcaption><p>Chainlink Witch</p></figcaption></figure>

### Development Best Practices <a href="#development-best-practices" id="development-best-practices"></a>

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.

### 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.

```solidity
// 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);
}
```

#### Verify Router addresses <a href="#verify-router-addresses" id="verify-router-addresses"></a>

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`.

#### Setting `gasLimit` <a href="#setting-gaslimit" id="setting-gaslimit"></a>

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.

#### Using `extraArgs` <a href="#using-extraargs" id="using-extraargs"></a>

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:

```solidity
// 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);
    }
}
```
