Skip to Content
Security, Guardian, Pausing

Security, Guardian, Pausing

This page explains:

  • roles (admin vs guardian)
  • pause surfaces
  • what happens when paused
  • how clients and bots should behave

Roles (high level)

Admin (owner):

  • config wiring via onlyOwner setters, governed by the live TimelockController
  • contracts enforce the live owner() path; in v1.0.0 that owner path is the TimelockController governed by the Safe
  • cannot use pause surfaces as a substitute for governance

Guardian:

  • fast incident response
  • long-term role is pause/unpause on core surfaces, subject to the MineCore genesis gate that blocks setTakeoversPaused(false) until genesisKingClaimCollected == true, disable-only response on registries, and documented emergency self-rotation via setGuardian(address)
  • launch-phase exception: MineCore.guardian may temporarily be the LaunchController contract, which also has the one-shot collectGenesisKingClaim(address) privilege used by finalizeGenesis(); installing that temporary contract guardian is owner-only, and only that canonical LaunchController-like guardian for this exact MineCore + CLAIM pair may use the privilege
  • production v1.0.0 uses a dedicated guardian multisig with strict op policy; the contracts enforce only the configured guardian address

Canonical reference:

  • docs/security/roles-and-permissions-matrix-v1.0.0.md

Config governance model

What is direct vs upgradeable

ClaimToken and VeClaimNFT are direct permanent roots.

MineCore, Furnace, MarketRouter, and ShareholderRoyalties are deployed behind transparent proxies. Their proxy addresses are the canonical runtime addresses, and governed upgrades happen through timelock-owned ProxyAdmin contracts until the final freeze-and-burn ceremony burns those proxy admins.

Freeze-and-burn finality

Five core contracts implement configFrozen + freezeConfig():

ContractFrozen setters
ClaimTokensetMineCore()
FurnacesetShareholderRoyalties(), setMineCore(), setMineMarket(), setFurnaceQuoter(), setLpRewardsVault()
MineCoresetFurnace(), setClaimAllHelper()
VeClaimNFTsetFurnace(), setMineMarket()
ShareholderRoyaltiessetWiring() (mineCore, mineMarket, furnace), setClaimAllHelper()

Each contract:

  • configFrozen (public bool, readable onchain)
  • freezeConfig() onlyOwner (one-way, irreversible)
  • whenNotFrozen modifier on frozen setters

Before freezing, each freezeConfig() validates that frozen pointers are non-zero. The current implementation also validates the canonical reciprocal bundle before locking: ClaimToken verifies MineCore identity, MineCore verifies the reciprocal Furnace bundle and ClaimAllHelper reciprocal wiring (helper.mineCore, helper.royalties, and same-helper equality with ShareholderRoyalties), VeClaimNFT verifies the canonical setter bundle, ShareholderRoyalties verifies the live Baron bundle and ClaimAllHelper reciprocal wiring (helper.royalties, helper.mineCore, and same-helper equality with MineCore), and Furnace verifies the canonical market bundle plus its pinned furnaceQuoter, guardian, and (if configured) lpRewardsVault.

The FreezeAndBurn.s.sol script is the canonical finality path. ClaimToken freezes at wire time; the script asserts it is already frozen. The batch freezes the remaining four contracts, then burns runtime ProxyAdmin ownership. Post-finality verification is performed by verify_deployment.py --require-frozen.

Atomicity (MANDATORY): The canonical mainnet finality step is a single timelock batch that freezes first and burns proxy-admin ownership second. If any freezeConfig() precondition fails, the entire batch reverts and no runtime ProxyAdmin is burned.

Operational peripherals (setDelegationHub, setEntryTokenRegistry, keeper allowlists, guardian rotation, metadata URIs) remain onlyOwner after freeze. The delayed Furnace emergency LP-vault recovery path (requestEmergencyVaultRewire(address), cancelEmergencyVaultRewire(), executeEmergencyVaultRewire()) also remains available after freeze as a narrow owner-only recovery mechanism for broken LP vaults. For the proxy-backed runtime quartet, freeze alone does not disable proxy-admin upgrades. The later burn is what removes that authority permanently.

Peripheral contracts: timelock-protected configurability

Core game-rule wiring setters on MineCore, VeClaimNFT, Furnace, and ShareholderRoyalties are onlyOwner whenNotFrozen — they are permanently locked after the finality batch runs. Furnace’s delayed emergency LP-vault recovery path is the explicit exception and remains owner-callable after finality. MarketRouter and EntryTokenRegistry wiring setters are onlyOwner with no freeze gate. In production, the owner is the timelock, so wiring changes require a governance delay.

Key runtime safeguards that constrain owner power even without a freeze:

  • Furnace.setMineCore(address _mineCore) atomically assigns guardian = _mineCore; no separate setGuardian call is needed. Furnace.setGuardian(address) only allows re-asserting the current MineCore, preserving the single pause surface
  • Wiring order: Furnace.setMineCore MUST be called before MineCore.setFurnace — MineCore validates Furnace.mineCore() == address(this) (reciprocal wiring check)
  • EntryTokenRegistry cannot change router / factory in place after route surfaces exist
  • Before freeze, Furnace cannot change or clear lpRewardsVault while already-earned LP liability remains attributable to the current vault

Launch sequence

Sequence: deploy, wire, stealth launch (live and playable), ownership finalization (transfer to timelock), timelock bootstrap, external audit, apply fixes if needed, schedule freeze-and-burn finality, wait the delay, execute the finality batch.

The protocol is fully playable before finality. The audit window allows wiring fixes before core game-rule pointers are permanently locked. The audit/governance window also allows governed runtime upgrades of the proxy-backed quartet without changing their live addresses. The timelock delay before the final batch is a public countdown to finality, not just an operational buffer.

Operational controls (always available)

These remain callable regardless of freeze status:

  • Guardian pausing on all pause surfaces
  • setGuardian(address) on rotatable surfaces (emergency key rotation), subject to per-contract pinning rules such as Furnace.guardian remaining pinned to MineCore
  • Operational allowlists:
    • MarketRouter.setSettlementKeeper(...)
    • ShareholderRoyalties.setAutoCompoundKeeper(...)
    • ShareholderRoyalties.setMinAutoCompoundEth(...) (global dust-guard floor for auto-compound)
    • LpStakingVault7D.setHarvestKeeper(...)
    • LpStakingVault7D.setMinCompoundReward(...) (global dust-guard floor for LP auto-compound)
    • LpStakingVault7D.setMinHarvestClaimFloor(...) (minimum CLAIM output to trigger a fee harvest)
  • MineCore genesis exception: before genesisKingClaimCollected == true, only owner may install the temporary contract guardian (LaunchController). Once installed, MineCore guardian rotation is locked until genesisKingClaimCollected == true. A pre-existing contract owner/guardian from split-key deployment does not trigger that lock and cannot call collectGenesisKingClaim(address) before the canonical handoff.

What agents and integrators should do

  • Check configFrozen on all 5 core contracts to detect whether game-rule wiring is permanently locked
  • Monitor for ownership changes on the live timelock owner path and the four runtime ProxyAdmins
  • Monitor live pause flags, guardian/owner changes, registry token disables, operational allowlist updates, and runtime implementation / proxy-admin owner changes

Pause surfaces

MineCore: takeoversPaused

Controlled by:

  • MineCore.guardian

Operational note:

  • guardian can always pause takeovers
  • unpausing (setTakeoversPaused(false)) reverts with Errors.GenesisKingClaimNotCollected until genesisKingClaimCollected == true

Effect when true:

  • takeover(maxPrice) reverts with Errors.TakeoversPaused
  • takeoverWithToken(tokenIn, amountIn, minEthOut, maxPrice) reverts with Errors.TakeoversPaused
  • getTakeoverPrice still works

Mining safety (important):

  • On pause transitions, MineCore clamps currentReignLastAccrualTime to block.timestamp.
  • This means paused time is never mined later.

Furnace: lockingPaused

Controlled by:

  • MineCore.guardian via MineCore.setLockingPaused(bool) forwarding into Furnace
  • Furnace.guardian is atomically set to _mineCore inside Furnace.setMineCore(address _mineCore); no separate setGuardian call is needed. Rotate the human guardian on MineCore, not on Furnace

Effect when true:

  • enterWithEth / enterWithClaim / enterWithToken reverts with Errors.LockingPaused
  • sellLockToFurnaceFromMarket and the MarketRouter sell paths that depend on it also revert with Errors.LockingPaused
  • FurnaceQuoter quote views (quoteEnterWith*, quoteSellLockToFurnace*, quoteSellLockForExecution) also revert (same pause gate)
  • ShareholderRoyalties claim mode=lock continues to work only when locking is enabled

Canonical single-switch wiring:

  • MineCore.setLockingPaused(bool) forwards to Furnace
  • Furnace.guardian MUST be MineCore (atomically assigned inside Furnace.setMineCore)

MarketRouter: tradingPaused

Controlled by:

  • MarketRouter.guardian

Effect when true:

  • listLock, sellLockToFurnace, sellListedLockToFurnace, createBonusTargetEscrowWithTarget, executeAutoFurnace, and extendBonusTargetEscrowExpiry revert with Errors.TradingPaused
  • unwind / housekeeping still work:
    • delistLock(tokenId)
    • cancelExpiredListing(tokenId) (permissionless after listing expiry)
    • cancelBonusTargetEscrow(offerId) (buyer while canonical; anyone as refund-only rescue if the router is no longer canonical for the live market bundle)
    • cancelExpiredBonusTargetEscrow(offerId) (permissionless after offer expiry)
    • emergencyDelist(tokenId) (seller-only after 7 days)

EntryTokenRegistry: disable tokens

Controlled by:

  • owner can enable and disable tokens
  • guardian can disable tokens only (emergency safety valve); guardian calling setTokenEnabled(token, true) reverts with NotAuthorized
  • 1-hour cooldown after guardian disable: when the guardian disables a token, the owner cannot re-enable it for 1 hour (GUARDIAN_DISABLE_COOLDOWN). Both setTokenEnabled(enabled=true) and setTokenConfig(enabled=true) respect this cooldown. This gives incident responders time to escalate before the owner can override.
  • guardian rotation via setGuardian(address) remains available for emergency key replacement

Effect:

  • disabled tokens cannot be used for:
    • Furnace.enterWithToken
    • MineCore.takeoverWithToken

WETH is not allowlisted and is always supported via the WETH special-case.

What pause does not mean

There is no global onchain maintenance pause.

  • MaintenanceHub.poke(...) stays permissionless onchain.
  • Keeper pause files and circuit breakers are offchain safety rails for the team-operated bot only.
  • Those keeper-local controls do not block public users or other callers from hitting onchain entrypoints.
  • Individual subcalls inside MaintenanceHub.poke(...) still obey their own onchain pause checks (tradingPaused, lockingPaused, takeoversPaused) and may fail best-effort.

Keeper daemon behaviour during pause (important clarification)

KEEPER_PAUSED=1 or a pause file prevents sendContractTx() from submitting onchain transactions, but the daemon loop continues running read-only tasks (scanning, quoting, logging). For full quiescence, stop the keeper process — do not rely on the pause file alone.


Guardian activation criteria (practical)

The guardian should pause only for clear protocol safety reasons. Common triggers:

  • takeover pricing anomalies (unexpected getTakeoverPrice outputs, reference price corruption)
  • unexpected revert spikes on core paths (takeover, enter, offer acceptance)
  • swap routing anomalies (registry routes mismatching router.poolFor validation)
  • reserve accounting inconsistencies (reserve underflow attempts, abnormal bonus quotes)
  • suspected exploit paths or dependency failures (DEX router issues, pool hijack risk)

What clients should do when paused

UI behavior:

  • detect pause flags and display a clear banner
  • disable affected CTAs
  • keep exit paths available (delist, cancel offer, withdraw balances)

Bot behavior:

  • stop calling:
    • takeovers when takeoversPaused
    • Furnace entry when lockingPaused
    • executeAutoFurnace when tradingPaused
  • keep calling MaintenanceHub.poke({ offerIds: [], maxOffers: 0 }) for non-swap upkeep

User funds safety (important)

  • Pausing does not seize funds.
  • Pausing only prevents new state transitions on the paused surfaces.
  • Pull-based withdrawals remain available:
    • MineCore.withdrawKingBalance
    • MineCore.withdrawRefundBalance
  • Market unwind / housekeeping paths remain available:
    • MarketRouter.delistLock / cancelExpiredListing / cancelBonusTargetEscrow / cancelExpiredBonusTargetEscrow / emergencyDelist

FAQ for integrators: what happens when guardian pauses?

Takeovers paused (MineCore.takeoversPaused = true):

  • New takeovers revert.
  • The current reign cannot end.
  • Crown emission accrual is clamped at the pause boundary.
    • There is no “backpay” when unpaused.
  • UI should:
    • hide/disable takeover buttons
    • keep price display, history, and Crown ETH withdrawal UX

Locking paused (Furnace.lockingPaused = true):

  • New Furnace entries revert.
  • All FurnaceQuoter quoteEnterWith* and quoteSellLock* views revert.
  • MarketRouter sell paths that settle into Furnace also revert: sellLockToFurnace, sellListedLockToFurnace (downstream Furnace.sellLockToFurnaceFromMarket has whenLockingEnabled).
  • Barons ETH collection still works in mode 0 (Collect ETH).
  • Auto-compound paths behave safely:
    • ShareholderRoyalties.compoundFor(user) bubbles the Furnace revert atomically, so claimable ETH and cadence state stay untouched.
    • ShareholderRoyalties.compoundForMany(users[], maxUsers) restores the affected user’s accounting on downstream Furnace failure, but canonical Baron-bundle drift discovered during checkpointUser(user) can still fail close before mutation and revert the batch.
    • LP vault auto-compound keeps rewards intact on revert.
  • UI should:
    • disable lock actions and compounding CTAs
    • keep Collect ETH CTA available

Trading paused (MarketRouter.tradingPaused = true):

  • Market actions revert (listLock, sellLockToFurnace, sellListedLockToFurnace, createBonusTargetEscrowWithTarget, executeAutoFurnace, extendBonusTargetEscrowExpiry).
  • Unwind / housekeeping stay open:
    • delistLock, cancelExpiredListing, cancelBonusTargetEscrow, cancelExpiredBonusTargetEscrow, emergencyDelist (seller-only after 7d)
  • UI should:
    • disable new listings and bonus target escrow actions
    • keep cancel/delist buttons available

Token disabled (EntryTokenRegistry token enabled=false):

  • enterWithToken and/or takeoverWithToken revert for that token.
  • ETH entry remains available.
  • UI should:
    • gray out the token in token pickers
    • explain that it was disabled for safety

See also