Creating an On-Chain NFT: The ERC721 Contract Breakdown
In the evolving world of blockchain, Non-Fungible Tokens (NFTs) have revolutionized digital ownership. Unlike traditional tokens, NFTs are unique and indivisible, enabling ownership of digital assets like art, music, and virtual real estate. In this article, we’ll explore how to create an on-chain NFT using the ERC721 standard on the Ethereum blockchain.
What is an On-Chain NFT?
An on-chain NFT , is an NFT that stores its metadata and artwork directly on the blockchain, ensuring permanence and immutability. This contrasts with off-chain NFTs, which store data on centralized servers or decentralized storage solutions like IPFS.
Why ERC721?
ERC721 is the standard for non-fungible tokens on Ethereum. It ensures each token is unique, verifiable, and transferable. Key features include:
Uniqueness: Each token has a unique identifier (
tokenId
).Ownership Tracking: Built-in methods for transferring and tracking ownership.
Interoperability: ERC721 is widely supported across NFT marketplaces and wallets.
Contract Overview: OnChainNFT
Our OnChainNFT contract is built using OpenZeppelin's ERC721 implementation. It generates the NFT's artwork and metadata directly on-chain using SVG and Base64 encoding.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract OnChainNFT is ERC721 {
uint256 tokenID;
constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol){
mint(msg.sender);
}
1. Constructor
The contract constructor initializes the NFT collection with a name and symbol. It also mints the first token to the contract deployer.
constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol){
mint(msg.sender);
}
_name
: Name of the NFT collection (e.g., "OnChainNFT")._symbol
: Symbol representing the collection (e.g., "OCN").
2. Mint Function
This function allows anyone to mint a new NFT. The tokenID
increments with each mint to ensure uniqueness.
function mint(address _to) public returns(uint256) {
tokenID++;
_safeMint(msg.sender, tokenID);
return tokenID;
}
Public Access: Anyone can call this function to mint a new NFT.
Unique Token ID:
tokenID
increments to ensure each NFT is unique._safeMint: A safer version of
_mint
that ensures the recipient is a contract or externally owned account (EOA).
3. Generating On-Chain SVG Artwork
The genSVG()
function creates SVG graphics directly on-chain, showcasing the power of on-chain metadata.
function genSVG() internal pure returns (string memory) {
string memory svg =
"<svg width='500' height='500' xmlns='http://www.w3.org/2000/svg'>"
"<rect width='100%' height='100%' fill='#E0E0E0'/>"
"<circle cx='250' cy='250' r='150' fill='#1E90FF'/>"
"<text x='50%' y='55%' font-family='Arial' font-size='30' fill='#FFFFFF' text-anchor='middle'>"
"onChainNFT"
"</text>"
"</svg>";
return Base64.encode(bytes(svg));
}
SVG Image: The NFT is a blue circle on a grey background with the text "onChainNFT."
Base64 Encoding: The SVG is Base64 encoded for compatibility with marketplaces like OpenSea.
4. Metadata and tokenURI
The tokenURI()
function generates the metadata, encoding it as JSON in Base64 format. This makes the NFT fully on-chain, ensuring permanence.
function tokenURI(uint256 tokenId) public view override returns (string memory) {
string memory dataURI = string(
abi.encodePacked(
'{',
'"name": "onChain NFT #', Strings.toString(tokenId), '",',
'"description": "A simple on-ChainNFT",',
'"image": "data:image/svg+xml;base64,', genSVG(), '"',
'}'
)
);
return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(dataURI))));
}
Dynamic Metadata: The
tokenId
is used in the name, making each NFT unique.On-Chain Storage: All metadata, including the SVG image, is stored directly on-chain.
Deployment and Verification Guide
This section walks through compiling, deploying, verifying, and interacting with the smart contract, including minting the NFT and viewing it on OpenSea.
1. Setting up the Development Environment
Ensure Node.js and npm are installed.
Install Hardhat using
npm install --save-dev hardhat
Set up a Hardhat project with
npx hardhat
Install necessary dependencies such as ethers.js and hardhat-etherscan.
Hardhat Configuration ( hardhat.config.ts)
Below is the required Hardhat configuration for deploying the OnChainNFT contract. It specifies the compiler version, network settings, and etherscan API key for verification.
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@nomicfoundation/hardhat-ethers";
require('dotenv').config()
const {BASE_SEPOLIA_KEY, ACCOUNT_PRIVATE_KEY, BASESCAN_KEY } = process.env;
const config: HardhatUserConfig = {
solidity: "0.8.28",
networks: {
base: {
url: BASE_SEPOLIA_KEY,
accounts: [`0x${ACCOUNT_PRIVATE_KEY}`],
chainId: 84532,
},
},
etherscan: {
apiKey: BASESCAN_KEY,
}
};
export default config;
Environment Variables (.env)
Create a .env
file in the root directory with the following content:
BASE_RPC_URL=https://rpc.url.of.base.network
PRIVATE_KEY=<Your_Wallet_Private_Key>
ETHERSCAN_API_KEY=<Your_Etherscan_API_Key>
BASE_RPC_URL
: The RPC URL of the Base network you're deploying to.PRIVATE_KEY
: The private key of the wallet used for deployment (never share this publicly).ETHERSCAN_API_KEY
: API key from the block explorer for contract verification..
2. Compiling the Contract
Compile Command
npx hardhat compile
What It Does
This command compiles the Solidity files in your project and generates TypeScript typings for better integration with TypeScript projects. In this case, it generated typings for 19 artifacts and compiled 17 Solidity files, targeting the "paris" EVM version.
3. Deploying the Contract
Deployment Script Overview
The deployment script is located in ./scripts/deployNFT.ts
. It initializes the contract using ethers.js and deploys it to the specified network.
Deployment Script
We deploy the contract using Hardhat, specifying the collection name and symbol.
import { ethers } from "hardhat";
async function main() {
const signer = await ethers.provider.getSigner();
console.log("=======Deploying contract======");
console.log("Deployer Address: ", signer.address);
const token = await ethers.deployContract("OnChainNFT", ["OnChainNFT", "OCN"]);
await token.waitForDeployment();
console.log("======Contract Deployed======");
console.log("Contract Address: ", token.target);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Deploy Command
npx hardhat run ./scripts/deployNFT.ts --network base
What It Does
Deploys the smart contract to the selected network (
base
in this case).Outputs the deployer’s address and the newly deployed contract’s address.
Example Output:
4. Verifying the Contract
Verify Command
npx hardhat verify --network base <contract_address> "OnChainNFT" "OCN"
What It Does
Verifies the contract on the block explorer, making the source code visible and the contract "Verified".
Uses the Etherscan API and requires the contract address and constructor arguments.
Viewing Verified Contract: Visit the block explorer URL given in the output, e.g.,
https://sepolia.basescan.org/address/<contract_address>#code
.
5. Interacting with the Contract
Viewing and Writing Functions
Visit the verified contract page on the block explorer.
Connect your MetaMask wallet.
Interact with view functions (e.g., tokenURI) without gas fees.
Write functions (e.g., mint) require gas fees and MetaMask confirmation.
Minting the NFT
Use the
mint
function to create a new NFT.Confirm the transaction via MetaMask.
The newly minted NFT will appear in your wallet.
6. Viewing the NFT on OpenSea
After minting, go to OpenSea and connect your MetaMask wallet.
Your NFT should appear under your profile's "Collected" section, as long as the network is supported by OpenSea.
Conclusion
This OnChainNFT contract demonstrates the power of on-chain NFTs, ensuring metadata permanence and eliminating dependencies on centralized storage. By leveraging ERC721 and on-chain SVGs, this contract paves the way for truly decentralized digital ownership.
This article shows the full cycle of building, deploying, and verifying an on-chain NFT. Feel free to experiment with the SVG design or add more features like minting limits or access controls!