ERC-721 Token (NFT) Development
ERC-721 is standard everyone knows, but few write correctly. Typical story: project deploys collection, month later discovers royalties not paid on OpenSea (because EIP-2981 not implemented), metadata loads from centralized server (which crashed), and mint via _safeMint instead of _mint allows re-entrancy through onERC721Received in custom receiver contracts. Correct ERC-721 is not just interface compliance, it's understanding how marketplaces, wallets, and aggregators interact with contract.
What Must Be in Contract
Base Implementation via OpenZeppelin
Starting point — ERC721 from OpenZeppelin 5.x. Don't write standard from scratch: OZ passed dozens of audits, any custom implementation adds risk without obvious benefit. Extend via inheritance:
contract MyNFT is ERC721, ERC721Enumerable, ERC721URIStorage, ERC2981, Ownable {
uint256 private _nextTokenId;
uint256 public constant MAX_SUPPLY = 10000;
constructor(address initialOwner)
ERC721("My Collection", "MYC")
Ownable(initialOwner) {}
}
ERC721Enumerable needed if contract should return list of tokens by owner (tokenOfOwnerByIndex). Adds ~20K gas to each transfer due to additional storage operations. If marketplace doesn't require — better skip it and read data via The Graph.
ERC721URIStorage allows storing separate URI for each token. Alternative — baseURI + tokenId pattern, where all tokens use single base path. Second option cheaper on gas during mint.
EIP-2981: Royalty at Contract Level
// Set royalty via ERC2981
_setDefaultRoyalty(royaltyReceiver, royaltyBps); // bps: 500 = 5%
OpenSea and most modern marketplaces read royaltyInfo() from EIP-2981. Old marketplaces used off-chain config via Operator Filter Registry — this approach outdated. EIP-2981 implementation is minimum standard for any collection 2024+.
Important: royaltyBps not enforced on-chain — it's informational standard. Marketplace can ignore it. For enforced royalty need custom transfer hooks (EIP-2981 + transfer restrictions via operator whitelist).
Metadata and Storage
Token URI returns JSON with name, description, image, attributes fields. Where to store:
IPFS — decentralized, cheap, but no accessibility guarantee without pinning. Use Pinata or nft.storage for pinning. CID fixed in contract, metadata doesn't change.
Arweave — pay-once-store-forever. One payment on upload, data stored permanently (~200 years per protocol estimates). More expensive than IPFS upfront, but more reliable for long-term collections.
On-chain — metadata in base64 directly in tokenURI(). Fully decentralized, but expensive on gas during mint. Suitable for generative art with small collections (< 1000 tokens).
Centralized server only for pre-reveal phase. After reveal URI should switch to IPFS. Implement via revealed flag and two baseURIs.
Mint Gas Optimization
Standard _safeMint more expensive than _mint due to IERC721Receiver check on contract addresses. If mint intended only for EOA — use _mint. If need support for contract wallets (multisig) — _safeMint with explicit reentrancy guard.
For batch mint use ERC-721A (Azuki) instead of standard OZ ERC-721. ERC-721A stores owner data only on first batch mint, subsequent tokens deduced — saves up to 70% gas on minting 10+ tokens. Trade-off: transferring first token in batch slightly more expensive due to lazy initialization.
Workflow
Analysis (0.5-1 day). Supply, mint mechanics (public/whitelist/free), royalty, need Enumerable and URIStorage, deployment chain.
Development (1-2 days). Contract + Foundry tests + deployment script with verification.
Deployment. Testnet (Sepolia), then mainnet. Etherscan verification automatic via Foundry.
Basic ERC-721 with royalty and IPFS metadata — 2-3 days. With whitelist (Merkle proof), reveal mechanics and custom mint site — 5-7 days. Cost calculated individually.







