mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
Both sides carried work. Ours is the security pass: the stableswap amp=0 and empty-pool halts, the wei-per-swap leak, uint256 truncation in math, three verifiers that returned a verdict without verifying, the cgo/non-cgo consensus split in cggmp21, and the starkfri soundness floor. Theirs is the fhe parameter set, the mldsa crafted-length refusal, and the naming sweep. Resolved per hunk rather than by side. Where both fixed one defect the better fix survives alone, never two guards for one bug.
350 lines
17 KiB
Go
350 lines
17 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"
|
|
"github.com/luxfi/precompile/modules"
|
|
"github.com/luxfi/precompile/precompileconfig"
|
|
)
|
|
|
|
// 0x9999 settlement governance (setHaltGlobal/Market/Asset, seedSeamReserve) is
|
|
// gated to the per-NETWORK DEX GOVERNANCE CONTROLLER, resolved
|
|
// at RUNTIME from contract.AtomicState.GovernanceController() (the SAME runtime seam
|
|
// networkID/cChainID/dChainID flow through) — see governanceController in halt9999.go.
|
|
//
|
|
// There is NO hardcoded governance address. A single hardcoded EOA (the prior
|
|
// DefaultDAOTreasury) was mnemonic-derivable, so anyone with that mnemonic could halt
|
|
// an asset (censor) or halt globally (DoS the whole DEX). The authority is now a
|
|
// per-network governance CONTRACT (Governor/Timelock/multisig), configured by the host
|
|
// from its deployment topology, NEVER a dev-mnemonic EOA. When a network has not
|
|
// configured a governance controller, the halt/seed ops are fail-closed (uncallable) —
|
|
// strictly safer than a default key, and permissionless/decentralized by construction.
|
|
|
|
// settle_module.go registers 0x9999 — THE production DEX settlement precompile —
|
|
// as its own module (orthogonal: a new primitive at its own address, not bolted
|
|
// onto 0x9010). 0x9999 is the SOLE money path. It exposes:
|
|
//
|
|
// - swap(...) (V4 selector 0xF3CD914C): receipt-settlement (SettleSwap). The V4
|
|
// ABI is byte-for-byte unchanged; web/mobile change only the config address.
|
|
// - the existing custody/admin selectors (deposit/withdraw/balanceOf, pause/
|
|
// freeze) so a node serving 0x9999 has the full surface in one place.
|
|
// - settlement governance: set halt layers + seed pots (gated on the per-network
|
|
// DEX governance controller resolved from the runtime AtomicState, not a key).
|
|
//
|
|
// 0x9010 was removed; 0x9999 is the sole DEX precompile — one money path, one
|
|
// replay namespace (dex.precompile.v1.9999.*), never two.
|
|
|
|
var settleConfigKey = "dexSettleConfig"
|
|
|
|
// ErrSettleWrongContext rejects any invocation of 0x9999 whose self-address is not
|
|
// 0x9999 — in practice a DELEGATECALL into the settlement precompile from a delegating
|
|
// contract. The whole money path's correctness rests on ONE invariant: the ERC-20
|
|
// token leg (transferFrom / transfer) is made with 0x9999 as the token's msg.sender,
|
|
// so a deposit pulls against approve(0x9999, amount) and the vault holds the asset at
|
|
// 0x9999. geth derives the precompile self from the call opcode — CALL and CALLCODE
|
|
// both pass the TARGET (0x9999) as self (geth core/vm/evm.go Call/CallCode:
|
|
// NewPrecompileEnvironment(evm, caller, addr=target)), but DELEGATECALL passes the
|
|
// DELEGATING contract as self (DelegateCall: NewPrecompileEnvironment(evm,
|
|
// originCaller, caller) — self is the caller's context, not the target). Under
|
|
// DELEGATECALL the token's msg.sender would become the delegating contract, breaking
|
|
// the conservation invariant (realHolding == settleVault + makerLockedVault +
|
|
// custody) and stranding the caller's own funds. Pinning
|
|
// addr == 0x9999 at the top of Run rejects exactly DELEGATECALL while CALL and CALLCODE
|
|
// (both bind self = 0x9999) pass.
|
|
var ErrSettleWrongContext = errors.New("dex: 0x9999 must be entered with self == 0x9999 (CALL/CALLCODE); DELEGATECALL from a delegating contract is rejected")
|
|
|
|
// SettleContract is the 0x9999 stateful precompile. It is STATELESS with respect to
|
|
// the governance authority: halt/seed governance is gated to the per-network DEX
|
|
// governance controller resolved at RUNTIME from contract.AtomicState (see
|
|
// governanceController in halt9999.go), never a field set from config or a hardcoded
|
|
// key. So a single process-global SettlePrecompile serves every network correctly and
|
|
// no compromised default key exists on the struct.
|
|
type SettleContract struct{}
|
|
|
|
var _ contract.StatefulPrecompiledContract = (*SettleContract)(nil)
|
|
|
|
// SettlePrecompile is the singleton 0x9999 contract.
|
|
var SettlePrecompile = &SettleContract{}
|
|
|
|
// Governance selectors (computed in init via keccak4). The BLS-era
|
|
// registerValidatorSet / setHaltReceiptType / setHaltValidatorSet selectors are
|
|
// GONE with the cert value path — the native seam has no validator set and no cert
|
|
// scheme. Only the real kill switches remain.
|
|
var (
|
|
SelectorSetHaltGlobal uint32 // setHaltGlobal(bool)
|
|
SelectorSetHaltMarket uint32 // setHaltMarket(bytes32,bool)
|
|
SelectorSetHaltAsset uint32 // setHaltAsset(bytes32,bool)
|
|
|
|
// SelectorCollectPosition is a position's D->C money path: collectPosition(bytes32
|
|
// claimID, address asset, uint256 amount, bytes32 positionID) consumes a D->C claim
|
|
// ONCE and credits its beneficiary out of custody, then applies the collect to the
|
|
// named C-side position record. It is the SOLE C-credit path for an LP (collect,
|
|
// decrease, burn and cancel all return funds through here).
|
|
SelectorCollectPosition uint32 // collectPosition(bytes32,address,uint256,bytes32)
|
|
|
|
)
|
|
|
|
// SettleModule registers the 0x9999 precompile as ALWAYS-ON: it is active on every
|
|
// chain that loads the EVM plugin, from genesis, with NO config entry (neither a
|
|
// genesis-inlined precompile nor a precompileUpgrades timestamp). This is the money
|
|
// path — it must be present + callable the instant a node boots, on every Lux network,
|
|
// with ZERO per-net configuration. There is exactly ONE activation way and this is it.
|
|
//
|
|
// All parameters 0x9999 needs are resolved at RUNTIME, not from config:
|
|
// - governanceController: the per-network DEX governance CONTRACT, resolved from the
|
|
// host's atomic capability (contract.AtomicState.GovernanceController()). There is
|
|
// NO hardcoded default; an unconfigured network is fail-closed (halt/seed revert).
|
|
// - networkID / cChainID / dChainID: from the host's atomic capability
|
|
// (contract.AtomicState, sourced from the consensus context — see
|
|
// native_dchain_client.go). dChainID is the dexvm "D" alias resolved by the host;
|
|
// a network with no dexvm yields ids.Empty and the seam stays safely closed.
|
|
//
|
|
// The Configurator below exists only to satisfy modules.Module; for an AlwaysOn module
|
|
// the host never invokes Configure (there is no activating config). MakeConfig returns
|
|
// an empty marker so any tooling that enumerates configs gets a valid, parameter-free
|
|
// value that always Verifies.
|
|
var SettleModule = modules.Module{
|
|
ConfigKey: settleConfigKey,
|
|
Address: poolManagerAddr9999,
|
|
Contract: SettlePrecompile,
|
|
Configurator: &settleConfigurator{},
|
|
AlwaysOn: true,
|
|
}
|
|
|
|
type settleConfigurator struct{}
|
|
|
|
func init() {
|
|
SelectorSetHaltGlobal = keccak4("setHaltGlobal(bool)")
|
|
SelectorSetHaltMarket = keccak4("setHaltMarket(bytes32,bool)")
|
|
SelectorSetHaltAsset = keccak4("setHaltAsset(bytes32,bool)")
|
|
SelectorCollectPosition = keccak4("collectPosition(bytes32,address,uint256,bytes32)")
|
|
SelectorSeedSeamReserve = keccak4("seedSeamReserve(address,uint256)")
|
|
|
|
// No governance authority is set here: it is NOT a hardcoded key. The settlement
|
|
// governance authority is the per-network DEX governance controller, resolved at
|
|
// runtime from contract.AtomicState.GovernanceController() (see governanceController
|
|
// in halt9999.go). A single process-global SettlePrecompile thus serves every network
|
|
// correctly, with no compromised default key on the struct.
|
|
|
|
if err := modules.RegisterModule(SettleModule); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (*settleConfigurator) MakeConfig() precompileconfig.Config { return &SettleConfig{} }
|
|
|
|
// Configure is a no-op for the always-on 0x9999 precompile. It is never invoked by the
|
|
// host (an AlwaysOn module has no activating config), and 0x9999 takes no per-net
|
|
// parameters — the governance controller and chain identity are both resolved at runtime
|
|
// from the atomic capability (contract.AtomicState). The method exists solely to satisfy
|
|
// the contract.Configurator interface. It validates the config type so a misuse surfaces
|
|
// loudly rather than silently.
|
|
func (s *settleConfigurator) Configure(
|
|
_ precompileconfig.ChainConfig,
|
|
cfg precompileconfig.Config,
|
|
_ contract.StateDB,
|
|
_ contract.ConfigurationBlockContext,
|
|
) error {
|
|
if _, ok := cfg.(*SettleConfig); !ok {
|
|
return errors.New("dex: 0x9999 expected *SettleConfig")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SettleConfig is the 0x9999 config marker. It carries NO parameters: 0x9999 is
|
|
// always-on and resolves everything at runtime, so there is nothing to configure per
|
|
// network. It embeds Upgrade only so the precompile framework can represent it as a
|
|
// (parameter-free) genesis config when enumerated; it always Verifies.
|
|
type SettleConfig struct {
|
|
precompileconfig.Upgrade
|
|
}
|
|
|
|
func (c *SettleConfig) Key() string { return settleConfigKey }
|
|
func (c *SettleConfig) Timestamp() *uint64 { return c.Upgrade.Timestamp() }
|
|
func (c *SettleConfig) IsDisabled() bool { return c.Upgrade.Disable }
|
|
func (c *SettleConfig) Verify(_ precompileconfig.ChainConfig) error { return nil }
|
|
func (c *SettleConfig) Equal(cfg precompileconfig.Config) bool {
|
|
other, ok := cfg.(*SettleConfig)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return c.Upgrade.Equal(&other.Upgrade)
|
|
}
|
|
|
|
// Run dispatches the 0x9999 surface. The swap selector is the money path
|
|
// (SettleSwap); custody/admin selectors reuse the 0x9010 handlers via the shared
|
|
// pool manager; governance selectors gate on the runtime governance controller.
|
|
func (s *SettleContract) Run(
|
|
accessibleState contract.AccessibleState,
|
|
caller common.Address,
|
|
addr common.Address,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
cur := contract.Read(input)
|
|
selector, err := cur.Uint32()
|
|
if err != nil {
|
|
return nil, suppliedGas, errors.New("dex: input too short")
|
|
}
|
|
if accessibleState.GetBlockContext() == nil {
|
|
return nil, suppliedGas, errors.New("dex: block context unavailable")
|
|
}
|
|
// CALL-only guard: 0x9999 must execute with itself as self (msg.sender of its
|
|
// token sub-calls). geth passes the call opcode's self here as `addr`; only
|
|
// DELEGATECALL makes addr != 0x9999 (CALL/CALLCODE both pass self = 0x9999), which
|
|
// would pull/push ERC-20 value as the WRONG msg.sender and break conservation.
|
|
// Reject it before any state is touched (no gas charged — a context error, like
|
|
// the two checks above). See ErrSettleWrongContext.
|
|
if addr != poolManagerAddr9999 {
|
|
return nil, suppliedGas, ErrSettleWrongContext
|
|
}
|
|
data := cur.Rest()
|
|
|
|
switch selector {
|
|
case SelectorSwap:
|
|
// THE money path — SETTLE-ONLY. 0x9999 NEVER matches on C. Every swap routes to the
|
|
// native C<->D atomic seam (SettleSwap, settle9999.go), keyed on the hookData phase:
|
|
//
|
|
// - UNTAGGED / opaque / DI01 hookData => PHASE A (ORDER): lock the taker's input on
|
|
// C and write a C->D atomic object. D imports it and matches under ITS OWN
|
|
// consensus. Phase A returns the order id — NOT a fill; no output is credited here.
|
|
// - DS01-tagged hookData => PHASE B (SETTLEMENT): consume a real D->C atomic object
|
|
// ONCE and credit the output. This is the ONLY path that credits C, and the credit
|
|
// is the RECORDED object's amount — never a caller-supplied fill value.
|
|
//
|
|
// There is NO in-trie CLOB/AMM router, NO synchronous on-chain matcher, NO "money lives
|
|
// in the order book" maker custody. The embedded matcher (swap_sync / swap_custody) was
|
|
// REMOVED: a live matcher inside C-Chain block execution forks consensus (each validator
|
|
// observes independently-timed fills => divergent StateRoot). Deterministic consumption
|
|
// of a committed D->C object is fork-safe INLINE. A swap with no valid D-committed object
|
|
// cannot settle — that is the load-bearing "no-receipt-no-settle" invariant.
|
|
return SettleSwap(accessibleState, caller, data, suppliedGas, readOnly)
|
|
|
|
// Market creation — C-AUTHORITATIVE registry (settle_market.go). Computes the
|
|
// tick deterministically ON C; any D OpenMarket is best-effort + discarded.
|
|
case SelectorInitialize:
|
|
return s.runSettleInitialize(accessibleState, caller, data, suppliedGas, readOnly)
|
|
|
|
// LP D-committed liquidity path (position_commit.go): ADD commits the LP's funds
|
|
// to a D position via a C->D object; REMOVE requests a D->C withdraw. 0x9996
|
|
// PositionManager routes its lifecycle ops here.
|
|
case SelectorModifyLiquidity:
|
|
return s.runSettleModifyLiquidity(accessibleState, caller, data, suppliedGas, readOnly)
|
|
|
|
// LP D->C collect/withdraw money path (position_commit.go): consume a D->C atomic
|
|
// claim ONCE and credit its beneficiary out of custody. The SOLE C-credit
|
|
// path for an LP — collect/decrease/burn/cancel all return funds through here.
|
|
case SelectorCollectPosition:
|
|
return s.runSettleCollectPosition(accessibleState, caller, data, suppliedGas, readOnly)
|
|
|
|
// donate is explicitly UNSUPPORTED in the receipt model (settle_views9999.go):
|
|
// LP fee growth is D-authoritative, so a safe C-only donate cannot exist.
|
|
case SelectorDonate:
|
|
return s.runSettleDonate(accessibleState, caller, data, suppliedGas, readOnly)
|
|
|
|
// Raw slot reads of 0x9999's own storage (read-only; harmless).
|
|
case SelectorExtsload:
|
|
return s.runSettleExtsload(accessibleState, data, suppliedGas)
|
|
case SelectorExtsloadArray:
|
|
return s.runSettleExtsloadArray(accessibleState, data, suppliedGas)
|
|
|
|
// Vault funds-in / funds-out / balance — the 0x9999 receipt-settlement RESERVE
|
|
// (settle_custody.go), NOT the deprecated 0x9010 ZAP-custody ledger. These are
|
|
// NOT gated by the swap halt so funds can always exit even when new swaps are
|
|
// halted (safe default: never strand funds).
|
|
case SelectorDeposit:
|
|
return s.runSettleDeposit(accessibleState, caller, data, suppliedGas, readOnly)
|
|
case SelectorWithdraw:
|
|
return s.runSettleWithdraw(accessibleState, caller, data, suppliedGas, readOnly)
|
|
case SelectorBalanceOf:
|
|
return s.runSettleBalanceOf(accessibleState, data, suppliedGas)
|
|
|
|
// Settlement governance (governance-controller-gated, resolved from runtime
|
|
// AtomicState — never a hardcoded key). Only the real kill switches remain
|
|
// (global / market / asset); the BLS-era validator-set rotation and cert-type
|
|
// halt are gone with the cert value path.
|
|
case SelectorSetHaltGlobal:
|
|
return s.runSetHaltGlobal(accessibleState, caller, data, suppliedGas, readOnly)
|
|
case SelectorSetHaltMarket:
|
|
return s.runSetHaltScoped(accessibleState, caller, data, suppliedGas, readOnly, SetHaltMarket)
|
|
case SelectorSetHaltAsset:
|
|
return s.runSetHaltScoped(accessibleState, caller, data, suppliedGas, readOnly, SetHaltAsset)
|
|
|
|
// Operator funding of the settlement pots (governance-controller-gated): the swap
|
|
// custody counterparty seed (FIX-4 liveness — a market's first matched swap settles
|
|
// without manual state-poking). It is the ONLY way value enters custody other than
|
|
// a C->D export, which is what makes the custody identity in native_state.go hold.
|
|
case SelectorSeedSeamReserve:
|
|
return s.runSeedSeamReserve(accessibleState, caller, data, suppliedGas, readOnly)
|
|
|
|
default:
|
|
return nil, suppliedGas, errors.New("dex: unknown 0x9999 selector")
|
|
}
|
|
}
|
|
|
|
const gasHaltAdmin uint64 = 25_000
|
|
|
|
// runSetHaltGlobal toggles the global halt. Authority is the per-network DEX
|
|
// governance controller resolved from the runtime AtomicState (governanceController),
|
|
// NOT a hardcoded key: only the configured Governor/Timelock/multisig CONTRACT may
|
|
// halt. Fail-closed when no governance controller is configured (ErrSettleNoGovernance).
|
|
func (s *SettleContract) runSetHaltGlobal(
|
|
state contract.AccessibleState, caller common.Address, data []byte, gas uint64, readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
if readOnly {
|
|
return nil, gas, errors.New("dex: cannot halt in read-only mode")
|
|
}
|
|
if gas < gasHaltAdmin {
|
|
return nil, 0, errors.New("dex: out of gas")
|
|
}
|
|
gov, gerr := governanceController(state)
|
|
if gerr != nil {
|
|
return nil, gas - gasHaltAdmin, gerr
|
|
}
|
|
if caller != gov {
|
|
return nil, gas - gasHaltAdmin, ErrUnauthorized
|
|
}
|
|
if len(data) < 32 {
|
|
return nil, gas - gasHaltAdmin, errors.New("dex: setHaltGlobal needs a bool")
|
|
}
|
|
on := data[31] != 0
|
|
SetHaltGlobal(newPoolStateAdapter(state), on)
|
|
return nil, gas - gasHaltAdmin, nil
|
|
}
|
|
|
|
// runSetHaltScoped toggles a [32]byte-scoped halt (market/asset). Authority is the
|
|
// per-network DEX governance controller (governanceController), never a hardcoded key
|
|
// — the same decentralized gate as the global halt; fail-closed when unset.
|
|
func (s *SettleContract) runSetHaltScoped(
|
|
state contract.AccessibleState, caller common.Address, data []byte, gas uint64, readOnly bool,
|
|
apply func(stateKV, [32]byte, bool),
|
|
) ([]byte, uint64, error) {
|
|
if readOnly {
|
|
return nil, gas, errors.New("dex: cannot halt in read-only mode")
|
|
}
|
|
if gas < gasHaltAdmin {
|
|
return nil, 0, errors.New("dex: out of gas")
|
|
}
|
|
gov, gerr := governanceController(state)
|
|
if gerr != nil {
|
|
return nil, gas - gasHaltAdmin, gerr
|
|
}
|
|
if caller != gov {
|
|
return nil, gas - gasHaltAdmin, ErrUnauthorized
|
|
}
|
|
if len(data) < 64 {
|
|
return nil, gas - gasHaltAdmin, errors.New("dex: setHalt(bytes32,bool) needs 2 words")
|
|
}
|
|
var id [32]byte
|
|
copy(id[:], data[0:32])
|
|
on := data[63] != 0
|
|
apply(newPoolStateAdapter(state), id, on)
|
|
return nil, gas - gasHaltAdmin, nil
|
|
}
|