NFT and Digital Asset Development
At first glance, NFT contracts look simple: ERC-721, mint(), IPFS for metadata, done. In practice, most problems hide in this "simplicity" — from bots buying entire mint in the first block, to broken royalties on secondary market.
ERC-721 vs ERC-1155: When to Use What
ERC-721 is unique tokens, one owner each. Fits collections where each NFT has individual attributes with direct owner → tokenId binding.
ERC-1155 is multi-token standard: one contract stores both fungible and non-fungible tokens. balanceOf(address, tokenId) instead of ownerOf(tokenId). One transaction can transfer multiple different tokens via safeBatchTransferFrom. This saves gas for mass operations — critical for game items, tickets, edition collections.
Concrete case: game project with 50 item types, each with 10,000 edition. ERC-721 means 500,000 unique tokens, massive mapping overhead. ERC-1155 means 50 tokenIds, balanceOf per player. Transfer gas is 2–3x lower, contract deployment cheaper.
Metadata: On-chain vs IPFS vs Centralized
Standard approach — tokenURI() returns link to JSON with name, description, image, attributes. Three storage options:
Centralized server — cheapest and most flexible. Risk: server goes down, company shuts down — NFT loses metadata. Not suitable for collections claiming long-term value.
IPFS + Pinning — content-addressable storage, link bound to content hash. Pinata or NFT.Storage handle pinning. Important: IPFS doesn't guarantee availability itself — needs active pinning service. If pinning service shuts down, data can disappear if nobody stores a copy.
On-chain metadata — base64-encoded SVG or JSON directly in tokenURI. Maximum reliability, but expensive. Fits generative art projects where visual is generated from on-chain attributes (Nouns, Loot).
For most collections: IPFS with Pinata for images + on-chain attributes for traits — good balance.
Dynamic NFT: Metadata That Changes
Dynamic NFT updates metadata in response to external events — match results, character level, real data via Chainlink. Architecturally it's: smart contract stores state → tokenURI() generates metadata from state on-chain.
Caching problem: OpenSea and other marketplaces aggressively cache metadata. Standard invalidation mechanism — MetadataUpdate(tokenId) event from ERC-4906. OpenSea listens and clears cache. Without it, updated metadata may not display for weeks.
Chainlink Automation (formerly Keepers) for automatic state updates on contract by schedule or condition — standard solution.
Royalties: Real Market State
ERC-2981 is the on-chain royalties standard. Contract returns (recipient, amount) for any sale price via royaltyInfo(tokenId, salePrice). Marketplaces query this on each sale.
Problem: royalty compliance is voluntary marketplace decision. Blur launched in 2022 with zero royalties as competitive advantage, triggering wave of other platforms. Now partly stabilized: OpenSea supports ERC-2981, Blur added optional royalties.
Attempts to enforce royalties on-chain via restricting transfers only to approved marketplaces (operator filtering) that OpenSea proposed via OperatorFilterRegistry breaks composability — can't transfer NFT through custom contract. Most serious projects abandoned this approach.
For projects where royalties are critical: custom marketplace within ecosystem + incentive structure for users to trade there.
Mint Mechanics and Bot Protection
Allowlist via Merkle tree — standard. Address list hashed into merkle root, stored in contract. On mint, user provides merkle proof — contract verifies without storing full list. OpenZeppelin MerkleProof library.
Reveal mechanics — placeholder issued at mint, actual traits revealed after sale ends. Otherwise, bots scan pending transactions and snipe rare traits via frontrunning. But reveal requires commitment scheme — random seed must be fixed before mint or use Chainlink VRF.
Chainlink VRF for honest trait randomization. VRF request at mint → callback with verifiable random number → assign traits. Adds ~2 transactions and latency, but guarantees honesty.
Rate limiting — require(mintedPerWallet[msg.sender] < maxPerWallet). Doesn't prevent multi-wallet, but raises attack cost.
Lazy Minting and Gas-Free Mint
Gas-free mint through signature: creator signs voucher (tokenId, tokenURI, price, signature), buyer provides voucher in mint() — contract verifies signature via ECDSA.recover() and mints. Works on OpenSea via their Seaport protocol.
Seaport is optimized marketplace contract with minimal gas usage. Understanding its mechanics is important for custom marketplace logic integration.
NFT Project Stack
Contracts: Solidity 0.8.x, OpenZeppelin ERC721Enumerable or ERC721A (Azuki) for gas-optimized batch mint, ERC1155 from OpenZeppelin
VRF and automation: Chainlink VRF v2.5, Chainlink Automation
Storage: Pinata (IPFS pinning), NFT.Storage, Arweave for permanent storage
Marketplace: OpenSea Seaport protocol, custom integration
Frontend: wagmi v2 + viem, RainbowKit for wallet connection, React + TypeScript
Development Process
Mint mechanic design — allowlist, public sale, price curve (Dutch auction or fixed), per-wallet limits.
Contracts — with Foundry fuzz tests on mint limits, merkle proof verification, royalty calculations.
IPFS deployment — upload metadata and images before reveal, pin on at least two services.
Reveal — if using Chainlink VRF, testnet test is mandatory: VRF subscription must be funded with LINK tokens.
Marketplace integration — verify collection on OpenSea, configure royalties, test MetadataUpdate events.
Timelines
- Basic ERC-721 without reveal: 2–3 weeks
- NFT collection with allowlist, reveal, VRF: 4–7 weeks
- ERC-1155 with marketplace and royalties: 5–8 weeks
- Dynamic NFT with external data: 6–10 weeks







