Files
zeekayandHanzo Dev 84587ddb0f Resolve the market before a swap can name a halt
SettleSwap took the caller's PoolKey and never asked whether that pool
existed, so key.ID() fed the halt slot, the volume shard and the fill
event alike. A fabricated key names a slot nobody set, and the swap
settles while the real market is halted.

Resolving once above the phase switch and passing the resolved poolID
down means no phase -- including one added later -- can key on anything
the registry has not admitted. checkHalt no longer accepts a key, so it
cannot derive one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:41:14 -07:00

139 lines
6.8 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"errors"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
)
// halt9999.go is the MULTI-LAYER HALT for the 0x9999 settlement path. Authority
// is the per-network DEX GOVERNANCE CONTROLLER — a governance CONTRACT (Governor/
// Timelock/multisig), resolved at RUNTIME from the host's deployment topology via
// contract.AtomicState.GovernanceController(), the SAME runtime seam networkID/
// cChainID/dChainID flow through. It is NEVER a hardcoded address and NEVER an EOA
// derivable from a dev mnemonic: a single admin key could censor an asset or DoS
// the whole DEX, so the authority is decentralized governance, configured per net.
//
// FAIL-CLOSED authority: when no governance controller is configured on this
// network (the zero address — e.g. a network that has not yet deployed its Governor),
// the halt setters are UNCALLABLE (governanceController returns ErrSettleNoGovernance
// and every setHalt reverts). An unset authority can therefore DoS no one — strictly
// safer than a default key. Halt is governance, never a single hardcoded key.
//
// Halt state is DURABLE, consensus-shared StateDB read STRAIGHT from state on
// every settle (never cached in a process field). This is the RED V6 invariant
// the existing pause path relies on: if a halt-set tx reverts, its StateDB write
// reverts with it and the next call reads the un-halted value, so a reverted halt
// can never wedge the DEX permanently. A halted swap reverts cleanly with NO
// partial state (EVM revert rolls back every prior SetState/balance change).
//
// SAFE DEFAULT: halting stops NEW swaps but NEVER strands funds — withdraw /
// balanceOf / cancel / settle remain callable so escrowed value can always exit.
// withdraw dispatches directly on 0x9999 (settle_module.go) into the seam reserve,
// and it is deliberately NOT gated here: a halt exists to stop new exposure, and
// gating the exit would turn every halt into a freeze of funds already escrowed.
// Leaving it open is safe because it moves only what the caller already deposited —
// runSettleWithdraw clamps the amount to that caller's own depositor claim and again
// to the vault total, and debits both before the transfer — so it can neither open
// new exposure nor reach another account's funds.
// Halt layers (each an independent key; checked in order, cheapest scope first).
// The BLS-era certType / validatorSet halt layers are GONE with the cert value
// path — the native seam has no cert scheme and no validator set to halt. The
// real kill switches (global / market / asset) remain.
var (
haltGlobalKey = makeStorageKey([]byte(settleStateNamespace+"h.glb"), []byte{})
haltMarketPrefix = []byte(settleStateNamespace + "h.mkt")
haltAssetPrefix = []byte(settleStateNamespace + "h.ast")
)
// Halt errors (each a clean revert reason).
var (
ErrDEXHalted = errors.New("dex: settlement halted (global)")
ErrMarketHalted = errors.New("dex: settlement halted for this market")
ErrAssetHalted = errors.New("dex: settlement halted for this asset")
)
// ErrSettleNoGovernance is the fail-closed authority error: the host wired no
// AtomicState, or the network's governance controller is unset (the zero address).
// Every governance-gated 0x9999 op (halt + pot seeding) returns it rather than fall
// back to any default authority — there is NO hardcoded admin key to fall back to.
var ErrSettleNoGovernance = errors.New("dex: no DEX governance controller configured on this network — governance ops are fail-closed (a governance contract must be set per network; never a dev-mnemonic EOA)")
// governanceController resolves the per-network DEX governance authority from the
// runtime AtomicState capability — the SOLE source of "who may halt / seed", the
// SAME seam networkID/cChainID/dChainID flow through. It is fail-closed on two
// counts: the host wired no AtomicState (single-chain dev / non-atomic harness), or
// the network has no governance controller configured (the zero address). In either
// case it returns ErrSettleNoGovernance, so a governance op reverts cleanly with no
// state touched and there is no default key to abuse. The returned address is a
// governance CONTRACT (Governor/Timelock/multisig), never a mnemonic-derivable EOA.
func governanceController(state contract.AccessibleState) (common.Address, error) {
atomicState, ok := state.(contract.AtomicState)
if !ok {
return common.Address{}, ErrSettleNoGovernance
}
gov := atomicState.GovernanceController()
if gov == (common.Address{}) {
return common.Address{}, ErrSettleNoGovernance
}
return gov, nil
}
// haltSet is the non-zero sentinel a halt slot holds when active. Any non-zero
// value means halted; we write a fixed 1 for clarity.
var haltSet = common.Hash{31: 1}
func isHalted(stateDB stateKV, key common.Hash) bool {
return stateDB.GetState(poolManagerAddr9999, key) != (common.Hash{})
}
// checkHalt is the single, ordered halt gate the native settle handler calls
// before any value movement, for BOTH phases (order and settlement). It takes the
// scopes already resolved — a REGISTERED poolID (marketID) and the swap's two asset
// ids — and derives nothing itself, so the id it consults is never one the caller
// steered. Those are the SAME ids SetHaltMarket / SetHaltAsset, the registry,
// analytics and StateView use. A halted scope reverts cleanly with no partial state.
// Returns the FIRST applicable halt error, or nil.
func checkHalt(stateDB stateKV, poolID, in, out [32]byte) error {
if isHalted(stateDB, haltGlobalKey) {
return ErrDEXHalted
}
if isHalted(stateDB, makeStorageKey(haltMarketPrefix, poolID[:])) {
return ErrMarketHalted
}
if isHalted(stateDB, makeStorageKey(haltAssetPrefix, in[:])) ||
isHalted(stateDB, makeStorageKey(haltAssetPrefix, out[:])) {
return ErrAssetHalted
}
return nil
}
// --- Governance setters (governance-controller-gated; the caller resolves authority
// via governanceController() and checks caller == gov before invoking these).
// SetHaltGlobal halts/unhalts all new settlement.
func SetHaltGlobal(stateDB stateKV, on bool) { setHalt(stateDB, haltGlobalKey, on) }
// SetHaltMarket halts/unhalts settlement for one market (poolKeyHash / marketID).
func SetHaltMarket(stateDB stateKV, marketID [32]byte, on bool) {
setHalt(stateDB, makeStorageKey(haltMarketPrefix, marketID[:]), on)
}
// SetHaltAsset halts/unhalts settlement touching one asset (injective AssetID).
func SetHaltAsset(stateDB stateKV, assetID [32]byte, on bool) {
setHalt(stateDB, makeStorageKey(haltAssetPrefix, assetID[:]), on)
}
func setHalt(stateDB stateKV, key common.Hash, on bool) {
if on {
stateDB.SetState(poolManagerAddr9999, key, haltSet)
} else {
stateDB.SetState(poolManagerAddr9999, key, common.Hash{})
}
}