feat(dex): 0x9999 settlement logs emit unconditionally (genesis-active, no dated fork)

Mirror of luxfi/evm: first-run network, no legacy — delete the DexSettleActivationTime
gate mirror + dexLogsActive predicate; settlement/swap/market logs emit unconditionally;
drop the now-dead blockTimestamp param from syncSwap. One way, no activation config.
This commit is contained in:
zeekay
2026-06-26 14:40:01 -07:00
parent 053ea150f6
commit ef0b8eab88
8 changed files with 69 additions and 110 deletions
+31 -32
View File
@@ -97,15 +97,13 @@ func (h *settleHarness) countDEXFill() int {
return n
}
// TestDEXFillEvent_NotEmittedBeforeActivation proves the consensus-visible DEXFill
// log is GATED on the Dec-25 dated fork (defense in depth, FIX A): a Phase-B credit
// executed at a block BEFORE DexSettleActivationTime credits the output (the money
// path still runs) but emits NO log — so a settlement that somehow dispatched before
// the boundary cannot add a log to the receipt root that a re-syncing node would not
// reproduce. The credit happening proves the gate is on the LOG, not the settlement.
func TestDEXFillEvent_NotEmittedBeforeActivation(t *testing.T) {
// TestDEXFillEvent_EmittedAtGenesis proves the consensus-visible DEXFill log is AlwaysOn
// (active from genesis, no dated fork): a Phase-B credit executed at genesis (block
// timestamp 0) BOTH credits the output AND emits the DEXFill log — there is no
// pre-activation suppression. Every settlement that executes emits its log, from block 0.
func TestDEXFillEvent_EmittedAtGenesis(t *testing.T) {
h := newSettleHarness(t)
h.state.blockTimestamp = DexSettleActivationTime - 1 // one second before the fork
h.state.blockTimestamp = 0 // genesis — the earliest possible block
h.registerMarket(t)
h.fundVaultOut(10_000)
@@ -114,36 +112,33 @@ func TestDEXFillEvent_NotEmittedBeforeActivation(t *testing.T) {
before := h.tokenBal(h.outToken(), h.caller)
if _, err := h.runSwap(t, h.settlementCalldata(outputID, 250), false); err != nil {
t.Fatalf("phase-B settle (pre-activation): %v", err)
t.Fatalf("phase-B settle (genesis): %v", err)
}
// The credit DID happen (money path is unaffected by the log gate) — the output
// token (currency1) is credited from the seam reserve.
// The credit happened — the output token (currency1) is credited from the seam reserve.
after := h.tokenBal(h.outToken(), h.caller)
if new(big.Int).Sub(after, before).Int64() != 250 {
t.Fatalf("pre-activation settle must still credit 250, got delta %s", new(big.Int).Sub(after, before))
t.Fatalf("genesis settle must credit 250, got delta %s", new(big.Int).Sub(after, before))
}
// ... but NO DEXFill log was written.
if n := h.countDEXFill(); n != 0 {
t.Fatalf("DEXFill must NOT be emitted before activation, got %d", n)
// ... and the DEXFill log WAS written, from genesis.
if n := h.countDEXFill(); n != 1 {
t.Fatalf("DEXFill must be emitted from genesis (AlwaysOn), got %d", n)
}
}
// TestDEXFillEvent_EmittedAtActivation proves the boundary is inclusive: a settlement
// at EXACTLY DexSettleActivationTime emits the DEXFill (the relaunch genesis is at the
// boundary, so the very first settlement must be indexable).
func TestDEXFillEvent_EmittedAtActivation(t *testing.T) {
h := newSettleHarness(t)
h.state.blockTimestamp = DexSettleActivationTime // exactly at the fork
// TestDEXFillEvent_EmittedOnCredit proves a Phase-B credit at a normal block time emits
// exactly one DEXFill the standard indexable settlement signal on the money path.
func TestDEXFillEvent_EmittedOnCredit(t *testing.T) {
h := newSettleHarness(t) // harness default block time
h.registerMarket(t)
h.fundVaultOut(10_000)
outputID := ids.ID{0xDE, 0x44}
h.putDtoCObject(t, h.caller, outputID, h.outAssetID(), 250)
if _, err := h.runSwap(t, h.settlementCalldata(outputID, 250), false); err != nil {
t.Fatalf("phase-B settle (at activation): %v", err)
t.Fatalf("phase-B settle: %v", err)
}
if n := h.countDEXFill(); n != 1 {
t.Fatalf("DEXFill must be emitted at activation, got %d", n)
t.Fatalf("DEXFill must be emitted on a Phase-B credit, got %d", n)
}
}
@@ -154,7 +149,7 @@ func TestDEXFillEvent_EmittedAtActivation(t *testing.T) {
// Asserting against the REAL emitter output (not a fabricated log) is the point: the
// graph test mirrors this exact shape.
func TestInitializeEvent_EmittedAt9999WithPairAndFee(t *testing.T) {
h := newSettleHarness(t) // harness defaults to post-activation
h := newSettleHarness(t) // harness default block time (logs AlwaysOn)
h.registerMarket(t)
initSig := common.BytesToHash(crypto.Keccak256([]byte("Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)")))
@@ -194,22 +189,26 @@ func TestInitializeEvent_EmittedAt9999WithPairAndFee(t *testing.T) {
}
}
// TestInitializeEvent_NotEmittedBeforeActivation proves the Initialize log is gated on
// the SAME dated fork: registering a market before the boundary writes the
// C-authoritative state record (verified by a successful call) but emits no log.
func TestInitializeEvent_NotEmittedBeforeActivation(t *testing.T) {
// TestInitializeEvent_EmittedAtGenesis proves the Initialize log is AlwaysOn (active from
// genesis): registering a market at genesis (block timestamp 0) BOTH writes the
// C-authoritative state record AND emits the Initialize log — no pre-activation suppression.
func TestInitializeEvent_EmittedAtGenesis(t *testing.T) {
h := newSettleHarness(t)
h.state.blockTimestamp = DexSettleActivationTime - 1
h.registerMarket(t) // must still succeed — only the LOG is gated, not the registry
h.state.blockTimestamp = 0 // genesis
h.registerMarket(t)
initSig := common.BytesToHash(crypto.Keccak256([]byte("Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)")))
found := 0
for _, lg := range h.state.stateDB.Logs() {
if len(lg.Topics) > 0 && lg.Topics[0] == initSig {
t.Fatal("Initialize must NOT be emitted before activation")
found++
}
}
if found != 1 {
t.Fatalf("Initialize must be emitted from genesis (AlwaysOn), got %d", found)
}
// The registry write IS present (idempotent re-init reverts → proves it registered).
if _, _, err := h.c.Run(h.state, h.caller, poolManagerAddr9999, initCalldata(h.key, new(big.Int).Set(Q96)), 5_000_000, false); err == nil {
t.Fatal("re-init should revert ErrPoolAlreadyInitialized — registry must have been written pre-activation")
t.Fatal("re-init should revert ErrPoolAlreadyInitialized — registry must have been written")
}
}
+8 -35
View File
@@ -11,41 +11,14 @@ import (
ethtypes "github.com/luxfi/geth/core/types"
)
// DexSettleActivationTime is the layer-local mirror of the canonical 0x9999 DEX
// settlement activation boundary — unix 1766704800, i.e. 2025-12-25T23:20:00Z.
//
// DO NOT "round" this to midnight: the value (not the prose date) is the protocol
// constant. It is ALREADY LIVE — it gates 0x9999 dispatch on a settling devnet — so
// changing the number would move an activation boundary that historical receipts were
// already built against and FORK every chain that crossed it. Only the human-readable
// instant is annotated here; the number is authoritative.
//
// The CANONICAL definition lives in luxfi/evm params/extras.DexSettleActivationTime;
// that is the single source of truth the EVM dispatch gate and marker-installing
// state transition reference. This package (luxfi/precompile) sits BELOW evm in the
// import graph (evm imports precompile, never the reverse), so it cannot import the
// extras constant — it mirrors the value here. Drift between the two copies is a
// consensus bug, so an equality guard test in the evm layer (which imports BOTH)
// asserts dex.DexSettleActivationTime == extras.DexSettleActivationTime and fails CI
// if either moves. The value is a protocol constant (one decision), not config.
//
// Why the precompile gates on this AT ALL, given the EVM overrider already withholds
// 0x9999 from the enabled set pre-activation: defense in depth. The dispatch gate is
// in the high (evm) layer; a new consensus-visible log (one that changes the receipt
// root + bloom) is emitted in this low (precompile) layer. If any host dispatches
// SettleSwap at a pre-activation timestamp — a buggy overrider, a non-Lux EVM that
// integrates the precompile without the dated-fork gate, a future direct-call path —
// an ungated log would split the chain (a re-syncing node that did NOT emit the log
// would compute a different receipt root). Gating the log on the SAME timestamp the
// dispatch uses means: on every chain, every settlement that ever executes also emits
// the log, and no execution before the boundary ever can. No settlement-without-log
// window, no log-without-settlement window.
const DexSettleActivationTime uint64 = 1766704800 // 2025-12-25T23:20:00Z; canonical: evm extras.DexSettleActivationTime
// dexLogsActive reports whether 0x9999's new consensus-visible logs (DEXFill, and the
// V4 Initialize the native registry emits) may be written at blockTimestamp. It is the
// ONE policy predicate gating those logs; the emit functions stay pure log-builders.
func dexLogsActive(blockTimestamp uint64) bool { return blockTimestamp >= DexSettleActivationTime }
// The DEX settlement money path 0x9999 is a FIRST-RUN, no-legacy system precompile: it
// is ACTIVE FROM GENESIS, and so are its consensus-visible logs (DEXFill, and the V4
// Initialize the native registry emits). There is no activation boundary and no
// pre-activation history, so there is no log gate — every settlement that executes emits
// its log, from block 0. The emit functions below are pure log-builders, called
// unconditionally on the settlement money path. The dispatch side (the EVM overrider that
// injects 0x9999 into the enabled set) is likewise unconditional in luxfi/evm, so there
// is no settlement-without-log or log-without-settlement window on any chain.
// Event signature hashes (topic0) matching standard Uniswap V4 events.
// Computed as keccak256 of the canonical event signature string.
+9 -4
View File
@@ -98,6 +98,11 @@ type settleHarness struct {
memdbBacking *memdb.Database // shared-memory backing db (for batch tests)
}
// harnessBlockTime is an arbitrary post-genesis block timestamp for harness state.
// 0x9999 and its logs are AlwaysOn (active from genesis), so the exact value carries no
// protocol meaning — any block time exercises the live settlement path.
const harnessBlockTime uint64 = 1_700_000_000 // 2023-11-14T22:13:20Z, a normal block time
func newSettleHarness(t testing.TB) *settleHarness {
return newSettleHarnessN(t, 3)
}
@@ -124,10 +129,10 @@ func newSettleHarnessN(t testing.TB, _ int) *settleHarness {
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,
// 0x9999 and its DEXFill + Initialize logs are AlwaysOn (active from genesis,
// no dated fork), so the harness block time carries no protocol meaning — any
// value exercises the live path. A test that wants genesis sets blockTimestamp=0.
blockTimestamp: harnessBlockTime,
}
h := &settleHarness{
c: &SettleContract{},
+4 -6
View File
@@ -614,9 +614,8 @@ func (m *mockAccessibleState) GetStateDB() contract.StateDB {
}
func (m *mockAccessibleState) GetBlockContext() contract.BlockContext {
// Default to the activation boundary (live-chain era) so surface tests run with
// 0x9999 logs active, matching the production path under test.
return &mockBlockCtx{number: big.NewInt(int64(m.stateDB.blockNumber)), timestamp: DexSettleActivationTime}
// 0x9999 logs are AlwaysOn (active from genesis); any block time runs the live path.
return &mockBlockCtx{number: big.NewInt(int64(m.stateDB.blockNumber)), timestamp: harnessBlockTime}
}
func (m *mockAccessibleState) GetConsensusContext() context.Context {
@@ -632,9 +631,8 @@ func (m *mockAccessibleState) GetPrecompileEnv() contract.PrecompileEnvironment
}
// mockBlockCtx implements contract.BlockContext for testing. timestamp defaults to
// DexSettleActivationTime (the harness post-activation default) so settlement tests
// exercise the live-chain path where 0x9999 logs are active; tests that need a
// pre-activation block set timestamp explicitly.
// harnessBlockTime; 0x9999 and its logs are AlwaysOn (active from genesis), so the
// value carries no protocol meaning.
type mockBlockCtx struct {
number *big.Int
timestamp uint64
+4 -9
View File
@@ -226,15 +226,10 @@ func SettleSwap(
accrueVolume(stateDB, key.ID(), creditedAmt, blockNumber)
// Indexable settled-fill signal for the DEX graph / lux.exchange. Emitted
// on the money path (Phase-B credit) so eth_getLogs surfaces native-CLOB
// fills. accrueVolume is sharded state (not a log); this is the log. Gated on
// the SAME dated fork as 0x9999 dispatch (defense in depth — see dexLogsActive):
// a consensus-visible log MUST NOT enter a receipt root before the activation
// boundary, or a re-syncing node that did not emit it would compute a different
// root. On the relaunched chain every settlement is at/after the boundary, so
// every settlement emits the log — no ungated window.
if dexLogsActive(blockTimestamp) {
emitDEXFillEvent(stateDB, key.ID(), caller, creditedAmt, blockNumber)
}
// fills. accrueVolume is sharded state (not a log); this is the log. 0x9999 is
// AlwaysOn (active from genesis, no dated fork), so the log is emitted
// unconditionally — every settlement that executes emits it, from block 0.
emitDEXFillEvent(stateDB, key.ID(), caller, creditedAmt, blockNumber)
// V4 return: the taker received `credited` of the output asset. Map to the
// BalanceDelta direction (output paid out to taker = negative to pool).
delta := balanceDeltaForOutput(params, new(big.Int).SetUint64(credited))
+3 -3
View File
@@ -310,7 +310,7 @@ func TestERC20Settle_CallSeam_DepositWithdrawConserves(t *testing.T) {
state := &callSeamState{
sdb: &callSeamStateDB{inner: NewMockStateDB()},
env: env,
timestamp: DexSettleActivationTime, // live-chain era (0x9999 logs active)
timestamp: harnessBlockTime, // 0x9999 logs AlwaysOn (active from genesis)
}
// Load-bearing: the StateDB must NOT be an erc20Vault, forcing the Call seam.
assertNotERC20Vault(t, state.GetStateDB())
@@ -434,7 +434,7 @@ func TestERC20Settle_CallSeam_RejectsDelegatecall(t *testing.T) {
state := &callSeamState{
sdb: &callSeamStateDB{inner: NewMockStateDB()},
env: env,
timestamp: DexSettleActivationTime,
timestamp: harnessBlockTime,
}
c := &SettleContract{} // deposit/withdraw are custody ops, not governance-gated
aid := assetID(Currency{Address: token})
@@ -493,7 +493,7 @@ func TestERC20Settle_NoEnv_RefusesFailSecure(t *testing.T) {
state := &callSeamState{
sdb: &callSeamStateDB{inner: NewMockStateDB()},
env: nil, // NO env — GetPrecompileEnv() returns a typed-nil *callSeamEnv? guard below
timestamp: DexSettleActivationTime,
timestamp: harnessBlockTime,
}
// Guard against the typed-nil trap: GetPrecompileEnv must return an untyped nil so
// the precompile's nil check fires. callSeamState returns m.env directly; a nil
+3 -6
View File
@@ -248,13 +248,10 @@ func (s *SettleContract) runSettleInitialize(
// to eth_getLogs and the graph degrades to a poolID-only stub. It is the SAME
// event signature a vanilla V4 PoolManager emits (topic
// keccak256("Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)")),
// so the indexer needs no new handler. Gated on the SAME dated fork as 0x9999
// dispatch (defense in depth — see dexLogsActive): a consensus-visible log MUST
// NOT enter a receipt root before activation. The state write above is the
// so the indexer needs no new handler. 0x9999 is AlwaysOn (active from genesis, no
// dated fork), so the log is emitted unconditionally. The state write above is the
// authoritative registry (C is source of truth); the log is the indexable mirror.
if dexLogsActive(state.GetBlockContext().Timestamp()) {
emitInitializeEvent(stateDB, poolManagerAddr9999, poolID, key, sqrtPriceX96, tick)
}
emitInitializeEvent(stateDB, poolManagerAddr9999, poolID, key, sqrtPriceX96, tick)
// V4 initialize returns the int24 tick (ABI: int24, sign-extended to a word).
return encodeInt24Word(tick), gasLeft, nil
+7 -15
View File
@@ -73,13 +73,7 @@ func runSyncSwap(state contract.AccessibleState, caller common.Address, key Pool
return nil, gasLeft, err
}
// The block timestamp gates the consensus-visible DEXFill log (the activation-fork
// replay guard). It comes from the AccessibleState's block context (the StateDB
// adapter exposes only the number); the sync handler reads it here and threads it
// into syncSwap so the fill log fires on the SAME dated fork the async path uses.
blockTimestamp := state.GetBlockContext().Timestamp()
out, err := syncSwap(stateDB, resolver, caller, key, params, hookData, blockTimestamp)
out, err := syncSwap(stateDB, resolver, caller, key, params, hookData)
if err != nil {
return nil, gasLeft, err
}
@@ -124,11 +118,9 @@ func runSyncSwap(state contract.AccessibleState, caller common.Address, key Pool
// - the market exists / is openable (the maker-seed / OpenMarket path bound assets);
// - read-only callers are rejected before here (a swap mutates).
//
// blockTimestamp gates the DEXFill C-event: a consensus-visible log must not enter a
// receipt root before the activation boundary (dexLogsActive), or a node re-syncing
// from before the boundary would compute a divergent root. On the relaunched chain
// every swap is at/after the boundary, so every filled swap emits the fill log.
func syncSwap(stateDB StateDB, resolver dexcore.AssetResolver, caller common.Address, key PoolKey, params SwapParams, hookData []byte, blockTimestamp uint64) ([]byte, error) {
// 0x9999 is AlwaysOn (active from genesis), so the DEXFill C-event fires on every filled
// swap with no activation gate — see emitDEXFillEvent below.
func syncSwap(stateDB StateDB, resolver dexcore.AssetResolver, caller common.Address, key PoolKey, params SwapParams, hookData []byte) ([]byte, error) {
// Decode the V4 swap into the real-asset routing request (incl. the taker's min-out
// floor from hookData and the value-path slippage-protection policy).
req, inAssetAddr, err := buildSwapRequest(stateDB, caller, key, params, hookData)
@@ -211,10 +203,10 @@ func syncSwap(stateDB StateDB, resolver dexcore.AssetResolver, caller common.Add
// (C event) Emit the DEXFill log so the trade is visible in C events — the SAME
// indexable native-CLOB settlement signal the async Phase-B path emits, now fired
// on the SYNCHRONOUS money path. The graph indexer / lux.exchange scope CLOB fills
// to 0x9999 by this log. Gated on the activation fork (dexLogsActive) for replay
// safety, and only when the route actually produced output. amountOut is the
// to 0x9999 by this log. 0x9999 is AlwaysOn (active from genesis, no dated fork), so
// the log fires unconditionally whenever the route produced output. amountOut is the
// taker's realized proceeds; poolId + taker are the indexed topics.
if res.AmountOut > 0 && dexLogsActive(blockTimestamp) {
if res.AmountOut > 0 {
emitDEXFillEvent(stateDB, req.PoolID, caller, new(big.Int).SetUint64(res.AmountOut), stateDB.GetBlockNumber())
}