mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
Both sides carried work. Ours is the security pass: the stableswap amp=0 and empty-pool halts, the wei-per-swap leak, uint256 truncation in math, three verifiers that returned a verdict without verifying, the cgo/non-cgo consensus split in cggmp21, and the starkfri soundness floor. Theirs is the fhe parameter set, the mldsa crafted-length refusal, and the naming sweep. Resolved per hunk rather than by side. Where both fixed one defect the better fix survives alone, never two guards for one bug.
461 lines
17 KiB
Go
461 lines
17 KiB
Go
// 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/holiman/uint256"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/geth/common"
|
|
ethtypes "github.com/luxfi/geth/core/types"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/precompile/contract"
|
|
"github.com/luxfi/vm/chains/atomic"
|
|
)
|
|
|
|
// zzblueA_dchain_test.go pins the MONEY-IN path (native_zap.go's two-phase lock/commit),
|
|
// its D->C credit siblings (native_dchain_client.go), and the closed-on-ramp refusals
|
|
// (dchain_client.go).
|
|
//
|
|
// The property under test throughout is the NATIVE-ZAP discipline: no 0x9999 self-write
|
|
// may follow a nested EVM call. A test that only checks the RESULT of a lock cannot see
|
|
// that discipline break — the balances land either way. So the fakes here record the
|
|
// ORDERED SEQUENCE of effects (0x9999 storage writes, the record callback, and each
|
|
// token sub-call) and the assertions are on that ordering, not merely on the outcome.
|
|
|
|
// ─────────────────────────── ordered-effect fakes ───────────────────────────
|
|
|
|
// blueAKV is a minimal dex.StateDB that records the order of its effects. It
|
|
// deliberately implements NEITHER erc20Vault NOR nonceMarker, so it is the input that
|
|
// drives the ErrNativeERC20Vault arm and ensureVaultAccountPersists' no-op arm. Balance
|
|
// arithmetic is uint256-MODULAR (exactly like the production SubBalance from a
|
|
// precompile), so an unguarded underflow WRAPS here rather than being caught by the
|
|
// fake — which is what makes the vault-underflow assertions meaningful.
|
|
type blueAKV struct {
|
|
slots map[common.Address]map[common.Hash]common.Hash
|
|
bal map[common.Address]*uint256.Int
|
|
live map[common.Address]bool
|
|
logs []*ethtypes.Log
|
|
ops []string
|
|
}
|
|
|
|
func blueANewKV() *blueAKV {
|
|
return &blueAKV{
|
|
slots: map[common.Address]map[common.Hash]common.Hash{},
|
|
bal: map[common.Address]*uint256.Int{},
|
|
live: map[common.Address]bool{},
|
|
}
|
|
}
|
|
|
|
func (k *blueAKV) GetState(addr common.Address, key common.Hash) common.Hash {
|
|
return k.slots[addr][key]
|
|
}
|
|
|
|
func (k *blueAKV) SetState(addr common.Address, key, value common.Hash) {
|
|
if k.slots[addr] == nil {
|
|
k.slots[addr] = map[common.Hash]common.Hash{}
|
|
}
|
|
k.slots[addr][key] = value
|
|
if addr == poolManagerAddr9999 {
|
|
k.ops = append(k.ops, "set9999")
|
|
}
|
|
}
|
|
|
|
func (k *blueAKV) GetBalance(addr common.Address) *uint256.Int {
|
|
if v, ok := k.bal[addr]; ok {
|
|
return v
|
|
}
|
|
return uint256.NewInt(0)
|
|
}
|
|
|
|
func (k *blueAKV) AddBalance(addr common.Address, amount *uint256.Int) {
|
|
k.bal[addr] = new(uint256.Int).Add(k.GetBalance(addr), amount)
|
|
k.ops = append(k.ops, "addBalance")
|
|
}
|
|
|
|
func (k *blueAKV) SubBalance(addr common.Address, amount *uint256.Int) {
|
|
k.bal[addr] = new(uint256.Int).Sub(k.GetBalance(addr), amount) // modular, as in production
|
|
k.ops = append(k.ops, "subBalance")
|
|
}
|
|
|
|
func (k *blueAKV) Exist(addr common.Address) bool { return k.live[addr] }
|
|
func (k *blueAKV) CreateAccount(addr common.Address) { k.live[addr] = true }
|
|
func (k *blueAKV) GetBlockNumber() uint64 { return 1 }
|
|
func (k *blueAKV) AddLog(log *ethtypes.Log) { k.logs = append(k.logs, log) }
|
|
func (k *blueAKV) setBalance(a common.Address, v uint64) { k.bal[a] = uint256.NewInt(v) }
|
|
|
|
var _ StateDB = (*blueAKV)(nil)
|
|
|
|
// blueAVaultDB adds the erc20Vault capability to blueAKV. `delivered` chooses how much
|
|
// of a requested transfer the recipient ACTUALLY receives, which is how the
|
|
// under-delivery boundary (a fee-on-transfer token) is swept.
|
|
type blueAVaultDB struct {
|
|
*blueAKV
|
|
tok map[common.Address]map[common.Address]*big.Int
|
|
delivered func(*big.Int) *big.Int
|
|
failXfer bool
|
|
}
|
|
|
|
func blueANewVaultDB() *blueAVaultDB {
|
|
return &blueAVaultDB{
|
|
blueAKV: blueANewKV(),
|
|
tok: map[common.Address]map[common.Address]*big.Int{},
|
|
}
|
|
}
|
|
|
|
func (v *blueAVaultDB) bag(token, holder common.Address) *big.Int {
|
|
if v.tok[token] == nil {
|
|
v.tok[token] = map[common.Address]*big.Int{}
|
|
}
|
|
if v.tok[token][holder] == nil {
|
|
v.tok[token][holder] = big.NewInt(0)
|
|
}
|
|
return v.tok[token][holder]
|
|
}
|
|
|
|
// mint resets a token's ledger and gives holder exactly amount.
|
|
func (v *blueAVaultDB) mint(token, holder common.Address, amount int64) {
|
|
v.tok[token] = map[common.Address]*big.Int{holder: big.NewInt(amount)}
|
|
}
|
|
|
|
func (v *blueAVaultDB) TokenBalanceOf(token, owner common.Address) *big.Int {
|
|
v.ops = append(v.ops, "balanceOf")
|
|
return new(big.Int).Set(v.bag(token, owner))
|
|
}
|
|
|
|
func (v *blueAVaultDB) TransferTokenFrom(token, from, to common.Address, amount *big.Int) error {
|
|
v.ops = append(v.ops, "transferFrom")
|
|
if v.failXfer {
|
|
return errors.New("blueA: token reverted")
|
|
}
|
|
if v.bag(token, from).Cmp(amount) < 0 {
|
|
return errors.New("blueA: insufficient token balance")
|
|
}
|
|
got := amount
|
|
if v.delivered != nil {
|
|
got = v.delivered(amount)
|
|
}
|
|
v.tok[token][from] = new(big.Int).Sub(v.bag(token, from), amount)
|
|
v.tok[token][to] = new(big.Int).Add(v.bag(token, to), got)
|
|
return nil
|
|
}
|
|
|
|
func (v *blueAVaultDB) TransferTokenTo(token, to common.Address, amount *big.Int) error {
|
|
v.ops = append(v.ops, "transferTo")
|
|
if v.failXfer {
|
|
return errors.New("blueA: token reverted")
|
|
}
|
|
if v.bag(token, poolManagerAddr9999).Cmp(amount) < 0 {
|
|
return errors.New("blueA: vault holds too little")
|
|
}
|
|
v.tok[token][poolManagerAddr9999] = new(big.Int).Sub(v.bag(token, poolManagerAddr9999), amount)
|
|
v.tok[token][to] = new(big.Int).Add(v.bag(token, to), amount)
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
_ StateDB = (*blueAVaultDB)(nil)
|
|
_ erc20Vault = (*blueAVaultDB)(nil)
|
|
)
|
|
|
|
// blueANonceDB is blueAKV plus the OPTIONAL nonceMarker capability, so the
|
|
// vault-account persistence marker can be observed (and its idempotence pinned).
|
|
type blueANonceDB struct {
|
|
*blueAKV
|
|
nonces map[common.Address]uint64
|
|
}
|
|
|
|
func blueANewNonceDB() *blueANonceDB {
|
|
return &blueANonceDB{blueAKV: blueANewKV(), nonces: map[common.Address]uint64{}}
|
|
}
|
|
|
|
func (n *blueANonceDB) GetNonce(addr common.Address) uint64 { return n.nonces[addr] }
|
|
|
|
func (n *blueANonceDB) SetNonce(addr common.Address, nonce uint64) {
|
|
n.nonces[addr] = nonce
|
|
n.ops = append(n.ops, "setNonce")
|
|
}
|
|
|
|
var _ nonceMarker = (*blueANonceDB)(nil)
|
|
|
|
// ─────────────────────────── trace helpers ───────────────────────────
|
|
|
|
func blueACount(ops []string, want string) int {
|
|
n := 0
|
|
for _, o := range ops {
|
|
if o == want {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// blueAFirst / blueALast return the first / last index of an op, or -1.
|
|
func blueAFirst(ops []string, want string) int {
|
|
for i, o := range ops {
|
|
if o == want {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func blueALast(ops []string, want string) int {
|
|
last := -1
|
|
for i, o := range ops {
|
|
if o == want {
|
|
last = i
|
|
}
|
|
}
|
|
return last
|
|
}
|
|
|
|
// blueAAssertTerminalPull is THE native-zap property: the ERC-20 transferFrom is the
|
|
// frame's terminal external effect, with every 0x9999 accounting write already done.
|
|
// It fails if any 0x9999 write follows the pull — the exact shape of the dropped-write
|
|
// fund-strand this module exists to dissolve.
|
|
func blueAAssertTerminalPull(t *testing.T, ops []string) {
|
|
t.Helper()
|
|
pull := blueAFirst(ops, "transferFrom")
|
|
if pull < 0 {
|
|
t.Fatalf("no terminal transferFrom in trace %v", ops)
|
|
}
|
|
if n := blueACount(ops, "transferFrom"); n != 1 {
|
|
t.Fatalf("want exactly 1 transferFrom, got %d in %v", n, ops)
|
|
}
|
|
writes := blueALast(ops, "set9999")
|
|
if writes < 0 {
|
|
t.Fatalf("no 0x9999 accounting write in trace %v", ops)
|
|
}
|
|
if writes > pull {
|
|
t.Fatalf("0x9999 write at %d FOLLOWS the terminal transferFrom at %d: %v", writes, pull, ops)
|
|
}
|
|
// The only effects after the pull are the delivery check's own balance read.
|
|
for _, o := range ops[pull+1:] {
|
|
if o != "balanceOf" {
|
|
t.Fatalf("effect %q follows the terminal transferFrom: %v", o, ops)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────── production-path env ───────────────────────────
|
|
|
|
// blueATraceDB is the harness contract.StateDB with an ordered trace on the two things
|
|
// whose relative order is the whole point: 0x9999 storage writes and token sub-calls.
|
|
// Everything else is inherited, so this observes the REAL adapter path.
|
|
type blueATraceDB struct {
|
|
*contractStateDBWrapper
|
|
ops []string
|
|
}
|
|
|
|
func (w *blueATraceDB) SetState(addr common.Address, key, value common.Hash) common.Hash {
|
|
if addr == poolManagerAddr9999 {
|
|
w.ops = append(w.ops, "set9999")
|
|
}
|
|
return w.contractStateDBWrapper.SetState(addr, key, value)
|
|
}
|
|
|
|
func (w *blueATraceDB) TokenBalanceOf(token, owner common.Address) *big.Int {
|
|
w.ops = append(w.ops, "balanceOf")
|
|
return w.contractStateDBWrapper.TokenBalanceOf(token, owner)
|
|
}
|
|
|
|
func (w *blueATraceDB) TransferTokenFrom(token, from, to common.Address, amount *big.Int) error {
|
|
w.ops = append(w.ops, "transferFrom")
|
|
return w.contractStateDBWrapper.TransferTokenFrom(token, from, to, amount)
|
|
}
|
|
|
|
func (w *blueATraceDB) TransferTokenTo(token, to common.Address, amount *big.Int) error {
|
|
w.ops = append(w.ops, "transferTo")
|
|
return w.contractStateDBWrapper.TransferTokenTo(token, to, amount)
|
|
}
|
|
|
|
// blueAEnv is a self-contained C<->D environment: a real atomic shared memory (both
|
|
// sides), a mock StateDB behind the tracing wrapper, and the AccessibleState +
|
|
// AtomicState surface the client methods take. It installs NO package globals, so
|
|
// every test using it is order-independent.
|
|
// The chain ids are the EMBEDDED nativeAtomicState's own fields (promoted, never
|
|
// shadowed) so that closing the seam in a test — e.dChainID = ids.Empty — changes the
|
|
// value DChainID() actually reports.
|
|
type blueAEnv struct {
|
|
*nativeAtomicState
|
|
db *blueATraceDB
|
|
cSM, dSM atomic.SharedMemory
|
|
}
|
|
|
|
func (e *blueAEnv) GetStateDB() contract.StateDB { return e.db }
|
|
|
|
func blueANewEnv(t *testing.T) *blueAEnv {
|
|
t.Helper()
|
|
cChainID, dChainID := ids.ID{0xCC}, ids.ID{0xDD}
|
|
mem := atomic.NewMemory(memdb.New())
|
|
mock := NewMockStateDB()
|
|
e := &blueAEnv{
|
|
nativeAtomicState: &nativeAtomicState{
|
|
stateDB: mock,
|
|
sm: mem.NewSharedMemory(cChainID),
|
|
networkID: 1,
|
|
chainID: cChainID,
|
|
cChainID: cChainID,
|
|
dChainID: dChainID,
|
|
governance: testGovernance,
|
|
txID: ids.ID{0x7A},
|
|
blockTimestamp: harnessBlockTime,
|
|
},
|
|
db: &blueATraceDB{contractStateDBWrapper: &contractStateDBWrapper{inner: mock}},
|
|
dSM: mem.NewSharedMemory(dChainID),
|
|
}
|
|
e.cSM = e.sm
|
|
return e
|
|
}
|
|
|
|
var (
|
|
_ contract.AccessibleState = (*blueAEnv)(nil)
|
|
_ contract.AtomicState = (*blueAEnv)(nil)
|
|
)
|
|
|
|
// kv is the adapter every client method builds internally — the same view the tests
|
|
// read state back through.
|
|
func (e *blueAEnv) kv() *poolStateAdapter { return newPoolStateAdapter(e) }
|
|
|
|
// putD stages a D->C object under the C partition (what the dexvm's export does), so
|
|
// the C-side consume paths read it via Get(dChainID, key).
|
|
func (e *blueAEnv) putD(t *testing.T, key ids.ID, value []byte) {
|
|
t.Helper()
|
|
if err := e.dSM.Apply(map[ids.ID]*atomic.Requests{
|
|
e.cChainID: {PutRequests: []*atomic.Element{{Key: key[:], Value: value, Traits: [][]byte{}}}},
|
|
}); err != nil {
|
|
t.Fatalf("putD: %v", err)
|
|
}
|
|
}
|
|
|
|
var blueAAlice = common.HexToAddress("0xA11CE00000000000000000000000000000000001")
|
|
var blueABob = common.HexToAddress("0xB0B0000000000000000000000000000000000002")
|
|
var blueAToken = common.HexToAddress("0x00000000000000000000000000000000000000CC")
|
|
|
|
// TestBlueAEnsureVaultAccountPersists pins property (9): the marker only ever bumps a
|
|
// ZERO nonce (idempotent, and it never overwrites a real nonce), and a StateDB that
|
|
// models no empty-account reaping is a safe no-op rather than a panic.
|
|
func TestBlueAEnsureVaultAccountPersists(t *testing.T) {
|
|
t.Run("no nonceMarker is a no-op", func(t *testing.T) {
|
|
db := blueANewKV()
|
|
ensureVaultAccountPersists(db)
|
|
if len(db.ops) != 0 || len(db.slots) != 0 {
|
|
t.Fatalf("marker touched a non-nonceMarker StateDB: ops=%v slots=%v", db.ops, db.slots)
|
|
}
|
|
})
|
|
|
|
t.Run("zero nonce is bumped once", func(t *testing.T) {
|
|
db := blueANewNonceDB()
|
|
ensureVaultAccountPersists(db)
|
|
if got := db.nonces[poolManagerAddr9999]; got != 1 {
|
|
t.Fatalf("nonce = %d after first mark, want 1", got)
|
|
}
|
|
ensureVaultAccountPersists(db)
|
|
if got := db.nonces[poolManagerAddr9999]; got != 1 {
|
|
t.Fatalf("nonce = %d after second mark, want 1 (idempotent)", got)
|
|
}
|
|
if n := blueACount(db.ops, "setNonce"); n != 1 {
|
|
t.Fatalf("SetNonce called %d times, want 1 (idempotent)", n)
|
|
}
|
|
})
|
|
|
|
t.Run("a live nonce is never overwritten", func(t *testing.T) {
|
|
db := blueANewNonceDB()
|
|
db.nonces[poolManagerAddr9999] = 7
|
|
ensureVaultAccountPersists(db)
|
|
if got := db.nonces[poolManagerAddr9999]; got != 7 {
|
|
t.Fatalf("nonce = %d, want 7 (only a zero nonce is bumped)", got)
|
|
}
|
|
if n := blueACount(db.ops, "setNonce"); n != 0 {
|
|
t.Fatalf("SetNonce called %d times on a live nonce, want 0", n)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ═══════════════ native_dchain_client.go — the D->C credit helpers ═══════════════
|
|
|
|
// ═══════════════ native_dchain_client.go — the closed-seam refusals ═══════════════
|
|
|
|
// ═══════════════ the closed on-ramp: the synchronous Engine surface ═══════════════
|
|
|
|
// blueAEngineRefuses drives every Engine method and asserts it refuses with
|
|
// ErrDChainUnavailable, returns a ZERO delta, and mutates neither the pool it is handed
|
|
// nor the caller's view of it. Quote is the one benign exception: it returns zero rather
|
|
// than an error so a router treats it as "no route" instead of a failure.
|
|
func blueAEngineRefuses(t *testing.T, name string, eng Engine) {
|
|
t.Helper()
|
|
pool := &Pool{
|
|
SqrtPriceX96: new(big.Int).Set(Q96), Tick: 42, Liquidity: big.NewInt(1_000),
|
|
FeeGrowth0X128: big.NewInt(1), FeeGrowth1X128: big.NewInt(2),
|
|
ProtocolFees0: big.NewInt(3), ProtocolFees1: big.NewInt(4),
|
|
}
|
|
before := *pool
|
|
ps := NewPoolState(pool, 60, 3000)
|
|
|
|
if tick, err := eng.Initialize(new(big.Int).Set(Q96)); !errors.Is(err, ErrDChainUnavailable) || tick != 0 {
|
|
t.Fatalf("%s.Initialize = (%d, %v), want (0, ErrDChainUnavailable)", name, tick, err)
|
|
}
|
|
delta, err := eng.Swap(ps, blueAAlice, SwapParams{ZeroForOne: true, AmountSpecified: big.NewInt(-100)})
|
|
if !errors.Is(err, ErrDChainUnavailable) || !delta.IsZero() {
|
|
t.Fatalf("%s.Swap = (%v, %v), want (zero, ErrDChainUnavailable)", delta, err, name)
|
|
}
|
|
d0, d1, merr := eng.ModifyLiquidity(ps, blueAAlice, ModifyLiquidityParams{LiquidityDelta: big.NewInt(5)})
|
|
if !errors.Is(merr, ErrDChainUnavailable) || !d0.IsZero() || !d1.IsZero() {
|
|
t.Fatalf("%s.ModifyLiquidity = (%v, %v, %v), want (zero, zero, ErrDChainUnavailable)", name, d0, d1, merr)
|
|
}
|
|
dd, derr := eng.Donate(ps, big.NewInt(7), big.NewInt(8))
|
|
if !errors.Is(derr, ErrDChainUnavailable) || !dd.IsZero() {
|
|
t.Fatalf("%s.Donate = (%v, %v), want (zero, ErrDChainUnavailable)", name, dd, derr)
|
|
}
|
|
// Quote is a read-only estimate: a benign zero, NOT an error, so router fallbacks
|
|
// treat the absent chain as "no route" rather than a hard failure.
|
|
q := eng.Quote(pool, big.NewInt(1_000), true)
|
|
if q == nil || q.Sign() != 0 {
|
|
t.Fatalf("%s.Quote = %v, want a non-nil zero", name, q)
|
|
}
|
|
if eng.Brand() == "" {
|
|
t.Fatalf("%s.Brand is empty", name)
|
|
}
|
|
// A refusal moves no value: the pool it was handed is byte-identical.
|
|
if pool.Tick != before.Tick ||
|
|
pool.SqrtPriceX96.Cmp(before.SqrtPriceX96) != 0 ||
|
|
pool.Liquidity.Cmp(before.Liquidity) != 0 ||
|
|
pool.FeeGrowth0X128.Cmp(before.FeeGrowth0X128) != 0 ||
|
|
pool.FeeGrowth1X128.Cmp(before.FeeGrowth1X128) != 0 ||
|
|
pool.ProtocolFees0.Cmp(before.ProtocolFees0) != 0 ||
|
|
pool.ProtocolFees1.Cmp(before.ProtocolFees1) != 0 {
|
|
t.Fatalf("%s mutated the pool it refused: %+v -> %+v", name, before, *pool)
|
|
}
|
|
if len(ps.Positions) != 0 || len(ps.Ticks) != 0 {
|
|
t.Fatalf("%s created position/tick state on a refusal", name)
|
|
}
|
|
}
|
|
|
|
// TestBlueAClosedOnRampRefusesEveryEngineOp: BOTH closed-on-ramp clients — the
|
|
// no-local-D-Chain default and the native seam's deprecated synchronous surface — refuse
|
|
// every Engine operation identically. This is the fail-closed default a node runs with no
|
|
// local D-Chain: it never fabricates a fill.
|
|
func TestBlueAClosedOnRampRefusesEveryEngineOp(t *testing.T) {
|
|
blueAEngineRefuses(t, "dchainUnavailable", newDChainUnavailable())
|
|
blueAEngineRefuses(t, "NativeDChainClient", NewNativeDChainClient("Blue A DEX"))
|
|
}
|
|
|
|
// TestBlueANativeClientBrand: the brand is the white-label identity; an empty brand falls
|
|
// back to the OSS default rather than surfacing as an empty string in logs.
|
|
func TestBlueANativeClientBrand(t *testing.T) {
|
|
if got := NewNativeDChainClient("").Brand(); got != "Lux DEX" {
|
|
t.Fatalf("default brand = %q, want %q", got, "Lux DEX")
|
|
}
|
|
if got := NewNativeDChainClient("Blue A DEX").Brand(); got != "Blue A DEX" {
|
|
t.Fatalf("brand = %q, want %q", got, "Blue A DEX")
|
|
}
|
|
if got := newDChainUnavailable().Brand(); got != "DEX (local D-Chain unavailable)" {
|
|
t.Fatalf("unavailable brand = %q", got)
|
|
}
|
|
}
|