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:
- $UGEN — a fixed-supply 1B ERC-20 that exists on a self-launched Uniswap V4 pool.
- UnigenHook — a V4 hook attached to that pool at initialization. It listens to
afterSwapand triggers the mint when conditions match. - 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.
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.
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:
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:
- Reads the swap direction — confirms it's ETH→UGEN (a buy)
- Reads
amountIn(ETH)from the V4BalanceDelta - If below
minBuyEth→ returns silently. No mint, no revert. - If
UgenNFT.totalSupply() ≥ MAX_SUPPLY→ returns silently. Collection is closed. - Otherwise → proceeds to the mint logic below
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.
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.
/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):
| Tier | Count | TokenIds | % of supply |
|---|---|---|---|
| Mythic | 2 | 1 – 2 | 1.0% |
| Legendary | 8 | 3 – 10 | 4.0% |
| Epic | 20 | 11 – 30 | 10.0% |
| Rare | 40 | 31 – 70 | 20.0% |
| Uncommon | 60 | 71 – 130 | 30.0% |
| Common | 70 | 131 – 200 | 35.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:
| Tier | Slots (of 200) | Power range |
|---|---|---|
| Common | 70 | 50 – 149 |
| Uncommon | 60 | 150 – 299 |
| Rare | 40 | 300 – 499 |
| Epic | 20 | 500 – 699 |
| Legendary | 8 | 700 – 899 |
| Mythic | 2 | 900 – 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.
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:
0x87110017b88c3c9be357f4bdc66689035edbee95c87d7110ab18919a7efc7336The 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
- User connects wallet
- Frontend fetches the user's proof from a public JSON endpoint (or static asset)
- If found, frontend shows a "Register Genesis boost" CTA
- User signs
UgenNFT.registerAge(tier, proof[])— costs gas only - On-chain stores
ageTierOf[wallet] = tierpermanently - All future mints from this wallet get the corresponding bps reduction
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.
Season Rewards
SeasonRewards distributes a $UGEN pool per season, weighted by each held NFT's power.
Season Lifecycle
- Owner calls
startSeason(rewardPool)with a $UGEN amount - Season runs for a fixed duration (default 30 days)
- Owner calls
endSeason()to snapshot all NFTs and their owners - Holders claim via
claimRewards(seasonId, tokenIds[])for each token they owned at the snapshot block
Power-Weighted Formula
tokenReward = (powerOf(tokenId) / totalPowerAtSnapshot) * seasonPoolExample: season pool = 10M UGEN, totalPowerAtSnapshot = 100,000. Owner of a Legendary with 850 power claims:
850 / 100000 * 10_000_000 = 85_000 UGENEach (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) → boolKey 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) → uint16SeasonRewards
- 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:
hookaddress — no more swapping hooksbaseURI— metadata becomes immutableageMerkleRoot— 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:
afterSwapmint 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?
› What happens if I buy 0.099 ETH (under threshold)?
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?
› Can I buy a unigen on a secondary marketplace?
› What if the Arena steals my favorite unigen?
› Why are there both 200 and 3,000 numbers in the UI?
/collection vault grid + on-chain MAX_SUPPLY are 200.› Is the wallet-age boost retroactive if I register after my first mint?
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?
contracts/ethereum_genesis_wallets.csv.› What happens to unclaimed season rewards?
› When mainnet?
Resources
- GitHub —
unigenesis/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.
