ERC-721 NFT Token Standard
This guide explains the ERC-721 non-fungible token (NFT) standard, how to create your own NFT collection on Ettios, and best practices for implementation.
Prerequisites
Before you begin, make sure you have:
- Basic knowledge of Solidity programming
- A development environment set up (see our Development Environment guide)
- Some ETTIA for deploying contracts
- Basic understanding of NFT concepts
What are NFTs?
Non-Fungible Tokens (NFTs) are unique digital assets on the blockchain. Unlike fungible tokens (such as ERC-20 tokens) where each token is identical to every other token, each NFT has unique properties and is not interchangeable with other tokens.
Common NFT Use Cases
- Digital art and collectibles
- Gaming items and virtual real estate
- Event tickets and memberships
- Domain names
- Certificates of authenticity
- Identity and credential verification
ERC-721 Interface
The ERC-721 standard defines a set of functions and events for non-fungible tokens:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC721 {
// Core functions
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address operator);
function setApprovalForAll(address operator, bool _approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
// Events
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// Optional extension: metadata
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 tokenId) external view returns (string memory);
}Key Functions
balanceOf(address owner)
Returns the number of NFTs owned by a specific address.
ownerOf(uint256 tokenId)
Returns the address that owns a specific token ID.
transferFrom(address from, address to, uint256 tokenId)
Transfers ownership of an NFT from one address to another address.
tokenURI(uint256 tokenId)
Returns a URL or other identifier that points to off-chain metadata about the NFT. This is where the actual content, image, and attributes are stored.
Creating an NFT Collection
Let's create a simple NFT collection using OpenZeppelin's contracts. First, install the OpenZeppelin Contracts library:
npm install @openzeppelin/contractsNow, create a file called MyNFTCollection.sol with the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFTCollection is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// Maximum supply (optional)
uint256 public constant MAX_SUPPLY = 10000;
// Base URI for metadata
string private _baseTokenURI;
constructor(string memory name, string memory symbol, string memory baseTokenURI)
ERC721(name, symbol)
Ownable(msg.sender)
{
_baseTokenURI = baseTokenURI;
}
// Function to mint new NFTs
function mintNFT(address recipient, string memory tokenURI) public onlyOwner returns (uint256) {
// Ensure we don't exceed max supply
require(_tokenIds.current() < MAX_SUPPLY, "Max supply reached");
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
// Function to update base URI (optional)
function setBaseURI(string memory baseTokenURI) public onlyOwner {
_baseTokenURI = baseTokenURI;
}
function _baseURI() internal view virtual override returns (string memory) {
return _baseTokenURI;
}
}Understanding the NFT Collection
ERC721URIStorage
This extension adds storage for token URIs, which point to the metadata for each NFT. The metadata typically includes the image URL, name, description, and attributes of the NFT.
Token IDs
Each NFT has a unique ID (tokenId). In our example, we're using a counter to generate sequential IDs, but you could implement custom ID generation logic.
Metadata URI
The tokenURI for each NFT typically points to a JSON file with the following structure:
{
"name": "NFT Name",
"description": "Description of the NFT",
"image": "https://example.com/image.png",
"attributes": [
{ "trait_type": "Color", "value": "Blue" },
{ "trait_type": "Size", "value": "Large" }
]
}Metadata Storage Options
NFT metadata (including images) can be stored in different ways:
IPFS (Recommended)
InterPlanetary File System is a decentralized storage solution ideal for NFT data. Content on IPFS is addressed by its content hash, ensuring data integrity.
ipfs://QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX/1.jsonTools: NFT.Storage, Pinata
Arweave
Arweave provides permanent storage for a one-time fee, making it suitable for long-term NFT data persistence.
ar://AcuH5BdC_ib67Z2LQz2h2sptto5XmmPvYdCvLLDCGsA/1.jsonService: Arweave
Important Note on Metadata
Avoid storing metadata on centralized servers that may go offline in the future. NFTs are meant to be permanent, so their metadata should be stored in a permanent, decentralized way.
Deploying Your NFT Collection
Here's an example of deploying your NFT collection using Hardhat:
const hre = require("hardhat");
async function main() {
const MyNFTCollection = await hre.ethers.getContractFactory("MyNFTCollection");
// Deploy with collection name, symbol, and base URI
const myNFT = await MyNFTCollection.deploy(
"My Amazing NFTs",
"MANFT",
"ipfs://QmYourBaseURIHash/" // Replace with your actual base URI
);
await myNFT.deployed();
console.log("MyNFTCollection deployed to:", myNFT.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});To deploy to the Ettios mainnet:
npx hardhat run scripts/deploy-nft.js --network ettiosMainnetMinting NFTs
After deploying your collection, you can mint new NFTs:
const hre = require("hardhat");
async function main() {
// Get contract instance
const nft = await hre.ethers.getContractAt(
"MyNFTCollection",
"YOUR_DEPLOYED_CONTRACT_ADDRESS"
);
// Mint a new NFT
// recipientAddress: The address that will receive the NFT
// tokenURI: Metadata URI for this specific NFT (e.g., "ipfs://QmHash/1.json")
const tx = await nft.mintNFT(
"0xRecipientAddress",
"ipfs://QmYourTokenURIHash/1.json"
);
// Wait for the transaction to be confirmed
await tx.wait();
console.log("NFT minted successfully!");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});Advanced Features
Lazy Minting
Lazy minting allows NFTs to be created without paying the gas fees until the moment of purchase or transfer, making it more cost-effective for creators.
Implementation involves digital signatures and custom transfer logic. See our Lazy Minting guide for more details.
Royalties (ERC-2981)
Implement the ERC-2981 standard to receive royalties from secondary sales of your NFTs on marketplaces that support this standard.
// Add this to your contract imports
import "@openzeppelin/contracts/token/common/ERC2981.sol";
// And implement the interface
contract MyNFTCollection is ERC721URIStorage, ERC2981, Ownable {
// ...
// Set default royalty
constructor(...) {
_setDefaultRoyalty(msg.sender, 500); // 5% royalty
}
// Important: Override supportsInterface for compatibility
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC2981)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}On-chain Metadata
Instead of storing metadata off-chain, you can generate and store it entirely on-chain, ensuring that the complete NFT exists on the blockchain.
This approach is more gas-intensive but provides greater permanence.
Next Steps
Now that you know how to create NFTs, consider exploring: