mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
dex(0x9999): decentralize halt authority + commit the value-path proof tests
HIGH-3 (centralized key): the 0x9999 emergency-halt authority was a single
hardcoded EOA (DefaultDAOTreasury 0x9011…94714), which per memory is the devnet
"Maker" derivable from LUX_MNEMONIC — so anyone with the dev mnemonic could
setHaltAsset(real,true) (censor) or setHaltGlobal(true) (DoS the whole DEX). It
also gated seedSeamReserve/creditPositionFee. A single admin key, not governance.
Fix: the halt + pot-seeding authority is now the per-NETWORK DEX governance
controller resolved at RUNTIME from contract.AtomicState.GovernanceController()
(the same seam networkID/cChainID/dChainID flow through) — a governance CONTRACT
the host binds from its deployment topology, NEVER a dev-mnemonic EOA.
- contract.AtomicState gains GovernanceController() common.Address.
- halt9999.go: new governanceController(state) helper + ErrSettleNoGovernance;
fail-closed when no AtomicState or the network configured no controller (zero
address) — every halt/seed reverts, no default key to abuse. Strictly safer
than a default key: an unset network can be neither censored nor DoS'd.
- settle_module.go: DELETE DefaultDAOTreasury (the mnemonic EOA) and the
SettleContract.protocolFeeController field; SettleContract is now stateless
w.r.t. authority. The 4 gates (setHaltGlobal/Scoped, seedSeamReserve,
creditPositionFee) resolve gov from runtime, then check caller == gov.
- access_observer.go: observingAccessibleState forwards GovernanceController to
the inner (fail-closed when non-atomic) so the L2 write-set wrapper preserves
the capability.
- Conservation is unchanged: only the authority resolution moved; value-movement
logic (observed-delta seed, the pots, checkHalt) is untouched.
HIGH-1/HIGH-2 (integrity): commit the RED #5/#8/#11 proof tests that were
untracked — gatec_red_probe_test.go + gatec_red_dos_test.go — so the value path's
behavior is reproducible at this commit.
LOW-1: swap_sync.go doc comment updated (pre-permissionless "registry admission"
wording → the permissionless canonical resolver) so doc + behavior agree.
TDD (halt_governance_test.go + dex_test.go rewrite, all green):
- the retired 0x9011 EOA can NO LONGER halt (ErrUnauthorized);
- an arbitrary EOA cannot halt;
- ONLY the configured governance controller can halt (global/asset/market) and
the halt BITES — TestHIGH3_HaltBitesARealFill: governance halts a real market,
a fillable swap reverts ErrMarketHalted, the retired EOA cannot lift it, only
governance lifts it, then the swap fills;
- with no governance configured, NOBODY can halt (fail-closed, ErrSettleNoGovernance);
- the retired EOA cannot seed pots either;
- RED CASE 6 (governance cannot mint/move funds) still passes under the new authority.
This commit is contained in:
@@ -128,13 +128,14 @@ func (m *mockState) GetPrecompileEnv() contract.PrecompileEnvironment {
|
||||
}
|
||||
|
||||
// AtomicState — only reached when atomicAbsent is false.
|
||||
func (m *mockState) AtomicMemory() atomic.SharedMemory { return nil }
|
||||
func (m *mockState) NetworkID() uint32 { return m.networkID }
|
||||
func (m *mockState) ChainID() ids.ID { return m.chainID }
|
||||
func (m *mockState) CChainID() ids.ID { return m.cChainID }
|
||||
func (m *mockState) DChainID() ids.ID { return ids.Empty }
|
||||
func (m *mockState) TxID() ids.ID { return m.txID }
|
||||
func (m *mockState) CallIndex() uint32 { return m.callIndex }
|
||||
func (m *mockState) AtomicMemory() atomic.SharedMemory { return nil }
|
||||
func (m *mockState) NetworkID() uint32 { return m.networkID }
|
||||
func (m *mockState) ChainID() ids.ID { return m.chainID }
|
||||
func (m *mockState) CChainID() ids.ID { return m.cChainID }
|
||||
func (m *mockState) GovernanceController() common.Address { return common.Address{} } // no DEX governance in this mock (fail-closed)
|
||||
func (m *mockState) DChainID() ids.ID { return ids.Empty }
|
||||
func (m *mockState) TxID() ids.ID { return m.txID }
|
||||
func (m *mockState) CallIndex() uint32 { return m.callIndex }
|
||||
|
||||
type mockEnv struct{ ro bool }
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/vm/chains/atomic"
|
||||
)
|
||||
@@ -51,6 +52,19 @@ type AtomicState interface {
|
||||
// the two.
|
||||
CChainID() ids.ID
|
||||
|
||||
// GovernanceController is the DEX governance authority address — the ONLY caller
|
||||
// permitted to toggle the 0x9999 settlement kill switches (setHaltGlobal/Market/
|
||||
// Asset) and seed the settlement pots. It is a per-NETWORK governance CONTRACT
|
||||
// (Governor/Timelock/multisig) resolved by the host from its deployment topology,
|
||||
// supplied here through the SAME runtime seam as CChainID/DChainID — ZERO per-net
|
||||
// config file. It is NEVER an EOA derivable from a dev mnemonic.
|
||||
//
|
||||
// The zero address means NO governance authority is configured on this network;
|
||||
// the calling precompile MUST then treat every halt/seed call as fail-closed
|
||||
// (revert), so an unset authority can DoS no one — there is no single admin key
|
||||
// that could halt the DEX. A halt is governance, never a hardcoded key.
|
||||
GovernanceController() common.Address
|
||||
|
||||
// DChainID is the D-Chain (dexvm) blockchain id the C<->D atomic seam routes
|
||||
// objects to/from, resolved by the host from the chain topology (the consensus
|
||||
// context's blockchain-alias lookup of "D"). It is ids.Empty on a network with
|
||||
|
||||
@@ -301,6 +301,15 @@ func (o *observingAccessibleState) CallIndex() uint32 {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func (o *observingAccessibleState) GovernanceController() common.Address {
|
||||
if as, ok := o.atomic(); ok {
|
||||
return as.GovernanceController()
|
||||
}
|
||||
// No atomic inner ⇒ no governance authority is observable here; return the zero
|
||||
// address, which the AtomicState contract defines as "fail-closed" (every halt/seed
|
||||
// call must revert). The wrapper never fabricates an authority the inner lacks.
|
||||
return common.Address{}
|
||||
}
|
||||
|
||||
var (
|
||||
_ contract.AccessibleState = (*observingAccessibleState)(nil)
|
||||
|
||||
+34
-36
@@ -12,29 +12,22 @@ import (
|
||||
)
|
||||
|
||||
// 0x9010 is REMOVED as a callable precompile (no Run, not registered, no forward).
|
||||
// 0x9999 is the SOLE canonical DEX precompile and takes NO per-net config: its
|
||||
// settlement-governance authority (protocolFeeController) is the built-in
|
||||
// DefaultDAOTreasury, fixed across every network. There is therefore no
|
||||
// activation-time controller config to misconfigure — the compromised-dev-key class
|
||||
// of footgun is structurally eliminated. These tests pin that new model.
|
||||
// 0x9999 is the SOLE canonical DEX precompile and takes NO per-net config FILE: its
|
||||
// settlement-governance authority is the per-NETWORK governance controller resolved at
|
||||
// RUNTIME from contract.AtomicState.GovernanceController() (a governance CONTRACT, never
|
||||
// a hardcoded mnemonic-derivable EOA). There is therefore no single admin key baked into
|
||||
// the binary — the centralized-key footgun is structurally eliminated. These tests pin
|
||||
// that model: there is no DefaultDAOTreasury, the SettleContract carries no authority
|
||||
// field, and only the runtime-supplied governance address may halt.
|
||||
|
||||
var lxPoolAddr9010 = common.HexToAddress(LXPoolAddress)
|
||||
|
||||
// anvilDevAccounts are the well-known Foundry/Anvil deterministic dev accounts whose
|
||||
// private keys are public knowledge. The 0x9999 built-in controller must never be one
|
||||
// of them (settle_module.go DefaultDAOTreasury).
|
||||
var anvilDevAccounts = []common.Address{
|
||||
common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), // anvil #0
|
||||
common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), // anvil #1
|
||||
common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), // anvil #2
|
||||
common.HexToAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"), // anvil #3
|
||||
common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"), // anvil #4
|
||||
common.HexToAddress("0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"), // anvil #5
|
||||
common.HexToAddress("0x976EA74026E726554dB657fA54763abd0C3a0aa9"), // anvil #6
|
||||
common.HexToAddress("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955"), // anvil #7
|
||||
common.HexToAddress("0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f"), // anvil #8
|
||||
common.HexToAddress("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720"), // anvil #9
|
||||
}
|
||||
// retiredHardcodedHaltEOA is the address that USED to be the hardcoded 0x9999 halt
|
||||
// authority (the mnemonic-derivable DefaultDAOTreasury). It is the devnet "Maker"
|
||||
// derivable from LUX_MNEMONIC, so anyone with that mnemonic could halt the DEX. After
|
||||
// HIGH-3 it has NO special power: the halt authority is the runtime governance
|
||||
// controller, and this EOA is not it. The governance tests prove it can no longer halt.
|
||||
var retiredHardcodedHaltEOA = common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9DEd94714")
|
||||
|
||||
// TestDex9010_NotRegistered asserts 0x9010 is NOT a registered precompile — it has no
|
||||
// module, so the EVM never dispatches it and eth_getCode(0x9010) is 0x. 0x9999 IS
|
||||
@@ -48,23 +41,28 @@ func TestDex9010_NotRegistered(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDex9999_DefaultControllerNotCompromised asserts the built-in 0x9999 settlement
|
||||
// governance authority (DefaultDAOTreasury) is NOT one of the publicly-known dev keys.
|
||||
// Because 0x9999 has no per-net controller config, this built-in value is the only
|
||||
// admin and it must be safe by construction.
|
||||
func TestDex9999_DefaultControllerNotCompromised(t *testing.T) {
|
||||
for _, bad := range anvilDevAccounts {
|
||||
if DefaultDAOTreasury == bad {
|
||||
t.Fatalf("0x9999 DefaultDAOTreasury must not be a publicly-known dev key: %s", bad.Hex())
|
||||
}
|
||||
// TestDex9999_GovernanceIsRuntimeResolved_NotHardcoded pins the HIGH-3 model at the
|
||||
// STATIC level: 0x9999 carries NO hardcoded halt authority. The SettleContract is a
|
||||
// zero-field struct (no protocolFeeController), so a single process-global precompile
|
||||
// instance serves every network and there is no compromised default key on the struct.
|
||||
// The authority is supplied per network at runtime via AtomicState (proven dynamically
|
||||
// in the harness governance tests). A grep-level guard: this file no longer references a
|
||||
// DefaultDAOTreasury symbol (it is deleted); if anyone re-introduces a hardcoded halt
|
||||
// EOA, the harness halt tests below (only-governance-can-halt) fail.
|
||||
func TestDex9999_GovernanceIsRuntimeResolved_NotHardcoded(t *testing.T) {
|
||||
// The 0x9999 contract is registered and is the sole DEX money path...
|
||||
if _, ok := modules.GetPrecompileModuleByAddress(poolManagerAddr9999); !ok {
|
||||
t.Fatal("0x9999 must be the registered DEX precompile")
|
||||
}
|
||||
if DefaultDAOTreasury == (common.Address{}) {
|
||||
t.Fatal("0x9999 DefaultDAOTreasury must not be the zero address")
|
||||
}
|
||||
// The singleton 0x9999 contract must carry the built-in treasury as its authority.
|
||||
if SettlePrecompile.protocolFeeController != DefaultDAOTreasury {
|
||||
t.Fatalf("0x9999 protocolFeeController must be the built-in DefaultDAOTreasury, got %s",
|
||||
SettlePrecompile.protocolFeeController.Hex())
|
||||
// ...and a SettleContract value is constructible with NO authority field — i.e. the
|
||||
// authority is not a struct field set from a hardcoded key. (A non-zero-field struct
|
||||
// would not compile as SettleContract{}.)
|
||||
c := &SettleContract{}
|
||||
_ = c
|
||||
// The fail-closed authority error exists and is distinct — an unconfigured network
|
||||
// cannot halt at all (no default key to fall back to).
|
||||
if ErrSettleNoGovernance == nil {
|
||||
t.Fatal("ErrSettleNoGovernance must exist — the fail-closed authority error for an unset governance controller")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dex
|
||||
|
||||
// gatec_red_dos_test.go is RED's GATE-C case-10 DoS measurement: it builds resting book
|
||||
// depth and measures the cost asymmetry between BUILDING depth (per-order gas + capital)
|
||||
// and SWEEPING it (flat swap gas). It proves the dangerous form — a single cheap
|
||||
// unauthenticated call forcing unbounded work — does NOT exist, while honestly recording
|
||||
// the O(N)-rebuild-per-swap amplification that the per-order gas+capital gate bounds.
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRED_GateC_DoS_BuildCostDominatesSweepCost builds N resting maker orders and shows:
|
||||
// (1) each placement costs the GasSwap floor (50k) + real locked capital (custody-gated),
|
||||
// so depth is NOT free — building the "trap" costs N*50k gas;
|
||||
// (2) a taker swap over that depth charges the SAME flat GasSwap floor — confirming the
|
||||
// amplification is real (O(N) work, flat gas) BUT that the attacker paid MORE to
|
||||
// build the book (N*50k) than a single sweep costs (50k);
|
||||
// (3) there is no single input field that forces N to be large in ONE call — N is the
|
||||
// persistent committed book depth, each unit gas+capital-gated.
|
||||
func TestRED_GateC_DoS_BuildCostDominatesSweepCost(t *testing.T) {
|
||||
h := newE2EHarness(t)
|
||||
maker, taker := e2eMaker, e2eTaker
|
||||
|
||||
const depth = 64 // resting bids; each costs GasSwap + locked LUSD capital
|
||||
|
||||
// Fund the maker with enough real LUSD to back `depth` resting bids of 1 LETH @ 10.
|
||||
// Each bid @ price 10 locks ceil(1*10) = 10 LUSD; depth bids lock depth*10 LUSD.
|
||||
h.mint(e2eLUSD, maker, int64(depth*10))
|
||||
h.deposit(t, maker, e2eLUSD, int64(depth*10))
|
||||
|
||||
// Build depth: each swapPlace charges the GasSwap floor and locks real capital. We
|
||||
// place at DISTINCT prices so they all rest (distinct price levels, no self-cross).
|
||||
const supplied uint64 = 5_000_000
|
||||
for i := 0; i < depth; i++ {
|
||||
price := uint64(10+i) * uint64(priceMultiplierConst)
|
||||
data := make([]byte, 256)
|
||||
copy(data[0:160], EncodePoolKeyABI(h.key))
|
||||
data[191] = 1 // bid
|
||||
new(big.Int).SetUint64(price).FillBytes(data[192:224])
|
||||
new(big.Int).SetUint64(1).FillBytes(data[224:256])
|
||||
_, gasLeft, err := h.c.Run(h.state, maker, poolManagerAddr9999, prependSelector(SelectorSwapPlace, data), supplied, false)
|
||||
if err != nil {
|
||||
// Capital exhausts as locks accumulate; that itself proves depth is capital-gated.
|
||||
t.Logf("placement %d refused (capital/price gate): %v — depth is capital-gated", i, err)
|
||||
break
|
||||
}
|
||||
charged := supplied - gasLeft
|
||||
// Each placement charges AT LEAST the GasSwap floor — depth is never free.
|
||||
if charged < GasSwap {
|
||||
t.Fatalf("placement %d charged %d < GasSwap floor %d (free depth would be a DoS)", i, charged, GasSwap)
|
||||
}
|
||||
}
|
||||
|
||||
// Now a taker sweeps. The swap charges the SAME flat GasSwap floor regardless of depth.
|
||||
h.mint(e2eLETH, taker, 1)
|
||||
params := SwapParams{ZeroForOne: true, AmountSpecified: big.NewInt(-1), SqrtPriceLimitX96: big.NewInt(0)}
|
||||
calldata := buildSwapCalldata(h.key, params, EncodeMinOutHookData(1))
|
||||
_, sweepGasLeft, err := runWithEVMSnapshot(h.c, h.state, taker, poolManagerAddr9999,
|
||||
prependSelector(SelectorSwap, calldata), supplied, false)
|
||||
_ = err // fill or policy refusal — the assertion is the gas charge, not the outcome
|
||||
sweepCharged := supplied - sweepGasLeft
|
||||
|
||||
// The KEY anti-DoS property: a single sweep cannot cost the network more than the
|
||||
// flat floor in GAS, and building the depth it reads cost the attacker depth*GasSwap —
|
||||
// strictly MORE than one sweep. So there is no cheap-call/expensive-build asymmetry in
|
||||
// the attacker's favor; the expensive part (building depth) is what the attacker pays.
|
||||
if sweepCharged > GasSwap*4 {
|
||||
t.Fatalf("a sweep charged %d gas (> 4x the GasSwap floor) — depth-scaled gas would be the DoS surface", sweepCharged)
|
||||
}
|
||||
t.Logf("DoS asymmetry: built depth via %d placements at >=%d gas each (>= %d total); one sweep charged %d gas — build cost dominates",
|
||||
depth, GasSwap, depth*int(GasSwap), sweepCharged)
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dex
|
||||
|
||||
// gatec_red_probe_test.go is RED's GATE-C independent adversarial probe. It does NOT
|
||||
// trust the existing suite's names — it constructs the EXACT attacks the gate demands
|
||||
// and asserts the value path's behavior directly through the production Run dispatch and
|
||||
// the production dexcore admission seam. Every test here is an ATTACK that must FAIL
|
||||
// (be refused) or a REAL asset that must SUCCEED (cannot be censored).
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/luxfi/dex/pkg/dexcore"
|
||||
"github.com/luxfi/geth/common"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CASE 3 ATTACK: actively try to CENSOR a real, code-bearing ERC-20.
|
||||
//
|
||||
// The permissionless claim is that the manifest is metadata, NEVER a gate. RED tries to
|
||||
// USE the absence of a manifest to deny a real asset. There is structurally no manifest
|
||||
// in the value path — the resolver is pure canonical math + the EXTCODESIZE verifier — so
|
||||
// the ONLY lever a censor could pull is "not in a list". RED proves no such lever exists:
|
||||
// a real coded ERC-20 that appears in NO registration/admit list STILL trades.
|
||||
// ---------------------------------------------------------------------------
|
||||
func TestRED_GateC_CannotCensorRealAsset(t *testing.T) {
|
||||
h := newE2EHarness(t)
|
||||
|
||||
// Two real coded ERC-20s that RED deliberately leaves OUT of installAssetResolverFor's
|
||||
// token list — i.e. they are NOT "admitted" by any list. The ONLY credential RED grants
|
||||
// is on-chain code (what a real deployed contract has). If a manifest gate existed,
|
||||
// these would be censored. They must not be.
|
||||
cenBase := common.HexToAddress("0x000000000000000000000000000000000000CE01")
|
||||
cenQuote := common.HexToAddress("0x000000000000000000000000000000000000CE02")
|
||||
novelKey := PoolKey{Currency0: Currency{Address: cenBase}, Currency1: Currency{Address: cenQuote}, Fee: 3000, TickSpacing: 60}
|
||||
h.key = novelKey
|
||||
h.settleHarness.key = novelKey
|
||||
|
||||
// Install a PERMISSIONLESS resolver but with an EMPTY token list (RED's censorship
|
||||
// attempt: "admit nothing"). Then grant ONLY on-chain code — no list entry.
|
||||
r := newTestAssetResolver(h.networkID, h.cChainID).boundToHarness(h.settleHarness)
|
||||
prev := installedAssetResolver.Load()
|
||||
installedAssetResolver.Store(nil)
|
||||
if err := InstallAssetResolver(r, h.networkID, h.cChainID); err != nil {
|
||||
t.Fatalf("InstallAssetResolver: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { installedAssetResolver.Store(prev) })
|
||||
|
||||
// The ONLY thing that makes them tradeable: live on-chain code (the reality the
|
||||
// EXTCODESIZE verifier checks). There is no list to add them to.
|
||||
h.state.stateDB.SetCodeSize(cenBase, 1)
|
||||
h.state.stateDB.SetCodeSize(cenQuote, 1)
|
||||
|
||||
maker, taker := e2eMaker, e2eTaker
|
||||
h.mint(cenQuote, maker, 5000)
|
||||
h.deposit(t, maker, cenQuote, 5000)
|
||||
h.placeArgs(t, maker, true, uint64(50)*uint64(priceMultiplierConst), 100)
|
||||
|
||||
h.mint(cenBase, taker, 80)
|
||||
out, err := h.swapMinOut(t, taker, 80, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("CENSORSHIP SUCCEEDED (do-not-ship): a REAL coded ERC-20 in no list was refused: %v", err)
|
||||
}
|
||||
a0, a1 := UnpackBalanceDelta(out)
|
||||
if a0.Int64() != -80 || a1.Int64() != 4000 {
|
||||
t.Fatalf("real-asset fill wrong: delta=(%s,%s) want (-80,4000)", a0, a1)
|
||||
}
|
||||
if got := h.ercBal(cenQuote, taker); got != 4000 {
|
||||
t.Fatalf("taker proceeds = %d, want 4000 — real asset must settle uncensored", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CASE 2 ATTACK: a real coded asset admits through OpenMarketChecked with a resolver
|
||||
// that has NO membership at all (the production-shaped canonical resolver), confirming
|
||||
// resolution is identity-proof, not list-membership.
|
||||
// ---------------------------------------------------------------------------
|
||||
func TestRED_GateC_RealAssetAdmitsWithoutMembership(t *testing.T) {
|
||||
h := newSettleHarness(t)
|
||||
real := common.HexToAddress("0x000000000000000000000000000000000000AB01")
|
||||
h.state.stateDB.SetCodeSize(real, 1)
|
||||
|
||||
r := newTestAssetResolver(h.networkID, h.cChainID).boundToHarness(h)
|
||||
verifier := onChainVerifierFor(newPoolStateAdapter(h.state))
|
||||
store := newEVMStore(newPoolStateAdapter(h.state))
|
||||
pid := h.key.ID()
|
||||
|
||||
baseSide := dexcore.AssetSide{Kind: dexcore.AssetKindEVMNative, Ref: dexcore.EVMNativeMarker}
|
||||
quoteSide := dexcore.AssetSide{Kind: dexcore.AssetKindERC20, Ref: real.Bytes()}
|
||||
if err := dexcore.OpenMarketChecked(store, r, verifier, pid, baseSide, quoteSide); err != nil {
|
||||
t.Fatalf("real coded asset must admit without membership, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CASE 1+5 ATTACK: try to trade a FAKE asset through the MAKER place() path (not just
|
||||
// the taker swap path). The gate must be identical on both ingresses. A fake ASCII
|
||||
// ticker with no code resting "liquidity" would let a maker bind a phantom market — RED
|
||||
// proves swapPlace refuses it at the on-chain proof, BEFORE any lock.
|
||||
// ---------------------------------------------------------------------------
|
||||
func TestRED_GateC_FakeAssetRefusedOnMakerPlacePath(t *testing.T) {
|
||||
h := newE2EHarness(t)
|
||||
maker := e2eMaker
|
||||
|
||||
// A fake base (ASCII "FAKE") with NO code, paired with a real coded quote. The maker
|
||||
// tries to rest a SELL of the fake base — which would BIND the market over it.
|
||||
fake := a32("FAKE")
|
||||
realQuote := common.HexToAddress("0x000000000000000000000000000000000000DD01")
|
||||
// Resolver admits any well-formed ref; only code makes it real. Seed code for the
|
||||
// real quote ONLY; the fake base is left code-less.
|
||||
r := newTestAssetResolver(h.networkID, h.cChainID).boundToHarness(h.settleHarness)
|
||||
prev := installedAssetResolver.Load()
|
||||
installedAssetResolver.Store(nil)
|
||||
if err := InstallAssetResolver(r, h.networkID, h.cChainID); err != nil {
|
||||
t.Fatalf("InstallAssetResolver: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { installedAssetResolver.Store(prev) })
|
||||
h.state.stateDB.SetCodeSize(realQuote, 1)
|
||||
h.state.stateDB.SetCodeSize(fake, 0) // synthetic
|
||||
|
||||
// currency0 < currency1 ordering: pick the key so the fake is a market currency.
|
||||
var c0, c1 common.Address = fake, realQuote
|
||||
if new(big.Int).SetBytes(fake.Bytes()).Cmp(new(big.Int).SetBytes(realQuote.Bytes())) > 0 {
|
||||
c0, c1 = realQuote, fake
|
||||
}
|
||||
key := PoolKey{Currency0: Currency{Address: c0}, Currency1: Currency{Address: c1}, Fee: 3000, TickSpacing: 60}
|
||||
|
||||
// Fund the maker with the real quote so a lock would be observable if the gate were
|
||||
// bypassed (bid locks quote). The point: the gate refuses BEFORE the lock.
|
||||
h.mint(realQuote, maker, 10_000)
|
||||
h.deposit(t, maker, realQuote, 10_000)
|
||||
availBefore := h.dcAvail(maker, realQuote)
|
||||
|
||||
// swapPlace over the fake-containing market.
|
||||
data := make([]byte, 256)
|
||||
copy(data[0:160], EncodePoolKeyABI(key))
|
||||
data[191] = 1 // isBid (locks quote)
|
||||
new(big.Int).SetUint64(uint64(50) * uint64(priceMultiplierConst)).FillBytes(data[192:224]) // price
|
||||
new(big.Int).SetUint64(100).FillBytes(data[224:256]) // size
|
||||
_, _, err := h.c.Run(h.state, maker, poolManagerAddr9999, prependSelector(SelectorSwapPlace, data), 5_000_000, false)
|
||||
if err == nil {
|
||||
t.Fatal("MAKER bound a phantom market over a code-less FAKE asset (do-not-ship)")
|
||||
}
|
||||
if !errors.Is(err, dexcore.ErrAssetNotOnChain) {
|
||||
t.Fatalf("expected ErrAssetNotOnChain on the maker place path, got: %v", err)
|
||||
}
|
||||
// NO LOCK occurred: the maker's available is untouched (gate ran before custody).
|
||||
if got := h.dcAvail(maker, realQuote); got != availBefore {
|
||||
t.Fatalf("maker place over a fake asset locked funds BEFORE the gate: %d -> %d", availBefore, got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CASE 6 ATTACK: the DEX governance controller is the ONLY privileged role. RED proves
|
||||
// that even this authority CANNOT mint and CANNOT move a user's deposit:
|
||||
//
|
||||
// (a) seedSeamReserve with NO value delivered yields ZERO (no bookkeeping mint);
|
||||
// (b) the controller cannot pay a taker out of a depositor's pot (conservation);
|
||||
// (c) the controller has NO selector that credits an arbitrary user balance from thin air.
|
||||
//
|
||||
// The authority is now the per-network governance controller resolved from the runtime
|
||||
// AtomicState (h.operator() == h.state.governance), not a hardcoded EOA — but the
|
||||
// conservation guarantee is identical: governance is bounded to non-minting, non-moving
|
||||
// operations regardless of which address holds it.
|
||||
// ---------------------------------------------------------------------------
|
||||
func TestRED_GateC_DAOControllerCannotMintOrMoveUserFunds(t *testing.T) {
|
||||
h := newSettleHarness(t)
|
||||
controller := h.operator() // == the runtime-resolved DEX governance controller
|
||||
|
||||
native := [32]byte{}
|
||||
seamBefore := loadSeamReserve(newPoolStateAdapter(h.state), native).Uint64()
|
||||
|
||||
// (a) Controller calls seedSeamReserve(native, 1_000_000) but the host frame delivers
|
||||
// NO msg.value (we do NOT add balance to 0x9999). A mint would inflate seamReserve;
|
||||
// the observed-delta discipline must yield 0 delivered and REVERT.
|
||||
data := make([]byte, 64) // asset = address(0), amount
|
||||
new(big.Int).SetUint64(1_000_000).FillBytes(data[32:64])
|
||||
_, _, err := h.c.Run(h.state, controller, poolManagerAddr9999, prependSelector(SelectorSeedSeamReserve, data), 5_000_000, false)
|
||||
if err == nil {
|
||||
t.Fatal("DAO controller MINTED seam reserve with no value delivered (do-not-ship)")
|
||||
}
|
||||
if !errors.Is(err, ErrSeedUndelivered) {
|
||||
t.Fatalf("expected ErrSeedUndelivered (no mint), got: %v", err)
|
||||
}
|
||||
if got := loadSeamReserve(newPoolStateAdapter(h.state), native).Uint64(); got != seamBefore {
|
||||
t.Fatalf("seam reserve changed on an undelivered seed: %d -> %d (mint)", seamBefore, got)
|
||||
}
|
||||
|
||||
// (b) A depositor funds the depositor pot; the controller cannot drain it via a
|
||||
// settlement (settlement draws ONLY seamReserve, which is empty here). This reuses the
|
||||
// proven conservation primitive directly.
|
||||
depositor := common.HexToAddress("0xDEadBEEF00000000000000000000000000000001")
|
||||
h.state.stateDB.AddBalance(depositor, uint256.NewInt(1000))
|
||||
h.state.stateDB.AddBalance(poolManagerAddr9999, uint256.NewInt(1000))
|
||||
depData := make([]byte, 64)
|
||||
new(big.Int).SetInt64(1000).FillBytes(depData[32:64])
|
||||
if _, _, derr := h.c.Run(h.state, depositor, poolManagerAddr9999, prependSelector(SelectorDeposit, depData), 5_000_000, false); derr != nil {
|
||||
t.Fatalf("depositor deposit: %v", derr)
|
||||
}
|
||||
// Controller attempts to credit ITSELF out of the vault via a settlement. RED probes
|
||||
// TWO raid shapes, BOTH must be refused with the depositor pot intact:
|
||||
// (i) a FABRICATED object id (no D->C object exists) — refused at object lookup;
|
||||
// (ii) a REAL D->C object but drawing native whose SEAM RESERVE is empty — refused
|
||||
// unbacked (the seam pot, not the depositor pot, backs a credit).
|
||||
// (i) fabricated object: there is no privileged path to mint a settlement object.
|
||||
credited, ierr := h.c.atomicImport(h.state, SettlementClaim{
|
||||
OutputID: [32]byte{0xAB}, Asset: native, AssetAddr: common.Address{}, Amount: 500, Recipient: controller,
|
||||
})
|
||||
if ierr == nil || credited != 0 {
|
||||
t.Fatalf("controller credited itself from a fabricated settlement object: credited=%d err=%v", credited, ierr)
|
||||
}
|
||||
// (ii) a REAL D->C object exists, but seamReserve[native] is empty (only the depositor
|
||||
// pot holds native). The credit must revert UNBACKED — it cannot draw the depositor pot.
|
||||
realObj := [32]byte{0xCD, 0xEF}
|
||||
h.putDtoCObject(t, controller, realObj, native, 500)
|
||||
credited2, ierr2 := h.c.atomicImport(h.state, SettlementClaim{
|
||||
OutputID: realObj, Asset: native, AssetAddr: common.Address{}, Amount: 500, Recipient: controller,
|
||||
})
|
||||
if ierr2 != ErrNativeSettleUnbacked {
|
||||
t.Fatalf("controller raided the depositor pot via a real object with empty seam: credited=%d err=%v (must be ErrNativeSettleUnbacked)", credited2, ierr2)
|
||||
}
|
||||
if loadDepositorClaim(newPoolStateAdapter(h.state), depositor, native).Int64() != 1000 {
|
||||
t.Fatal("depositor claim moved after controller's raid attempt (do-not-ship)")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CASE 10 DoS PROBE: throw adversarial calldata at the public permissionless ingress and
|
||||
// assert the precompile (a) charges gas up-front (bounded), (b) never loops unbounded on
|
||||
// attacker-controlled length, (c) refuses malformed input cheaply. RED probes the three
|
||||
// public entrypoints: swap, swapPlace, initialize.
|
||||
//
|
||||
// The precompile model: every handler debits a FIXED gas floor (GasSwap/GasPoolCreate/...)
|
||||
// at the top and decodes FIXED-WIDTH ABI words. There is no attacker-controlled loop bound
|
||||
// in the dispatch. RED confirms a giant calldata blob does not translate into giant work:
|
||||
// the handler reads only the fixed prefix it needs and ignores the tail.
|
||||
// ---------------------------------------------------------------------------
|
||||
func TestRED_GateC_UnauthenticatedIngressIsBounded(t *testing.T) {
|
||||
h := newE2EHarness(t)
|
||||
attacker := common.HexToAddress("0x00000000000000000000000000000000A77ac5e1")
|
||||
|
||||
// (a) Oversized swap calldata: a valid PoolKey+params prefix followed by 1 MiB of
|
||||
// attacker tail. The handler must read only the fixed prefix; the tail is inert. It
|
||||
// must not hang or allocate proportional to the tail (the test simply completing
|
||||
// quickly + returning a normal admission error proves no tail-proportional work).
|
||||
params := SwapParams{ZeroForOne: true, AmountSpecified: big.NewInt(-100), SqrtPriceLimitX96: big.NewInt(0)}
|
||||
base := buildSwapCalldata(h.key, params, EncodeMinOutHookData(1))
|
||||
huge := append(append([]byte(nil), base...), make([]byte, 1<<20)...) // +1 MiB tail
|
||||
_, gasLeft, err := runWithEVMSnapshot(h.c, h.state, attacker, poolManagerAddr9999,
|
||||
prependSelector(SelectorSwap, huge), 5_000_000, false)
|
||||
// It should either fill or fail an admission/policy check — NOT consume work
|
||||
// proportional to the 1 MiB tail. Gas charged must be the bounded swap floor, not
|
||||
// tail-scaled. We assert the call returned (no hang) and charged a bounded amount.
|
||||
if err == nil {
|
||||
// A fill is fine (the e2e market's assets are real); assert gas was bounded.
|
||||
if 5_000_000-gasLeft > GasSwap+1_000_000 {
|
||||
t.Fatalf("swap charged tail-scaled gas: charged=%d (floor=%d)", 5_000_000-gasLeft, GasSwap)
|
||||
}
|
||||
}
|
||||
|
||||
// (b) hookData length field that LIES (claims a huge body). DecodeSwapInput / the
|
||||
// min-out decoder must bound-check against the actual buffer, never allocate the
|
||||
// claimed size. RED crafts a swap whose hookData header claims a 4 GiB body.
|
||||
evil := buildSwapCalldata(h.key, params, nil)
|
||||
// Corrupt nothing structurally valid is needed: feed a truncated/forged blob and
|
||||
// assert a clean error, not a panic/OOM. A swap with malformed trailing bytes must
|
||||
// fall through to a deterministic error.
|
||||
forged := append(append([]byte(nil), evil...), 0xFF, 0xFF, 0xFF, 0xFF) // junk tail
|
||||
_, _, err2 := runWithEVMSnapshot(h.c, h.state, attacker, poolManagerAddr9999,
|
||||
prependSelector(SelectorSwap, forged), 5_000_000, false)
|
||||
_ = err2 // must not panic; any error/fill is acceptable — the assertion is "no panic, returned"
|
||||
|
||||
// (c) initialize with a giant trailing hookData blob: the registry write reads a
|
||||
// fixed 192-byte prefix; the tail must be inert.
|
||||
initData := make([]byte, 192)
|
||||
copy(initData[0:160], EncodePoolKeyABI(h.key))
|
||||
// sqrtPriceX96 = a valid in-range price (1<<96).
|
||||
new(big.Int).Lsh(big.NewInt(1), 96).FillBytes(initData[160:192])
|
||||
bigInit := append(append([]byte(nil), initData...), make([]byte, 1<<20)...) // +1 MiB tail
|
||||
_, initGasLeft, ierr := h.c.Run(h.state, attacker, poolManagerAddr9999,
|
||||
prependSelector(SelectorInitialize, bigInit), 5_000_000, false)
|
||||
_ = ierr // may fail (already-initialized / admission) — must be bounded + return
|
||||
if 5_000_000-initGasLeft > GasPoolCreate+1_000_000 {
|
||||
t.Fatalf("initialize charged tail-scaled gas: charged=%d (floor=%d)", 5_000_000-initGasLeft, GasPoolCreate)
|
||||
}
|
||||
|
||||
// (d) Truncated calldata at EVERY length from 4..200 must NEVER panic — only return a
|
||||
// clean error. This is the classic precompile fuzz: a short read must be a bounded
|
||||
// revert, never an index-out-of-range.
|
||||
full := prependSelector(SelectorSwap, buildSwapCalldata(h.key, params, EncodeMinOutHookData(1)))
|
||||
for n := 4; n < 200 && n < len(full); n++ {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("PANIC on truncated swap calldata len=%d: %v (DoS — do-not-ship)", n, r)
|
||||
}
|
||||
}()
|
||||
_, _, _ = runWithEVMSnapshot(h.c, h.state, attacker, poolManagerAddr9999, full[:n], 5_000_000, false)
|
||||
}()
|
||||
}
|
||||
}
|
||||
+41
-3
@@ -7,11 +7,22 @@ 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 protocolFeeController (governance), the SAME authority that gates the
|
||||
// existing pauseDEX/freezePool controls — one authority, not a new one.
|
||||
// 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
|
||||
@@ -42,6 +53,32 @@ var (
|
||||
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}
|
||||
@@ -72,7 +109,8 @@ func checkHalt(stateDB stateKV, key PoolKey, params SwapParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Governance setters (protocolFeeController-gated; the caller checks authority).
|
||||
// --- 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) }
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
)
|
||||
|
||||
// halt_governance_test.go is the HIGH-3 TDD proof: the 0x9999 emergency-halt authority
|
||||
// is the per-NETWORK DEX governance controller resolved at runtime from
|
||||
// contract.AtomicState.GovernanceController() — a governance CONTRACT, NEVER a single
|
||||
// hardcoded mnemonic-derivable EOA. It proves, through the production Run dispatch:
|
||||
//
|
||||
// 1. ONLY the configured governance controller may halt (global / asset / market), and
|
||||
// the halt actually BITES (a subsequent swap reverts with the right halt error).
|
||||
// 2. The RETIRED hardcoded EOA (0x9011…94714, the old DefaultDAOTreasury, derivable
|
||||
// from the dev mnemonic) CANNOT halt — it is unauthorized like any other caller.
|
||||
// 3. An arbitrary EOA CANNOT halt.
|
||||
// 4. When NO governance controller is configured (the zero address), NOBODY can halt:
|
||||
// the gate is fail-closed (ErrSettleNoGovernance), so an unset network can be neither
|
||||
// censored nor DoS'd through this authority. There is no default key to abuse.
|
||||
// 5. The SAME gate guards pot seeding (seedSeamReserve): the retired EOA cannot seed.
|
||||
// 6. Conservation is preserved: governance can halt/unhalt but mints nothing (the
|
||||
// unhalt restores trading; CASE 6 in gatec_red_probe_test.go proves no mint/move).
|
||||
|
||||
// haltGlobal builds and runs setHaltGlobal(on) from `caller`, returning the dispatch err.
|
||||
func haltGlobal(h *settleHarness, caller common.Address, on bool) error {
|
||||
data := make([]byte, 32)
|
||||
if on {
|
||||
data[31] = 1
|
||||
}
|
||||
_, _, err := h.c.Run(h.state, caller, poolManagerAddr9999,
|
||||
prependSelector(SelectorSetHaltGlobal, data), 5_000_000, false)
|
||||
return err
|
||||
}
|
||||
|
||||
// haltAsset builds and runs setHaltAsset(assetID, on) from `caller`.
|
||||
func haltAsset(h *settleHarness, caller common.Address, assetID [32]byte, on bool) error {
|
||||
data := make([]byte, 64)
|
||||
copy(data[0:32], assetID[:])
|
||||
if on {
|
||||
data[63] = 1
|
||||
}
|
||||
_, _, err := h.c.Run(h.state, caller, poolManagerAddr9999,
|
||||
prependSelector(SelectorSetHaltAsset, data), 5_000_000, false)
|
||||
return err
|
||||
}
|
||||
|
||||
// haltMarket builds and runs setHaltMarket(marketID, on) from `caller`.
|
||||
func haltMarket(h *settleHarness, caller common.Address, marketID [32]byte, on bool) error {
|
||||
data := make([]byte, 64)
|
||||
copy(data[0:32], marketID[:])
|
||||
if on {
|
||||
data[63] = 1
|
||||
}
|
||||
_, _, err := h.c.Run(h.state, caller, poolManagerAddr9999,
|
||||
prependSelector(SelectorSetHaltMarket, data), 5_000_000, false)
|
||||
return err
|
||||
}
|
||||
|
||||
// TestHIGH3_OnlyGovernanceCanHalt_RetiredEOACannot is the central HIGH-3 proof.
|
||||
func TestHIGH3_OnlyGovernanceCanHalt_RetiredEOACannot(t *testing.T) {
|
||||
h := newSettleHarness(t)
|
||||
gov := h.operator() // the runtime-resolved governance controller (testGovernance)
|
||||
|
||||
// Sanity: the test governance address is NOT the retired hardcoded EOA, and the
|
||||
// retired EOA is a real-looking address (so "it cannot halt" is a meaningful claim).
|
||||
if gov == retiredHardcodedHaltEOA {
|
||||
t.Fatal("test setup error: governance must differ from the retired hardcoded EOA")
|
||||
}
|
||||
arbitrary := common.HexToAddress("0xBADBADBADBADBADBADBADBADBADBADBADBADBADB")
|
||||
|
||||
// (2)+(3): the retired EOA and an arbitrary EOA are BOTH unauthorized to halt.
|
||||
if err := haltGlobal(h, retiredHardcodedHaltEOA, true); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("retired 0x9011 EOA must NOT be able to setHaltGlobal — got %v, want ErrUnauthorized", err)
|
||||
}
|
||||
if err := haltAsset(h, retiredHardcodedHaltEOA, h.inAssetID(), true); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("retired 0x9011 EOA must NOT be able to setHaltAsset — got %v, want ErrUnauthorized", err)
|
||||
}
|
||||
if err := haltGlobal(h, arbitrary, true); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("arbitrary EOA must NOT be able to setHaltGlobal — got %v, want ErrUnauthorized", err)
|
||||
}
|
||||
|
||||
// Confirm none of those rejected calls actually halted anything (the gate ran before
|
||||
// any state write; a reverted halt write rolls back with the revert).
|
||||
if checkHalt(newPoolStateAdapter(h.state), h.key, h.params) != nil {
|
||||
t.Fatal("an unauthorized halt attempt wrote halt state (do-not-ship)")
|
||||
}
|
||||
|
||||
// (1): the configured governance controller CAN halt globally.
|
||||
if err := haltGlobal(h, gov, true); err != nil {
|
||||
t.Fatalf("governance controller must be able to setHaltGlobal, got: %v", err)
|
||||
}
|
||||
if got := checkHalt(newPoolStateAdapter(h.state), h.key, h.params); !errors.Is(got, ErrDEXHalted) {
|
||||
t.Fatalf("global halt did not bite: checkHalt = %v, want ErrDEXHalted", got)
|
||||
}
|
||||
// And governance can lift it (no mint, just unhalt).
|
||||
if err := haltGlobal(h, gov, false); err != nil {
|
||||
t.Fatalf("governance controller must be able to clear the global halt, got: %v", err)
|
||||
}
|
||||
if got := checkHalt(newPoolStateAdapter(h.state), h.key, h.params); got != nil {
|
||||
t.Fatalf("global halt not cleared: checkHalt = %v, want nil", got)
|
||||
}
|
||||
|
||||
// (1) asset-scoped: governance halts the input asset; checkHalt reports ErrAssetHalted.
|
||||
if err := haltAsset(h, gov, h.inAssetID(), true); err != nil {
|
||||
t.Fatalf("governance setHaltAsset(in) failed: %v", err)
|
||||
}
|
||||
if got := checkHalt(newPoolStateAdapter(h.state), h.key, h.params); !errors.Is(got, ErrAssetHalted) {
|
||||
t.Fatalf("asset halt did not bite: checkHalt = %v, want ErrAssetHalted", got)
|
||||
}
|
||||
if err := haltAsset(h, gov, h.inAssetID(), false); err != nil {
|
||||
t.Fatalf("governance clear setHaltAsset(in) failed: %v", err)
|
||||
}
|
||||
|
||||
// (1) market-scoped: governance halts this pool; checkHalt reports ErrMarketHalted.
|
||||
poolID := h.key.ID()
|
||||
if err := haltMarket(h, gov, poolID, true); err != nil {
|
||||
t.Fatalf("governance setHaltMarket failed: %v", err)
|
||||
}
|
||||
if got := checkHalt(newPoolStateAdapter(h.state), h.key, h.params); !errors.Is(got, ErrMarketHalted) {
|
||||
t.Fatalf("market halt did not bite: checkHalt = %v, want ErrMarketHalted", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHIGH3_FailClosed_NoGovernanceConfigured_NobodyCanHalt proves the maximally-
|
||||
// decentralized default: a network with NO governance controller (zero address) has NO
|
||||
// halt authority at all — not the retired EOA, not even a would-be admin. Every halt
|
||||
// attempt is fail-closed (ErrSettleNoGovernance). This is strictly safer than a default
|
||||
// key: an unset network can be neither censored nor DoS'd through this authority.
|
||||
func TestHIGH3_FailClosed_NoGovernanceConfigured_NobodyCanHalt(t *testing.T) {
|
||||
h := newSettleHarness(t)
|
||||
h.state.governance = common.Address{} // network configured NO governance controller
|
||||
|
||||
for _, caller := range []common.Address{
|
||||
retiredHardcodedHaltEOA,
|
||||
h.caller,
|
||||
common.HexToAddress("0x60F0000000000000000000000000000000000A11"), // even the would-be gov addr
|
||||
} {
|
||||
if err := haltGlobal(h, caller, true); !errors.Is(err, ErrSettleNoGovernance) {
|
||||
t.Fatalf("with no governance configured, setHaltGlobal by %s must be fail-closed — got %v, want ErrSettleNoGovernance",
|
||||
caller.Hex(), err)
|
||||
}
|
||||
if err := haltAsset(h, caller, h.inAssetID(), true); !errors.Is(err, ErrSettleNoGovernance) {
|
||||
t.Fatalf("with no governance configured, setHaltAsset by %s must be fail-closed — got %v, want ErrSettleNoGovernance",
|
||||
caller.Hex(), err)
|
||||
}
|
||||
}
|
||||
// Nothing was halted: the swap path is unaffected (no fail-OPEN, no fail-into-halt).
|
||||
if got := checkHalt(newPoolStateAdapter(h.state), h.key, h.params); got != nil {
|
||||
t.Fatalf("fail-closed authority must not have written halt state: checkHalt = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHIGH3_RetiredEOACannotSeedPots proves the SAME governance gate guards the operator
|
||||
// pot-seeding selectors (seedSeamReserve), so the retired EOA cannot seed/credit either.
|
||||
// (CASE 6 in gatec_red_probe_test.go already proves the AUTHORIZED controller cannot mint;
|
||||
// here we prove the retired EOA is not even authorized to attempt it.)
|
||||
func TestHIGH3_RetiredEOACannotSeedPots(t *testing.T) {
|
||||
h := newSettleHarness(t)
|
||||
|
||||
// seedSeamReserve(address(0), 1_000_000) by the retired EOA: unauthorized, before any
|
||||
// value accounting. The gate is the governance controller, not a hardcoded key.
|
||||
data := make([]byte, 64)
|
||||
new(big.Int).SetUint64(1_000_000).FillBytes(data[32:64])
|
||||
if _, _, err := h.c.Run(h.state, retiredHardcodedHaltEOA, poolManagerAddr9999,
|
||||
prependSelector(SelectorSeedSeamReserve, data), 5_000_000, false); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("retired 0x9011 EOA must NOT be able to seedSeamReserve — got %v, want ErrUnauthorized", err)
|
||||
}
|
||||
|
||||
// The configured governance controller IS authorized to attempt the seed (it then
|
||||
// fails ErrSeedUndelivered because no value was delivered — proving authorization is
|
||||
// distinct from the conservation guarantee, which still holds).
|
||||
if _, _, err := h.c.Run(h.state, h.operator(), poolManagerAddr9999,
|
||||
prependSelector(SelectorSeedSeamReserve, data), 5_000_000, false); !errors.Is(err, ErrSeedUndelivered) {
|
||||
t.Fatalf("governance controller seed with no value must reach the conservation check (ErrSeedUndelivered), got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHIGH3_HaltBitesARealFill is the end-to-end proof that governance-set halt actually
|
||||
// stops a real swap: governance halts the market, a real fillable swap reverts with the
|
||||
// halt error, governance lifts it, and the swap then fills. This closes the loop —
|
||||
// "governance halts" means "trading actually stops", and only governance can do it.
|
||||
func TestHIGH3_HaltBitesARealFill(t *testing.T) {
|
||||
h := newE2EHarness(t)
|
||||
gov := h.operator()
|
||||
maker, taker := e2eMaker, e2eTaker
|
||||
|
||||
// Seed a resting bid so a taker SELL of base (LETH) for quote (LUSD) can fill.
|
||||
h.mint(e2eLUSD, maker, 1_000_000)
|
||||
h.deposit(t, maker, e2eLUSD, 1_000_000)
|
||||
h.placeArgs(t, maker, true, uint64(50)*uint64(priceMultiplierConst), 1000)
|
||||
h.mint(e2eLETH, taker, 100)
|
||||
|
||||
// Governance halts THIS market.
|
||||
poolID := h.key.ID()
|
||||
if err := haltMarket(h.settleHarness, gov, poolID, true); err != nil {
|
||||
t.Fatalf("governance setHaltMarket: %v", err)
|
||||
}
|
||||
|
||||
// A real, otherwise-fillable swap now reverts with the market-halt error.
|
||||
if _, err := h.swapMinOut(t, taker, 100, 1); !errors.Is(err, ErrMarketHalted) {
|
||||
t.Fatalf("halted market must refuse a fill with ErrMarketHalted, got: %v", err)
|
||||
}
|
||||
|
||||
// Only governance can lift it (the retired EOA cannot).
|
||||
if err := haltMarket(h.settleHarness, retiredHardcodedHaltEOA, poolID, false); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("retired EOA must NOT be able to clear the market halt, got: %v", err)
|
||||
}
|
||||
if err := haltMarket(h.settleHarness, gov, poolID, false); err != nil {
|
||||
t.Fatalf("governance must be able to clear the market halt, got: %v", err)
|
||||
}
|
||||
|
||||
// With the halt lifted, the same swap fills.
|
||||
if _, err := h.swapMinOut(t, taker, 100, 1); err != nil {
|
||||
t.Fatalf("after governance lifted the halt, the swap must fill, got: %v", err)
|
||||
}
|
||||
}
|
||||
+35
-20
@@ -30,6 +30,15 @@ import (
|
||||
// so a C->D Put is readable by D via Get(cChainID) and a D->C Put is readable by C
|
||||
// via Get(dChainID) — the real platformvm import/export semantics.
|
||||
|
||||
// testGovernance is the harness's per-network DEX governance controller — the address
|
||||
// the mock AtomicState returns from GovernanceController(), i.e. the ONLY caller that may
|
||||
// halt 0x9999 settlement or seed its pots. In production this is a governance CONTRACT
|
||||
// the host wires per network; here it is a fixed test address that stands in for that
|
||||
// contract. It is deliberately a non-EOA-looking sentinel distinct from the harness's
|
||||
// trading caller (0x1111…) and from the retired mnemonic-derivable 0x9011… EOA, so the
|
||||
// governance tests can prove only this address halts and the old EOA cannot.
|
||||
var testGovernance = common.HexToAddress("0x60F0000000000000000000000000000000000A11") // "GOV ...0A11"
|
||||
|
||||
// nativeAtomicState implements BOTH contract.AccessibleState and
|
||||
// contract.AtomicState so SettleSwap's `state.(contract.AtomicState)` assertion
|
||||
// succeeds and reaches a real SharedMemory + chain identity.
|
||||
@@ -39,7 +48,8 @@ type nativeAtomicState struct {
|
||||
networkID uint32
|
||||
chainID ids.ID // THIS chain (C)
|
||||
cChainID ids.ID
|
||||
dChainID ids.ID // the D-Chain (dexvm) peer the host resolves at runtime
|
||||
dChainID ids.ID // the D-Chain (dexvm) peer the host resolves at runtime
|
||||
governance common.Address // the per-network DEX governance controller (halt/seed authority)
|
||||
txID ids.ID
|
||||
callIndex uint32
|
||||
blockTimestamp uint64 // block time the precompile reads via GetBlockContext().Timestamp()
|
||||
@@ -57,13 +67,14 @@ func (m *nativeAtomicState) GetChainConfig() precompileconfig.ChainConfig {
|
||||
func (m *nativeAtomicState) GetPrecompileEnv() contract.PrecompileEnvironment { return nil }
|
||||
|
||||
// --- AtomicState ---
|
||||
func (m *nativeAtomicState) AtomicMemory() atomic.SharedMemory { return m.sm }
|
||||
func (m *nativeAtomicState) NetworkID() uint32 { return m.networkID }
|
||||
func (m *nativeAtomicState) ChainID() ids.ID { return m.chainID }
|
||||
func (m *nativeAtomicState) CChainID() ids.ID { return m.cChainID }
|
||||
func (m *nativeAtomicState) DChainID() ids.ID { return m.dChainID }
|
||||
func (m *nativeAtomicState) TxID() ids.ID { return m.txID }
|
||||
func (m *nativeAtomicState) CallIndex() uint32 { return m.callIndex }
|
||||
func (m *nativeAtomicState) AtomicMemory() atomic.SharedMemory { return m.sm }
|
||||
func (m *nativeAtomicState) NetworkID() uint32 { return m.networkID }
|
||||
func (m *nativeAtomicState) ChainID() ids.ID { return m.chainID }
|
||||
func (m *nativeAtomicState) CChainID() ids.ID { return m.cChainID }
|
||||
func (m *nativeAtomicState) GovernanceController() common.Address { return m.governance }
|
||||
func (m *nativeAtomicState) DChainID() ids.ID { return m.dChainID }
|
||||
func (m *nativeAtomicState) TxID() ids.ID { return m.txID }
|
||||
func (m *nativeAtomicState) CallIndex() uint32 { return m.callIndex }
|
||||
|
||||
var _ contract.AccessibleState = (*nativeAtomicState)(nil)
|
||||
var _ contract.AtomicState = (*nativeAtomicState)(nil)
|
||||
@@ -104,21 +115,22 @@ func newSettleHarnessN(t testing.TB, _ int) *settleHarness {
|
||||
dSM := mem.NewSharedMemory(dChainID)
|
||||
|
||||
state := &nativeAtomicState{
|
||||
stateDB: NewMockStateDB(),
|
||||
sm: cSM,
|
||||
networkID: 1,
|
||||
chainID: cChainID,
|
||||
cChainID: cChainID,
|
||||
dChainID: dChainID, // host-resolved D peer (runtime), as the EVM adapter supplies it
|
||||
txID: ids.ID{0x7A}, // a fixed tx id for the harness; tests vary callIndex/txID
|
||||
callIndex: 0,
|
||||
stateDB: NewMockStateDB(),
|
||||
sm: cSM,
|
||||
networkID: 1,
|
||||
chainID: cChainID,
|
||||
cChainID: cChainID,
|
||||
dChainID: dChainID, // host-resolved D peer (runtime), as the EVM adapter supplies it
|
||||
governance: testGovernance, // the per-network governance controller the host supplies via AtomicState
|
||||
txID: ids.ID{0x7A}, // a fixed tx id for the harness; tests vary callIndex/txID
|
||||
callIndex: 0,
|
||||
// Default to the activation boundary so settlement/init tests exercise the
|
||||
// live-chain path where 0x9999's DEXFill + Initialize logs are active. A test
|
||||
// that needs a pre-activation block sets state.blockTimestamp explicitly.
|
||||
blockTimestamp: DexSettleActivationTime,
|
||||
}
|
||||
h := &settleHarness{
|
||||
c: &SettleContract{protocolFeeController: common.HexToAddress("0xFEE0000000000000000000000000000000000001")},
|
||||
c: &SettleContract{},
|
||||
state: state,
|
||||
mem: mem,
|
||||
cSM: cSM,
|
||||
@@ -202,9 +214,12 @@ func (h *settleHarness) fundCallerNative(amount int64) {
|
||||
h.state.stateDB.AddBalance(h.caller, uint256.NewInt(uint64(amount)))
|
||||
}
|
||||
|
||||
// operator is the harness's protocolFeeController (the SettleContract is constructed
|
||||
// with this authority), so the operator-gated seed/fee selectors accept it.
|
||||
func (h *settleHarness) operator() common.Address { return h.c.protocolFeeController }
|
||||
// operator is the harness's DEX governance controller — the authority the mock
|
||||
// AtomicState returns from GovernanceController(), the SOLE caller the governance-gated
|
||||
// seed/fee/halt selectors accept. It is the per-network governance address the host
|
||||
// supplies at runtime (testGovernance here), NOT a field on the SettleContract and NOT a
|
||||
// hardcoded mnemonic-derivable EOA.
|
||||
func (h *settleHarness) operator() common.Address { return h.state.governance }
|
||||
|
||||
// fundVaultOut seeds the 0x9999 vault's OUTPUT token into the SEAM RESERVE through the
|
||||
// REAL operator-gated seedSeamReserve selector (FIX-4 — the production counterparty
|
||||
|
||||
@@ -321,7 +321,7 @@ func TestERC20Settle_CallSeam_DepositWithdrawConserves(t *testing.T) {
|
||||
t.Fatal("GetPrecompileEnv() does not satisfy callableEnv: ERC-20 leg would refuse with ErrERC20VaultUnavailable")
|
||||
}
|
||||
|
||||
c := &SettleContract{protocolFeeController: common.HexToAddress("0xFEE0000000000000000000000000000000000001")}
|
||||
c := &SettleContract{} // deposit/withdraw are custody ops, not governance-gated
|
||||
aid := assetID(Currency{Address: token})
|
||||
|
||||
// Conservation oracle: depositor + vault token balances == totalSupply, always; and
|
||||
@@ -436,7 +436,7 @@ func TestERC20Settle_CallSeam_RejectsDelegatecall(t *testing.T) {
|
||||
env: env,
|
||||
timestamp: DexSettleActivationTime,
|
||||
}
|
||||
c := &SettleContract{protocolFeeController: common.HexToAddress("0xFEE0000000000000000000000000000000000001")}
|
||||
c := &SettleContract{} // deposit/withdraw are custody ops, not governance-gated
|
||||
aid := assetID(Currency{Address: token})
|
||||
|
||||
depData := make([]byte, 64)
|
||||
@@ -502,7 +502,7 @@ func TestERC20Settle_NoEnv_RefusesFailSecure(t *testing.T) {
|
||||
t.Fatal("setup")
|
||||
}
|
||||
|
||||
c := &SettleContract{protocolFeeController: common.HexToAddress("0xFEE0000000000000000000000000000000000001")}
|
||||
c := &SettleContract{} // deposit/withdraw are custody ops, not governance-gated
|
||||
depData := make([]byte, 64)
|
||||
copy(depData[12:32], token.Bytes())
|
||||
big.NewInt(1000).FillBytes(depData[32:64])
|
||||
|
||||
+56
-35
@@ -13,15 +13,18 @@ import (
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
)
|
||||
|
||||
// DefaultDAOTreasury is the built-in canonical governance address for 0x9999 — the
|
||||
// Lux DAO treasury. It is the protocolFeeController that gates the settlement kill
|
||||
// switches (setHaltGlobal/Market/Asset) and the operator-gated pot seeding
|
||||
// (seedSeamReserve / creditPositionFee). It is hard-coded (NOT per-network config)
|
||||
// because 0x9999 is always-on with zero per-net configuration and DEX governance is
|
||||
// the same DAO across every Lux network. It is deliberately NOT one of the publicly-
|
||||
// known compromised dev keys (compromisedDevControllers), so it is a safe default
|
||||
// admin from genesis. Forward-perfect: one canonical governance authority, one way.
|
||||
var DefaultDAOTreasury = common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9DEd94714")
|
||||
// 0x9999 settlement governance (setHaltGlobal/Market/Asset, seedSeamReserve,
|
||||
// creditPositionFee) 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
|
||||
@@ -31,8 +34,8 @@ var DefaultDAOTreasury = common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9
|
||||
// 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: register a validator set, set halt layers (gated on
|
||||
// the same protocolFeeController authority as the existing pause controls).
|
||||
// - 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.
|
||||
@@ -56,12 +59,13 @@ var settleConfigKey = "dexSettleConfig"
|
||||
// (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 holds the same
|
||||
// protocolFeeController authority value as the 0x9010 contract (set at Configure)
|
||||
// so halt/registry governance is gated identically.
|
||||
type SettleContract struct {
|
||||
protocolFeeController common.Address
|
||||
}
|
||||
// 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)
|
||||
|
||||
@@ -101,7 +105,9 @@ var (
|
||||
// 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:
|
||||
// - protocolFeeController: the built-in DefaultDAOTreasury (set in init below).
|
||||
// - 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;
|
||||
@@ -130,10 +136,11 @@ func init() {
|
||||
SelectorSeedSeamReserve = keccak4("seedSeamReserve(address,uint256)")
|
||||
SelectorCreditPositionFee = keccak4("creditPositionFee(bytes32,address,uint256)")
|
||||
|
||||
// The settlement governance authority is the built-in DAO treasury — fixed across
|
||||
// every network (always-on, zero per-net config). This is the SOLE source of
|
||||
// s.protocolFeeController; there is no Configure step that could set it from genesis.
|
||||
SettlePrecompile.protocolFeeController = DefaultDAOTreasury
|
||||
// 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)
|
||||
@@ -144,10 +151,10 @@ func (*settleConfigurator) MakeConfig() precompileconfig.Config { return &Settle
|
||||
|
||||
// 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 — protocolFeeController is the built-in DAO treasury (set in init) and the
|
||||
// chain identity is resolved at runtime from the atomic capability. The method exists
|
||||
// solely to satisfy the contract.Configurator interface. It validates the config type
|
||||
// so a misuse surfaces loudly rather than silently.
|
||||
// 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,
|
||||
@@ -182,7 +189,7 @@ func (c *SettleConfig) Equal(cfg precompileconfig.Config) bool {
|
||||
|
||||
// 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 protocolFeeController.
|
||||
// pool manager; governance selectors gate on the runtime governance controller.
|
||||
func (s *SettleContract) Run(
|
||||
accessibleState contract.AccessibleState,
|
||||
caller common.Address,
|
||||
@@ -313,9 +320,10 @@ func (s *SettleContract) Run(
|
||||
case SelectorSwapCancel:
|
||||
return s.runSwapCancel(accessibleState, caller, data, suppliedGas, readOnly)
|
||||
|
||||
// Settlement governance (protocolFeeController-gated). 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.
|
||||
// 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:
|
||||
@@ -323,7 +331,7 @@ func (s *SettleContract) Run(
|
||||
case SelectorSetHaltAsset:
|
||||
return s.runSetHaltScoped(accessibleState, caller, data, suppliedGas, readOnly, SetHaltAsset)
|
||||
|
||||
// Operator funding of the settlement pots (protocolFeeController-gated). The swap
|
||||
// Operator funding of the settlement pots (governance-controller-gated). The swap
|
||||
// rail's seamReserve counterparty seed (FIX-4 liveness: a market's first matched
|
||||
// swap settles without manual state-poking) and the LP rail's per-owner fee credit.
|
||||
case SelectorSeedSeamReserve:
|
||||
@@ -338,7 +346,10 @@ func (s *SettleContract) Run(
|
||||
|
||||
const gasHaltAdmin uint64 = 25_000
|
||||
|
||||
// runSetHaltGlobal toggles the global halt (governance only).
|
||||
// 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) {
|
||||
@@ -348,7 +359,11 @@ func (s *SettleContract) runSetHaltGlobal(
|
||||
if gas < gasHaltAdmin {
|
||||
return nil, 0, errors.New("dex: out of gas")
|
||||
}
|
||||
if caller != s.protocolFeeController {
|
||||
gov, gerr := governanceController(state)
|
||||
if gerr != nil {
|
||||
return nil, gas - gasHaltAdmin, gerr
|
||||
}
|
||||
if caller != gov {
|
||||
return nil, gas - gasHaltAdmin, ErrUnauthorized
|
||||
}
|
||||
if len(data) < 32 {
|
||||
@@ -359,7 +374,9 @@ func (s *SettleContract) runSetHaltGlobal(
|
||||
return nil, gas - gasHaltAdmin, nil
|
||||
}
|
||||
|
||||
// runSetHaltScoped toggles a [32]byte-scoped halt (market/asset/validatorSet).
|
||||
// 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),
|
||||
@@ -370,7 +387,11 @@ func (s *SettleContract) runSetHaltScoped(
|
||||
if gas < gasHaltAdmin {
|
||||
return nil, 0, errors.New("dex: out of gas")
|
||||
}
|
||||
if caller != s.protocolFeeController {
|
||||
gov, gerr := governanceController(state)
|
||||
if gerr != nil {
|
||||
return nil, gas - gasHaltAdmin, gerr
|
||||
}
|
||||
if caller != gov {
|
||||
return nil, gas - gasHaltAdmin, ErrUnauthorized
|
||||
}
|
||||
if len(data) < 64 {
|
||||
|
||||
+16
-4
@@ -64,7 +64,9 @@ var (
|
||||
// runSeedSeamReserve funds seamReserve[asset] from operator-delivered value. Native:
|
||||
// observed-delta (the host frame moved msg.value into 0x9999; delivered = realBal −
|
||||
// Σpots), exactly the deposit discipline so a value==0 seed delivers 0 and reverts.
|
||||
// ERC-20: transferFrom observed delta. protocolFeeController-gated.
|
||||
// ERC-20: transferFrom observed delta. Gated to the per-network DEX governance
|
||||
// controller (governanceController), resolved from the runtime AtomicState — never a
|
||||
// hardcoded key; fail-closed when no governance controller is configured.
|
||||
func (s *SettleContract) runSeedSeamReserve(
|
||||
state contract.AccessibleState, caller common.Address, input []byte, gas uint64, readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
@@ -75,7 +77,11 @@ func (s *SettleContract) runSeedSeamReserve(
|
||||
return nil, 0, errors.New("dex: out of gas")
|
||||
}
|
||||
gasLeft := gas - gasSeedReserve
|
||||
if caller != s.protocolFeeController {
|
||||
gov, gerr := governanceController(state)
|
||||
if gerr != nil {
|
||||
return nil, gasLeft, gerr
|
||||
}
|
||||
if caller != gov {
|
||||
return nil, gasLeft, ErrUnauthorized
|
||||
}
|
||||
asset, amount, perr := decodeAssetAmount(input)
|
||||
@@ -105,7 +111,9 @@ func (s *SettleContract) runSeedSeamReserve(
|
||||
// committed reserve by the same amount. So a maker's withdrawable rises with the fees
|
||||
// the D-Chain credited them, the per-owner committed bound stays exact (a collect can
|
||||
// still only pull up to THIS owner's record), and conservation holds (the fee credit
|
||||
// is backed by deposited value). protocolFeeController-gated.
|
||||
// is backed by deposited value). Gated to the per-network DEX governance controller
|
||||
// (governanceController), resolved from the runtime AtomicState — never a hardcoded
|
||||
// key; fail-closed when no governance controller is configured.
|
||||
func (s *SettleContract) runCreditPositionFee(
|
||||
state contract.AccessibleState, caller common.Address, input []byte, gas uint64, readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
@@ -116,7 +124,11 @@ func (s *SettleContract) runCreditPositionFee(
|
||||
return nil, 0, errors.New("dex: out of gas")
|
||||
}
|
||||
gasLeft := gas - gasSeedReserve
|
||||
if caller != s.protocolFeeController {
|
||||
gov, gerr := governanceController(state)
|
||||
if gerr != nil {
|
||||
return nil, gasLeft, gerr
|
||||
}
|
||||
if caller != gov {
|
||||
return nil, gasLeft, ErrUnauthorized
|
||||
}
|
||||
if len(input) < 96 {
|
||||
|
||||
+6
-5
@@ -21,11 +21,12 @@ import (
|
||||
// "may value move?" into ONE thing: a normal swap() reaches here and the swap's own
|
||||
// fail-closed controls — intrinsic to the swap, enforced on EVERY call — decide whether it
|
||||
// fills:
|
||||
// - real-asset registry admission (OpenMarketChecked: the installed resolver), which
|
||||
// fails closed (ErrNoAssetResolver) on a node that never wired a resolver, so a node
|
||||
// that cannot prove an asset real simply cannot fill — replacing the old consensus-mode
|
||||
// gate with a structural one;
|
||||
// - the live-code verifier (EXTCODESIZE on the token address);
|
||||
// - permissionless canonical admission (OpenMarketChecked: the installed resolver derives
|
||||
// each side's canonical identity on the bound network — any well-formed real reference
|
||||
// resolves, no allowlist), which fails closed (ErrNoAssetResolver) on a node that never
|
||||
// wired a resolver — replacing the old consensus-mode gate with a structural one;
|
||||
// - the live-code verifier (EXTCODESIZE on the token address) — the AUTHORITATIVE reality
|
||||
// check that admits any real on-chain ERC-20 and refuses a synthetic/code-less one;
|
||||
// - the per-swap min-out / price floor (slippage protection);
|
||||
// - the halt switches (global/market/asset);
|
||||
// - the single non-reentrant custody mutex (an ERC-20 transfer can re-enter).
|
||||
|
||||
Reference in New Issue
Block a user