Skip to content

Arbitrum NFT Integration — Diamond Standard

Status

Architecture: ERC-2535 Diamond Standard Tooling: Hardhat 3 + Solidity ^0.8.0 Target Chain: Arbitrum Sepolia (testnet) → Arbitrum One (mainnet) Contracts: packages/nft-contracts/ — 28 tests passing

What We're Building

Each NFT collection deploys as its own Diamond proxy (ERC-2535). The Diamond pattern lets collections share logic via facets while keeping storage isolated per collection. Facets can be added, replaced, or removed post-deployment — meaning collections can evolve without redeploying.

Why Diamond over ERC1167 Minimal Proxies

AspectERC1167 (Old Plan)ERC-2535 Diamond (Current)
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

Smart Contract Architecture

DiamondFactory

    │ createCollection(CollectionParams)
    │ → deploys Diamond proxy via CREATE2
    │ → configures facet selectors
    │ → initializes collection state
    │ → transfers ownership to collection owner

    ├── Diamond Instance A (Collection "Cosmic Dreams")
    │   ├── DiamondCutFacet     → manage facets
    │   ├── DiamondLoupeFacet   → introspection + ERC-165
    │   ├── ERC721Facet         → ERC-721 + Metadata (name, symbol, tokenURI, transfers)
    │   ├── MintFacet           → mintTo(address), batchMintTo(address, qty)
    │   └── OwnershipFacet      → ERC-173 ownership

    ├── Diamond Instance B (Collection "Neon Horizons")
    │   └── (same shared facets, own storage)

    └── Diamond Instance N ...

Contract Inventory

ContractFilePurpose
DiamondDiamond.solBase Diamond proxy — receives calls, routes to facets
DiamondFactoryDiamondFactory.solDeploys collections via CREATE2, configures facets
DiamondCutFacetfacets/DiamondCutFacet.solAdd/replace/remove facets on a Diamond
DiamondLoupeFacetfacets/DiamondLoupeFacet.solIntrospection — facets(), facetAddress(), supportsInterface()
ERC721Facetfacets/ERC721Facet.solERC-721 + Metadata — transfers, approvals, tokenURI(), setBaseURI()
MintFacetfacets/MintFacet.solCustodial minting — mintTo(), batchMintTo(), supply management
OwnershipFacetfacets/OwnershipFacet.solERC-173 — owner(), transferOwnership()
CollectionInitupgradeInitializers/CollectionInit.solOne-time initializer — sets name, symbol, baseURI, maxSupply, minter

Storage Libraries

LibraryPurpose
LibDiamondDiamond storage — facet routing, ownership, diamondCut logic
LibERC721ERC-721 storage — token ownership, balances, approvals, metadata
LibMintMint storage — minter address, mint active flag

Key Design Decisions

Minter role vs Owner role: The owner (collection creator) controls facets and admin functions. A separate minter address (platform wallet) controls minting. This lets us run custodial minting without giving the platform full collection ownership.

No ERC721A: We use standard ERC-721 (not ERC721A) because Diamond storage pattern is incompatible with ERC721A's consecutive-ownership optimization. Standard ERC-721 works cleanly with Diamond's namespaced storage slots.

No RoyaltyFacet yet: ERC-2981 royalties will be added as a facet in a future phase — the Diamond pattern lets us add it to existing collections without redeployment.


DiamondFactory

The factory deploys new collection Diamonds with all facets pre-configured.

CollectionParams

solidity
struct CollectionParams {
    bytes32 salt;           // CREATE2 salt for deterministic addresses
    string name;            // Collection name (e.g., "Cosmic Dreams")
    string symbol;          // Token symbol (e.g., "COSMIC")
    string baseURI;         // CDN base URI for metadata
    uint256 maxSupply;      // Max tokens (0 = unlimited)
    address collectionOwner; // Collection creator address
    address minter;         // Platform wallet authorized to mint
}

Functions

FunctionAccessPurpose
createCollection(CollectionParams)Owner onlyDeploy new Diamond proxy
predictAddress(bytes32 salt)ViewPredict deployment address before deploying
transferOwnership(address)Owner onlyTransfer factory ownership

Deployment Flow

Platform API calls createCollection(params)

    ├── 1. Deploy Diamond proxy via CREATE2 (deterministic address)
    │       └── Factory is temporary owner

    ├── 2. diamondCut — add all facets + run CollectionInit
    │       ├── DiamondLoupeFacet (5 selectors)
    │       ├── OwnershipFacet (2 selectors)
    │       ├── ERC721Facet (13 selectors)
    │       └── MintFacet (9 selectors)

    └── 3. Transfer ownership to collectionOwner

Facets

ERC721Facet (13 functions)

Combines ERC-721 core + metadata in one facet.

FunctionTypePurpose
name()ViewCollection name
symbol()ViewToken symbol
tokenURI(uint256)ViewReturns baseURI + tokenId
balanceOf(address)ViewToken count for address
ownerOf(uint256)ViewToken owner
approve(address, uint256)WriteApprove transfer
getApproved(uint256)ViewGet approved address
setApprovalForAll(address, bool)WriteOperator approval
isApprovedForAll(address, address)ViewCheck operator approval
transferFrom(address, address, uint256)WriteTransfer token
safeTransferFrom (2 variants)WriteSafe transfer with receiver check
setBaseURI(string)AdminUpdate metadata base URI

MintFacet (9 functions)

Custodial minting — only the authorized minter address can mint.

FunctionTypePurpose
mintTo(address)MinterMint next token to recipient
batchMintTo(address, uint256)MinterBatch mint to recipient
totalSupply()ViewCurrent token count
maxSupply()ViewMaximum supply (0 = unlimited)
mintActive()ViewIs minting enabled
minter()ViewAuthorized minter address
setMintActive(bool)OwnerToggle minting
setMinter(address)OwnerChange minter address
setMaxSupply(uint256)OwnerUpdate max supply

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)
  • No IPFS for v1 — GCS + CDN is fast, cheap, and updateable

Minting Flow

1. User triggers generation in Emerge miniapp
2. API generates artwork via job queue (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 MintFacet.mintTo(recipientAddress) on collection Diamond
6. Token minted on Arbitrum, tokenURI points to CDN metadata
7. DB updated: output record linked to collection + token

Custodial minting: Users never pay gas. The platform minter wallet submits all transactions. Payments happen off-chain via the miniapp.


What's Not Included (vs Old Plan)

FeatureOld PlanCurrent Status
Ponder indexerReal-time blockchain indexingSkipped — custodial mintTo means we track all mints server-side
0xSplitsTrustless revenue distributionNot in v1 — revenue handled off-chain
Token upgradesEIP-712 version trackingNot in v1 — can be added as a new facet later
ERC721AGas-optimized batch mintingStandard ERC-721 — incompatible with Diamond storage
IPFS metadataImmutable metadata via PinataGCS + CDN instead

All of these can be added later through new facets or infrastructure — that's the advantage of the Diamond pattern.


Monorepo Structure

packages/nft-contracts/
├── contracts/
│   ├── Diamond.sol                     # Base Diamond proxy
│   ├── DiamondFactory.sol              # Factory deploys collections via CREATE2
│   ├── facets/
│   │   ├── DiamondCutFacet.sol         # Manage facets
│   │   ├── DiamondLoupeFacet.sol       # Introspection
│   │   ├── ERC721Facet.sol             # ERC-721 + Metadata
│   │   ├── MintFacet.sol               # Custodial minting
│   │   └── OwnershipFacet.sol          # ERC-173 ownership
│   ├── libraries/
│   │   ├── LibDiamond.sol              # Diamond storage + cut logic
│   │   ├── LibERC721.sol               # ERC-721 storage
│   │   └── LibMint.sol                 # Mint storage
│   ├── interfaces/
│   │   ├── IDiamondCut.sol
│   │   ├── IDiamondLoupe.sol
│   │   ├── IERC165.sol
│   │   ├── IERC173.sol
│   │   └── IERC721.sol
│   └── upgradeInitializers/
│       └── CollectionInit.sol          # One-time initializer
├── test/                               # 28 tests passing
├── hardhat.config.ts                   # Hardhat 3
└── package.json

Next Steps

  1. NFT API endpoints in emprops-api — setup, deploy, generate, mint, metadata
  2. Database schema migration — NFT fields on collection model
  3. Deploy to Arbitrum Sepolia — testnet validation
  4. RoyaltyFacet — ERC-2981 royalties as a new facet

See the Implementation Plan for the full 6-phase breakdown.


Released under the MIT License.