Deploying an ERC-721 NFT on Rootstock Testnet: A Complete Guide for Developers

Aapsi KhairaAapsi Khaira
8 min read

In this tutorial, we will walk through how to set up, develop, and deploy an ERC-721 Non-Fungible Token (NFT) on the Rootstock network using the Foundry development framework. Whether you're a beginner or an experienced developer, this guide will help you get started with smart contract development and NFT minting on Rootstock.

Prerequisites
Before you begin, ensure you have the following installed:

  • Foundry: A fast, portable, and modular toolkit for Ethereum application development.

  • OpenZeppelin Contracts: A library for secure smart contract development.

  • IPFS: A decentralized file system for hosting NFT metadata.

Project Overview

In this project, you'll implement an ERC-721 compliant NFT with the following features:

  • Custom Token Name and Symbol: Define a unique name and symbol for your token.

  • Ownership Control: Restrict certain functions to the contract owner.

  • Sequential Token Minting: Mint tokens in a sequential order.

Fixed Metadata URI for All Tokens: Assign a uniform metadata URI to all tokens.

Steps

1. Install Foundry

Start by installing Foundry with the following commands:

curl -L https://foundry.paradigm.xyz | bash
foundryup

2. Create a New Project

forge init Rootstock-NFT
cd Rootstock-NFT

3. Install Dependencies

Install the OpenZeppelin contracts library which provides ERC-721 implementation:

forge install OpenZeppelin/openzeppelin-contracts

Add the dependency to your remappings file:

echo "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" >> remappings.txt

4. Create the NFT Contract

Create the MyNFT.sol file in the src directory:

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract MyNFT is ERC721, ERC721URIStorage, Ownable {
    string private _tokenUri;
    uint256 private _currentTokenId;

    /**
     * @notice Constructor to create a new NFT
     * @param initialOwner  The address of the owner of the contract
     * @param tokenUri  The URI of the token
     */
    constructor(address initialOwner, string memory tokenUri)
        ERC721("MyNFT", "MTK")
        Ownable(initialOwner)
    {
        _tokenUri = tokenUri;
        _currentTokenId = 0;
    }

    /**
     * @notice Function to mint a new NFT
     * @param to  The address of the owner of the NFT
     * @return bool  Returns true if the minting is successful
     * @dev This function is only accessible by the owner of the contract
     */
    function safeMint(address to) public onlyOwner returns (bool) {
        _safeMint(to, _currentTokenId);
        _setTokenURI(_currentTokenId, _tokenUri);
        _currentTokenId++;
        return true;
    }

    /**
     * @notice Function to get the current token ID
     * @return uint256  Returns the current token ID
     * @dev This function is only accessible by the owner of the contract
     */
    function currentTokenId() public view returns (uint256) {
        return _currentTokenId;
    }

    // The following functions are overrides required by Solidity.
    /**
     * @notice Function to get the URI of the token
     * @param tokenId The ID of the token
     * @return string  Returns the URI of the token
     * @dev This function is an override of the tokenURI function in the ERC721URIStorage contract
     */
    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    /**
     * @notice Function to check if the contract supports an interface
     * @param interfaceId The ID of the interface
     * @return bool  Returns true if the contract supports the interface
     * @dev This function is an override of the supportsInterface function in the ERC721URIStorage contract
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

5. Hosting NFT Metadata on IPFS

To add metadata to your NFT, create a metadata JSON file and host it on IPFS (or a centralized server).

  1. **Download and Install IPFS
    **If you haven't already, install IPFS on your system by following the official IPFS documentation.

  2. Upload Your NFT Image to IPFS

    • Open IPFS and navigate to the Files section.

    • Click Import, then select the image file you want to use as your NFT.

    • Once the upload is complete, click Share Link and copy the URL of the uploaded image.

  3. Create a Metadata JSON File

    • Create a new JSON file and structure it as follows, replacing the "image" field with the IPFS link to your uploaded image:

        { 
           "name": "Test NFT", 
           "description": "This is a description of my NFT", 
           "image": "https://bafybeiaoxwueucnjivxxdd4nli6mwie3sx7xvob42ddy7bshd45jtbveli.ipfs.dweb.link?filename=6c00716e850e85b3035546eec3b07bdc.jpg"
        }
      
  4. Upload the JSON File to IPFS

    • Go back to IPFS and upload the newly created JSON file.

    • Copy the IPFS link to this file once the upload is complete.

  5. Use the Metadata URI for Your NFT

    • The copied IPFS link to the JSON file will serve as the metadata URI for your NFT when minting or interacting with smart contracts.

6. Create a Deployment Script

Create the MyNFT.s.sol file in script folder:

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

import {Script, console} from "forge-std/Script.sol";
import {MyNFT} from "../src/MyNFT.sol";

contract MyNFTScript is Script {
    MyNFT public NFT;

    function setUp() public {}

    function run() public {
        vm.startBroadcast();

        NFT = new MyNFT(address(0x..admin-wallet-address), "https://bafybeierp7noqo7ejmdqhimsu6hd6fa5migukvktzwhpovfqlfu7k3xrue.ipfs.dweb.link/?filename=uri.json");
        // Replace with your actual admin wallet address and ipfs url
        vm.stopBroadcast();
    }
}

7. Write Tests

Create the MyNFT.t.sol file in test folder:

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

import {Test, console} from "forge-std/Test.sol";
import {MyNFT} from "../src/MyNFT.sol";

contract MyNFTTest is Test {
    MyNFT public nft;

    function setUp() public {
        nft = new MyNFT(address(123), "uri.json");
    }

    /**
     * @notice Function to test the safeMint function
     * @dev This function tests the safeMint function of the MyNFT contract
     */
    function testSafeMint() public {
        vm.startPrank(address(123));
        nft.safeMint(address(123));
        assertEq(nft.ownerOf(0), address(123));
    }

    /**
     * @notice Function to test the tokenURI function
     * @dev This function tests the tokenURI function of the MyNFT contract
     */
    function testTokenURI() public {
        vm.startPrank(address(123));
        nft.safeMint(address(123));
        assertEq(nft.tokenURI(0), "uri.json");
    }

    /**
     * @notice Function to test the supportsInterface function
     * @dev This function tests the supportsInterface function of the MyNFT contract
     */
    function testSupportsInterface() public view {
        assert(nft.supportsInterface(0x80ac58cd));
    }

    /**
     * @notice Function to test the owner function
     * @dev This function tests the owner function of the MyNFT contract
     */
    function testOwner() public view {
        assertEq(nft.owner(), address(123));
    }


    /**
     * @notice Function to test the currentTokenId function
     * @dev This function tests the currentTokenId function of the MyNFT contract
     */
    function testCurrentTokenId() public view {
        assertEq(nft.currentTokenId(), 0);
    }

    /**
     * @notice Function to test the safeMint function
     * @dev This function tests the safeMint function of the MyNFT contract
     * This function tests the fail safeMint function of the MyNFT contract, where the owner of the contract is not the caller of the function
     */
    function testFailSafeMint() public {
        vm.startPrank(address(123));
        assertEq(nft.safeMint(address(234)), true, "Test1");
        vm.startPrank(address(234));
        assertEq(nft.safeMint(address(234)), false, "Test2");
        nft.safeMint(address(456));
    }
}

8. Add the below to the foundry.toml file

Info : To avoid Foundry's compatibility issues, we are using the evm version: london

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.20"
evm_version = "london"

9. Build and Test

Run the following commands to build and test your NFT contract:

# Build the project
forge build
# Run tests
forge test -vvv

The -vvv flag provides verbose output, showing details of each test.

10. Setting Up for Rootstock Deployment

Create .env file for storing the private key and rpc url

ROOTSTOCK_RPC_URL=https://public-node.testnet.rsk.co
PRIVATE_KEY=0x.....

Important: Never commit this file to version control. Add it to your .gitignore.

Load environment variables

source .env

11. Deploy to Rootstock Testnet

NOTE: To deploy the ERC721 contract and send transactions you will require some Test RBTC. Visit the Rootstock faucet to claim some test RBTC.

# Deploy to Rootstock testnet
forge script script/MyNFT.s.sol --rpc-url $ROOTSTOCK_RPC_URL --private-key $PRIVATE_KEY --legacy --broadcast

Info : EIP-1559 is not supported or not activated on the Rootstock RPC url.The --legacy flag is passed to use legacy transactions instead of EIP-1559.

You will get a similar output:

12. Interacting with Your NFT

**Using Cast (CLI)
**To interact with your deployed NFT contract, you can use the Cast CLI tool. Below are the steps to check ownership and mint an NFT to a recipient.

Check the Owner of the NFT Contract

This command will verify the current owner of the NFT contract. It should return the address you provided as the constructor argument during deployment:

# Check the current owner of the NFT contract
cast call <contract-address> "owner()(address)" --rpc-url $ROOTSTOCK_RPC_URL

Dry Run: Mint an NFT to a Recipient
Perform a dry run to simulate the minting process without actually sending a transaction. This step helps ensure there are no errors before you mint the NFT:

# Simulate minting an NFT to the recipient address
# The command will return true if minting is successful (this is the return value of the safeMint function)
cast call <contract-address> "safeMint(address)(bool)" <recipient-address> --from <nft-contract-owner> --rpc-url $ROOTSTOCK_RPC_URL

13. Mint NFT to the recipient

cast send <contract-address> "safeMint(address)" <recipient-address> --private-key $PRIVATE_KEY --rpc-url $ROOTSTOCK_RPC_URL --legacy

Once the transaction is mined you will see something similar to this:

14. Check your transactions on Rootstock Testnet Explorer

URL : Rootstock testnet Explorer

  • To view your deployed contract, simply paste your contract address into the search bar.

  • You can also check the transaction history for any minted NFTs in a similar manner.

Wrapping Up

Congratulations! 🎉 You've successfully built, tested, and deployed an ERC-721 NFT smart contract on the Rootstock testnet using Foundry.

In this tutorial, you learned how to:

  • Set up a Foundry-based development environment

  • Implement an ERC-721 NFT smart contract with ownership control and uniform metadata

  • Host NFT metadata on IPFS

  • Write unit tests using Forge’s testing framework

  • Deploy your contract to Rootstock and interact with it via Cast CLI

If you encounter any errors, feel free to join the Rootstock Discord and ask for help in the appropriate channel.

To dive deeper into Rootstock, explore the official documentation.

0
Subscribe to my newsletter

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

Written by

Aapsi Khaira
Aapsi Khaira

I am a Blockchain Developer with a core focus on Smart Contract development. My expertise is centered on building secure, efficient, and upgradeable smart contracts using Solidity, Hardhat, and Foundry. With hands-on experience in decentralized finance (DeFi) and tokenization, I have developed a range of blockchain solutions, including staking contracts, onchain games, RWA Tokenization, and NFT marketplaces. Currently, I am heavily involved in Real World Asset (RWA) tokenization and exploring advanced cryptographic techniques like Partially Homomorphic Encryption (PHE) and Fully Homomorphic Encryption( TFHE) to enhance data privacy in smart contracts. My development process prioritizes gas optimization, ensuring transactions are cost-effective and scalable. Additionally, I specialize in integrating smart contracts with decentralized applications (dApps) using ethers.js and have a strong track record of performing thorough security audits to ensure the integrity of blockchain protocols. I thrive in the evolving blockchain ecosystem, constantly refining my skills and contributing to the development of decentralized, transparent, and secure solutions for the future.