Skip to content

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 mintTo means 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:

AspectERC1167 Minimal ProxyERC-2535 Diamond
UpgradeabilityNone per collectionIndividual facets upgradeable per collection
Feature compositionFixed at deployAdd/remove capabilities post-deployment
Shared logicShared via delegatecallShared via facets with selector routing
StorageProxy's own storageDiamond storage pattern (namespaced slots)
IntrospectionNoneDiamondLoupe + ERC-165
Future extensibilityDeploy new implementationAdd new facet to existing collections

The Diamond pattern is particularly valuable for Emerge because:

  1. Collections can evolve post-deployment (add auction mechanics, new royalty schemes, etc.)
  2. Facets are deployed once and shared across all collections (gas efficient)
  3. Individual collections can have different capability sets
  4. 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.sol files)

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):

FacetPurposeKey Functions
DiamondCutFacetAdd/replace/remove facetsdiamondCut()
DiamondLoupeFacetIntrospectionfacets(), facetAddress()
ERC721AFacetGas-optimized NFT coretransferFrom(), balanceOf(), ownerOf()
ERC721MetadataFacetToken metadataname(), symbol(), tokenURI()
MintFacetCustodial mintingmintTo(address, uint256), totalSupply(), maxSupply()
RoyaltyFacetERC-2981 royaltiesroyaltyInfo(), setDefaultRoyalty()
OwnershipFacetAccess controlowner(), 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 returns baseURI + 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 + token

Database Changes

Add to existing collection model:

prisma
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/:

EndpointMethodPurposeAuthQueue
/nft/collections/setupPOSTGemini LLM → instruction setYesNo (direct)
/nft/collections/:id/deployPOSTDeploy Diamond on ArbitrumYesNo (direct)
/nft/collections/:id/generatePOSTGenerate token artworkYesYes (job queue)
/nft/collections/:id/tokens/:tokenId/mintPOSTmintTo on DiamondYesNo (direct)
/nft/collections/:id/tokensGETList tokens with metadataYesNo
/nft/collections/:id/metadata/:tokenIdGETGet/serve token metadataPublicNo

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 management

Implementation Strategy

Phase 1: Smart Contracts (Days 1-3)

  1. Create packages/nft-contracts with Hardhat 3
  2. Implement Diamond proxy + factory
  3. Implement all facets (ERC721A, Metadata, Mint, Royalty, Ownership)
  4. Write tests
  5. Deploy to Arbitrum Sepolia testnet

Phase 2: API Endpoints (Days 3-6)

  1. Add NFT routes to emprops-api
  2. Implement collection setup endpoint (Gemini LLM integration)
  3. Implement deploy endpoint (ethers.js → DiamondFactory)
  4. Implement generate endpoint (leverage existing job queue)
  5. Implement metadata management (GCS write)
  6. Implement mint endpoint (ethers.js → MintFacet.mintTo)

Phase 3: Integration & Testing (Days 6-8)

  1. Database schema updates (add NFT fields to collection model)
  2. End-to-end testing (setup → deploy → generate → mint)
  3. Verify metadata serves correctly from CDN
  4. 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 mintTo eliminates 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

  1. Use well-tested Diamond reference implementations (diamond-3-hardhat by Nick Mudge)
  2. Hardhat 3 declared production-ready; fallback to Hardhat 2 if blockers emerge
  3. Add Ponder in v2 when secondary market features are needed
  4. 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
  • [ ] mintTo successfully mints tokens to arbitrary addresses
  • [ ] tokenURI returns 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

Released under the MIT License.