Files
precompile/dex/asset_resolver_testhelp_test.go
zeekayandHanzo Dev d30014b419 fix(dex/9999): exact-integer ZAP price wire + fold dexcore/lx -> pkg/dex
Bump github.com/luxfi/dex v1.5.20 -> v1.14.0. v1.14.0 (a) folded pkg/lx and
pkg/dexcore into a single pkg/dex, and (b) froze the ZAP wire to EXACT
INTEGERS: price is uint64 fixed-point ×1e8 (the PriceInt grid), no float64.
The 0x9999 EVM surface must speak that same grid or it prices a limit the
native venue reads at a different scale — an interop break.

Package fold (mechanical, behavior-neutral — build + full suite green after):
- import path pkg/dexcore -> pkg/dex (alias dexcore), pkg/lx -> pkg/dex.
- v1.14.0 renames: NewCLOBSource->NewOrderBookSource, SourceCLOB->SourceOrderBook,
  ClassPublicCLOB->ClassPublicDEX.
- collapsed the 4 files that imported the fold under both aliases to a single
  dexcore alias (no redundant same-package import).

Exact-integer price wire (the 7 float-wire sites):
- settle9999.go priceLimitToCLOB: return the CLOB limit as a uint64 FIXED-POINT
  ×priceScale integer computed by EXACT big.Int (round-to-nearest of
  s^2·priceScale/2^192), not math.Float64bits of a float. Round-to-nearest (not
  floor) so a sqrt-derived "50" ceiling still crosses a maker resting AT 50.
- swap_sync.go: limitPrice = lx.PriceInt(limitUnits) directly — no
  PriceToInt(Float64frombits(...)) roundtrip.
- native_dchain_client.go enforceProceedsPriceFloor: the MEV floor compares the
  integer limit (limit = priceLimit/priceScale) by big.Int cross-multiply
  (out·priceScale vs priceLimit·spent), dropping math.Float64frombits +
  big.Rat.SetFloat64. Determinism + no 2^53 truncation preserved.
- new package const priceScale (== dexcore.PriceMultiplier == zapwire.PriceScale);
  the intent/relay PriceLimit is now this fixed-point grid value.

swap_custody.go's runSwapPlace keeps its derived float for dexcore.PlaceOrder —
that is the matcher's in-process crossing-loop API (unchanged signature), a
projection, never a consensus/settlement value.

Tests: MEV-floor / price-limit / exact-precision (>2^53) redteam tests
translated to the fixed-point integer domain without weakening any invariant.
New wire_parity_test.go pins priceScale == dexcore.PriceMultiplier ==
zapwire.PriceScale so the grid can never drift from the venue. 437 pass, 0 fail.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-02 10:56:00 -07:00

108 lines
5.4 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"testing"
dexcore "github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/geth/common"
"github.com/luxfi/ids"
)
// asset_resolver_testhelp_test.go provides the test-only, PERMISSIONLESS AssetResolver the
// sync-swap harnesses install so the value path's admission is exercised against the real
// production model (never stubbed). It mirrors the production canonicalAssetResolver exactly:
// it RESOLVES the canonical AssetID for ANY well-formed reference on its bound network — it
// has NO membership set, NO Enabled flag, NO manifest. An asset is tradeable iff its identity
// resolves here AND it is proven real on-chain by the EXTCODESIZE verifier; a synthetic asset
// is refused at the on-chain proof (no code at its address), never by an allowlist.
//
// The "admit" helpers below therefore do NOT register membership (there is none). admitERC20
// SEEDS LIVE ON-CHAIN CODE for the token — that is what makes it tradeable under permissionless
// admission, and it is exactly what production requires (a deployed contract has bytecode).
// admitNative is a no-op: the native coin is always real. A token that is NOT code-seeded has
// no code and is refused at the live-reality gate, modelling a fabricated/synthetic asset.
// testAssetResolver is a PERMISSIONLESS AssetResolver bound to a fixed network identity. It
// derives the canonical AssetID with its OWN bound ids, identical to the production
// canonicalAssetResolver, and admits any well-formed reference. It holds an optional
// back-reference to the harness so the `admit` helpers can seed live on-chain code (the
// authoritative reality signal) for a token's address.
type testAssetResolver struct {
networkID uint32
chainID ids.ID
h *settleHarness // optional; set when constructed via the harness, for code seeding
}
func newTestAssetResolver(networkID uint32, chainID ids.ID) *testAssetResolver {
return &testAssetResolver{networkID: networkID, chainID: chainID}
}
// boundToHarness returns the resolver with a harness back-reference so admitERC20 can seed
// on-chain code through the harness's StateDB.
func (r *testAssetResolver) boundToHarness(h *settleHarness) *testAssetResolver {
r.h = h
return r
}
// ResolveAsset implements dexcore.AssetResolver PERMISSIONLESSLY: derive the canonical
// AssetID from the bound (networkID, chainID) and the per-asset (kind, ref). It returns an
// error ONLY when the reference is malformed (wrong shape, rejected by DeriveAssetID), never
// because the asset is "unregistered". Native decimals are the intrinsic 18; ERC-20/UTXO
// decimals are a live-chain fact (returned 0, proven by the verifier), matching production.
func (r *testAssetResolver) ResolveAsset(kind dexcore.AssetKind, ref []byte) (ids.ID, uint8, error) {
id, err := dexcore.DeriveAssetID(r.networkID, r.chainID, kind, ref)
if err != nil {
return ids.Empty, 0, err
}
var decimals uint8
if kind == dexcore.AssetKindEVMNative {
decimals = 18
}
return id, decimals, nil
}
// admitNative is a no-op under permissionless admission: the native coin is the chain's own
// coin and is always real (no contract code needed). Kept so native-inclusive test setups
// read symmetrically with admitERC20. The decimals arg is ignored (native is intrinsically 18).
func (r *testAssetResolver) admitNative(_ testing.TB, _ uint8) {}
// admitERC20 makes an ERC-20 TRADEABLE under permissionless admission by seeding LIVE ON-CHAIN
// CODE at its address (the authoritative reality signal the value path's verifier checks) — it
// does NOT register membership (there is none). It requires the resolver to be bound to a
// harness (via the harness installer or boundToHarness). The decimals arg is ignored (decimals
// is a live-chain fact, not a resolver fact).
func (r *testAssetResolver) admitERC20(t testing.TB, addr common.Address, _ uint8) {
t.Helper()
if r.h == nil {
t.Fatal("admitERC20: resolver is not bound to a harness (cannot seed on-chain code); construct via installAssetResolverFor or boundToHarness")
}
// A real, deployed ERC-20 has bytecode at its address; seed code so the on-chain verifier
// admits it permissionlessly.
r.h.state.stateDB.SetCodeSize(addr, 1)
}
// installAssetResolverFor installs a fresh PERMISSIONLESS test resolver bound to the harness
// identity and seeds LIVE on-chain code for the given ERC-20 token addresses (so the value
// path's OnChainAssetVerifier — the authoritative reality gate — admits them). It restores
// the prior global resolver on cleanup so tests do not leak install state. The resolver
// itself admits ANY well-formed reference; what makes a token tradeable is the code seeded
// here. A token a test deliberately leaves OUT of `tokens` (and does not code-seed) has no
// code and is refused at the live-reality gate — exactly as a fabricated/synthetic asset is.
// Returns the resolver (harness-bound, so further admitERC20 calls seed code).
func (h *settleHarness) installAssetResolverFor(t testing.TB, tokens ...common.Address) *testAssetResolver {
t.Helper()
r := newTestAssetResolver(h.networkID, h.cChainID).boundToHarness(h)
for _, tok := range tokens {
h.state.stateDB.SetCodeSize(tok, 1)
}
prev := installedAssetResolver.Load()
if err := InstallAssetResolver(r, h.networkID, h.cChainID); err != nil {
t.Fatalf("InstallAssetResolver: %v", err)
}
t.Cleanup(func() { installedAssetResolver.Store(prev) })
return r
}