Creating a Cross-Chain NFT with Axelar: A Step-by-Step Guide ✏️
Table of contents
- 1. Introduction to Axelar and Cross-Chain Architecture
- 2. Understanding the Burn-and-Mint Mechanism
- 3. Setting Up the Development Environment
- 4. Implementing the Cross-Chain NFT Smart Contract
- 5. Deploying the Smart Contract
- 6. Minting and Transferring NFTs Across Chains
- 7. Tracking Cross-Chain Transactions
- 8. Testing and Verification
- 9. Conclusion and Next Steps
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:
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.
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.
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.
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
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.
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.
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:
NFT owner initiates a transfer on the source chain
NFT is burned on the source chain
Axelar relays a message to the destination chain
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:
Open Remix IDE: https://remix.ethereum.org/
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:
In Remix, go to the "Deploy & Run Transactions" tab
Select "Injected Web3" as the environment (ensure MetaMask is connected)
Choose the appropriate network in MetaMask
Compile the contract and deploy with these parameters:
gateway_
: Axelar Gateway addressgasService_
: 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)
Connect MetaMask to Ethereum Sepolia
In Remix, connect to the deployed contract on Sepolia
Call the
mint
function:to
: Your wallet addresstokenURI
: IPFS URI of your NFT metadata (e.g., "ipfs://Qm...")
Confirm the transaction and wait for it to be mined
Initiating Cross-Chain Transfer (from Ethereum Sepolia to Avalanche Fuji)
Ensure you're connected to the Sepolia network
Call
setTrustedRemote
(if not already set):chain
: "Avalanche"remoteAddress
: Address of the deployed contract on Avalanche Fuji
Call
setTrustedRemote
(on the avalanche deployed NFT contract):chain
: "ethereum-sepolia"remoteAddress
: Address of the deployed contract on Ethereum Sepolia
Call
crossChainTransfer
:destinationChain
: "Avalanche"destinationAddress
: Your wallet address on AvalanchetokenId
: The ID of the NFT to transfer
Include ETH to cover gas fees on the destination chain (Add 10e8 Gwei i.e. 0.1 ETH for example in the VALUE field )
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
Switch MetaMask to the Avalanche Fuji network
The NFT should automatically be minted to your address once the cross-chain message is processed
Verify NFT ownership by calling
ownerOf
with the sametokenId
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:
Enter your transaction hash or address in the search bar
View the detailed status of your cross-chain transfer
8. Testing and Verification
Ensure correct functionality:
Check NFT ownership on both chains using
ownerOf
Verify the token URI using
tokenURI
Transfer the NFT back to Ethereum Sepolia
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:
Implement batch transfers or metadata updates
Explore integration with other Axelar-supported chains
Develop a frontend interface for easier interaction
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 . 👋👋
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