Files
precompile/v3/zzv3_custody_test.go
zeekay ca0bf6b303 precompile: seven packages to 96-99%, from lanes killed before they could commit
slhdsa 97.9 · mlkem 97.4 · mldsa 96.8 · xwing 96.2 · bls12381 97.2 ·
kzg4844 97.8 · v3 98.7. All build, vet and gofmt clean, all green under
-count=1, and preserved here rather than lost to a session limit.

v3's lane did report before it died: 78.1% to 98.7%, 70 of 70 mutations caught,
four of which survived a first pass and needed tests written. The two that
matter are rounding: burn rounding principal UP, and fee growth rounding UP,
neither of which any behaviour test could see -- a Q128.128 off-by-one is
2^-128 of a token and floors to zero through every payout path, so it took an
exact-value assertion. It also found that a full drain of a pool with
overlapping positions REVERTS rather than paying out, because deposits round
down once per position while payouts round down once per step over combined
liquidity, and floor(a+b) >= floor(a)+floor(b). Custody holds; not one unbacked
wei leaves. Liveness, not theft.

The other six carry no report -- their lanes were killed mid-run. Build, vet,
gofmt and the suite are all I can vouch for.
2026-08-28 13:27:00 -07:00

578 lines
24 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Custody: the money path. Every unit the precompile pays out must first have
// been observed inbound, and the per-asset `reserve` ledger must equal deposits
// minus withdrawals at every instant. These tests cover BOTH asset kinds — the
// ERC-20 vault AND the native-LUX path — to the same depth.
package v3
import (
"math/big"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/tracing"
"github.com/luxfi/precompile/dex"
"github.com/stretchr/testify/require"
)
// ---------------------------------------------------------------------------
// The conservation anchor, for any asset: reserve[asset] == real holdings.
// ---------------------------------------------------------------------------
// assertAnchor asserts the ledger equals the real balance for an ERC-20 or, for
// the zero address, the precompile's real native balance.
func assertAnchor(t *testing.T, m *mockState, asset common.Address) {
t.Helper()
if asset == native {
eqBig(t, m.nativeBal(v3Addr), loadReserve(m, native))
return
}
eqBig(t, m.bal(asset, v3Addr), loadReserve(m, asset))
}
// ---------------------------------------------------------------------------
// Native path — pullExactNative / pushOutNative through the real entry points.
// ---------------------------------------------------------------------------
// TestNative_MintPullsExactDeliveredValue drives the ENTIRE native inbound path:
// the EVM has already moved msg.value into v3Addr before Run, so the precompile
// must credit the OBSERVED surplus over its accounted reserve — never the
// requested amount — and must refuse when the two disagree in either direction.
func TestNative_MintPullsExactDeliveredValue(t *testing.T) {
L := oneE18
want0, want1 := mintAmounts(dex.Q96, -600, 600, L)
require.True(t, want0.Sign() > 0 && want1.Sign() > 0, "the fixture must move both legs")
// --- under-delivered: one wei short of what the position costs ---
e := newEnv()
e.db().fund(tokenB, trader, e18x1000)
_, err := e.initialize(nativePool, dex.Q96, false)
require.NoError(t, err)
e.db().fundNative(v3Addr, new(big.Int).Sub(want0, big.NewInt(1)))
_, _, err = e.mint(trader, nativePool, -600, 600, L, false)
require.ErrorIs(t, err, ErrDeltaMismatch, "a short msg.value must be refused, not credited")
eqBig(t, big.NewInt(0), loadReserve(e.db(), native))
// --- over-delivered: one wei too much is ALSO refused (never silently kept) ---
e = newEnv()
e.db().fund(tokenB, trader, e18x1000)
_, err = e.initialize(nativePool, dex.Q96, false)
require.NoError(t, err)
e.db().fundNative(v3Addr, new(big.Int).Add(want0, big.NewInt(1)))
_, _, err = e.mint(trader, nativePool, -600, 600, L, false)
require.ErrorIs(t, err, ErrDeltaMismatch, "an over-delivering msg.value must be refused")
// --- exact: the only accepted delivery ---
e = newEnv()
e.db().fund(tokenB, trader, e18x1000)
_, err = e.initialize(nativePool, dex.Q96, false)
require.NoError(t, err)
e.db().fundNative(v3Addr, want0)
got0, got1, err := e.mint(trader, nativePool, -600, 600, L, false)
require.NoError(t, err)
eqBig(t, want0, got0)
eqBig(t, want1, got1)
// The reserve now equals the delivered value exactly, on BOTH legs.
assertAnchor(t, e.db(), native)
assertAnchor(t, e.db(), tokenB)
eqBig(t, want0, loadReserve(e.db(), native))
}
// TestNative_SecondMintMeasuresAgainstReserve proves the delta is taken against
// the ACCOUNTED reserve, not against zero: with value already custodied from an
// earlier mint, a second mint must demand exactly its own cost and no more.
func TestNative_SecondMintMeasuresAgainstReserve(t *testing.T) {
L := oneE18
first0, _ := mintAmounts(dex.Q96, -600, 600, L)
second0, _ := mintAmounts(dex.Q96, -1200, 1200, L)
e := newEnv()
e.db().fund(tokenB, trader, e18x1000)
_, err := e.initialize(nativePool, dex.Q96, false)
require.NoError(t, err)
e.db().fundNative(v3Addr, first0)
_, _, err = e.mint(trader, nativePool, -600, 600, L, false)
require.NoError(t, err)
// Deliver nothing: the surplus over the reserve is 0, so the mint is refused
// even though v3Addr's raw balance is large.
_, _, err = e.mint(trader, nativePool, -1200, 1200, L, false)
require.ErrorIs(t, err, ErrDeltaMismatch, "already-custodied value must not pay for a new mint")
e.db().fundNative(v3Addr, second0)
_, _, err = e.mint(trader, nativePool, -1200, 1200, L, false)
require.NoError(t, err)
eqBig(t, new(big.Int).Add(first0, second0), loadReserve(e.db(), native))
assertAnchor(t, e.db(), native)
}
// TestNative_SwapPaysOutNative drives the native OUTBOUND path end to end: a
// oneForZero swap pays the trader in native LUX via SubBalance/AddBalance. The
// pool's native balance must fall by exactly the payout, the trader's must rise
// by exactly the same, and the ledger must track it — no mint, no loss.
func TestNative_SwapPaysOutNative(t *testing.T) {
L := oneE18
want0, _ := mintAmounts(dex.Q96, -600, 600, L)
e := newEnv()
e.db().fund(tokenB, trader, e18x1000)
_, err := e.initialize(nativePool, dex.Q96, false)
require.NoError(t, err)
e.db().fundNative(v3Addr, want0)
_, _, err = e.mint(trader, nativePool, -600, 600, L, false)
require.NoError(t, err)
poolNativeBefore := e.db().nativeBal(v3Addr)
traderNativeBefore := e.db().nativeBal(trader)
totalNativeBefore := new(big.Int).Add(poolNativeBefore, traderNativeBefore)
// oneForZero exact input: pay tokenB in, receive native out.
amt := new(big.Int).Neg(mustBig("1000000000000000")) // 1e15
limit := new(big.Int).Sub(dex.MaxSqrtRatio, big.NewInt(1))
a0, a1, err := e.swap(trader, nativePool, false, amt, limit)
require.NoError(t, err)
require.True(t, a0.Sign() < 0, "native leg is the payout, so amount0 must be negative")
require.True(t, a1.Sign() > 0)
paid := new(big.Int).Neg(a0)
require.True(t, paid.Sign() > 0)
// Exact movement, both sides, and nothing created or destroyed.
eqBig(t, new(big.Int).Sub(poolNativeBefore, paid), e.db().nativeBal(v3Addr))
eqBig(t, new(big.Int).Add(traderNativeBefore, paid), e.db().nativeBal(trader))
eqBig(t, totalNativeBefore, new(big.Int).Add(e.db().nativeBal(v3Addr), e.db().nativeBal(trader)))
assertAnchor(t, e.db(), native)
assertAnchor(t, e.db(), tokenB)
}
// TestNative_SwapPullsNativeIn is the mirror: a zeroForOne swap on a native pool
// takes native IN. The amount is not knowable ahead of the call, so it is read
// off an identically-configured ERC-20 pool — proving, as a side effect, that
// settlement size is a function of the pool parameters and not of the addresses.
func TestNative_SwapPullsNativeIn(t *testing.T) {
L := oneE18
amt := new(big.Int).Neg(mustBig("1000000000000000")) // 1e15 exact input
limit := new(big.Int).Add(dex.MinSqrtRatio, big.NewInt(1))
// Mirror pool: same fee, spacing, price, liquidity — only the addresses differ.
mirror := fundedEnv(e18x1000)
_, _, err := mirror.mint(trader, stdPool, -600, 600, L, false)
require.NoError(t, err)
mIn, _, err := mirror.swap(trader, stdPool, true, amt, limit)
require.NoError(t, err)
require.True(t, mIn.Sign() > 0)
want0, _ := mintAmounts(dex.Q96, -600, 600, L)
e := newEnv()
e.db().fund(tokenB, trader, e18x1000)
_, err = e.initialize(nativePool, dex.Q96, false)
require.NoError(t, err)
e.db().fundNative(v3Addr, want0)
_, _, err = e.mint(trader, nativePool, -600, 600, L, false)
require.NoError(t, err)
// Deliver exactly the mirror's settlement: the native pool must accept it and
// credit the reserve by exactly that.
reserveBefore := loadReserve(e.db(), native)
e.db().fundNative(v3Addr, mIn)
a0, a1, err := e.swap(trader, nativePool, true, amt, limit)
require.NoError(t, err)
eqBig(t, mIn, a0)
require.True(t, a1.Sign() < 0)
eqBig(t, new(big.Int).Add(reserveBefore, mIn), loadReserve(e.db(), native))
assertAnchor(t, e.db(), native)
assertAnchor(t, e.db(), tokenB)
}
// TestNative_CollectPaysOutNative walks the burn→collect payout on the native leg
// and asserts the trader ends up with exactly what the pool released.
func TestNative_CollectPaysOutNative(t *testing.T) {
L := oneE18
want0, _ := mintAmounts(dex.Q96, -600, 600, L)
e := newEnv()
e.db().fund(tokenB, trader, e18x1000)
_, err := e.initialize(nativePool, dex.Q96, false)
require.NoError(t, err)
e.db().fundNative(v3Addr, want0)
_, _, err = e.mint(trader, nativePool, -600, 600, L, false)
require.NoError(t, err)
burn0, burn1, err := e.burn(trader, nativePool, -600, 600, L)
require.NoError(t, err)
// Burn credits owed; it must not have moved a single wei yet.
eqBig(t, want0, e.db().nativeBal(v3Addr))
eqBig(t, big.NewInt(0), e.db().nativeBal(trader))
maxU := new(big.Int).Set(maxUint128)
got0, got1, err := e.collect(trader, nativePool, -600, 600, maxU, maxU)
require.NoError(t, err)
eqBig(t, burn0, got0)
eqBig(t, burn1, got1)
eqBig(t, got0, e.db().nativeBal(trader))
// Round-down on both legs means the pool never pays out more than it took in.
require.Truef(t, got0.Cmp(want0) <= 0, "native payout %s must not exceed the %s taken in", got0, want0)
assertAnchor(t, e.db(), native)
assertAnchor(t, e.db(), tokenB)
}
// TestPushOutNative_RefusesBeyondRealBalance is the last line of native defence:
// even with the ledger claiming coverage, a payout larger than the precompile's
// REAL native balance must abort loudly rather than wrap v3Addr's modular
// balance into a huge number.
func TestPushOutNative_RefusesBeyondRealBalance(t *testing.T) {
m := newMockState()
m.fundNative(v3Addr, big.NewInt(100))
require.NoError(t, pushOutNative(m, trader, big.NewInt(100)))
eqBig(t, big.NewInt(0), m.nativeBal(v3Addr))
eqBig(t, big.NewInt(100), m.nativeBal(trader))
// One wei beyond holdings: refused, and nothing moved.
m.fundNative(v3Addr, big.NewInt(50))
require.ErrorIs(t, pushOutNative(m, trader, big.NewInt(51)), ErrReserveUnderflow)
eqBig(t, big.NewInt(50), m.nativeBal(v3Addr))
eqBig(t, big.NewInt(100), m.nativeBal(trader))
// A value beyond uint256 cannot even be expressed as a balance: refused too.
tooBig := new(big.Int).Lsh(big.NewInt(1), 256)
require.ErrorIs(t, pushOutNative(m, trader, tooBig), ErrTransferFailed)
eqBig(t, big.NewInt(50), m.nativeBal(v3Addr))
}
// TestPullExactNative_Direct pins the surplus arithmetic itself, including the
// case the entry points cannot produce: a reserve ABOVE the real balance makes
// the delivered surplus negative, which must never compare equal to a positive
// request.
func TestPullExactNative_Direct(t *testing.T) {
m := newMockState()
m.fundNative(v3Addr, big.NewInt(1000))
require.NoError(t, pullExactNative(m, big.NewInt(400), big.NewInt(600)))
require.ErrorIs(t, pullExactNative(m, big.NewInt(400), big.NewInt(599)), ErrDeltaMismatch)
require.ErrorIs(t, pullExactNative(m, big.NewInt(400), big.NewInt(601)), ErrDeltaMismatch)
// Reserve exceeds holdings: surplus is negative, so no positive request matches.
require.ErrorIs(t, pullExactNative(m, big.NewInt(1500), big.NewInt(1)), ErrDeltaMismatch)
}
// ---------------------------------------------------------------------------
// ERC-20 path — the vault capability, the observed delta, and its refusals.
// ---------------------------------------------------------------------------
// TestVaultUnavailable proves a host StateDB that does not implement the ERC-20
// vault refuses custody cleanly on BOTH directions rather than faking a credit.
func TestVaultUnavailable(t *testing.T) {
db := newNonVaultState()
r := newRig(db)
gas := uint64(50_000_000)
_, _, err := r.call(trader, inInitialize(stdPool, dex.Q96), gas, false)
require.NoError(t, err)
// mint -> pullAsset -> no vault.
_, _, err = r.call(trader, inMint(stdPool, -600, 600, oneE18), gas, false)
require.ErrorIs(t, err, ErrVaultUnavailable)
// payAsset -> no vault. Reach it by crediting owed directly, so the pay-out leg
// is the first thing that needs the capability.
poolId := poolIdOf(stdPool)
posKey := dex.PositionKey(trader, -600, 600, salt)
pos := loadPosition(db, poolId, posKey)
pos.TokensOwed0 = big.NewInt(500)
storePosition(db, poolId, posKey, pos)
storeReserve(db, tokenA, big.NewInt(500))
_, _, err = r.call(trader, inCollect(stdPool, -600, 600, big.NewInt(500), big.NewInt(0)), gas, false)
require.ErrorIs(t, err, ErrVaultUnavailable)
}
// TestDeltaMismatch_FeeOnTransferToken proves the OpenZeppelin-style observed
// delta is what gets credited: a deflationary token that delivers less than the
// pool asked for is REFUSED, never credited at face value. Crediting the request
// instead of the delivery is exactly how a pool becomes insolvent.
func TestDeltaMismatch_FeeOnTransferToken(t *testing.T) {
db := &shortTransferState{mockState: newMockState(), shortfall: big.NewInt(1)}
db.fund(tokenA, trader, e18x1000)
db.fund(tokenB, trader, e18x1000)
r := newRig(db)
gas := uint64(50_000_000)
_, _, err := r.call(trader, inInitialize(stdPool, dex.Q96), gas, false)
require.NoError(t, err)
_, _, err = r.call(trader, inMint(stdPool, -600, 600, oneE18), gas, false)
require.ErrorIs(t, err, ErrDeltaMismatch)
// Nothing was credited on the leg that under-delivered.
eqBig(t, big.NewInt(0), loadReserve(db, tokenA))
}
// TestTransferFailed_InsufficientCallerBalance proves a reverting transferFrom
// (the caller simply does not have the tokens) surfaces as ErrTransferFailed and
// credits nothing.
func TestTransferFailed_InsufficientCallerBalance(t *testing.T) {
e := newEnv()
_, err := e.initialize(stdPool, dex.Q96, false)
require.NoError(t, err)
// token0 funded, token1 not: the SECOND leg must revert the call.
want0, _ := mintAmounts(dex.Q96, -600, 600, oneE18)
e.db().fund(tokenA, trader, want0)
_, _, err = e.mint(trader, stdPool, -600, 600, oneE18, false)
require.ErrorIs(t, err, ErrTransferFailed)
eqBig(t, big.NewInt(0), e.reserve(tokenB))
}
// TestPushOut_TransferReverts proves a failing outbound ERC-20 transfer surfaces
// as ErrTransferFailed. It is reached by corrupting the ledger UPWARD, which is
// the shape of the bug the check exists for: the reserve claims coverage the
// real balance does not have.
func TestPushOut_TransferReverts(t *testing.T) {
e := fundedEnv(e18x1000)
_, _, err := e.mint(trader, stdPool, -600, 600, oneE18, false)
require.NoError(t, err)
poolId := poolIdOf(stdPool)
posKey := dex.PositionKey(trader, -600, 600, salt)
pos := loadPosition(e.db(), poolId, posKey)
inflated := new(big.Int).Add(e.reserve(tokenA), oneE18)
pos.TokensOwed0 = inflated
storePosition(e.db(), poolId, posKey, pos)
storeReserve(e.db(), tokenA, inflated) // ledger now lies about holdings
_, _, err = e.collect(trader, stdPool, -600, 600, inflated, big.NewInt(0))
require.ErrorIs(t, err, ErrTransferFailed, "the token itself must stop an unbacked payout")
}
// TestPullExact_Direct pins the observed-delta helper on its own, including the
// over-delivering token (an inflationary rebase mid-transfer), which the entry
// points cannot produce but which must be refused all the same.
func TestPullExact_Direct(t *testing.T) {
m := newMockState()
m.fund(tokenA, trader, big.NewInt(1000))
got, err := pullExact(m, tokenA, trader, v3Addr, big.NewInt(400))
require.NoError(t, err)
eqBig(t, big.NewInt(400), got)
eqBig(t, big.NewInt(400), m.bal(tokenA, v3Addr))
// Caller cannot cover it.
_, err = pullExact(m, tokenA, trader, v3Addr, big.NewInt(10_000))
require.ErrorIs(t, err, ErrTransferFailed)
// Over-delivery is refused as firmly as under-delivery.
over := &overTransferState{mockState: m, surplus: big.NewInt(3)}
_, err = pullExact(over, tokenA, trader, v3Addr, big.NewInt(100))
require.ErrorIs(t, err, ErrDeltaMismatch)
}
// overTransferState is an inflationary token: transferFrom delivers MORE than
// requested. Only used by TestPullExact_Direct.
type overTransferState struct {
*mockState
surplus *big.Int
}
func (o *overTransferState) TransferTokenFrom(token, from, to common.Address, amount *big.Int) error {
if err := o.mockState.TransferTokenFrom(token, from, to, amount); err != nil {
return err
}
o.setBal(token, to, new(big.Int).Add(o.bal(token, to), o.surplus))
return nil
}
// TestPushOut_Direct pins the outbound ERC-20 helper's success and failure.
func TestPushOut_Direct(t *testing.T) {
m := newMockState()
m.fund(tokenA, v3Addr, big.NewInt(500))
require.NoError(t, pushOut(m, tokenA, trader, big.NewInt(200)))
eqBig(t, big.NewInt(300), m.bal(tokenA, v3Addr))
eqBig(t, big.NewInt(200), m.bal(tokenA, trader))
require.ErrorIs(t, pushOut(m, tokenA, trader, big.NewInt(301)), ErrTransferFailed)
eqBig(t, big.NewInt(300), m.bal(tokenA, v3Addr))
}
// ---------------------------------------------------------------------------
// The reserve ledger itself.
// ---------------------------------------------------------------------------
// TestSubReserve_UnderflowAborts proves the conservation backstop refuses rather
// than wrapping. A modular subtraction here would turn a 1-wei overdraw into a
// ~2^256 reserve — the classic underflow that lets a pool be drained.
func TestSubReserve_UnderflowAborts(t *testing.T) {
m := newMockState()
storeReserve(m, tokenA, big.NewInt(100))
require.False(t, subReserve(m, tokenA, big.NewInt(101)), "overdraw must be refused")
require.Zerof(t, big.NewInt(100).Cmp(loadReserve(m, tokenA)),
"a refused debit must leave the ledger untouched, got %s", loadReserve(m, tokenA))
require.True(t, subReserve(m, tokenA, big.NewInt(100)), "an exact debit is allowed")
eqBig(t, big.NewInt(0), loadReserve(m, tokenA))
// At zero, any positive debit is refused and the ledger stays at zero — never
// wraps to 2^256-1.
require.False(t, subReserve(m, tokenA, big.NewInt(1)))
eqBig(t, big.NewInt(0), loadReserve(m, tokenA))
require.True(t, loadReserve(m, tokenA).Sign() >= 0)
}
// TestPayAsset_ReserveUnderflowRefused proves the ledger check fires BEFORE any
// value moves: an unbacked payout must abort with the reserve and both balances
// exactly as they were.
func TestPayAsset_ReserveUnderflowRefused(t *testing.T) {
// ERC-20 leg: the vault could physically pay (v3Addr holds the tokens) but the
// ledger says they are not this pool's to give.
m := newMockState()
m.fund(tokenA, v3Addr, big.NewInt(1000))
storeReserve(m, tokenA, big.NewInt(10))
require.ErrorIs(t, payAsset(m, tokenA, trader, big.NewInt(11)), ErrReserveUnderflow)
eqBig(t, big.NewInt(10), loadReserve(m, tokenA))
eqBig(t, big.NewInt(1000), m.bal(tokenA, v3Addr))
eqBig(t, big.NewInt(0), m.bal(tokenA, trader))
// Native leg: same refusal.
m.fundNative(v3Addr, big.NewInt(1000))
storeReserve(m, native, big.NewInt(10))
require.ErrorIs(t, payAsset(m, native, trader, big.NewInt(11)), ErrReserveUnderflow)
eqBig(t, big.NewInt(1000), m.nativeBal(v3Addr))
eqBig(t, big.NewInt(0), m.nativeBal(trader))
}
// TestZeroAmountIsANoOp proves both money paths short-circuit on zero without
// touching the ledger, the vault, or the native balance — so a zero-value leg of
// a single-sided position never needs a vault capability at all.
func TestZeroAmountIsANoOp(t *testing.T) {
db := newNonVaultState() // no vault: any real move would fail here
require.NoError(t, pullAsset(db, tokenA, trader, big.NewInt(0)))
require.NoError(t, payAsset(db, tokenA, trader, big.NewInt(0)))
require.NoError(t, pullAsset(db, native, trader, big.NewInt(0)))
require.NoError(t, payAsset(db, native, trader, big.NewInt(0)))
eqBig(t, big.NewInt(0), loadReserve(db, tokenA))
eqBig(t, big.NewInt(0), loadReserve(db, native))
}
// TestSingleSidedPositionMovesOneLegOnly proves the zero short-circuit on a real
// call: a position entirely above the current price is funded in token1 only, so
// the token0 reserve must stay at zero and the caller must need no token0.
func TestSingleSidedPositionMovesOneLegOnly(t *testing.T) {
e := newEnv()
e.db().fund(tokenB, trader, e18x1000) // deliberately NO tokenA
_, err := e.initialize(stdPool, dex.Q96, false)
require.NoError(t, err)
// [-1200, -600] is entirely BELOW the price, so it costs token1 only.
a0, a1, err := e.mint(trader, stdPool, -1200, -600, oneE18, false)
require.NoError(t, err)
eqBig(t, big.NewInt(0), a0)
require.True(t, a1.Sign() > 0)
eqBig(t, big.NewInt(0), e.reserve(tokenA))
assertCustodyConsistent(t, e)
// [600, 1200] is entirely ABOVE the price, so it costs token0 only — and the
// trader has none, which is what makes the leg observable.
_, _, err = e.mint(trader, stdPool, 600, 1200, oneE18, false)
require.ErrorIs(t, err, ErrTransferFailed)
}
// TestReserveIsGlobalAcrossPools proves the reserve is keyed by ASSET, not by
// pool: it shadows the precompile's real balance of that asset, which is shared.
// Two pools over the same token must sum into one ledger entry, or the anchor
// (reserve == balanceOf) cannot hold.
func TestReserveIsGlobalAcrossPools(t *testing.T) {
e := newEnv()
e.db().fund(tokenA, trader, e18x1000)
e.db().fund(tokenB, trader, e18x1000)
e.db().fund(tokenC, trader, e18x1000)
other := poolCfg{c0: tokenA, c1: tokenC, fee: uint32(dex.Fee030), tickSpacing: int32(dex.TickSpacing030)}
_, err := e.initialize(stdPool, dex.Q96, false)
require.NoError(t, err)
_, err = e.initialize(other, dex.Q96, false)
require.NoError(t, err)
a0, _, err := e.mint(trader, stdPool, -600, 600, oneE18, false)
require.NoError(t, err)
b0, _, err := e.mint(trader, other, -600, 600, oneE18, false)
require.NoError(t, err)
eqBig(t, new(big.Int).Add(a0, b0), e.reserve(tokenA))
eqBig(t, e.db().bal(tokenA, v3Addr), e.reserve(tokenA))
}
// TestNativeBalanceCodecRoundTrip guards the uint256 boundary the native path
// crosses on every call: the balance the precompile reads must be the value the
// host holds, at the very top of the range.
func TestNativeBalanceCodecRoundTrip(t *testing.T) {
m := newMockState()
maxU256 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))
m.setNative(v3Addr, maxU256)
eqBig(t, maxU256, m.GetBalance(v3Addr).ToBig())
// And the subtract path leaves exactly the remainder.
m.SubBalance(v3Addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
eqBig(t, new(big.Int).Sub(maxU256, big.NewInt(1)), m.nativeBal(v3Addr))
}
// TestSingleSidedPositionBurnsAndCollectsItsPrincipal proves a position funded
// on ONE leg gets that leg back. A position entirely outside the current price
// costs exactly one token, so the other leg's principal is legitimately zero —
// and a credit rule that demanded BOTH legs be non-zero would silently strand
// the LP's whole deposit in the pool with no way to withdraw it.
func TestSingleSidedPositionBurnsAndCollectsItsPrincipal(t *testing.T) {
maxU := new(big.Int).Set(maxUint128)
// --- entirely BELOW the price: funded in token1 only ---
e := fundedEnv(e18x1000)
before1 := e.db().bal(tokenB, trader)
a0, a1, err := e.mint(trader, stdPool, -1200, -600, oneE18, false)
require.NoError(t, err)
eqBig(t, big.NewInt(0), a0)
require.True(t, a1.Sign() > 0)
b0, b1, err := e.burn(trader, stdPool, -1200, -600, oneE18)
require.NoError(t, err)
eqBig(t, big.NewInt(0), b0)
eqBig(t, a1, b1)
// The credit landed: owed must hold the freed principal, not zero.
_, owed0, owed1 := e.positionOf(trader, stdPool, -1200, -600)
eqBig(t, big.NewInt(0), owed0)
eqBig(t, a1, owed1)
c0, c1, err := e.collect(trader, stdPool, -1200, -600, maxU, maxU)
require.NoError(t, err)
eqBig(t, big.NewInt(0), c0)
eqBig(t, a1, c1)
eqBig(t, before1, e.db().bal(tokenB, trader))
assertCustodyConsistent(t, e)
// --- entirely ABOVE the price: funded in token0 only ---
e2 := fundedEnv(e18x1000)
before0 := e2.db().bal(tokenA, trader)
a0, a1, err = e2.mint(trader, stdPool, 600, 1200, oneE18, false)
require.NoError(t, err)
require.True(t, a0.Sign() > 0)
eqBig(t, big.NewInt(0), a1)
b0, b1, err = e2.burn(trader, stdPool, 600, 1200, oneE18)
require.NoError(t, err)
eqBig(t, a0, b0)
eqBig(t, big.NewInt(0), b1)
c0, c1, err = e2.collect(trader, stdPool, 600, 1200, maxU, maxU)
require.NoError(t, err)
eqBig(t, a0, c0)
eqBig(t, big.NewInt(0), c1)
eqBig(t, before0, e2.db().bal(tokenA, trader))
assertCustodyConsistent(t, e2)
}