mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
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:
+31
-32
@@ -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
@@ -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.
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user