Skip to Content
Genesis

Genesis

Genesis is the one-time launch sequence that:

  • materializes the first 10d of Crown stream (King-stream) emissions
  • seeds the initial WETH/CLAIM liquidity pool with the materialized 10d King-stream CLAIM bucket
  • locks genesis LP for 24 months
  • activates takeovers

Contracts:

  • LaunchController
  • GenesisLPVault24M

LaunchController.finalizeGenesis

One-shot and guardian-authorized, with strict guards.

Hard requirements (v1.0.0):

  • msg.value == requiredSeedEth (exact; 10 ETH * duration / 10 days — 10 ETH on mainnet, 1 ETH on testnets)
  • block.timestamp >= emissionStartTime + 10 days
  • MineCore.takeoversPaused() == true
  • MineCore.guardian() == LaunchController during the genesis window
  • the pre-genesis handoff that installs LaunchController as MineCore.guardian must be done by MineCore.owner(); the current guardian cannot self-install that contract role
  • MineCore.collectGenesisKingClaim(...) must be called by the canonical LaunchController contract itself (MineCore rejects EOAs and unrelated contract guardians even if they are the current guardian)
  • Aerodrome WETH/CLAIM pool is empty (no LP minted)
  • LaunchController pins router.weth() and router.defaultFactory() at deployment. Later router.defaultFactory() drift is ignored, but router.weth() or router.poolFor(..., cachedFactory) drift still aborts finalization.

Operational note:

  • Deploy.s.sol sets LaunchController.guardian = INITIAL_OWNER.
  • For the supported production/testnet flow, leave INITIAL_OWNER unset so the deployer remains the temporary owner until after genesis and ownership handoff. On mainnet, finality is deferred and executed through the timelock as the freeze-and-burn ceremony.
  • If INITIAL_OWNER != deployer, Deploy.s.sol requires ALLOW_NON_DEPLOYER_INITIAL_OWNER=true; split-key ownership at deploy time is explicit opt-in. The wrapper scripts/deploy_prod.mjs --deploy --wire refuses that configuration, so use the manual split-key flow instead.
  • Deploy.s.sol simulates the full deploy sequence (constructors + proxy initializations) before broadcasting and fails closed if the wrapped DexAdapter resolves weth() / defaultFactory() to non-contract roots, so a malformed Aerodrome router cannot leave a partial live deployment onchain.
  • Advanced split-key deployments with a contract INITIAL_OWNER remain supported: that pre-existing contract owner/guardian can still perform the first owner-only handoff into LaunchController, because the MineCore genesis lock starts only after the installed guardian is the canonical LaunchController-like contract. That pre-existing contract owner/guardian still cannot call MineCore.collectGenesisKingClaim(...) directly before the handoff.
  • FinalizeGenesis.s.sol assumes one operator key can both call LaunchController.finalizeGenesis() and rotate MineCore.guardian, so keep MineCore.owner == LaunchController.guardian until post-genesis guardian rotation is complete.
  • Accepted mainnet posture: pre-genesis deployments are disposable until LaunchController.finalizeGenesis() completes successfully. Once the canonical LaunchController-style guardian is installed, MineCore will not let operators rotate to a replacement guardian until collectGenesisKingClaim() succeeds. If finalization is miswired or permanently reverting, recover by redeploying/restarting the pre-genesis bundle rather than expecting in-place guardian recovery. Do not treat the deployment as publicly green until finalization completes and MineCore.guardian rotates away from LaunchController.

Flow (all steps execute atomically inside finalizeGenesis()):

  1. Pre-seed pool guard: skim any donations at the pool address
  2. MineCore.collectGenesisKingClaim(to=LaunchController)
  3. Seed liquidity with:
    • the CLAIM minted by MineCore.collectGenesisKingClaim(...) in step 2
    • donated/pre-existing CLAIM already sitting on LaunchController is excluded from the canonical seed and swept to guardian
    • exactly requiredSeedEth worth of WETH (10 ETH on mainnet, proportionally scaled for testnets)
    • LP minted directly to GenesisLPVault24M
  4. GenesisLPVault24M.startLock() (24 months)
  5. MineCore.setTakeoversPaused(false)
  6. MineCore.setGuardian(guardian) — LaunchController atomically rotates the MineCore guardian from itself to the operational guardian baked into its immutable guardian field

Important:

  • finalizeGenesis() does not create a veCLAIM position. The first ve lock still comes from a normal user / Furnace lock flow after launch.

Scripted execution

Production / Base Sepolia:

export PRIVATE_KEY=0x... export GUARDIAN=0x... # required on production/testnet finalize runs, including reruns forge script script/FinalizeGenesis.s.sol:FinalizeGenesis \ --rpc-url "$RPC_URL" \ --broadcast \ -vvv

FinalizeGenesis.s.sol simulates the full finalize + guardian-rotation + postcondition sequence before broadcasting. It fails closed if GUARDIAN is missing or points back at LaunchController, if PRIVATE_KEY no longer controls both LaunchController.guardian and MineCore.owner while post-genesis rotation is pending, if the broadcaster cannot fund the fixed 10 ETH genesis seed, or if the simulated final state still leaves takeovers paused / LP lock unset.

Local Anvil:

make finalize-genesis

FinalizeLocalGenesis.s.sol also simulates the full finalize + guardian-rotation + postcondition sequence before broadcasting, so a bad local state or missing 10 ETH seed balance fails closed before any live tx. make finalize-genesis / scripts/finalize_genesis.sh are local-only helpers and refuse nonlocal RPC chain ids. They also assume LOCAL_PRIVATE_KEY still controls both LaunchController.guardian and MineCore.owner while the post-genesis guardian rotation is pending, reject GUARDIAN == LaunchController, pin GUARDIAN to the derived local deployer during the standard helper flow so ambient shell overrides cannot split that path, and only report success after guardian rotation plus the genesis LP lock state are confirmed. DeployLocalExtras.s.sol also simulates the full local extras constructor sequence before broadcasting, rejects LOCAL_GENESIS_BURN_GUARDIAN != local deployer, and cross-checks the local DexAdapter/router/factory/WETH/pool roots so the local deploy cannot bake an unfinishable genesis path.

After a successful production/testnet finalization:

  • update deployments/<network>.json
  • set aerodrome.claimWethPool.startBlock to the finalization tx block
  • set aerodrome.lpToken.startBlock to the same block
  • re-run Wire.s.sol once so FurnaceEntryTokenRegistry can bind the now-live canonical WETH/CLAIM hop
  • note: Wire.s.sol deploys FurnaceQuoter inline (not via Deploy.s.sol) and wires it into Furnace.setFurnaceQuoter(...). The quoter address is not tracked in deployments/<network>.json. To discover it post-wire, read Furnace.furnaceQuoter() on-chain or from the Wire broadcast log.
  • re-run scripts/verify_deployment.py --expected-guardian "$GUARDIAN" after wiring so the post-genesis hop, guardian target, and manifest start blocks are all verified against the manifest
  • confirm MineCore.guardian == GUARDIAN
  • regenerate mirrors with bash scripts/sync_deployments_all.sh --write and python3 scripts/sync_docs_deployments.py --write

GenesisLPVault24M

Responsibilities:

  • custody the canonical Aerodrome WETH/CLAIM LP from genesis
  • enforce a 24 month lock (INITIAL_LOCK_DURATION = 730 days)
  • allow lpWithdrawRecipient to extend the lock to a later unlockTime (never shorten)

After unlock:

  • withdrawLp() is callable only by lpWithdrawRecipient (recipient-only access control)
  • destination is fixed (lpWithdrawRecipient)

Production / testnet deploy note:

  • GenesisLPVault24M bakes lpWithdrawRecipient immutably at deploy time.
  • Set LP_WITHDRAW_RECIPIENT explicitly before Deploy.s.sol; later ownership transfer does not change the eventual LP withdrawal destination.

Pre-genesis wiring preflight

Before any genesis assets move:

  • Run Wire.s.sol once after Deploy.s.sol
  • Wire.s.sol simulates the full sequence before broadcasting, so treat a preflight revert as a real owner/guardian/config mismatch and fix it before sending live wiring txs
  • Deploy MaintenanceHub only after that first wire pass
  • DeployMaintenanceHub.s.sol resolves its constructor roots from DEPLOYMENTS_MANIFEST_JSON or deployments/<network>.json, so in manual Foundry-only flows refresh the core manifest before broadcasting and treat env roots as cross-checks only
  • In manual Foundry-only flows, update deployments/<network>.json with the MaintenanceHub address/startBlock before re-running Wire.s.sol; the sync scripts only mirror manifests and do not parse broadcasts into JSON
  • Re-run Wire.s.sol after DeployMaintenanceHub.s.sol so keeper roles and any explicitly configured settlement roles are attached; MaintenanceHub itself only joins the settlement-keeper allowlist if ALLOW_MAINTENANCE_HUB_SETTLEMENT_KEEPER=true is set for that pass
  • Run scripts/verify_deployment.py and fix any wiring mismatches before touching genesis funds. If you intentionally allowlisted MaintenanceHub as a settlement keeper, pass --expect-maintenancehub-settlement-keeper too

Production / testnet note:

  • The canonical CLAIM/WETH pool does not have live code until LaunchController.finalizeGenesis() succeeds.
  • Because EntryTokenRegistry.setWethClaimHop(...) requires live pool code, the first two production/testnet wire passes intentionally defer the Furnace registry WETH/CLAIM hop until after genesis finalization.
  • Re-run Wire.s.sol once after finalization to bind that hop.

Operationally important:

  • Do not delay genesis for finality. The v1.0.0 deployment flow launches without freezing and reaches permanent finality only after the external audit.
  • FreezeAndBurn.s.sol is the canonical finality path. ClaimToken freezes at wire time; the script asserts it is already frozen, then freezes the remaining four contracts and burns runtime ProxyAdmins in one timelock batch.
  • Pre-seeding the WETH/CLAIM pool causes LaunchController.finalizeGenesis() to revert with PoolNotEmpty().

Post-genesis checks

After finalizeGenesis() succeeds:

  • LaunchController.genesisFinalized() == true
  • MineCore.takeoversPaused() == false
  • GenesisLPVault24M.lockStartTime() != 0
  • GenesisLPVault24M.lpLockedAmount() > 0
  • FurnaceEntryTokenRegistry.getWethClaimHop().pool == aerodrome.claimWethPool.address
  • MineCoreEntryTokenRegistry.getWethClaimHop().pool may still be address(0) if that registry is takeover-only
  • MineCore.guardian is rotated to the long-term guardian
  • On Base mainnet / Base Sepolia, ownership handoff happens before the freeze-and-burn ceremony. FinalizeOwnership.s.sol initiate mode fails closed unless genesis is finalized, MineCore.guardian has rotated away from LaunchController, and ClaimToken is already frozen plus owner-renounced from Wire.s.sol. Set NEW_OWNER to the deployed TimelockController address, not ADMIN_SAFE. Before running FinalizeOwnership.s.sol, bootstrap the timelock with FinalizeTimelockBootstrap.s.sol and have ADMIN_SAFE pre-schedule the TimelockAcceptOwnership.s.sol batch so the delay matures first. Once the operation is ready, run FinalizeOwnership.s.sol and immediately execute the ready acceptOwnership() batch through the timelock path. Direct protocol contracts still use Ownable2Step; runtime ProxyAdmin contracts use plain Ownable and transfer ownership immediately during initiate mode. Use OWNERSHIP_ADDRS_FROM_ENV=true only for deliberate manual subset recovery, and set at least one explicit ownership target when that mode is enabled.

See also: freeze-and-burn-finality.md.

See also