Skip to content

Smart Contracts

Navigation

This document details the smart contract architecture for the Arbitrum NFT Launchpad—a factory-based system where each collection deploys as its own contract.

Architecture Overview

┌────────────────────────────────────────────────────────────────┐
│                    NFTContractFactory (UUPS Proxy)                     │
│                    ├── Owner: Platform Admin                   │
│                    └── Upgradeable via UUPS                    │
└───────────────┬─────────────────────┬──────────────────────────┘
                │                     │
    ┌───────────▼───────────┐   ┌─────▼─────────────┐
    │  Emerge_721 V1 (Impl) │   │  Future Versions  │
    │  └── ERC721A          │   │  └── EditionApp   │
    └───────────┬───────────┘   │  └── AuctionApp   │
                │               └───────────────────┘

┌───────────────▼────────────────────────────────────────────────┐
│                    ERC1167 Minimal Proxy                       │
│     45-byte contract that delegates all calls to Emerge_721    │
└───────────────┬─────────────────────┬─────────────────┬────────┘
                │                     │                 │
    ┌───────────▼───────────┐ ┌───────▼───────┐ ┌───────▼───────┐
    │      Clone 1          │ │    Clone 2    │ │    Clone N    │
    │    (Collection A)     │ │ (Collection B)│ │ (Collection Z)│
    │  Own storage/tokens   │ │Own storage/.. │ │ Own storage/..│
    └───────────────────────┘ └───────────────┘ └───────────────┘

Design Principles

PrincipleImplementation
Each collection = own contractFactory deploys minimal proxy per collection
Gas efficient deploymentERC1167 proxies - Significant gas reduction
Deterministic addressesCREATE2 for predictable addresses before deployment
Upgradeable factoryUUPS pattern allows factory evolution; collections are immutable
Versioned implementationsNew collection types without affecting existing collections

MVP Approach: Centralized Mint-To

For the initial launch, we're using a centralized minting model:

User pays via Diamo/card/crypto


┌─────────────────┐
│  EmProps API    │  ← Receives payment, verifies
└────────┬────────┘


┌─────────────────┐
│  Admin Wallet   │  ← Platform-controlled EOA or multisig
└────────┬────────┘
         │ calls mintTo(recipient, quantity)

┌─────────────────┐
│  Emerge_721     │  ← Mints NFT(s) directly to user's wallet
│  (Clone)        │
└─────────────────┘

Why centralized for MVP:

  • Users never need to sign blockchain transactions
  • Payment flexibility (credit card, any crypto via Diamo)
  • Simpler onboarding for non-crypto-native users
  • Platform absorbs gas costs

Contract Inventory

ContractTypePurposeUpgradeable
NFTContractFactoryUUPS ProxyDeploys collections, registers implementationsYes
Emerge_721 V1ImplementationERC721A collection templateNo (immutable)
Emerge_721 CloneERC1167 ProxyIndividual collection instanceNo

NFTContractFactory Contract

The NFTContractFactory is the deployment engine. It creates new collections and manages implementation versions.

Deployment Flow

API requests collection creation

    ├──▶ 1. Factory deploys Emerge_721 clone (CREATE2)
    │        └── Deterministic address from salt

    └──▶ 2. Clone is initialized
             └── Set name, symbol, maxSupply, owner, baseURI

Functions

solidity
// === Deployment ===
function createNFTContract(
    bytes32 appType,           // e.g., keccak256("SIMPLE_APP_V1")
    string calldata appId,     // Unique identifier for CREATE2
    string calldata name,      // Collection name
    string calldata symbol,    // Token symbol
    bytes calldata initData    // Encoded initialization params
) external onlyOwner returns (address app)

// === Implementation Management ===
function registerImplementation(
    bytes32 appType,
    address implementation
) external onlyOwner

function getImplementation(bytes32 appType) external view returns (address)

// === Address Prediction (CREATE2) ===
function generateAppSalt(
    string calldata appId,
    address deployer
) public pure returns (bytes32)

function predictAppAddress(
    bytes32 appType,
    bytes32 salt
) public view returns (address)

// === UUPS Upgrade ===
function upgradeTo(address newImplementation) external onlyOwner
function upgradeToAndCall(address newImplementation, bytes calldata data) external onlyOwner

Events

solidity
event AppCreated(bytes32 indexed appType, address indexed app, string appId, bytes32 salt)
event ImplementationRegistered(bytes32 indexed appType, address implementation)

CREATE2 Determinism

Collections get predictable addresses before deployment:

Benefits:

  • Pre-publish metadata with correct contract address
  • Same address across multiple chains (if we expand to other L2s)
  • Share collection links before deployment completes

Emerge_721 V1 Contract

Emerge_721 is the actual NFT collection—an ERC721A contract deployed as a minimal proxy (clone). Each collection is a separate instance with its own address, storage, and token IDs.

Features

FeatureImplementation
Batch mintingERC721A for gas-efficient multi-mint
Admin mintingmintTo() for centralized minting to recipients
ConfigurableSupply, baseURI
PausableAdmin can pause/unpause minting
Token upgradesVersion tracking with EIP-712 approval signatures
Dynamic metadataHTTP API serves version-aware metadata

Functions

solidity
// === Initialization (called once via clone) ===
function initialize(
    string calldata name,
    string calldata symbol,
    address owner,
    uint256 maxSupply,
    string calldata baseURI,
    address royaltySplit,      // ERC-2981 royalty receiver
    uint96 royaltyBps          // Royalty percentage (e.g., 500 = 5%)
) external initializer

// === Minting (admin only for MVP) ===
function mintTo(
    address recipient,
    uint256 quantity
) external onlyOwner

function mintBatchTo(
    address[] calldata recipients,
    uint256[] calldata quantities
) external onlyOwner

// === Configuration ===
function setMaxSupply(uint256 _maxSupply) external onlyOwner
function setBaseURI(string calldata _baseURI) external onlyOwner
function pause() external onlyOwner
function unpause() external onlyOwner

// === Token Upgrades ===
function initiateUpgrade() external onlyOwner
function upgradeTokenFor(uint256 tokenId) external onlyOwner
function upgradeTokensFor(uint256[] calldata tokenIds) external onlyOwner
function upgradeTokenWithSignature(
    uint256 tokenId,
    uint256 upgradeRound,
    bytes calldata signature
) external onlyOwner

// === View Functions ===
function totalMinted() external view returns (uint256)
function maxSupply() external view returns (uint256)
function baseURI() external view returns (string memory)
function paused() external view returns (bool)
function currentUpgradeRound() external view returns (uint256)
function tokenVersion(uint256 tokenId) external view returns (uint256)
function nonces(address holder) external view returns (uint256)

// === ERC721A Standard (inherited) ===
function balanceOf(address owner) external view returns (uint256)
function ownerOf(uint256 tokenId) external view returns (address)
function safeTransferFrom(address from, address to, uint256 tokenId) external
function transferFrom(address from, address to, uint256 tokenId) external
function approve(address to, uint256 tokenId) external
function setApprovalForAll(address operator, bool approved) external
function getApproved(uint256 tokenId) external view returns (address)
function isApprovedForAll(address owner, address operator) external view returns (bool)
function tokenURI(uint256 tokenId) external view returns (string memory)
function name() external view returns (string memory)
function symbol() external view returns (string memory)
function totalSupply() external view returns (uint256)

// === Ownership ===
function owner() external view returns (address)
function transferOwnership(address newOwner) external onlyOwner

// === ERC-2981 Royalties ===
function royaltyInfo(uint256 tokenId, uint256 salePrice)
    external view returns (address receiver, uint256 royaltyAmount)
function setDefaultRoyalty(address receiver, uint96 royaltyBps) external onlyOwner

Events

solidity
event TokensMinted(address indexed recipient, uint256 startTokenId, uint256 quantity)
event BaseURIUpdated(string newBaseURI)
event MaxSupplyUpdated(uint256 newMaxSupply)
event DefaultRoyaltyUpdated(address receiver, uint96 royaltyBps)
event UpgradeInitiated(uint256 indexed upgradeRound)
event TokenUpgraded(uint256 indexed tokenId, uint256 indexed upgradeRound)

Metadata Architecture

On-Chain vs Off-Chain

The contract stores minimal on-chain data:

On-ChainOff-Chain
baseURI (string)Token images
tokenVersion[tokenId]Token attributes/traits
Collection name/symbolRarity calculations
Royalty configCollection description

How tokenURI Works

solidity
function tokenURI(uint256 tokenId) public view returns (string memory) {
    return string(abi.encodePacked(baseURI, tokenId.toString()));
}
// Example: baseURI = "https://api.emerge.app/collections/ABC123/tokens/"
// tokenURI(42) → "https://api.emerge.app/collections/ABC123/tokens/42"

Dynamic Metadata via HTTP API

For MVP, we serve metadata from our API (not IPFS):

┌───────────────────────────────────────────────────────────────┐
│  OpenSea/Marketplace                                           │
│  └── Calls tokenURI(42)                                        │
└──────────────────────────────┬────────────────────────────────┘


┌───────────────────────────────────────────────────────────────┐
│  Emerge_721 Contract                                           │
│  └── Returns "https://api.emerge.app/.../tokens/42"           │
└──────────────────────────────┬────────────────────────────────┘


┌───────────────────────────────────────────────────────────────┐
│  EmProps API                                                   │
│  ├── Looks up token in database                               │
│  ├── Checks tokenVersion (upgrade status)                     │
│  └── Returns ERC721 metadata JSON                             │
└───────────────────────────────────────────────────────────────┘

Why HTTP instead of IPFS for MVP:

  • Dynamic content generation (on-demand, not pre-generated)
  • Upgrade system requires metadata to change based on token version
  • Simpler infrastructure (GCS + CDN)
  • Can migrate to IPFS later for specific collections

ERC721 Metadata JSON

json
{
  "name": "Collection Name #42",
  "description": "Token description",
  "image": "https://cdn.emerge.app/collections/ABC123/42.png",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Style", "value": "Abstract" },
    { "trait_type": "Version", "value": "2" }
  ]
}

Collection-Level Metadata

solidity
function contractURI() public view returns (string memory) {
    return string(abi.encodePacked(baseURI, "contract-metadata"));
}

OpenSea and other marketplaces call contractURI() for collection-level information:

json
{
  "name": "My Collection",
  "description": "A generative art collection",
  "image": "https://cdn.emerge.app/collections/ABC123/cover.png",
  "external_link": "https://emerge.app/collections/ABC123",
  "seller_fee_basis_points": 500,
  "fee_recipient": "0xRoyaltySplitAddress..."
}

Token Upgrade System

Creators can "upgrade" their collections by adding new content. Token holders opt-in to receive upgrades.

How It Works

┌─────────────────────────────────────────────────────────────────┐
│  1. Creator adds new content to existing collection             │
│     └── New images, updated traits, enhanced versions           │
└──────────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  2. Creator requests upgrade activation (can pay fee)           │
│     └── Off-chain: payment via Diamo/card/crypto                │
└──────────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  3. Platform admin calls initiateUpgrade()                      │
│     └── On-chain: increments currentUpgradeRound                │
└──────────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  4. Token holders opt-in (can pay fee)                          │
│     └── Sign EIP-712 message (gasless approval)                 │
│     └── Platform submits signature to contract                  │
└──────────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  5. API serves upgraded metadata                                │
│     └── Checks tokenVersion[tokenId] vs currentUpgradeRound    │
│     └── Returns appropriate version content                     │
└─────────────────────────────────────────────────────────────────┘

Upgrade State

solidity
// === Upgrade State ===
uint256 public currentUpgradeRound;
mapping(uint256 => uint256) public tokenVersion;  // tokenId → version

// === Admin Functions ===
function initiateUpgrade() external onlyOwner {
    currentUpgradeRound++;
    emit UpgradeInitiated(currentUpgradeRound);
}

function upgradeTokenFor(uint256 tokenId) external onlyOwner {
    require(tokenVersion[tokenId] < currentUpgradeRound, "Already upgraded");
    tokenVersion[tokenId] = currentUpgradeRound;
    emit TokenUpgraded(tokenId, currentUpgradeRound);
}

function upgradeTokensFor(uint256[] calldata tokenIds) external onlyOwner {
    for (uint256 i = 0; i < tokenIds.length; i++) {
        uint256 tokenId = tokenIds[i];
        require(tokenVersion[tokenId] < currentUpgradeRound, "Already upgraded");
        tokenVersion[tokenId] = currentUpgradeRound;
        emit TokenUpgraded(tokenId, currentUpgradeRound);
    }
}

Upgrade Events

solidity
event UpgradeInitiated(uint256 indexed upgradeRound);
event TokenUpgraded(uint256 indexed tokenId, uint256 indexed upgradeRound);

Monetization Opportunity

Upgrades can be monetized at two points:

WhoPays ForExample
CreatorActivating upgrade capability1 ETH to enable upgrades
Token HolderUpgrading their specific token0.1 ETH per token upgrade

All payments are handled off-chain (via Diamo, credit card, or crypto to platform wallet). The contract only tracks on-chain state.

EIP-712 Signatures (Gasless Approvals)

Token holders can cryptographically prove they approve an upgrade without paying gas. The platform submits the signature to the contract.

Domain Separator

solidity
bytes32 public DOMAIN_SEPARATOR;

constructor() {
    DOMAIN_SEPARATOR = keccak256(abi.encode(
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
        keccak256(bytes("Emerge721")),
        keccak256(bytes("1")),
        block.chainid,
        address(this)
    ));
}

Upgrade Approval Type

solidity
bytes32 constant UPGRADE_APPROVAL_TYPEHASH = keccak256(
    "UpgradeApproval(uint256 tokenId,uint256 upgradeRound,uint256 nonce)"
);

mapping(address => uint256) public nonces;

Signature Verification

solidity
function upgradeTokenWithSignature(
    uint256 tokenId,
    uint256 upgradeRound,
    bytes calldata signature
) external onlyOwner {
    address holder = ownerOf(tokenId);

    bytes32 digest = keccak256(abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        keccak256(abi.encode(
            UPGRADE_APPROVAL_TYPEHASH,
            tokenId,
            upgradeRound,
            nonces[holder]++
        ))
    ));

    require(ECDSA.recover(digest, signature) == holder, "Invalid signature");
    require(upgradeRound == currentUpgradeRound, "Wrong round");
    require(tokenVersion[tokenId] < currentUpgradeRound, "Already upgraded");

    tokenVersion[tokenId] = upgradeRound;
    emit TokenUpgraded(tokenId, upgradeRound);
}

Why EIP-712?

BenefitDescription
Gasless for usersHolders sign a message, don't pay gas
Cryptographic proofSignature proves holder approved the upgrade
Replay protectionNonce prevents signature reuse
Human-readableWallets display what's being signed
StandardWidely supported by wallets (MetaMask, Rainbow, etc.)

Understanding ERC1167 (Clones)

ERC1167 is a deployment technique, not an NFT standard. Here's how it works with our architecture:

What is ERC1167?

A minimal proxy contract (45 bytes) that delegates all calls to a shared implementation. Think of it like a shortcut:

┌──────────────────────────────────────────────────────────┐
│                    Without Clones                         │
│                                                           │
│  Collection A: [Full ERC721A code] ~20KB                 │
│  Collection B: [Full ERC721A code] ~20KB  (duplicate!)   │
│  Collection C: [Full ERC721A code] ~20KB  (duplicate!)   │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│                     With Clones                           │
│                                                           │
│  Emerge_721 V1 Implementation: [Full ERC721A code] ~20KB  │
│                                                           │
│  Collection A: [45-byte pointer] → Implementation        │
│  Collection B: [45-byte pointer] → Implementation        │
│  Collection C: [45-byte pointer] → Implementation        │
└──────────────────────────────────────────────────────────┘

Key Points

  • Clones ARE ERC721A contracts - they just share code with other clones
  • Each clone has its own storage - token ownership, balances, metadata are independent
  • Clones are immutable - once deployed, they always point to the same implementation
  • Collectors see normal NFTs - the proxy is transparent to users and marketplaces

Versioned Implementations

When we improve Emerge_721, we deploy a new implementation:

Emerge_721 V1 Implementation
    └── Clone 1 (Collection A) → forever on V1
    └── Clone 2 (Collection B) → forever on V1

Emerge_721 V2 Implementation (new features!)
    └── Clone 3 (Collection C) → uses V2
    └── Clone 4 (Collection D) → uses V2

Old collections keep working. New collections get improvements.

Revenue Distribution (0xSplits)

Revenue is distributed via 0xSplits—an audited, immutable payment splitting protocol.

Two Splits Per Collection

Each collection has two separate Split contracts with different recipient structures:

SplitWhenRecipientsPercentages
Primary SalesUser mints NFTCreator, Arbitrum Foundation, PlatformTBD
RoyaltiesNFT resold on marketplaceCreator, Platform80% / 20%

Why Different Splits?

  • Primary sales include Arbitrum Foundation (grant requirement)
  • Royalties are simpler: creator gets majority, platform takes small cut
  • Arbitrum Foundation only participates in initial sales, not secondary market

Primary Sales Flow

User pays via Diamo/card/crypto


┌─────────────────┐
│  Platform       │  ← Receives payment, mints NFT
└────────┬────────┘
         │ Sends revenue to Split

┌─────────────────┐
│  0xSplits       │  ← Immutable split contract
│  (Primary)      │
└────────┬────────┘
         │ Auto-distributes:
         ├── X% → Creator wallet
         ├── Y% → Arbitrum Foundation
         └── Z% → Platform (Emerge)

Royalties Flow (Secondary Sales)

Emerge_721 implements ERC-2981 (NFT Royalty Standard):

solidity
// Returns royalty info for marketplaces
function royaltyInfo(uint256 tokenId, uint256 salePrice)
    external view returns (address receiver, uint256 royaltyAmount)
{
    // receiver = royalty Split contract address
    // royaltyAmount = salePrice * royaltyBps / 10000
}
Collector sells NFT on OpenSea/Blur


┌─────────────────┐
│  Marketplace    │  ← Queries royaltyInfo()
└────────┬────────┘
         │ Sends royalty to Split

┌─────────────────┐
│  0xSplits       │  ← Royalty-specific split
│  (Royalties)    │
└────────┬────────┘
         │ Auto-distributes:
         ├── 80% → Creator wallet
         └── 20% → Platform (Emerge)

Emerge_721 Royalty Functions

solidity
// === ERC-2981 Royalties ===
function royaltyInfo(uint256 tokenId, uint256 salePrice)
    external view returns (address receiver, uint256 royaltyAmount)

function setRoyaltyInfo(address receiver, uint96 royaltyBps) external onlyOwner
function setDefaultRoyalty(address receiver, uint96 royaltyBps) external onlyOwner

Why 0xSplits?

BenefitDescription
ImmutableSplit ratios cannot be changed after creation
TrustlessNo admin can redirect funds
AuditedExtensively audited contracts
Gas efficientBatch withdrawals, minimal storage
StandardWidely used across NFT ecosystem

Split Configuration

When a collection is deployed, we create two split contracts:

solidity
// === Primary Sales Split (includes Arbitrum) ===
address[] primaryRecipients = [creatorWallet, arbitrumWallet, platformWallet];
uint32[] primaryPercentages = [TBD, TBD, TBD]; // Percentages TBD

address primarySplit = splitMain.createSplit(
    primaryRecipients, primaryPercentages, 0, address(0)
);

// === Royalties Split (creator + platform only) ===
address[] royaltyRecipients = [creatorWallet, platformWallet];
uint32[] royaltyPercentages = [800000, 200000]; // 80%, 20%

address royaltySplit = splitMain.createSplit(
    royaltyRecipients, royaltyPercentages, 0, address(0)
);

How each Split is used:

SplitStored InUsed By
primarySplitPlatform databasePlatform sends mint revenue here (off-chain decision)
royaltySplitEmerge_721 contractReturned by royaltyInfo() for marketplaces

The contract only knows about royaltySplit (via ERC-2981). The primarySplit address is stored off-chain and used by the platform when forwarding mint payments.

Withdrawal

Recipients can withdraw their accumulated balance at any time:

solidity
// Anyone can trigger distribution to all recipients
splitMain.distributeETH(split, recipients, percentages, 0, address(0));

// Or individual recipient can withdraw their share
splitMain.withdraw(recipient, ethAmount, tokens);

Gas Optimization

ERC721A Batch Minting

Tokens MintedStandard ERC721ERC721ASavings
1~51,000 gas~51,000 gas0%
5~255,000 gas~53,000 gas79%
10~510,000 gas~55,000 gas89%
20~1,020,000 gas~57,000 gas94%

Clone Deployment

Deployment TypeGas CostUSD on Arbitrum (est.)
Full contract~2,000,000 gas~$20
ERC1167 clone~45,000 gas~$0.50

Access Control

The platform uses a centralized admin model where the platform admin wallet performs all operations on behalf of collection creators. This enables gasless UX for creators and holders.

ActionWho Can Perform
Deploy collectionPlatform admin
Mint NFTs (mintTo)Platform admin
Pause/unpause mintingPlatform admin
Initiate upgrade roundPlatform admin
Upgrade token (with signature)Platform admin (submits holder's signature)
Set base URIPlatform admin
Set royalty infoPlatform admin
Upgrade factoryPlatform admin
Register new implementationsPlatform admin
Transfer ownershipCollection owner (only action available to owner)

Why Centralized Admin?

  • Gasless UX: Creators never pay gas fees
  • Simplified operations: Single admin key manages all collections
  • Revenue handling: Platform handles payments and 0xSplits distribution
  • Future option: Ownership can be transferred to creators who want direct control

Security Considerations

UUPS Upgrade Safety

The Factory uses UUPS (Universal Upgradeable Proxy Standard):

solidity
// Implementation includes upgrade logic
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
    // Only owner can upgrade
}

// Built-in protection against bricking
function proxiableUUID() external view returns (bytes32) {
    return _IMPLEMENTATION_SLOT;
}

Bricking protection: New implementations must include proxiableUUID() or the upgrade reverts.

Clone Immutability

Once deployed, a Emerge_721 clone cannot be modified:

  • No upgrades possible (minimal proxy, not UUPS)
  • Code is fixed forever
  • Provides security guarantee to collectors

Reentrancy Protection

  • mintTo() and mintBatchTo() protected by nonReentrant
  • Follows checks-effects-interactions pattern

Deployment Addresses

Testnet Only

These are Arbitrum Sepolia (testnet) addresses. Mainnet addresses will be added after production deployment.

ContractArbitrum SepoliaArbitrum One
NFTContractFactory (Proxy)TBDTBD
NFTContractFactory (Impl)TBDTBD
Emerge_721 V1 (Impl)TBDTBD

MVP vs Future

MVP (Now)

FeatureDescription
Centralized deploymentCollections created via EmProps API
Admin-controlled mintingmintTo() and mintBatchTo() only
No gas for usersPlatform pays all transaction costs
Off-chain paymentsDiamo, credit cards, crypto to platform wallet
HTTP metadataDynamic API-served metadata (not IPFS)
Token upgradesVersion tracking with EIP-712 approvals
Two-split revenuePrimary sales + royalties via 0xSplits

Future Enhancements

FeatureDescription
Permissionless mintingPublic mint() function with price validation
Creator-controlled collectionsTransfer ownership to creators
Direct on-chain paymentsUsers pay contract directly
IPFS metadataImmutable metadata for static collections
Multiple collection typesEditionApp, AuctionApp, AllowlistApp

New app types are registered with the factory:

solidity
factory.registerImplementation(
    keccak256("EDITION_APP_V1"),
    editionAppImplementation
);

Existing collections are unaffected—they continue using their original implementation.


Released under the MIT License.