precompile/dex: EVM-path 0x9010 fixes — StateDB-authoritative pool reads + modifyLiquidity idempotency

getPool/getPoolState now read the StateDB sqrtPrice slot FIRST (revert-safe authority) and drop
any in-memory cache entry StateDB contradicts — fixes a speculative-exec cache poisoning where a
reverted exec left a stale 'pool already initialized' entry that reverted re-execs (the EVM runs
a tx ~5x; only canonical exec commits). modifyLiquidity now replay-idempotent via modifyBindKey
(mirrors swapBindKey). cmd/evme2e = the V4-calldata EVM e2e driver. Proven live: real C-Chain tx
→ 0x9010 → ZAP → D-Chain → fills → native LUX settle.
This commit is contained in:
2026-06-17 19:29:27 -07:00
parent bd0acfc716
commit 30e1b14172
2 changed files with 374 additions and 5 deletions
+257
View File
@@ -0,0 +1,257 @@
// Command evme2e is the PATH-A (EVM 0x9010) e2e driver: it sends REAL signed
// C-Chain transactions to the V4 PoolManager precompile at
// 0x0000000000000000000000000000000000009010 and proves the call ROUTES through
// NewZAPEngine -> the venue D-Chain -> consensus fills -> a V4 BalanceDelta +
// native settlement.
//
// It encodes the three V4 calls with the precompile's own ABI encoders
// (dex.EncodePoolKeyABI etc. — byte-identical to what the on-chain decoder
// expects):
//
// initialize((c0,c1,fee,tickSpacing,hooks), sqrtPriceX96) sel 0x6276CBBE
// modifyLiquidity(key,(tickLower,tickUpper,liqDelta,salt),hookData) sel 0x5A6BCFDA
// swap(key,(zeroForOne,amountSpecified,sqrtPriceLimitX96),hookData) sel 0xF3CD914C
//
// It also opens a direct ZAP connection to the same venue to OBSERVE the resting
// book (clob_depth) before/after each EVM call — the routing proof: liquidity
// the EVM precompile placed/consumed shows up on the venue under the exact
// keccak256(PoolKey) poolId.
//
// Funding is a localnet genesis account derived from the well-known LightMnemonic
// dev seed (m/44'/60'/0'/0/0) — NOT an EWOQ key.
package main
import (
"context"
"encoding/binary"
"encoding/hex"
"flag"
"fmt"
"math"
"math/big"
"os"
"time"
dex "github.com/luxfi/precompile/dex"
"github.com/luxfi/crypto"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/ethclient"
"github.com/luxfi/rpc"
)
var lxpool = common.HexToAddress("0x0000000000000000000000000000000000009010")
const (
selInitialize = 0x6276CBBE
selModifyLiquidity = 0x5A6BCFDA
selSwap = 0xF3CD914C
)
func sel(s uint32) []byte { b := make([]byte, 4); binary.BigEndian.PutUint32(b, s); return b }
func i256(v *big.Int) []byte {
out := make([]byte, 32)
if v.Sign() >= 0 {
v.FillBytes(out)
return out
}
new(big.Int).Add(new(big.Int).Lsh(big.NewInt(1), 256), v).FillBytes(out)
return out
}
func u256(v *big.Int) []byte { out := make([]byte, 32); v.FillBytes(out); return out }
func boolWord(b bool) []byte {
out := make([]byte, 32)
if b {
out[31] = 1
}
return out
}
// sqrtPriceX96 = floor(sqrt(price) * 2^96).
func sqrtPriceX96(price float64) *big.Int {
q96 := new(big.Float).SetPrec(256)
q96.SetInt(new(big.Int).Lsh(big.NewInt(1), 96))
sp := new(big.Float).SetPrec(256).Sqrt(big.NewFloat(price))
sp.Mul(sp, q96)
r, _ := sp.Int(nil)
return r
}
func key(c0, c1 common.Address, fee uint32, ts int32) dex.PoolKey {
return dex.PoolKey{
Currency0: dex.Currency{Address: c0}, Currency1: dex.Currency{Address: c1},
Fee: fee, TickSpacing: ts, Hooks: common.Address{},
}
}
func encInitialize(k dex.PoolKey, sqrtP *big.Int) []byte {
return append(append(sel(selInitialize), dex.EncodePoolKeyABI(k)...), u256(sqrtP)...)
}
func encModifyLiquidity(k dex.PoolKey, tickLower, tickUpper int32, liqDelta *big.Int, salt [32]byte) []byte {
out := append(sel(selModifyLiquidity), dex.EncodePoolKeyABI(k)...)
out = append(out, i256(big.NewInt(int64(tickLower)))...)
out = append(out, i256(big.NewInt(int64(tickUpper)))...)
out = append(out, i256(liqDelta)...)
out = append(out, salt[:]...)
return out
}
func encSwap(k dex.PoolKey, zeroForOne bool, amt, sqrtLimit *big.Int) []byte {
out := append(sel(selSwap), dex.EncodePoolKeyABI(k)...)
out = append(out, boolWord(zeroForOne)...)
out = append(out, i256(amt)...)
out = append(out, u256(sqrtLimit)...)
return out
}
func main() {
rpcURL := flag.String("rpc", "http://127.0.0.1:9650/ext/bc/C/rpc", "C-Chain RPC")
venueAddr := flag.String("venue", "127.0.0.1:9099", "venue ZAP addr (for book observation)")
pkHex := flag.String("pk", "ed0c0416e953639c0ae02e313c2c73a84dd509937e64330b303342d16af7394e", "funded genesis privkey hex (LightMnemonic idx0)")
flag.Parse()
ctx := context.Background()
cl, err := ethclient.Dial(*rpcURL)
must("dial", err)
chainID, err := cl.ChainID(ctx)
must("chainID", err)
pk, err := crypto.HexToECDSA(*pkHex)
must("key", err)
from := common.BytesToAddress(crypto.PubkeyToAddress(pk.PublicKey).Bytes())
fmt.Printf("PATH-A driver rpc=%s chainID=%s from=%s\n", *rpcURL, chainID, from.Hex())
// Venue observation channel (read-only clob_depth on the same poolId).
vctx, vcancel := context.WithTimeout(ctx, 90*time.Second)
defer vcancel()
vconn, verr := rpc.ZAPDial(vctx, *venueAddr)
if verr != nil {
fmt.Printf("WARN venue dial (%s): %v — book observation disabled\n", *venueAddr, verr)
}
c0 := common.Address{} // native LUX
c1 := common.HexToAddress("0x0000000000000000000000000000000000000042") // currency1
const fee = uint32(3000)
const ts = int32(60)
k := key(c0, c1, fee, ts)
poolID := k.ID()
fmt.Printf("pool c0=%s c1=%s fee=%d tickSpacing=%d\npoolID(keccak256(PoolKey))=0x%s\n",
c0.Hex(), c1.Hex(), fee, ts, hex.EncodeToString(poolID[:]))
depth := func(tag string) {
if vconn == nil {
return
}
r, e := vconn.Call(vctx, "clob_depth", poolID[:])
if e != nil || len(r) < 29 {
fmt.Printf(" VENUE %-12s depth err: %v\n", tag, e)
return
}
fmt.Printf(" VENUE %-12s orders=%d remaining=%.3f bestBid=%.4f bestAsk=%.4f found=%v\n",
tag, binary.BigEndian.Uint32(r[0:4]), math.Float64frombits(binary.BigEndian.Uint64(r[4:12])),
math.Float64frombits(binary.BigEndian.Uint64(r[12:20])), math.Float64frombits(binary.BigEndian.Uint64(r[20:28])), r[28] == 1)
}
send := func(label string, data []byte) *types.Receipt {
nonce, e := cl.PendingNonceAt(ctx, from)
must(label+" nonce", e)
gp, e := cl.SuggestGasPrice(ctx)
must(label+" gasprice", e)
tx := types.NewTx(&types.LegacyTx{Nonce: nonce, GasPrice: gp, Gas: 5_000_000, To: &lxpool, Value: big.NewInt(0), Data: data})
signed, e := types.SignTx(tx, types.LatestSignerForChainID(chainID), pk)
must(label+" sign", e)
if e := cl.SendTransaction(ctx, signed); e != nil {
fmt.Printf(" %-16s SEND ERROR: %v\n", label, e)
return nil
}
h := signed.Hash()
r := waitReceipt(ctx, cl, h)
if r == nil {
fmt.Printf(" %-16s tx=%s NO RECEIPT\n", label, h.Hex())
return nil
}
st := "SUCCESS"
if r.Status == 0 {
st = "REVERT"
}
fmt.Printf(" %-16s tx=%s status=%d(%s) block=%d gasUsed=%d\n", label, h.Hex(), r.Status, st, r.BlockNumber.Uint64(), r.GasUsed)
return r
}
bal := func(tag string) {
b, _ := cl.BalanceAt(ctx, from, nil)
pm, _ := cl.BalanceAt(ctx, lxpool, nil)
fmt.Printf(" BAL %-12s from=%s poolMgr=%s\n", tag, b.String(), pm.String())
}
// ---- STEP 1: initialize price=100 -> clob_ensure_market on venue ----
fmt.Println("\n--- STEP 1: initialize(price=100) -> clob_ensure_market ---")
depth("pre-init")
send("initialize", encInitialize(k, sqrtPriceX96(100)))
depth("post-init")
// ---- STEP 2: modifyLiquidity (+) -> clob_place a resting ASK ----
// init price=100 (tick 46054). Place an ASK just above: tick 46554 (~price 105).
// An ask funds base=currency0=native LUX (clean settlement).
fmt.Println("\n--- STEP 2: modifyLiquidity(+10 @ ask~105) -> clob_place ---")
bal("pre-place")
var salt [32]byte
salt[31] = 1
send("modifyLiq-ask", encModifyLiquidity(k, 46500, 46560, big.NewInt(10), salt))
bal("post-place")
depth("post-place")
// ---- STEP 3: swap marketable SELL crossing... need a BID to cross a sell ----
// Place a resting BID just below init: tick 45600 (~price 96), funds quote=c1.
// (LP needs c1 for the bid; if c1 settlement is unfunded this place may revert —
// reported honestly. The ASK above is the native-clean leg.)
fmt.Println("\n--- STEP 3: modifyLiquidity(+8 @ bid~96) -> clob_place ---")
var salt2 [32]byte
salt2[31] = 2
send("modifyLiq-bid", encModifyLiquidity(k, 45540, 45600, big.NewInt(8), salt2))
depth("post-place-bid")
// ---- STEP 4: swap marketable BUY (cross the ASK) -> clob_submit -> fills ----
// !zeroForOne buy: taker RECEIVES base=native (credit, clean), OWES quote=c1.
fmt.Println("\n--- STEP 4: swap BUY 5 (cross ask) -> clob_submit -> fills+BalanceDelta ---")
bal("pre-swap-buy")
depth("pre-swap-buy")
maxSqrt := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1))
send("swap-buy", encSwap(k, false, big.NewInt(-5), maxSqrt))
bal("post-swap-buy")
depth("post-swap-buy")
// ---- STEP 5: swap marketable SELL (cross the BID) -> owed leg = native ----
// zeroForOne sell: taker OWES base=native (clean SubBalance), RECEIVES quote=c1.
fmt.Println("\n--- STEP 5: swap SELL 4 (cross bid) -> clob_submit -> fills (native owed) ---")
bal("pre-swap-sell")
depth("pre-swap-sell")
minSqrt := big.NewInt(1)
send("swap-sell", encSwap(k, true, big.NewInt(-4), minSqrt))
bal("post-swap-sell")
depth("post-swap-sell")
fmt.Println("\nPATH-A complete. VENUE depth deltas under poolID = routing proof;")
fmt.Println("BAL deltas (native currency0) = on-chain settlement proof.")
if vconn != nil {
_ = vconn.Close()
}
}
func waitReceipt(ctx context.Context, cl *ethclient.Client, h common.Hash) *types.Receipt {
dl := time.Now().Add(20 * time.Second)
for time.Now().Before(dl) {
r, err := cl.TransactionReceipt(ctx, h)
if err == nil && r != nil {
return r
}
time.Sleep(250 * time.Millisecond)
}
return nil
}
func must(stage string, err error) {
if err != nil {
fmt.Printf("FATAL %s: %v\n", stage, err)
os.Exit(1)
}
}
+117 -5
View File
@@ -43,6 +43,7 @@ var (
pauseStatePrefix = []byte("paus")
freezeStatePrefix = []byte("frzn")
swapBindPrefix = []byte("swpb")
modBindPrefix = []byte("modb")
)
// PoolState extends the basic Pool with V4 tick-level state for concentrated
@@ -219,6 +220,78 @@ func slotToSigned(h common.Hash) *big.Int {
return v
}
// modifyBindKey is the ModifyLiquidity analog of swapBindKey (RED H1, same
// reasoning): a place/cancel is an IRREVERSIBLE clob_place/clob_cancel on the
// d-chain book, so the EVM's repeated executions of ONE modifyLiquidity tx
// (gas-estimate, mempool validate, block build, block verify) must issue the
// venue op EXACTLY ONCE. Without this, exec #1 rests the order on the venue and
// every re-exec gets "order rejected" off the now-occupied book, reverting the
// tx while the venue keeps the order — the C↔D split the swap path already
// closes. The key is the consensus-deterministic tuple (txHash, poolId,
// modifyParams): same on every validator, durable across restart, revert-safe
// (StateDB snapshot). ok=false for a non-EVM caller (no tx identity) — that
// caller skips the guard rather than colliding on the zero slot.
func modifyBindKey(stateDB StateDB, poolId [32]byte, params ModifyLiquidityParams) (common.Hash, bool) {
idr, ok := stateDB.(txIdentified)
if !ok {
return common.Hash{}, false
}
txHash := idr.TxHash()
if txHash == (common.Hash{}) {
return common.Hash{}, false
}
h := blake3.New()
h.Write(txHash[:])
h.Write(poolId[:])
h.Write([]byte(modifyParamsDigest(params)))
var id [32]byte
h.Digest().Read(id[:])
return makeStorageKey(modBindPrefix, id[:]), true
}
// modifyParamsDigest serializes the position parameters that distinguish two
// modifyLiquidity calls issued by the SAME tx against the SAME pool (tick range,
// signed liquidity delta, salt) so each gets its own durable slot.
func modifyParamsDigest(p ModifyLiquidityParams) string {
var b [4 + 4 + 1 + 32 + 32]byte
binary.BigEndian.PutUint32(b[0:4], uint32(p.TickLower))
binary.BigEndian.PutUint32(b[4:8], uint32(p.TickUpper))
if p.LiquidityDelta != nil && p.LiquidityDelta.Sign() < 0 {
b[8] = 1
}
if p.LiquidityDelta != nil {
new(big.Int).Abs(p.LiquidityDelta).FillBytes(b[9:41])
}
copy(b[41:73], p.Salt[:])
return string(b[:])
}
// loadModifyBinding / storeModifyBinding mirror loadSwapBinding / storeSwapBinding
// (same three-slot flag+amount0+amount1 layout, reusing deriveSlot's suffix
// scheme under modBindPrefix). A bound modifyLiquidity returns its recorded
// callerDelta verbatim on re-execution WITHOUT a second clob_place/clob_cancel.
func loadModifyBinding(stateDB StateDB, bindKey common.Hash) (delta BalanceDelta, settled bool) {
flag := stateDB.GetState(poolManagerAddr, deriveModSlot(bindKey, swapBindFlagSuffix))
if flag[31] != 1 {
return ZeroBalanceDelta(), false
}
a0 := stateDB.GetState(poolManagerAddr, deriveModSlot(bindKey, swapBindAmt0Suffix))
a1 := stateDB.GetState(poolManagerAddr, deriveModSlot(bindKey, swapBindAmt1Suffix))
return NewBalanceDelta(slotToSigned(a0), slotToSigned(a1)), true
}
func storeModifyBinding(stateDB StateDB, bindKey common.Hash, delta BalanceDelta) {
stateDB.SetState(poolManagerAddr, deriveModSlot(bindKey, swapBindAmt0Suffix), signedToSlot(delta.Amount0))
stateDB.SetState(poolManagerAddr, deriveModSlot(bindKey, swapBindAmt1Suffix), signedToSlot(delta.Amount1))
var flag common.Hash
flag[31] = 1
stateDB.SetState(poolManagerAddr, deriveModSlot(bindKey, swapBindFlagSuffix), flag)
}
func deriveModSlot(base common.Hash, suffix []byte) common.Hash {
return makeStorageKey(modBindPrefix, append(base[:], suffix...))
}
// getOrCreateTick returns the TickInfo for a tick, creating it if absent.
func (ps *PoolState) getOrCreateTick(tick int32) *TickInfo {
ti, ok := ps.Ticks[tick]
@@ -275,8 +348,18 @@ func NewPoolManager(engine ...Engine) *PoolManager {
// getPoolState returns the extended V4 pool state, creating one from the
// base pool if it doesn't exist yet.
func (pm *PoolManager) getPoolState(stateDB StateDB, poolId [32]byte, tickSpacing int32, lpFee uint32) *PoolState {
// Same revert-safety rule as getPool: the poolStates cache survives the EVM's
// repeated executions of one tx, so a cached PoolState from a speculative
// (later reverted) Initialize must NOT mask the StateDB truth. Gate the cache
// on the pool's StateDB existence; a cached state for a pool StateDB no longer
// has is stale and is rebuilt from the (StateDB-authoritative) getPool.
sqrtPriceKey := makeStorageKey(poolStatePrefix, append(poolId[:], []byte("sqrtPrice")...))
committed := stateDB.GetState(poolManagerAddr, sqrtPriceKey) != (common.Hash{})
if ps, ok := pm.poolStates[poolId]; ok {
return ps
if committed {
return ps
}
delete(pm.poolStates, poolId)
}
pool := pm.getPool(stateDB, poolId)
ps := NewPoolState(pool, tickSpacing, lpFee)
@@ -678,11 +761,25 @@ func (pm *PoolManager) ModifyLiquidity(
}
}
// Replay-idempotency (RED H1), mirroring Swap: a place/cancel is an
// irreversible venue op, so the EVM's repeated executions of one tx must issue
// it exactly once. A bound modify returns its recorded delta without a second
// clob_place/clob_cancel.
bindKey, bound := modifyBindKey(stateDB, poolId, params)
if bound {
if prior, settled := loadModifyBinding(stateDB, bindKey); settled {
return prior, ZeroBalanceDelta(), nil
}
}
pm.routePool(poolId, ps)
callerDelta, feesAccrued, err := pm.engine.ModifyLiquidity(ps, caller, params)
if err != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), err
}
if bound {
storeModifyBinding(stateDB, bindKey, callerDelta)
}
posKey := PositionKey(caller, params.TickLower, params.TickUpper, params.Salt)
if pos, ok := ps.Positions[posKey]; ok {
@@ -802,14 +899,29 @@ func (pm *PoolManager) Flash(
// =========================================================================
func (pm *PoolManager) getPool(stateDB StateDB, poolId [32]byte) *Pool {
// StateDB is the revert-safe authority for pool EXISTENCE: a pool's sqrtPrice
// slot is zero unless an Initialize committed it, and it reverts to zero with
// any tx that rolls back. The process-memory cache (pm.pools) does NOT roll
// back, so trusting it first leaks state across the EVM's multiple
// (estimate/build/verify) executions of one tx — a speculative Initialize
// would populate the cache, then every re-execution sees "already
// initialized" off the stale cache and reverts. So consult StateDB FIRST;
// only serve the cached *Pool when StateDB confirms the pool still exists
// (matches the StateDB-authoritative discipline already used for
// pause/freeze — see isDEXPaused, RED V6). A cache entry contradicted by
// StateDB is stale and dropped.
sqrtPriceKey := makeStorageKey(poolStatePrefix, append(poolId[:], []byte("sqrtPrice")...))
sqrtPriceHash := stateDB.GetState(poolManagerAddr, sqrtPriceKey)
committed := sqrtPriceHash != (common.Hash{})
if pool, ok := pm.pools[poolId]; ok {
return pool
if committed {
return pool
}
delete(pm.pools, poolId)
}
pool := NewPool()
sqrtPriceKey := makeStorageKey(poolStatePrefix, append(poolId[:], []byte("sqrtPrice")...))
sqrtPriceHash := stateDB.GetState(poolManagerAddr, sqrtPriceKey)
if sqrtPriceHash != (common.Hash{}) {
if committed {
pool.SqrtPriceX96 = new(big.Int).SetBytes(sqrtPriceHash[:])
}