EntryTokenRegistry and DexAdapter
EntryTokenRegistry is the allowlist and deterministic routing table for multi-token entry. It controls which tokens can be used to enter the Furnace or take over the Crown, and how they swap to CLAIM or ETH. DexAdapter wraps the Aerodrome v2 router to keep the direct roots and proxy-backed runtime decoupled from a specific DEX.
It enables:
- Furnace.enterWithToken(tokenIn, amountIn, …): tokenIn -> CLAIM (direct or via WETH)
- MineCore.takeoverWithToken(tokenIn, amountIn, minEthOut, maxPrice): tokenIn -> WETH -> unwrap -> ETH
Key constraints (v1.0.0):
- no user-supplied routes
- allowlist only
- every hop is validated against allowlisted pools via router.poolFor(…)
Critical token-safety policy:
- fee-on-transfer tokens MUST NOT be allowlisted
- rebasing / elastic-supply tokens MUST NOT be allowlisted
- tokens with transfer hooks or other non-standard ERC20 balance semantics MUST NOT be allowlisted
- only standard ERC20s with fixed balances and vanilla transfer semantics should be enabled in either registry instance
- Furnace enforces this fail-closed at execution time for non-WETH token entry: if the initial transfer into Furnace does not credit exactly
amountIn, the call reverts before any swap executes FurnaceQuoter.quoteEnterWithToken(...)still models the requestedamountIn, so unsupported tokens may quote but are only execution-faithful for standard ERC20 semantics
Policy split: two registry instances
In v1.0.0 the protocol wires two separate instances of the same EntryTokenRegistry contract:
- FurnaceEntryTokenRegistry (deployment manifest key)
- tokens allowed for onboarding into the Furnace
- supports token routes
tokenIn -> CLAIM(direct) ortokenIn -> WETH -> CLAIM(via WETH)
- MineCoreEntryTokenRegistry (deployment manifest key)
- tokens allowed for token-based takeovers
- supports the takeover route
tokenIn -> WETH -> unwrap -> ETH - recommended to start empty or extremely conservative
These labels are used in deployments/<network>.json and on the app’s Security page. They are not different contract types.
This is a MUST, not just a recommendation: do not wire the same registry into both surfaces. Wire.s.sol rejects shared registries at wiring time (registries must differ (policy split)), because separate allowlists reduce the blast radius of a bad token/pool config.
WETH special-case (required)
WETH is always supported without allowlisting:
- token configs forbid tokenIn == wrappedNative
- core entrypoints special-case tokenIn == WETH:
- MineCore.takeoverWithToken(WETH, amountIn, minEthOut, maxPrice): unwrap 1:1, enforce ethOut >= minEthOut
- Furnace.enterWithToken(WETH, amountIn,…): unwrap 1:1, then treat as ETH input
- the non-WETH exact-receipt guard applies only to ERC20 token entry; it does not change the built-in WETH unwrap path
- helper route resolution special-cases WETH too:
FurnaceQuoter.resolveFurnaceRoute(WETH)and SDKresolveFurnaceEntryRoute({ tokenIn: WETH })return the canonical single-hopWETH -> CLAIMroute built from the pinned WETH/CLAIM hop- they report
routeTokenId = 1(VIA_WETH) because this is the WETH-boundary path, not an allowlisted directtokenIn -> CLAIMconfig
RouterConfig
Registry stores global router config:
- router (v1.0.0 expects DexAdapter)
- factory (must equal router.defaultFactory())
- wrappedNative (must equal router.weth())
- claimToken (must match the immutable ClaimToken)
Important invariants:
- router, factory, wrappedNative, and claimToken must all be live contracts when set
- wrappedNative and claimToken are immutable after first set
- once the global WETH/CLAIM hop or any per-token pool config has been set, router/factory rewiring is no longer allowed in-place
- previously validated
expectedPooladdresses are tied to that router/factory pair - use a fresh registry deployment if you must migrate routers or factories after route surfaces are configured
- previously validated
Access control:
- Router/hop/token-config setters are
onlyOwner. In v1.0.0, the live owner path is theTimelockControllergoverned by the Safe. - Runtime safeguards prevent dangerous in-place changes even without a freeze: once the global WETH/CLAIM hop or any per-token pool config has been set, router/factory rewiring is no longer allowed in-place (see above).
- The guardian can disable tokens only; enabling and routing config changes require the owner.
Canonical WETH/CLAIM hop
The registry stores the pinned WETH/CLAIM pool:
- used for Furnace ETH entry (ETH -> CLAIM)
- used for token routes that go tokenIn -> WETH -> CLAIM
setWethClaimHop(...)requires live code at the pool address, so production/testnet genesis flows must defer this binding untilLaunchController.finalizeGenesis()creates the canonical pool onchain- a takeover-only registry wired only into MineCore may leave this hop unset; the Furnace-wired registry requires it
- per-token pool bindings have the same live-code rule:
tokenWethPooland any directtokenClaimPoolmust already exist onchain when configured
Per-token TokenConfig
Each allowlisted token has:
- enabled (gate inside this registry instance)
- tokenIn -> WETH hop (always required)
- optional tokenIn -> CLAIM direct hop (Furnace-only)
Route resolution (ABI surface):
resolveTakeoverRoute(tokenIn) -> RegistryRoute[]- always a single hop:
tokenIn -> WETH
- always a single hop:
resolveFurnaceRoute(tokenIn) -> (RegistryRoute[] route, uint256 routeTokenId)- if
directToClaimEnabled:- route:
[tokenIn -> CLAIM] routeTokenId = 0(DIRECT_TO_CLAIM)
- route:
- else:
- route:
[tokenIn -> WETH, WETH -> CLAIM] routeTokenId = 1(VIA_WETH)
- route:
- if
Important: the registry itself still forbids tokenIn == WETH.
- WETH route introspection lives in FurnaceQuoter / SDK helpers, not in
EntryTokenRegistry.resolveFurnaceRoute(...) - WETH helper resolution returns
[WETH -> CLAIM]withrouteTokenId = 1
routeTokenId is a stable codebook pinned by docs/spec/entry-token-registry-v1.0.0.md.
Note: This routeTokenId refers to the swap route codebook (0 = DIRECT_TO_CLAIM, 1 = VIA_WETH). The 4th return value of FurnaceQuoter.quoteEnterWith* is also named routeTokenId but carries the destination lock’s token ID (or 0 for a new lock). Do not conflate the two.
Guardian: disable-only token response + emergency key rotation
EntryTokenRegistry has a guardian role for incident response:
- owner can enable and disable tokens
- guardian can disable tokens only (emergency safety valve); guardian calling
setTokenEnabled(token, true)reverts withNotAuthorized - 1-hour cooldown: when guardian disables a token,
guardianDisabledUntil[tokenIn]is set toblock.timestamp + 1 hours. During this window, the owner cannot re-enable that token (bothsetTokenEnabledandsetTokenConfigwithenabled=truerevert). This gives incident responders time to escalate before the owner can override. - guardian rotation via
setGuardian(address)remains available for emergency key replacement
This is how the protocol can rapidly remove a problematic token or pool route without reopening routing config.
DexAdapter
DexAdapter is the Aerodrome v2 router wrapper.
Why it exists:
- keeps the protocol’s direct roots and runtime quartet from hard-binding to a specific DEX router forever
- pins the minimal swap ABI surface used by MineCore and Furnace
Exception: LpStakingVault7D uses the raw Aerodrome router directly (IAerodromeRouter) for its WETH→CLAIM fee-harvest swap path. It does not route through DexAdapter because the vault’s swap surface is independent of the EntryTokenRegistry routing table — it only ever swaps harvested LP fees on the canonical WETH/CLAIM pool, not arbitrary user-supplied token routes.
Operational model:
- the constructor rejects zero / non-contract router pins, then snapshots that live router’s
defaultFactory()andweth()immutably at deployment time aerodromeRouterandaerodromeFactoryare immutable — DexAdapter cannot change its router after deployment- if the upstream DEX must be migrated, deploy a fresh DexAdapter, a fresh EntryTokenRegistry, and rewire the consuming core contract via the timelocked owner
Quoters (recommended)
Token-entry calls require user slippage guards:
- Furnace:
minVeOut - MineCore takeoverWithToken:
minEthOut
Do not attempt to recreate routing offchain by hand.
Use canonical quote surfaces:
- FurnaceQuoter (resolve via
Furnace.furnaceQuoter()):quoteEnterWithEthquoteEnterWithClaimquoteEnterWithToken- Notes: quote calls revert if
furnaceQuoteris unset orlockingPaused == true.
- MineCore token takeovers use a separate view contract:
MineCoreQuoter.quoteTakeoverWithToken(tokenIn, amountIn) -> (ethOut, takeoverPrice)MineCoreQuoter.resolveTakeoverRoute(tokenIn) -> RegistryRoute[]- Pause caveat:
quoteTakeoverWithToken(...)reverts whiletakeoversPaused == true, butresolveTakeoverRoute(...)remains callable for route discovery because it does not enforce the takeover pause gate.
Why MineCoreQuoter exists:
- keeps MineCore bytecode smaller (DEX quote + validation lives here)
- mirrors MineCore’s swap validation:
- routes are registry-resolved (no user-supplied routes)
- allowlisted pool must match
router.poolFor(...) - tokenIn == wrapped native is special-cased (unwrap 1:1)
Agent / UI recipe (token takeovers):
- quote
ethOutfor your chosenamountIn - choose
slippageBps - compute
minEthOut = ethOut * (10_000 - slippageBps) / 10_000 - call
MineCore.takeoverWithToken(tokenIn, amountIn, minEthOut, maxPrice)
Agent SDK helpers
The TypeScript Agent SDK provides a canonical way to consume these routes and quotes. The quoting helpers below take the configured contracts map returned by await getClaimRushContracts({ publicClient, manifest, abiNetwork }) (async — resolves FurnaceQuoter from chain):
- Spot swap quoting via the protocol’s DexAdapter:
quoteDexAmountsOut({ contracts, amountIn, routes })
- Route resolution (allowlisted):
resolveMineCoreTakeoverRoute({ contracts, tokenIn })(token -> WETH)resolveFurnaceEntryRoute({ contracts, tokenIn })(token -> CLAIM direct or via WETH; fortokenIn == WETH, returns the canonical single-hopWETH -> CLAIMroute withrouteTokenId = 1)
- Common spot price helpers:
quoteEthToClaim({ contracts, ethIn })quoteClaimToEth({ contracts, claimIn })quoteEntryTokenToEth({ contracts, tokenIn, amountIn })quoteEntryTokenToClaim({ contracts, tokenIn, amountIn })
For a “bot-ready” aggregated snapshot (CLAIM spot + all enabled entry tokens), use:
getLivePrices({ contracts, publicClient, subgraphUrl, ... })getLivePrices({ contracts, publicClient, entryTokens, ... })when you want to bypass subgraph token discovery- Optional caching / throttling for polling loops:
getLivePrices({ contracts, publicClient, subgraphUrl, cache: createLivePricesCache(), ... }) - CLI:
npm -C agents/sdk run example:prices - CLI with caching:
PRICES_CACHE=1 ... npm -C agents/sdk run example:prices
See also
- Furnace — lock entry paths that consume token swaps
- Core Mechanics — takeover loop and entry token flows
- Security, Guardian, Pausing — token disable via Guardian
- Getting Started — deployed addresses
- User manual: Buy CLAIM