Creating a Cross-Chain NFT with Axelar: A Step-by-Step Guide ✏️

VaradVarad
9 min read

1. Introduction to Axelar and Cross-Chain Architecture

Axelar is a decentralized interoperability network that enables secure communication between different blockchain ecosystems. For our cross-chain NFT, Axelar will facilitate the transfer of NFT ownership and metadata between Ethereum (Sepolia testnet) and Avalanche (Fuji testnet).

General Message Passing Architecture Using Axelar Nework

Axelar Network in Our Cross-Chain NFT walkthrough:

Before we dive into the step-by-step process of creating a cross-chain NFT, it's important to understand how Axelar facilitates this functionality:

  1. Axelar's Role: Axelar acts as a decentralized network that connects different blockchains, enabling seamless communication between them. In our tutorial, Axelar will allow our NFT to move between Ethereum (Sepolia) and Avalanche (Fuji) testnets.

  2. General Message Passing (GMP): Axelar uses GMP to facilitate secure, Turing-complete cross-chain computation. This is what allows us to send not just tokens, but also complex data and function calls between chains.

  3. Gateway Smart Contracts: Axelar deploys Gateway smart contracts on each connected blockchain. These Gateways are chain-specific and serve as entry and exit points for cross-chain communications. In our tutorial, we'll interact with these Gateways to initiate cross-chain NFT transfers.

  4. Flow of Cross-Chain NFT Transfer: a. User initiates transfer on the source chain (e.g., Ethereum Sepolia) b. Our smart contract interacts with the Axelar Gateway on Sepolia c. Axelar validators verify the transaction and reach consensus d. Axelar network relays the message to the destination chain (Avalanche Fuji) e. The Axelar Gateway on Fuji receives the message f. Our smart contract on Fuji processes the message and mints the NFT

  5. Gas Handling: Axelar Gas Service automates the conversion of gas fees, allowing users to pay in the source chain's native token. This simplifies the user experience in our tutorial.

  6. Security: Axelar uses a proof-of-stake consensus mechanism secured by the AXL token. This provides a robust security model for our cross-chain NFT transfers.

  7. Hub-and-Spoke Architecture: Axelar's design allows our NFT to move directly between chains without needing to pass through intermediary chains, improving efficiency and reducing potential points of failure.

By leveraging Axelar's infrastructure, our cross-chain NFT tutorial demonstrates how to create a truly interoperable NFT that can exist on multiple blockchains. This opens up new possibilities for NFT utility, liquidity, and user experience across different blockchain ecosystems.

2. Understanding the Burn-and-Mint Mechanism

Our cross-chain NFT implementation uses the burn-and-mint mechanism:

  1. NFT owner initiates a transfer on the source chain

  2. NFT is burned on the source chain

  3. Axelar relays a message to the destination chain

  4. An equivalent NFT is minted on the destination chain

    Burn and Mint architecture for cross-chain NFTs

This approach maintains the NFT's uniqueness across chains.

3. Setting Up the Development Environment

We'll use Remix IDE for this tutorial. Follow these steps:

  1. Open Remix IDE: https://remix.ethereum.org/

  2. Create a new file named CrossChainNFT.sol

4. Implementing the Cross-Chain NFT Smart Contract

Copy and paste the following code into CrossChainNFT.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { AxelarExecutable } from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
import "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol";

contract CrossChainNFT is ERC721, ERC721URIStorage,AxelarExecutable {

    IAxelarGasService public immutable gasService;
    string public message;
    string public sourceChain;
    string public sourceAddress;

    event Executed(string _from, string _message);

    uint256 private _tokenIds;
    mapping(string => string) public trustedRemotes;

    /**
     * @dev Constructor that initializes the contract with the Axelar Gateway and Gas Service addresses.
     * @param _gateway The address of the Axelar Gateway contract.
     * @param _gasService The address of the Axelar Gas Service contract.
     */
    constructor(address _gateway, address _gasService) ERC721("CrossChainNFT", "CCNFT") AxelarExecutable(_gateway) {
        gasService = IAxelarGasService(_gasService);
    }

    /**
     * @dev Function to set trusted remote addresses for cross-chain communication.
     * @param _chain The chain for which the trusted remote address is being set.
     * @param _address The trusted remote address for the specified chain.
     */
    function setTrustedRemote(string calldata _chain, string calldata _address) external {
        trustedRemotes[_chain] = _address;
    }

    /**
     * @dev Function to mint a new NFT.
     * @param _tokenURI The URI of the metadata associated with the new NFT.
     * @return The ID of the newly minted NFT.
     */
    function mint(string memory _tokenURI) external returns (uint256) {
        uint256 newTokenId = _tokenIds++;
        _safeMint(msg.sender, newTokenId);
        _setTokenURI(newTokenId, _tokenURI);
        return newTokenId;
    }

    /**
     * @dev Internal function to check if the provided initiator address is the owner of the NFT with the specified tokenId.
     * @param initiator The address that is attempting to interact with the NFT.
     * @param tokenId The ID of the NFT.
     * @return true if the initiator is the owner of the NFT, false otherwise.
     */
    function _isApprovedOrOwner(address initiator,uint256 tokenId) internal view returns (bool) {
        if(ownerOf(tokenId)==initiator){
            return true;
        }
        else{
            return false;
        }
    }

    /**
     * @dev Function to initiate a cross-chain transfer of an NFT.
     * @param destinationChain The chain to which the NFT will be transferred.
     * @param destinationAddress The address on the destination chain where the NFT will be received.
     * @param tokenId The ID of the NFT being transferred.
     */
    function crossChainTransfer(
        string calldata destinationChain,
        address destinationAddress,
        uint256 tokenId
    ) external payable {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "Not approved or owner");
        require(bytes(trustedRemotes[destinationChain]).length > 0, "Destination chain not trusted");

        string memory _tokenURI = tokenURI(tokenId);

        _burn(tokenId);

        bytes memory payload = abi.encode(destinationAddress, tokenId, _tokenURI);

        if (msg.value > 0) {
            gasService.payNativeGasForContractCall{value: msg.value}(
                address(this),
                destinationChain,
                trustedRemotes[destinationChain],
                payload,
                msg.sender
            );
        }

        gateway.callContract(destinationChain, trustedRemotes[destinationChain], payload);
    }

    /**
     * @dev Internal function that is called when a cross-chain message is received.
     * @param _sourceChain The chain from which the message originated.
     * @param _sourceAddress The address on the source chain that initiated the cross-chain transfer.
     * @param _payload The encoded data containing the destination address, token ID, and token URI.
     */
    function _execute(
        string calldata _sourceChain,
        string calldata _sourceAddress,
        bytes calldata _payload
    ) internal override  {
        require(keccak256(bytes(sourceAddress)) == keccak256(bytes(trustedRemotes[_sourceChain])), "Not a trusted source");

        sourceChain=_sourceChain;
        sourceAddress = _sourceAddress;

        (address to, uint256 tokenId, string memory _tokenURI) = abi.decode(_payload, (address, uint256, string));

        _safeMint(to, tokenId);
        _setTokenURI(tokenId, _tokenURI);
    }

    /**
     * @dev Override function to return the token URI associated with the specified tokenId.
     * @param tokenId The ID of the NFT.
     * @return The token URI of the NFT.
     */
    function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }

    /**
     * @dev Override function to handle the interface ID functionality.
     * @param interfaceId The interface ID to check.
     * @return true if the contract supports the specified interface, false otherwise.
     */
    function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

Save this file and remix will install the required dependencies.

5. Deploying the Smart Contract

Deploy the contract on both Ethereum Sepolia and Avalanche Fuji testnets:

  1. In Remix, go to the "Deploy & Run Transactions" tab

  2. Select "Injected Web3" as the environment (ensure MetaMask is connected)

  3. Choose the appropriate network in MetaMask

  4. Compile the contract and deploy with these parameters:

    • gateway_: Axelar Gateway address

    • gasService_: Axelar Gas Service address

It should look something like this:

Axelar Gateway and Gas Service addresses:

  • Ethereum Sepolia:

    • Gateway: 0xe432150cce91c13a887f7D836923d5597adD8E31

    • Gas Service: 0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6

  • Avalanche Fuji:

    • Gateway: 0xC249632c2D40b9001FE907806902f63038B737Ab

    • Gas Service: 0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6

Deploy on both networks and note the deployed addresses.

After deployment of contracts you can see various functions from the contract :

6. Minting and Transferring NFTs Across Chains

Minting an NFT (on Ethereum Sepolia)

  1. Connect MetaMask to Ethereum Sepolia

  2. In Remix, connect to the deployed contract on Sepolia

  3. Call the mint function:

    • to: Your wallet address

    • tokenURI: IPFS URI of your NFT metadata (e.g., "ipfs://Qm...")

  4. Confirm the transaction and wait for it to be mined

Initiating Cross-Chain Transfer (from Ethereum Sepolia to Avalanche Fuji)

  1. Ensure you're connected to the Sepolia network

  2. Call setTrustedRemote (if not already set):

    • chain: "Avalanche"

    • remoteAddress: Address of the deployed contract on Avalanche Fuji

  1. Call setTrustedRemote (on the avalanche deployed NFT contract):

    • chain: "ethereum-sepolia"

    • remoteAddress: Address of the deployed contract on Ethereum Sepolia

  1. Call crossChainTransfer:

    • destinationChain: "Avalanche"

    • destinationAddress: Your wallet address on Avalanche

    • tokenId: The ID of the NFT to transfer

  2. Include ETH to cover gas fees on the destination chain (Add 10e8 Gwei i.e. 0.1 ETH for example in the VALUE field )

  3. Confirm the transaction and wait for processing

Once you click on transact the request will pass through the Axelar network and first the NFT is burned on the source chain and then it is minted on the destination chain when the execute function is called on the destination chains NFT smart contract.

You can track your Axelar request on Axelar Explorer for testnets :

Once the Message/Request is passed on to the destination chain’s smart contract, the execute function will get called and the NFT will get minted on user’s destination chain wallet address (which is provided when the crossChainTransfer function is called ).

When the function is successfully executed we can see it’s status on Axelar Explorer :

Receiving the NFT on Avalanche Fuji

  1. Switch MetaMask to the Avalanche Fuji network

  2. The NFT should automatically be minted to your address once the cross-chain message is processed

  3. Verify NFT ownership by calling ownerOf with the same tokenId

Here, we have transferred NFT with token ID 2 from Ethereum Sepolia to Avalanche Fuji testnet .

7. Tracking Cross-Chain Transactions

Monitor your cross-chain transfer using the Axelar Explorer:

  1. Visit https://testnet.axelarscan.io/

  2. Enter your transaction hash or address in the search bar

  3. View the detailed status of your cross-chain transfer

8. Testing and Verification

Ensure correct functionality:

  1. Check NFT ownership on both chains using ownerOf

  2. Verify the token URI using tokenURI

  3. Transfer the NFT back to Ethereum Sepolia

  4. Test error cases:

    • Transferring an NFT you don't own

    • Setting trusted remotes from a non-owner account

    • Pausing the contract and attempting a transfer

9. Conclusion and Next Steps

Tadaa! You have successfully created and transferred a cross-chain NFT using Axelar. This implementation allows for seamless NFT movement between Ethereum and Avalanche.

Consider these next steps:

  1. Implement batch transfers or metadata updates

  2. Explore integration with other Axelar-supported chains

  3. Develop a frontend interface for easier interaction

  4. Optimize gas usage for lower transaction costs

Remember, the code provided in this tutorial is an example implementation and should not be used in a production environment without thorough testing and a professional audit. Secure coding practices and rigorous testing are essential when working with cross-chain applications and smart contracts.

This tutorial has laid the foundation for creating cross-chain NFTs with Axelar. Expand on this concept to explore more advanced features and use cases in the cross-chain NFT ecosystem. The possibilities are endless, and the future of interoperable NFTs is bright!

Thank you for reading the blog . Meet you next time with some other blog. Do give me a follow on twitter @0xvarad and share this article with your friends . 👋👋

0
Subscribe to my newsletter

Read articles from Varad directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Varad
Varad

I am a full stack blockchain developer having expertise in DeFi