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 daysMineCore.takeoversPaused() == trueMineCore.guardian() == LaunchControllerduring the genesis window- the pre-genesis handoff that installs
LaunchControllerasMineCore.guardianmust be done byMineCore.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)
LaunchControllerpinsrouter.weth()androuter.defaultFactory()at deployment. Laterrouter.defaultFactory()drift is ignored, butrouter.weth()orrouter.poolFor(..., cachedFactory)drift still aborts finalization.
Operational note:
Deploy.s.solsetsLaunchController.guardian = INITIAL_OWNER.- For the supported production/testnet flow, leave
INITIAL_OWNERunset 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.solrequiresALLOW_NON_DEPLOYER_INITIAL_OWNER=true; split-key ownership at deploy time is explicit opt-in. The wrapperscripts/deploy_prod.mjs --deploy --wirerefuses that configuration, so use the manual split-key flow instead. Deploy.s.solsimulates the full deploy sequence (constructors + proxy initializations) before broadcasting and fails closed if the wrappedDexAdapterresolvesweth()/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_OWNERremain supported: that pre-existing contract owner/guardian can still perform the first owner-only handoff intoLaunchController, 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 callMineCore.collectGenesisKingClaim(...)directly before the handoff. FinalizeGenesis.s.solassumes one operator key can both callLaunchController.finalizeGenesis()and rotateMineCore.guardian, so keepMineCore.owner == LaunchController.guardianuntil 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 untilcollectGenesisKingClaim()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 andMineCore.guardianrotates away fromLaunchController.
Flow (all steps execute atomically inside finalizeGenesis()):
- Pre-seed pool guard: skim any donations at the pool address
MineCore.collectGenesisKingClaim(to=LaunchController)- Seed liquidity with:
- the
CLAIMminted byMineCore.collectGenesisKingClaim(...)in step 2 - donated/pre-existing
CLAIMalready sitting onLaunchControlleris excluded from the canonical seed and swept toguardian - exactly
requiredSeedEthworth of WETH (10 ETH on mainnet, proportionally scaled for testnets) - LP minted directly to
GenesisLPVault24M
- the
GenesisLPVault24M.startLock()(24 months)MineCore.setTakeoversPaused(false)MineCore.setGuardian(guardian)— LaunchController atomically rotates the MineCore guardian from itself to the operational guardian baked into its immutableguardianfield
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 \
-vvvFinalizeGenesis.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-genesisFinalizeLocalGenesis.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.startBlockto the finalization tx block - set
aerodrome.lpToken.startBlockto the same block - re-run
Wire.s.solonce soFurnaceEntryTokenRegistrycan bind the now-live canonical WETH/CLAIM hop - note:
Wire.s.soldeploysFurnaceQuoterinline (not viaDeploy.s.sol) and wires it intoFurnace.setFurnaceQuoter(...). The quoter address is not tracked indeployments/<network>.json. To discover it post-wire, readFurnace.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 --writeandpython3 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
lpWithdrawRecipientto extend the lock to a laterunlockTime(never shorten)
After unlock:
withdrawLp()is callable only bylpWithdrawRecipient(recipient-only access control)- destination is fixed (
lpWithdrawRecipient)
Production / testnet deploy note:
GenesisLPVault24MbakeslpWithdrawRecipientimmutably at deploy time.- Set
LP_WITHDRAW_RECIPIENTexplicitly beforeDeploy.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.solonce afterDeploy.s.sol Wire.s.solsimulates 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
MaintenanceHubonly after that first wire pass DeployMaintenanceHub.s.solresolves its constructor roots fromDEPLOYMENTS_MANIFEST_JSONordeployments/<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>.jsonwith theMaintenanceHubaddress/startBlock before re-runningWire.s.sol; the sync scripts only mirror manifests and do not parse broadcasts into JSON - Re-run
Wire.s.solafterDeployMaintenanceHub.s.solso keeper roles and any explicitly configured settlement roles are attached;MaintenanceHubitself only joins the settlement-keeper allowlist ifALLOW_MAINTENANCE_HUB_SETTLEMENT_KEEPER=trueis set for that pass - Run
scripts/verify_deployment.pyand fix any wiring mismatches before touching genesis funds. If you intentionally allowlistedMaintenanceHubas a settlement keeper, pass--expect-maintenancehub-settlement-keepertoo
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.solonce 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.solis the canonical finality path. ClaimToken freezes at wire time; the script asserts it is already frozen, then freezes the remaining four contracts and burns runtimeProxyAdmins in one timelock batch.- Pre-seeding the WETH/CLAIM pool causes
LaunchController.finalizeGenesis()to revert withPoolNotEmpty().
Post-genesis checks
After finalizeGenesis() succeeds:
LaunchController.genesisFinalized() == trueMineCore.takeoversPaused() == falseGenesisLPVault24M.lockStartTime() != 0GenesisLPVault24M.lpLockedAmount() > 0FurnaceEntryTokenRegistry.getWethClaimHop().pool == aerodrome.claimWethPool.addressMineCoreEntryTokenRegistry.getWethClaimHop().poolmay still beaddress(0)if that registry is takeover-onlyMineCore.guardianis rotated to the long-term guardian- On Base mainnet / Base Sepolia, ownership handoff happens before the freeze-and-burn ceremony.
FinalizeOwnership.s.solinitiate mode fails closed unless genesis is finalized,MineCore.guardianhas rotated away fromLaunchController, andClaimTokenis already frozen plus owner-renounced fromWire.s.sol. SetNEW_OWNERto the deployedTimelockControlleraddress, notADMIN_SAFE. Before runningFinalizeOwnership.s.sol, bootstrap the timelock withFinalizeTimelockBootstrap.s.soland haveADMIN_SAFEpre-schedule theTimelockAcceptOwnership.s.solbatch so the delay matures first. Once the operation is ready, runFinalizeOwnership.s.soland immediately execute the readyacceptOwnership()batch through the timelock path. Direct protocol contracts still useOwnable2Step; runtimeProxyAdmincontracts use plainOwnableand transfer ownership immediately during initiate mode. UseOWNERSHIP_ADDRS_FROM_ENV=trueonly 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
- Protocol Overview — architecture and contract relationships
- Core Mechanics — genesis accrual bucket and emission schedule
- Getting Started — deployed addresses
- Constants Reference — shared
Constants.solvalues used around genesis