protocol docs

Unigen Genesis

A 200-piece skeleton-unicorn collection that auto-mints through a Uniswap V4 hook. No mint button, no waitlist, no allowlist drama — just buy $UGEN above the threshold and your unigen appears.

Living document. Updated as milestones ship. Last sync: 2026-05-15.

Overview

Unigen Genesis is built around a single idea: the trade is the mint. Three layers stack to make that work:

  1. $UGEN — a fixed-supply 1B ERC-20 that exists on a self-launched Uniswap V4 pool.
  2. UnigenHook — a V4 hook attached to that pool at initialization. It listens to afterSwap and triggers the mint when conditions match.
  3. UgenNFT — a capped-200 ERC-721 with on-chain rarity, power, and a wallet-age registry. Only the hook can mint. No public mint function.
On top of that: a Genesis Arena where NFTs fight for ownership (winner steals loser's token), Season Rewards that redistribute $UGEN by power, and a wallet-age Merkle registry that boosts old-Ethereum wallets' chances of pulling rare tiers.

The frontend ships in dual-chain mode: contracts run on Sepolia for the public testnet rehearsal (deployed by Raph on 2026-05-05) and on local Anvil for backend dev. Mainnet target: May 2026.

Tokenomics

$UGEN is the protocol's utility/meme token. Fixed supply, no inflation, no mint after deploy.

Symbol
$UGEN
Total supply
1,000,000,000 (1B)
Decimals
18
Mint authority after deploy
Nobody. Fixed.
Trading pair
ETH / $UGEN on Uniswap V4
LP fee
Dynamic (1% default, 0.01% for tier-S buyers)

Distribution

The full 1B is seeded into the V4 pool at launch (with a paired ETH amount), establishing the initial price. There is no team allocation held outside the pool, no vesting schedule, no airdrop. The token lives or dies by trading liquidity.

Read this carefully: $UGEN price is set by the market. The protocol doesn't define a target price. It defines a mint threshold in ETH (≥ 0.1 ETH on mainnet) — the amount of $UGEN you receive for that ETH varies with pool depth at the moment of your swap.

Season rewards stream

A configurable portion of $UGEN can be allocated to SeasonRewards by the contract owner at season start. That pool is then claimable by NFT holders, weighted by their token power (see Season Rewards).

How It Works

The end-to-end mint path, from your wallet click to a Genesis NFT landing in your wallet, in one V4 transaction:

01
You swap
ETH → UGEN, public Uniswap UI
02
Pool fires
afterSwap() callback
03
Hook reads buyer
abi.decode(hookData)
04
RNG + age
tier from seeded curve
05
Mint
ERC-721 lands in wallet

1. The Swap

The user opens the public Uniswap app (we don't deploy our own router) and swaps ETH for $UGEN against the V4 pool that's wired with our hook. Any amount is accepted by the pool itself; the NFT mint only triggers if the trade clears the minBuyEth threshold.

Frontend default input
0.001 ETH
Mint threshold — Sepolia
0.01 ETH
Mint threshold — Mainnet
0.1 ETH
Buys above threshold
Mint 1 NFT to buyer
Buys below threshold
Get UGEN only, no NFT
UGEN sells
Never mint an NFT (one-way)

2. The Hook Fires

Uniswap V4 calls our hook's afterSwap() callback. Inside, the hook:

  1. Reads the swap direction — confirms it's ETH→UGEN (a buy)
  2. Reads amountIn(ETH) from the V4 BalanceDelta
  3. If below minBuyEth → returns silently. No mint, no revert.
  4. If UgenNFT.totalSupply() ≥ MAX_SUPPLY → returns silently. Collection is closed.
  5. Otherwise → proceeds to the mint logic below
Hook callbacks are written non-reverting for any non-mint condition. This is critical — a revert in afterSwap would undo the entire swap, including the UGEN delivery. Users always get their UGEN, NFT or not.

3. Buyer Resolution

In Uniswap V4, the msg.sender inside afterSwap is the router, not the actual buyer. To know who gets the NFT, the buyer address must travel through hookData — an arbitrary bytes payload that the router appends to the swap call.

// Inside the public Uniswap router (or our SwapTest harness):
poolManager.swap(
    poolKey,
    swapParams,
    abi.encode(buyer)  // <-- hookData
);

// Inside UnigenHook._afterSwap:
address buyer = abi.decode(hookData, (address));

The public Uniswap app encodes the connected wallet here automatically when interacting with a hooked pool. If a router forgets to pass hookData, the hook silently no-ops (no NFT minted) — never reverts the swap.

4. RNG + Age Bias

The hook calls UgenNFT.mintNext(buyer, seed). The NFT contract uses a deterministic seeded RNG to pick a rarity tier. The seed comes from block.prevrandao + buyer address + an internal counter — same inputs always produce the same outputs (auditable, non-grindable post-tx).

Wallet-age boost: if the buyer has called registerAge() with a valid Merkle proof, their assigned tier (S/A/B/C) subtracts basis points from the rolled rng before tier matching. Lower rng = rarer tier.

Tier S — boost
−4000 bps (−40 percentage points)
Tier A — boost
−2000 bps
Tier B — boost
−800 bps
Tier C — boost
−200 bps
Not registered
0 bps (raw rng)

Example: rolled rng = 1200 (raw). Buyer is registered tier-S. Adjusted rng = 1200 − 4000 = −2800, clamped to 0 → falls into the Mythic bucket. A non-registered buyer with the same roll lands in Epic.

5. Tier Walk-Down Fallback

If the rolled tier is exhausted (all 2 Mythics already minted, say), the contract walks down to the next available tier: Mythic → Legendary → Epic → Rare → Uncommon → Common.

Important: the walk is always downward, never upward. A tier-S buyer can't accidentally mint a Common because Mythic is empty — they get a Legendary instead. Only if every tier above Common is empty would they roll Common.

Per the metadata-driven mint (PR #7 — "Pre-defined Rarity Pools"), each tier has a fixed pool of tokenIds. Once a tier is rolled, the contract picks a tokenId from that pool via swap-and-pop (O(1)), guaranteeing the minted tokenId always matches its IPFS metadata's declared rarity.

The Collection

Supply Numbers

200 unigens total. Bumped from the original 100 to 200 on 2026-05-08 (Raph's commit 2124281) with proportional tier rebalancing.

You'll see the number 3,000 referenced in marketing copy (hero, footer, narrative sections). That's the long-term vision — the actual prod deployment is 200 unigens. The frontend displays 200 in functional places (the /collection vault grid) and 3,000 in storytelling places (Pact section, hero). When more generations ship, more unigens come.

Tier Distribution

The 200 unigens are split across 6 rarity tiers using a linear ranking strategy ("Option A" per PR #8):

TierCountTokenIds% of supply
Mythic21 – 21.0%
Legendary83 – 104.0%
Epic2011 – 3010.0%
Rare4031 – 7020.0%
Uncommon6071 – 13030.0%
Common70131 – 20035.0%

TokenId Convention

tokenId 1 = Mythic, tokenId 200 = Common. The numbering is inverted to match the rarity ranking: lower IDs are rarer. This was committed in 882ee01 (2026-05-11).

1 – 2
Mythic (Ascended, Origin)
3 – 10
Legendary
11 – 30
Epic
31 – 70
Rare
71 – 130
Uncommon
131 – 200
Common

Metadata & IPFS

Each tokenId has a fixed JSON metadata file pinned on IPFS (via Pinata). The metadata declares the unigen's name, concept, trait breakdown, and IPFS image URL.

{
  "name": "Unigen Genesis #1 — Ascended",
  "description": "The first soul. Mythic tier.",
  "image": "ipfs://IMAGE_CID/1.png",
  "attributes": [
    { "trait_type": "Rarity", "value": "Mythic" },
    { "trait_type": "Power",  "value": 950 },
    { "trait_type": "Concept", "value": "Ascended" }
  ]
}

The UgenNFT.baseURI is set to ipfs://METADATA_CID/ once at deploy. tokenURI(id) returns ipfs://METADATA_CID/{id}.json. The base URI can be frozen via freezeBaseURI() (owner-only, one-way) to make the metadata immutable post-mint.

Rarity Tiers

Each tier has a fixed slot count and a power range:

TierSlots (of 200)Power range
Common7050 – 149
Uncommon60150 – 299
Rare40300 – 499
Epic20500 – 699
Legendary8700 – 899
Mythic2900 – 1000

Power is rolled within the tier's range at mint time using the same seed (deterministic, replayable). It's the unigen's combat stat in the Arena and the weight used by Season Rewards.

A Common can roll up to 149 power, and a Legendary can roll as low as 700. The ranges don't overlap tier-to-tier; rarity strictly dominates power.

Wallet-Age Boost

Snapshot Source

We compute a Merkle tree from the historical Ethereum genesis wallet snapshot (contracts/ethereum_genesis_wallets.csv). The current root is:

0x87110017b88c3c9be357f4bdc66689035edbee95c87d7110ab18919a7efc7336

The full proof JSON is generated locally with script/snapshot/build-merkle.js and served on-demand. The on-chain contract only stores the root.

Tier Thresholds

The snapshot maps each eligible address to one of four tiers:

S — Genesis allocation wallet
Touched ETH in the first 12 months
A
Active before block 4M (mid 2017)
B
Active before block 8M (early 2019)
C
Active before block 12M (mid 2021)
Not eligible
Outside snapshot; 0 boost

Registration Flow

  1. User connects wallet
  2. Frontend fetches the user's proof from a public JSON endpoint (or static asset)
  3. If found, frontend shows a "Register Genesis boost" CTA
  4. User signs UgenNFT.registerAge(tier, proof[]) — costs gas only
  5. On-chain stores ageTierOf[wallet] = tier permanently
  6. All future mints from this wallet get the corresponding bps reduction
Registration is one-shot, irreversible, gas-only. You can't register a different tier later, you can't transfer the registration. The boost belongs to the wallet, not the human.

RNG Bias Math

The rng → tier table (assumes already age-adjusted):

rng < 100
Mythic (1%)
rng < 500
Legendary (4%)
rng < 1500
Epic (10%)
rng < 3500
Rare (20%)
rng < 6500
Uncommon (30%)
rng ≥ 6500
Common (35%)

Out of 10,000 cumulative bps. A tier-S buyer drops their rng by 4,000 before this table is consulted — pushing their effective odds way up the curve.

Genesis Arena

Each unigen can fight. The Arena is a PvP combat system where winner steals loser's NFT. Combat is deterministic, settled on-chain in one transaction.

Entering Combat

The owner of an NFT must first approve(arena, tokenId) or setApprovalForAll(arena, true). Then call:

GenesisArena.enterCombat(tokenId)

This locks the NFT as the wallet's active fighter. Each wallet can have at most one active fighter at a time.

Challenge Flow

A challenger picks one of their owned NFTs and calls:

GenesisArena.challenge(myTokenId, defenderAddress)

The defender must have an active fighter. The combat resolves instantly in the same transaction.

Combat Resolution

Combat is a probability roll weighted by each fighter's effective power:

effectivePower(token) = baseRarityPower(token) * ageMultiplierBps(owner) / 10_000

attackerOdds = effectivePower(attackerToken) / (effectivePower(attackerToken) + effectivePower(defenderToken))

rng = uint256(keccak256(blockhash, attackerToken, defenderToken)) % 10_000
attackerWins = rng < (attackerOdds * 10_000)

Outcome is emitted via the CombatResolved event.

Power × Age Multiplier

The wallet-age boost doesn't just bias mints — it also boosts your combat power.

S-tier owner
× 1.40 (14000 bps)
A-tier owner
× 1.20
B-tier owner
× 1.10
C-tier owner
× 1.05
No registration
× 1.00 (raw power)

The multiplier follows the current owner of the token, not the minter. Transfer an NFT to a tier-S wallet, it gets the boost immediately.

Cooldown

After any combat (win or lose), both NFTs enter a 6-hour cooldown. They can't fight again until cooldownUntil(tokenId) ≤ block.timestamp. Cooldown is per-token, not per-wallet — you can fight your other unigens while one is cooling down.

NFT Theft

When a combat resolves, the loser's NFT is transferFrom(loser, winner, loserTokenId). Permanent. No undo, no escrow grace period.

Don't enter your favorite unigen if you're not prepared to lose it. Or do — and steal someone else's.

Season Rewards

SeasonRewards distributes a $UGEN pool per season, weighted by each held NFT's power.

Season Lifecycle

  1. Owner calls startSeason(rewardPool) with a $UGEN amount
  2. Season runs for a fixed duration (default 30 days)
  3. Owner calls endSeason() to snapshot all NFTs and their owners
  4. Holders claim via claimRewards(seasonId, tokenIds[]) for each token they owned at the snapshot block

Power-Weighted Formula

tokenReward = (powerOf(tokenId) / totalPowerAtSnapshot) * seasonPool

Example: season pool = 10M UGEN, totalPowerAtSnapshot = 100,000. Owner of a Legendary with 850 power claims:

850 / 100000 * 10_000_000 = 85_000 UGEN

Each (seasonId, tokenId) can only be claimed once. If you transfer the NFT before claiming, the new owner can't claim for past seasons.

Networks

Sepolia (testnet rehearsal, live)

Chain ID
11155111
Mint threshold
0.01 ETH
Pool fee
Dynamic 1% / 0.01% tier-S
Public Uniswap URL
app.uniswap.org/swap?chain=sepolia&outputCurrency=0xae327…
Status
Deployed 2026-05-05 (PR #5)

Mainnet (target)

Chain ID
1
Mint threshold
0.1 ETH
Estimated launch
May 2026
Status
Pre-deploy

Anvil (local dev)

Chain ID
31337
RPC
http://127.0.0.1:8545
Mint threshold
0.1 ETH (default)
Use
Backend / hook integration testing only

Smart Contracts

All addresses are environment-driven on the frontend (NEXT_PUBLIC_* on Anvil, hardcoded for Sepolia). The Sepolia set is stable as of PR #5.

UgenToken (ERC-20)

Sepolia
0xae327FAefD77535dF15035da7a0bFdBe6cde7EF2
Type
Fixed-supply ERC-20
Total supply
1,000,000,000 UGEN
Decimals
18

UgenNFT (ERC-721 + Enumerable)

Sepolia
0xcD78d11c83d1BB7A7c1D98EEABDFF9Ef0CCD2D8f
MAX_SUPPLY
200
Mint authority
UnigenHook only
Base URI
ipfs://METADATA_CID/

Key read functions:

totalSupply() → uint256
balanceOf(address) → uint256
ownerOf(uint256 tokenId) → address
tokenOfOwnerByIndex(address, uint256) → uint256
tokenURI(uint256) → string
rarityOf(uint256) → uint8 (0..5)
powerOf(uint256) → uint16
mintedAtOf(uint256) → uint40
remainingPerTier(uint8) → uint16
ageTierOf(address) → uint8
ageRegistered(address) → bool

Key writes (user-facing):

registerAge(uint8 tier, bytes32[] proof)

UnigenHook (V4 Hook)

Sepolia
0x83087fF09622c9081959241293976112c32690c0
Permissions
afterInitialize, beforeSwap, afterSwap
minBuyEth
Immutable, set at deploy
LP fee defaults
DEFAULT_LP_FEE = 10000 bps (1%)
Tier-S fee override
TIER_S_FEE = 100 bps (0.01%)

The hook is constructed with the minBuyEth param. Passing 0 defaults to 0.1 ether. Sepolia was deployed with 0.01 ether for cheaper testing.

GenesisArena

Sepolia
0x2B67013Fe769E2FFAe3107a6b53377c93E9774D7
Cooldown
6 hours
Active fighter limit
1 per wallet
enterCombat(uint256 tokenId)
challenge(uint256 myTokenId, address defender)
activeTokenOf(address) → uint256
cooldownUntil(uint256) → uint40
ageMultiplierBps(address) → uint16

SeasonRewards

Sepolia
0xD2E9D13f04D70D2Fab34afc58332B94ea3AbdC88
Reward token
UGEN
Default duration
30 days per season
currentSeasonId() → uint256
seasons(uint256 id) → Season struct
claimedTokenInSeason(uint256 seasonId, uint256 tokenId) → bool
claimRewards(uint256 seasonId, uint256[] tokenIds)

Owner-only: startSeason, endSeason.

Frontend Architecture

Stack:

  • Next.js 15 (App Router) + TypeScript
  • Tailwind CSS with custom "cozy bones" tokens
  • wagmi + viem for chain reads/writes
  • RainbowKit for wallet connect (dark theme tinted pink)
  • TanStack Query as wagmi's cache layer

Multi-chain routing

lib/contracts.ts exports useAddresses() and useChainConfig() hooks. Both inspect the currently connected chainId via wagmi and return the right contract addresses / mint threshold / Uniswap URL for that chain. Sepolia addresses are hardcoded (stable); Anvil addresses come from .env.local (rotate per redeploy).

Page map

/
Hero video + Genesis Wall marquee + narrative sections + roadmap
/library
Connected wallet's owned unigens, filterable by tier
/collection
Vault grid: 200 unigens with locked/unlocked states + 2.8K teaser
/arena
Combat UI, archetype roster, rules
/feed
Simulated activity stream — 12 archetypes trading wins
/docs
You're here

SSR polyfill

frontend/instrumentation.ts + frontend/lib/ssr-polyfill.ts install an SSR-safe localStorage/sessionStorage/indexedDB stub before any wagmi/viem module loads. Without it, viem@2.48's tempoDevnet chain definition crashes Node at module-load.

Security

Audit status

v1 backend reviewed internally — 83/83 forge tests pass. PR #7 (security audit fixes) merged 2026-05-10 addressing critical/high findings. No external audit yet. Mainnet launch is gated on completing one.

freezeConfig()

After deployment, the owner can call freezeConfig() on UgenNFT (one-way, irreversible). This locks:

  • hook address — no more swapping hooks
  • baseURI — metadata becomes immutable
  • ageMerkleRoot — snapshot is final

Recommended for prod: call freezeConfig() right after verifying the deployment.

Known limitations

  • MEV on the swap: a sophisticated searcher could front-run a big buy. We rely on Uniswap V4's built-in protections — no additional anti-MEV layer.
  • Hook gas: afterSwap mint costs ~150k extra gas. Users above threshold pay that. Not a vuln, just a cost.
  • Arena DoS: a defender can grief by entering combat with their weakest NFT. The challenger always picks their fighter — they accept the matchup.

Glossary

$UGEN
The ERC-20 utility token. 1B fixed supply.
Unigen / unicorn / NFT
Used interchangeably. The 200 ERC-721 collection.
Hook
Uniswap V4's afterSwap callback contract.
hookData
Arbitrary bytes the router passes to the hook — we use it to encode the buyer address.
Tier
Rarity bucket: Common, Uncommon, Rare, Epic, Legendary, Mythic.
Power
Combat stat rolled within the tier's range at mint time.
Age tier
S/A/B/C — the buyer's wallet historic boost (separate from NFT rarity).
Mint threshold
Minimum ETH in a buy that triggers the NFT mint. 0.01 Sepolia, 0.1 mainnet.
Walk-down
If rolled tier is empty, mint walks downward (Mythic → Common) until a tier has supply.
Season
Time-windowed UGEN reward distribution, weighted by held NFT power.
Sticker DA
The frontend's visual style — thick black borders + offset shadows + pastel + neon.

FAQ

Why is there no mint button?
Because the hook is the mint. Adding a button would either lie (mint from a different contract path) or duplicate the on-chain logic. The swap is the action. Anyone selling you a separate "mint" is selling you nothing.
What happens if I buy 0.099 ETH (under threshold)?
The swap still goes through. You receive UGEN. The hook's afterSwap sees the low amount and returns silently — no NFT minted, no revert. Same on Sepolia for buys under 0.01 ETH.
Can I get a refund if I get a Common when I wanted a Mythic?
No. The mint is on-chain final. The seeded RNG is replayable and auditable — the result is what it is.
Can I buy a unigen on a secondary marketplace?
Yes — once the collection is live, it's a standard ERC-721 transferable on OpenSea, Blur, etc. You don't get the wallet-age boost on secondary purchases — that's tied to the primary mint via the hook.
What if the Arena steals my favorite unigen?
That's the deal. Either don't enter your favorite, or stake it knowing the risk. The protocol won't reverse a combat outcome.
Why are there both 200 and 3,000 numbers in the UI?
3,000 = the long-term vision (future generations of unigens). 200 = the actual prod deployment (this first generation, Pinata-pinned). The hero copy / narrative uses 3,000 to signal scope. The /collection vault grid + on-chain MAX_SUPPLY are 200.
Is the wallet-age boost retroactive if I register after my first mint?
No. The boost is applied at mint time. Mints before your registerAge() call use the raw RNG. Register first if you want the bias.
How do I know if my wallet is in the Genesis snapshot?
The proof JSON file (large, generated locally) lists every eligible address. Frontend integration is on the roadmap (planned for the wallet-age boost flow milestone). For now: ask Raph or check the CSV in contracts/ethereum_genesis_wallets.csv.
What happens to unclaimed season rewards?
They stay in the contract. There's no expiry on past-season claims (yet) — but a future governance proposal could recycle unclaimed pools into the next season.
When mainnet?
Target May 2026. Gated on: external audit completion, Pinata pin confirmation, frontend wallet-age boost UI, final tier rebalancing sign-off.

Resources

  • GitHubunigenesis/geni (private — ask Raph or Greg for access)
  • Sepolia explorer — search any contract address from Contracts on Etherscan's Sepolia.
  • Live activity — check /feed for the simulated (soon real) event stream.
  • The Genesis Vault — browse all 200 tokens at /collection.
  • Arena roster /arena for the 12 archetypes and their signature moves.

Found a bug, a typo, or something unclear in these docs? Ping Greg directly. Living document.