Files
precompile/dex/settle_decomplect_test.go
T
zeekayandHanzo Dev 0b433110ac dex: the funding rail carries value, and nothing else
Move once, trade many. Value crosses the C<->D boundary twice in its life; in
between, D owns it and every order, match and trade is a local balance movement.
So the crossing has exactly one job — move ownership exactly once — and the
object shrinks to what that job needs:

    beneficiary(20) | asset(32) | amount(8)   = 60 bytes

Out of the wire: the 49-byte market/side/limit/size tail, the `spent` execution
witness, and the lane byte selecting a trading pot. Out of the precompile: the
per-order escrow with its principal cap and deadline, the taker-authenticated
price floor, the deadline reclaim and its selector, the shared-memory discovery
trait, and the .v2 domain rotation that existed only because the tail had
changed the object's width.

ONE CUSTODY POT. seamReserve and committedPositions were the swap lane and the
LP lane of one quantity — "value C holds because D owns it" — and the lane byte
was the only thing keeping them apart. Splitting them is what created the
cross-lane drain the byte then had to defend against. One number per asset, one
credit path, nothing to cross.

WHAT REPLACES THE PER-ORDER CAP AND THE PRICE FLOOR: D refuses an over-debit on
export, C refuses a credit custody cannot back, a claim is consumable exactly
once, and Block.Verify proves the carried bytes are the recorded object. Those
are properties of the value. The cap and the floor were properties of an order,
which is why the order had to ride the object for them to be checkable at all.

The claim id is one derivation for both directions and both repos:
SHA-256(domain | source | dest | sourceTx | index). It replaces DeriveOrderID,
DerivePositionCommitID and deriveSeamOutputID, which were three spellings of the
same fact.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:33:14 -07:00

312 lines
16 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"encoding/binary"
"errors"
"math/big"
"testing"
"github.com/holiman/uint256"
dexcore "github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/geth/common"
"github.com/luxfi/ids"
)
// settle_decomplect_test.go pins the CANONICAL DECOMPLECT invariant: 0x9999 is the SOLE
// money path and it SETTLES ONLY — it never matches. Every swap routes to the native C<->D
// atomic seam; the ONLY way C is credited is by consuming a real D->C atomic object. There
// is NO embedded matcher (swap_sync), NO in-trie maker order book (swap_custody), NO
// synchronous on-chain router. These tests are the load-bearing guardrail for that property
// (they replace the deleted swap_sync_* / gatec_* suites, keeping the still-relevant
// permissionless-admission and governance-cannot-drain invariants).
// c1MarketKey is a market over a SYNTHETIC base (currency0, no on-chain code) and a REAL
// quote (currency1, code-backed). V4 requires currency0 < currency1, so the synthetic base
// is at the lower address and the real quote at the higher. Relocated from the deleted
// swap_sync_c1_redteam_test.go — the initialize-admission tests below still need it.
var (
c1FabricatedBase = common.HexToAddress("0x00000000000000000000000000000000000000Ee") // synthetic: no on-chain code
c1RealQuote = common.HexToAddress("0x0000000000000000000000000000000000000F02") // real: code-backed ERC-20
)
func c1MarketKey() PoolKey {
return PoolKey{
Currency0: Currency{Address: c1FabricatedBase},
Currency1: Currency{Address: c1RealQuote},
Fee: 3000,
TickSpacing: 60,
Hooks: common.Address{},
}
}
// TestDecomplect_NoReceiptNoSettle is THE negative invariant, unchanged in substance
// and relocated in enforcement: a PHASE B settlement naming a D->C object that does
// NOT exist in shared memory must never result in an accepted credit — even with the
// output vault fully funded. "0x9999 cannot fabricate a fill."
//
// WHERE IT IS PROVEN NOW. Execution can no longer consult shared memory (that read is
// what made a settled swap unsyncable — see settle_import.go), so a phantom object
// with WELL-FORMED bytes does execute. What it cannot do is survive: it necessarily
// DECLARES the bytes it used, and that declaration provably disagrees with shared
// memory, so every validator — including the producer, which verifies its own block
// before proposing it — rejects the block. The forger buys a rejected block, never
// value. This test pins both halves: an ill-formed object still fails closed at
// execution with nothing moved, and a well-formed forgery is provably rejectable.
func TestDecomplect_NoReceiptNoSettle(t *testing.T) {
h := newSettleHarness(t)
h.registerMarket(t)
// Fund the output vault generously so that IF any credit path existed that did not require
// a real D->C object, it COULD pay out — making the "no credit" assertion decisive.
h.fundVaultOut(1_000_000)
// An outputID that was NEVER exported by D (never PUT into shared memory).
phantom := ids.ID{0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x9999 & 0xFF}
callerOutBefore := h.tokenBal(h.outToken(), h.caller)
vaultOutBefore := h.tokenBal(h.outToken(), poolManagerAddr9999)
// (1) NO object bytes at all: fails closed at execution, nothing moves. The
// harness builder reads the recorded object, which for a phantom is empty, so the
// fixed-width body gate refuses it.
_, err := h.runSwap(t, h.settlementCalldata(phantom, 500), false)
if !errors.Is(err, ErrSettleBodyMalformed) && !errors.Is(err, ErrImportObjectMalformed) {
t.Fatalf("Phase-B settle with NO object bytes must fail closed, got: %v", err)
}
// (2) FABRICATED well-formed bytes for the same phantom, in a FRESH harness so the
// balance assertions above stay clean. This executes — it must, because execution
// is now a pure function of the block — but it declares the forgery, and the
// declaration cannot authenticate against shared memory, so the block dies.
forgeH := newSettleHarness(t)
forgeH.registerMarket(t)
forgeH.fundVaultOut(1_000_000)
forged := encodeClaim(forgeH.caller, forgeH.outAssetID(), 500)
forgedCalldata := buildSwapCalldata(forgeH.key, forgeH.params,
EncodeSettlementHookData(phantom, forged))
if _, ferr := forgeH.runSwap(t, forgedCalldata, false); ferr != nil {
t.Fatalf("execution must bind the carried bytes without consulting shared memory: %v", ferr)
}
imports := DecodeSettleImports(forgeH.state.stateDB.Logs())
if len(imports) != 1 {
t.Fatalf("a forged settlement must still DECLARE what it consumed; got %d declarations", len(imports))
}
// The host's Verify check: shared memory holds nothing at this key, so the block is
// rejected. Absence is itself the disproof — nothing the forgery credited is ever
// accepted, which is the ship rule, relocated.
if vals, gerr := forgeH.cSM.Get(forgeH.dChainID, [][]byte{phantom[:]}); gerr == nil && len(vals) == 1 && len(vals[0]) > 0 {
t.Fatal("the phantom object must NOT be in shared memory — the test premise is broken")
}
// No output token moved out of the vault; the caller was credited nothing.
if got := h.tokenBal(h.outToken(), h.caller); got.Cmp(callerOutBefore) != 0 {
t.Fatalf("no-object settle credited the caller %s (must be 0)", new(big.Int).Sub(got, callerOutBefore))
}
if got := h.tokenBal(h.outToken(), poolManagerAddr9999); got.Cmp(vaultOutBefore) != 0 {
t.Fatalf("no-object settle drained the vault by %s (must be 0)", new(big.Int).Sub(vaultOutBefore, got))
}
}
// TestDecomplect_UntaggedSwapIsOrderNotFill proves the positive half: a PLAIN swap (empty
// hookData — no DI01/DS02 tag) is treated as a PHASE A ORDER. It LOCKS the taker's input and
// returns a 32-byte order id; it does NOT match, does NOT credit any output. There is no
// synchronous in-trie fill. (Before the decomplect this routed to the embedded sync matcher.)
func TestDecomplect_UntaggedSwapIsOrderNotFill(t *testing.T) {
h := newSettleHarness(t)
h.registerMarket(t)
h.fundCallerNative(1000)
h.fundVaultOut(1_000_000) // a matcher COULD pay from here — assert it never does.
callerNativeBefore := h.state.stateDB.GetBalance(h.caller).ToBig()
callerOutBefore := h.tokenBal(h.outToken(), h.caller)
// Empty hookData: the common plain-swap case. Post-decomplect this is a Phase A order.
out, _, err := runWithEVMSnapshot(h.c, h.state, h.caller, poolManagerAddr9999,
prependSelector(SelectorSwap, buildSwapCalldata(h.key, h.params, nil)), 5_000_000, false)
if err != nil {
t.Fatalf("a plain (empty-hookData) swap must create a Phase A order, got err: %v", err)
}
if len(out) != 32 {
t.Fatalf("Phase A order must return a 32-byte order id, got %d bytes", len(out))
}
// Input was LOCKED (debited) — |AmountSpecified| = 100 in the standard harness params.
callerNativeAfter := h.state.stateDB.GetBalance(h.caller).ToBig()
if new(big.Int).Sub(callerNativeBefore, callerNativeAfter).Sign() <= 0 {
t.Fatal("a Phase A order must LOCK (debit) the taker's input")
}
// NO output credited — an order is not a fill.
if got := h.tokenBal(h.outToken(), h.caller); got.Cmp(callerOutBefore) != 0 {
t.Fatalf("an order must NOT credit output; credited %s", new(big.Int).Sub(got, callerOutBefore))
}
}
// TestDecomplect_MakerBookSelectorsGone proves the in-trie maker order book is REMOVED: the
// swapDeposit / swapPlace / swapCancel selectors (the "money lives in the order book" custody
// surface the sync matcher fed) are no longer dispatchable — 0x9999 rejects them as unknown.
func TestDecomplect_MakerBookSelectorsGone(t *testing.T) {
h := newSettleHarness(t)
h.registerMarket(t)
for _, sig := range []string{
"swapDeposit(address,uint256)",
"swapWithdraw(address,uint256)",
"swapPlace((address,address,uint24,int24,address),bool,uint256,uint256)",
"swapCancel((address,address,uint24,int24,address),uint256)",
} {
sel := keccak4(sig)
var selb [4]byte
selb[0], selb[1], selb[2], selb[3] = byte(sel>>24), byte(sel>>16), byte(sel>>8), byte(sel)
// 256 bytes of zero args is enough for any of these to reach the dispatch switch.
input := append(selb[:], make([]byte, 256)...)
_, _, err := h.c.Run(h.state, h.caller, poolManagerAddr9999, input, 5_000_000, false)
if err == nil || err.Error() != "dex: unknown 0x9999 selector" {
t.Fatalf("maker-book selector %q must be unknown/undispatchable, got: %v", sig, err)
}
}
}
// TestDecomplect_Router9012ValueSelectorsMoved proves the SECOND money path is deprecated: the
// LP-9012 router's VALUE-moving selectors (exactInput* / exactOutput* — which routed swaps
// through the synchronous engine matcher) now REVERT PRECOMPILE_MOVED, while the read-only
// quote/route VIEWS still dispatch. There is exactly ONE money path (0x9999); 0x9012 moves no
// value.
func TestDecomplect_Router9012ValueSelectorsMoved(t *testing.T) {
h := newSettleHarness(t)
rc := RouterPrecompile // the 0x9012 RouterContract singleton
// (a) Every value-moving selector reverts PRECOMPILE_MOVED — no swap routes through 0x9012.
for _, sel := range []uint32{
SelectorExactInputSingle, SelectorExactInput, SelectorExactOutputSingle, SelectorExactOutput,
} {
var selb [4]byte
binary.BigEndian.PutUint32(selb[:], sel)
input := append(selb[:], make([]byte, 256)...)
_, _, err := rc.Run(h.state, h.caller, lxRouterAddr, input, 5_000_000, false)
if !errors.Is(err, ErrPrecompileMoved) {
t.Fatalf("0x9012 value selector %#08x must revert ErrPrecompileMoved, got: %v", sel, err)
}
}
// (b) A read-only quote VIEW is NOT moved — it dispatches (does not return PRECOMPILE_MOVED
// nor "unknown selector"). It may fail for lack of bound liquidity, but that is a view-level
// error, not the deprecation revert.
var qselb [4]byte
binary.BigEndian.PutUint32(qselb[:], SelectorGetBestRoute)
qinput := append(qselb[:], make([]byte, 128)...)
_, _, qerr := rc.Run(h.state, h.caller, lxRouterAddr, qinput, 5_000_000, true)
if errors.Is(qerr, ErrPrecompileMoved) {
t.Fatalf("a read-only quote/route view must NOT be moved; got PRECOMPILE_MOVED")
}
}
// TestDecomplect_InitializeRefusesSyntheticAsset — the permissionless real-asset admission is
// PRESERVED on the KEEP initialize path: a market over a SYNTHETIC base (no on-chain code) is
// REFUSED (ErrAssetNotOnChain) and NO MarketRecord is written. (Relocated from the deleted
// swap_sync_decomplect_test.go; it exercises settle_market.go admitMarketAssets, not a matcher.)
func TestDecomplect_InitializeRefusesSyntheticAsset(t *testing.T) {
h := newSettleHarness(t)
h.key = c1MarketKey() // base = c1FabricatedBase (synthetic, no code)
r := newTestAssetResolver(h.networkID, h.cChainID).boundToHarness(h)
r.admitERC20(t, c1RealQuote, 6)
h.state.stateDB.SetCodeSize(c1FabricatedBase, 0) // synthetic base: NO code
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) })
_, _, err := h.c.Run(h.state, h.caller, poolManagerAddr9999,
initCalldata(h.key, new(big.Int).Set(Q96)), 5_000_000, false)
if !errors.Is(err, dexcore.ErrAssetNotOnChain) {
t.Fatalf("initialize over a SYNTHETIC base (no code) must be REFUSED (ErrAssetNotOnChain), got: %v", err)
}
if rec := loadMarket(newPoolStateAdapter(h.state), h.key.ID()); rec.Status != MarketStatusNone {
t.Fatalf("a refused initialize must NOT have written a MarketRecord, got status %v", rec.Status)
}
}
// TestDecomplect_InitializeNoResolverFailsClosed proves the initialize path fails closed when
// NO resolver is installed (ErrNoAssetResolver) — a node that serves 0x9999 without wiring the
// resolver cannot even create a market record.
func TestDecomplect_InitializeNoResolverFailsClosed(t *testing.T) {
h := newSettleHarness(t)
prev := installedAssetResolver.Load()
installedAssetResolver.Store(nil)
t.Cleanup(func() { installedAssetResolver.Store(prev) })
_, _, err := h.c.Run(h.state, h.caller, poolManagerAddr9999,
initCalldata(h.key, new(big.Int).Set(Q96)), 5_000_000, false)
if !errors.Is(err, dexcore.ErrNoAssetResolver) {
t.Fatalf("initialize with no resolver must fail closed (ErrNoAssetResolver), got: %v", err)
}
if rec := loadMarket(newPoolStateAdapter(h.state), h.key.ID()); rec.Status != MarketStatusNone {
t.Fatal("a fail-closed initialize must NOT have written a MarketRecord")
}
}
// TestDecomplect_DAOControllerCannotMintOrMoveUserFunds — the governance controller (the
// per-network DEX authority) can HALT but can NEVER mint the seam reserve nor drain a
// depositor's pot. Relocated from the deleted gatec_red_probe_test.go: it uses ONLY the KEEP
// surface (seedSeamReserve / deposit / atomicImport), no maker path. A settlement draws ONLY
// from seamReserve and ONLY against a real D->C object — a fabricated object or an empty seam
// reverts, leaving the depositor pot intact.
func TestDecomplect_DAOControllerCannotMintOrMoveUserFunds(t *testing.T) {
h := newSettleHarness(t)
controller := h.operator() // == the runtime-resolved DEX governance controller
native := [32]byte{}
seamBefore := loadCustody(newPoolStateAdapter(h.state), native).Uint64()
// (a) Controller calls seedSeamReserve(native, 1_000_000) but the host frame delivers NO
// msg.value. A mint would inflate seamReserve; the observed-delta discipline yields 0
// delivered and REVERTS.
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 := loadCustody(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).
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)
}
// (i) fabricated object: no privileged path to mint a settlement object.
credited, ierr := h.c.atomicImport(h.state, Claim{
ID: [32]byte{0xAB}, Asset: native, Beneficiary: 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 custody[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, Claim{
ID: realObj, Asset: native, Beneficiary: controller,
Object: encodeClaim(controller, native, 500),
})
if ierr2 != ErrCustodyUnbacked {
t.Fatalf("controller raided the depositor pot via a real object with empty seam: credited=%d err=%v (must be ErrCustodyUnbacked)", credited2, ierr2)
}
if loadDepositorClaim(newPoolStateAdapter(h.state), depositor, native).Int64() != 1000 {
t.Fatal("depositor claim moved after controller's raid attempt (do-not-ship)")
}
}