ADR: NFT Diamond Standard (ERC-2535) on Arbitrum
Date: 2026-02-06 Status: Proposed Supersedes: ADR-019 (NFT Minting Infrastructure Integration - 2025-11-09) Decision Makers: Architecture Team Deadline: End of next week (~8 working days)
Executive Summary
This ADR proposes building NFT collection support for Emerge using the ERC-2535 Diamond Standard on Arbitrum, replacing the previously planned ERC721A + ERC1167 minimal proxy approach. The system enables creators to deploy NFT collections backed by AI-generated content, with a Gemini LLM-powered collection setup flow.
Key Changes from ADR-019:
- Contract Pattern: ERC-2535 Diamond (multi-facet proxy) instead of ERC721A + ERC1167 minimal proxies
- Tooling: Hardhat 3 (beta) instead of Hardhat 2
- Chain: Arbitrum (grant-funded)
- Collection Setup: Gemini LLM generates EmProps instruction sets (no job queue)
- No Ponder for v1: Custodial
mintTomeans we track all mints server-side - Metadata: GCS + CDN (not IPFS) -
tokenURI/{tokenId}pattern with metadata JSON pointing to GCS assets
Context
Why Diamond Standard (ERC-2535)?
The Diamond pattern provides advantages over the previously planned ERC1167 minimal proxies:
| Aspect | ERC1167 Minimal Proxy | ERC-2535 Diamond |
|---|---|---|
| Upgradeability | None per collection | Individual facets upgradeable per collection |
| Feature composition | Fixed at deploy | Add/remove capabilities post-deployment |
| Shared logic | Shared via delegatecall | Shared via facets with selector routing |
| Storage | Proxy's own storage | Diamond storage pattern (namespaced slots) |
| Introspection | None | DiamondLoupe + ERC-165 |
| Future extensibility | Deploy new implementation | Add new facet to existing collections |
The Diamond pattern is particularly valuable for Emerge because:
- Collections can evolve post-deployment (add auction mechanics, new royalty schemes, etc.)
- Facets are deployed once and shared across all collections (gas efficient)
- Individual collections can have different capability sets
- Bug fixes can be applied to existing collections without redeployment
Why Hardhat 3?
- Declared production-ready (beta since Aug 2025, no major breaking API changes expected)
- 2-10x faster tests via Rust-based EDR
- Declarative config (cleaner than Hardhat 2's side-effect imports)
- Native multichain support (relevant for Arbitrum)
- Built-in Solidity tests (Foundry-compatible
.t.solfiles)
Why Skip Ponder for v1?
All minting is custodial via mintTo - the platform API has full knowledge of every mint. Secondary market tracking is out of scope (just link to OpenSea). Ponder adds infrastructure complexity that isn't needed when we control all write operations.
Decision
Architecture Overview
┌──────────────────────────────────┐
│ EmProps Studio (FE dev) │
│ Collection UI + Wallet Connect │
└───────────────┬──────────────────┘
│ API calls
▼
┌───────────────────────────────────────────────────────────────┐
│ EmProps API (emprops-api) │
│ │
│ POST /nft/collections/setup → Gemini LLM → instruction │
│ POST /nft/collections/:id/deploy → Deploy Diamond on Arb │
│ POST /nft/collections/:id/generate → Generate token artwork │
│ POST /nft/collections/:id/mint → mintTo on Diamond │
│ GET /nft/collections/:id → Collection + token data │
│ GET /nft/collections/:id/tokens → Token list with metadata │
└───────────┬───────────────────┬───────────────────┬───────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Gemini LLM │ │ GCS + CDN │ │ Arbitrum Network │
│ (direct) │ │ (metadata) │ │ (Diamond proxy) │
└──────────────┘ └──────────────┘ └──────────────────┘Smart Contract Architecture (ERC-2535)
DiamondFactory (Deployer)
│
│ createCollection(name, symbol, baseURI, maxSupply, owner)
│ → deploys new Diamond proxy
│ → configures facet selectors
│ → emits CollectionCreated event
│
├── Diamond Instance A (Collection "Cosmic Dreams")
│ ├── DiamondCutFacet → manage facets
│ ├── DiamondLoupeFacet → introspection
│ ├── ERC721AFacet → core NFT (transfer, approve, balance)
│ ├── ERC721MetadataFacet → name, symbol, tokenURI(baseURI + tokenId)
│ ├── MintFacet → mintTo(address, qty), maxSupply enforcement
│ ├── RoyaltyFacet → ERC-2981 royalty info
│ └── OwnershipFacet → Ownable, access control
│
├── Diamond Instance B (Collection "Neon Horizons")
│ └── (same shared facets, own storage)
│
└── Diamond Instance N ...Shared Facets (deployed once):
| Facet | Purpose | Key Functions |
|---|---|---|
| DiamondCutFacet | Add/replace/remove facets | diamondCut() |
| DiamondLoupeFacet | Introspection | facets(), facetAddress() |
| ERC721AFacet | Gas-optimized NFT core | transferFrom(), balanceOf(), ownerOf() |
| ERC721MetadataFacet | Token metadata | name(), symbol(), tokenURI() |
| MintFacet | Custodial minting | mintTo(address, uint256), totalSupply(), maxSupply() |
| RoyaltyFacet | ERC-2981 royalties | royaltyInfo(), setDefaultRoyalty() |
| OwnershipFacet | Access control | owner(), transferOwnership() |
Metadata Pattern
Token URI: https://cdn.emerge.pizza/nft-metadata/{collection_id}/{tokenId}
metadata.json at that URL:
{
"name": "Cosmic Dreams #1",
"description": "AI-generated artwork from Emerge",
"image": "https://cdn.emerge.pizza/generations/{collection_id}/1.png",
"external_url": "https://emerge.pizza/nft/{collection_id}/1",
"attributes": [
{ "trait_type": "Generator", "value": "ComfyUI" },
{ "trait_type": "Collection", "value": "Cosmic Dreams" }
]
}tokenURI(tokenId)on-chain returnsbaseURI + tokenId- Metadata JSON served from GCS via CDN
- Image/video assets stored in existing GCS buckets
- Metadata is updatable (just overwrite the JSON file) - useful for reveal mechanics
Collection Setup Flow (Gemini LLM)
POST /nft/collections/setup
{
"prompt": "I want to create a collection of cosmic dreamscapes",
"style_preferences": "vibrant, surreal"
}
→ Calls Gemini LLM with system prompt
→ Returns EmProps v2 instruction set:
{
"version": "v2",
"steps": [
{
"id": 1,
"nodeName": "prompt",
"nodePayload": { "prompt": "cosmic dreamscape, vibrant colors..." }
},
{
"id": 2,
"nodeName": "comfyui_workflow",
"nodePayload": { "workflow": "sdxl-base-v1", "steps": 30 }
}
],
"generations": { "generations": 1, "hashes": [], "use_custom_hashes": false },
"variables": [
{
"name": "style",
"type": "pick",
"value_type": "strings",
"value": {
"display_names": ["Nebula", "Galaxy", "Aurora"],
"values": ["nebula swirl", "spiral galaxy", "aurora borealis"],
"weights": [1, 1, 1]
},
"lock_value": false,
"test_value": null
}
]
}The user then edits variables, prompts, and settings in the studio before generating.
Minting Flow
1. User triggers "Generate Token #N" in studio
2. API generates artwork via job queue (existing ComfyUI pipeline)
3. Asset stored in GCS: generations/{collection_id}/{tokenId}.png
4. Metadata JSON written to GCS: nft-metadata/{collection_id}/{tokenId}
5. API calls collection Diamond: mintTo(recipientAddress, 1)
6. Token minted on Arbitrum, tokenURI points to CDN metadata
7. DB updated: output record linked to collection + tokenDatabase Changes
Add to existing collection model:
model collection {
// ... existing fields ...
// NFT-specific
contract_address String? // Diamond proxy address on Arbitrum
base_token_uri String? // CDN base URI for metadata
max_supply Int? // On-chain max supply
tokens_minted Int @default(0)
royalty_bps Int? // Basis points (e.g., 500 = 5%)
royalty_receiver String? // Address for royalty payments
mint_status String? // "pending" | "active" | "paused" | "completed"
deploy_tx_hash String? // Deployment transaction hash
}Add "ARBITRUM" to the blockchain enum/values.
API Endpoints
All under apps/emprops-api/src/routes/nft/:
| Endpoint | Method | Purpose | Auth | Queue |
|---|---|---|---|---|
/nft/collections/setup | POST | Gemini LLM → instruction set | Yes | No (direct) |
/nft/collections/:id/deploy | POST | Deploy Diamond on Arbitrum | Yes | No (direct) |
/nft/collections/:id/generate | POST | Generate token artwork | Yes | Yes (job queue) |
/nft/collections/:id/tokens/:tokenId/mint | POST | mintTo on Diamond | Yes | No (direct) |
/nft/collections/:id/tokens | GET | List tokens with metadata | Yes | No |
/nft/collections/:id/metadata/:tokenId | GET | Get/serve token metadata | Public | No |
Monorepo Structure
emerge-turbo/
├── packages/
│ └── nft-contracts/ # NEW: Hardhat 3 + Solidity
│ ├── contracts/
│ │ ├── DiamondFactory.sol # Factory deploys Diamond collections
│ │ ├── facets/
│ │ │ ├── DiamondCutFacet.sol
│ │ │ ├── DiamondLoupeFacet.sol
│ │ │ ├── ERC721AFacet.sol
│ │ │ ├── ERC721MetadataFacet.sol
│ │ │ ├── MintFacet.sol
│ │ │ ├── RoyaltyFacet.sol
│ │ │ └── OwnershipFacet.sol
│ │ ├── libraries/
│ │ │ └── LibDiamond.sol # Diamond storage + cut logic
│ │ ├── interfaces/
│ │ │ ├── IDiamondCut.sol
│ │ │ ├── IDiamondLoupe.sol
│ │ │ └── IERC721A.sol
│ │ └── Diamond.sol # Base Diamond proxy
│ ├── test/
│ ├── scripts/
│ │ └── deploy.ts
│ ├── hardhat.config.ts
│ └── package.json
│
├── apps/
│ └── emprops-api/
│ └── src/routes/
│ └── nft/ # NEW: NFT API routes
│ ├── index.ts # Router
│ ├── setup.ts # Gemini collection setup
│ ├── deploy.ts # Diamond deployment
│ ├── generate.ts # Token artwork generation
│ ├── mint.ts # mintTo calls
│ └── metadata.ts # Metadata managementImplementation Strategy
Phase 1: Smart Contracts (Days 1-3)
- Create
packages/nft-contractswith Hardhat 3 - Implement Diamond proxy + factory
- Implement all facets (ERC721A, Metadata, Mint, Royalty, Ownership)
- Write tests
- Deploy to Arbitrum Sepolia testnet
Phase 2: API Endpoints (Days 3-6)
- Add NFT routes to
emprops-api - Implement collection setup endpoint (Gemini LLM integration)
- Implement deploy endpoint (ethers.js → DiamondFactory)
- Implement generate endpoint (leverage existing job queue)
- Implement metadata management (GCS write)
- Implement mint endpoint (ethers.js → MintFacet.mintTo)
Phase 3: Integration & Testing (Days 6-8)
- Database schema updates (add NFT fields to collection model)
- End-to-end testing (setup → deploy → generate → mint)
- Verify metadata serves correctly from CDN
- Verify tokens appear on OpenSea (Arbitrum testnet)
Consequences
Positive
- Extensible collections: Diamond pattern allows adding features to deployed collections
- Gas efficient: Shared facets mean only proxy deployment cost per collection
- Future-proof: New facet types (auction, reveal, governance) can be added without redeployment
- Clean metadata: GCS + CDN is fast, cheap, and updateable
- Quick to ship: Custodial
mintToeliminates user-facing wallet complexity
Negative
- Diamond complexity: More complex than ERC1167 proxies; more code to audit
- Hardhat 3 beta risk: Fewer community resources, potential undiscovered issues
- No on-chain indexing: Without Ponder, historical queries rely on our DB being correct
- Centralization risk: Custodial minting means platform controls all mints
Mitigations
- Use well-tested Diamond reference implementations (diamond-3-hardhat by Nick Mudge)
- Hardhat 3 declared production-ready; fallback to Hardhat 2 if blockers emerge
- Add Ponder in v2 when secondary market features are needed
- Document upgrade path to user-initiated minting in future phases
Alternatives Considered
1. Keep ERC721A + ERC1167 (Previous Plan)
- Pro: Simpler, existing code in emprops-hardhat
- Con: No per-collection upgradeability, less flexible
- Rejected: User explicitly wants Diamond pattern for extensibility
2. ERC-2535 with Hardhat 2
- Pro: Mature tooling, more community examples
- Con: Slower tests, imperative config
- Rejected: User explicitly wants Hardhat 3
3. Include Ponder in v1
- Pro: Clean on-chain data indexing from day 1
- Con: Additional infrastructure, ~3 extra days
- Rejected: All mints are custodial; we have full data in our DB. Add later.
Success Criteria
- [ ] Diamond Factory deploys collections on Arbitrum Sepolia
- [ ]
mintTosuccessfully mints tokens to arbitrary addresses - [ ]
tokenURIreturns valid metadata from CDN - [ ] Gemini LLM generates valid EmProps instruction sets
- [ ] Generate → metadata → mint pipeline works end-to-end
- [ ] Tokens visible on OpenSea (Arbitrum testnet)
- [ ] Front-end dev has working API endpoints to integrate against
Related Documentation
- Previous ADR: NFT Minting Infrastructure (superseded)
- Arbitrum Overview — Diamond Standard architecture
- ERC-2535 Specification
- Diamond Reference Implementation
