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.
482 lines
23 KiB
Go
482 lines
23 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dex
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/precompile/contract"
|
|
"github.com/luxfi/precompile/precompileconfig"
|
|
"github.com/luxfi/vm/chains/atomic"
|
|
)
|
|
|
|
// native_harness_test.go is the SHARED test harness for the native C<->D atomic
|
|
// 0x9999 money path. It replaces the BLS-era settleHarness (quarantined under
|
|
// _deprecated_bls): no validator set, no cert — instead a REAL in-memory atomic
|
|
// shared memory (atomic.NewMemory over memdb) wired into an AtomicState-capable
|
|
// accessible state, so SettleSwap and the dexvm import/export operate over the
|
|
// genuine bidirectional shared-memory channel (not a fake).
|
|
//
|
|
// The harness exposes the C-Chain's SharedMemory to the precompile (via
|
|
// nativeAtomicState) and the D-Chain's SharedMemory to the dexvm-side test driver,
|
|
// so a C->D Put is readable by D via Get(cChainID) and a D->C Put is readable by C
|
|
// via Get(dChainID) — the real platformvm import/export semantics.
|
|
|
|
// testGovernance is the harness's per-network DEX governance controller — the address
|
|
// the mock AtomicState returns from GovernanceController(), i.e. the ONLY caller that may
|
|
// halt 0x9999 settlement or seed its pots. In production this is a governance CONTRACT
|
|
// the host wires per network; here it is a fixed test address that stands in for that
|
|
// contract. It is deliberately a non-EOA-looking sentinel distinct from the harness's
|
|
// trading caller (0x1111…) and from the retired mnemonic-derivable 0x9011… EOA, so the
|
|
// governance tests can prove only this address halts and the old EOA cannot.
|
|
var testGovernance = common.HexToAddress("0x60F0000000000000000000000000000000000A11") // "GOV ...0A11"
|
|
|
|
// nativeAtomicState implements BOTH contract.AccessibleState and
|
|
// contract.AtomicState so SettleSwap's `state.(contract.AtomicState)` assertion
|
|
// succeeds and reaches a real SharedMemory + chain identity.
|
|
type nativeAtomicState struct {
|
|
stateDB *MockStateDB
|
|
sm atomic.SharedMemory
|
|
networkID uint32
|
|
chainID ids.ID // THIS chain (C)
|
|
cChainID ids.ID
|
|
dChainID ids.ID // the D-Chain (dexvm) peer the host resolves at runtime
|
|
governance common.Address // the per-network DEX governance controller (halt/seed authority)
|
|
txID ids.ID
|
|
callIndex uint32
|
|
blockTimestamp uint64 // block time the precompile reads via GetBlockContext().Timestamp()
|
|
}
|
|
|
|
// --- AccessibleState ---
|
|
func (m *nativeAtomicState) GetStateDB() contract.StateDB {
|
|
return &contractStateDBWrapper{inner: m.stateDB}
|
|
}
|
|
func (m *nativeAtomicState) GetBlockContext() contract.BlockContext {
|
|
return &mockBlockCtx{number: big.NewInt(int64(m.stateDB.blockNumber)), timestamp: m.blockTimestamp}
|
|
}
|
|
func (m *nativeAtomicState) GetConsensusContext() context.Context { return context.Background() }
|
|
func (m *nativeAtomicState) GetChainConfig() precompileconfig.ChainConfig { return nil }
|
|
func (m *nativeAtomicState) GetPrecompileEnv() contract.PrecompileEnvironment { return nil }
|
|
|
|
// --- AtomicState ---
|
|
func (m *nativeAtomicState) AtomicMemory() atomic.SharedMemory { return m.sm }
|
|
func (m *nativeAtomicState) NetworkID() uint32 { return m.networkID }
|
|
func (m *nativeAtomicState) ChainID() ids.ID { return m.chainID }
|
|
func (m *nativeAtomicState) CChainID() ids.ID { return m.cChainID }
|
|
func (m *nativeAtomicState) GovernanceController() common.Address { return m.governance }
|
|
func (m *nativeAtomicState) DChainID() ids.ID { return m.dChainID }
|
|
func (m *nativeAtomicState) TxID() ids.ID { return m.txID }
|
|
func (m *nativeAtomicState) CallIndex() uint32 { return m.callIndex }
|
|
|
|
var _ contract.AccessibleState = (*nativeAtomicState)(nil)
|
|
var _ contract.AtomicState = (*nativeAtomicState)(nil)
|
|
|
|
// settleHarness wires a 0x9999 contract over a real atomic shared-memory channel.
|
|
// cSM is the C-Chain side (used by the precompile); dSM is the D-Chain side (used
|
|
// by the dexvm-side test driver in the round-trip test).
|
|
type settleHarness struct {
|
|
c *SettleContract
|
|
state *nativeAtomicState
|
|
mem *atomic.Memory
|
|
cSM atomic.SharedMemory
|
|
dSM atomic.SharedMemory
|
|
dChainID ids.ID
|
|
cChainID ids.ID
|
|
networkID uint32
|
|
key PoolKey
|
|
params SwapParams
|
|
caller common.Address
|
|
lastFlushed uint64 // harness's parent-boundary seq for the flush window
|
|
memdbBacking *memdb.Database // shared-memory backing db (for batch tests)
|
|
}
|
|
|
|
// harnessBlockTime is an arbitrary post-genesis block timestamp for harness state.
|
|
// 0x9999 and its logs are AlwaysOn (active from genesis), so the exact value carries no
|
|
// protocol meaning — any block time exercises the live settlement path.
|
|
const harnessBlockTime uint64 = 1_700_000_000 // 2023-11-14T22:13:20Z, a normal block time
|
|
|
|
func newSettleHarness(t testing.TB) *settleHarness {
|
|
return newSettleHarnessN(t, 3)
|
|
}
|
|
|
|
// newSettleHarnessN keeps the n parameter for source-compat with call sites; the
|
|
// native model has no validator count, so n is ignored (kept so existing surface
|
|
// tests that pass a count still compile).
|
|
func newSettleHarnessN(t testing.TB, _ int) *settleHarness {
|
|
t.Helper()
|
|
cChainID := ids.ID{0xCC}
|
|
dChainID := ids.ID{0xDD}
|
|
backing := memdb.New()
|
|
mem := atomic.NewMemory(backing)
|
|
cSM := mem.NewSharedMemory(cChainID)
|
|
dSM := mem.NewSharedMemory(dChainID)
|
|
|
|
state := &nativeAtomicState{
|
|
stateDB: NewMockStateDB(),
|
|
sm: cSM,
|
|
networkID: 1,
|
|
chainID: cChainID,
|
|
cChainID: cChainID,
|
|
dChainID: dChainID, // host-resolved D peer (runtime), as the EVM adapter supplies it
|
|
governance: testGovernance, // the per-network governance controller the host supplies via AtomicState
|
|
txID: ids.ID{0x7A}, // a fixed tx id for the harness; tests vary callIndex/txID
|
|
callIndex: 0,
|
|
// 0x9999 and its DEXFill + Initialize logs are AlwaysOn (active from genesis,
|
|
// no dated fork), so the harness block time carries no protocol meaning — any
|
|
// value exercises the live path. A test that wants genesis sets blockTimestamp=0.
|
|
blockTimestamp: harnessBlockTime,
|
|
}
|
|
h := &settleHarness{
|
|
c: &SettleContract{},
|
|
state: state,
|
|
mem: mem,
|
|
cSM: cSM,
|
|
dSM: dSM,
|
|
dChainID: dChainID,
|
|
cChainID: cChainID,
|
|
networkID: 1,
|
|
caller: common.HexToAddress("0x1111111111111111111111111111111111111111"),
|
|
memdbBacking: backing,
|
|
}
|
|
// V4 pool: currency0 = native (0x0), currency1 = token 0x..02. zeroForOne = swap
|
|
// native in, token out. AmountSpecified < 0 (exact-input) for the native order.
|
|
h.key = PoolKey{
|
|
Currency0: Currency{Address: common.Address{}},
|
|
Currency1: Currency{Address: common.HexToAddress("0x0000000000000000000000000000000000000002")},
|
|
Fee: 3000,
|
|
TickSpacing: 60,
|
|
Hooks: common.Address{},
|
|
}
|
|
// The V4 SwapParams tuple is unchanged, so the harness fills SqrtPriceLimitX96 with
|
|
// what a router actually sends. The funding rail never reads it: a crossing moves
|
|
// value, and the price the caller eventually trades at is settled on D.
|
|
h.params = SwapParams{ZeroForOne: true, AmountSpecified: big.NewInt(-100), SqrtPriceLimitX96: sqrtX96For(2.0)}
|
|
|
|
// The native-seam chain identity (networkID, cChainID) and the D-Chain peer are
|
|
// supplied at RUNTIME by the host via the AtomicState capability (nativeAtomicState
|
|
// above) — there is NO per-net config to seed. This mirrors production exactly:
|
|
// 0x9999 is always-on and resolves its chain topology from the consensus context.
|
|
|
|
// C1 admission (the value path's real-asset gate, repointed onto 0x9999): every
|
|
// market open / swap / settle now resolves both sides through the AssetResolver, so
|
|
// the SHARED harness installs a default resolver admitting ITS OWN market assets — the
|
|
// native coin (currency0) and currency1 (0x..02), with live code at the token address —
|
|
// exactly as a real chain's registry is populated. Without this, registerMarket
|
|
// (SelectorInitialize) and every settle path fail closed ("no asset resolver
|
|
// configured"), and tests would depend on a process-global resolver leaking from
|
|
// another test (an order-dependent, non-hermetic green). A test that needs a DIFFERENT
|
|
// admission policy (the C1 redteam: a restrictive or absent resolver) overrides the
|
|
// global via InstallAssetResolver / Store; t.Cleanup restores the prior state LIFO so
|
|
// nothing leaks. This is the ONE place the harness's market is admitted.
|
|
h.installDefaultMarketResolver(t)
|
|
return h
|
|
}
|
|
|
|
// installDefaultMarketResolver installs the PERMISSIONLESS resolver and gives the shared
|
|
// harness market's two sides live on-chain code (native needs none; each ERC-20 token gets
|
|
// code) and restores the prior global on cleanup. Under permissionless admission, code presence
|
|
// — not membership — is what admits an asset, so seeding code is the harness analog of two real
|
|
// deployed tokens. The admission gate then passes for the legitimate harness market in EVERY
|
|
// test by default. Tests that probe admission failure install their own resolver and leave the
|
|
// probed asset WITHOUT code (so the on-chain proof refuses it).
|
|
func (h *settleHarness) installDefaultMarketResolver(t testing.TB) {
|
|
t.Helper()
|
|
r := newTestAssetResolver(h.networkID, h.cChainID).boundToHarness(h)
|
|
if h.key.Currency1.Address != (common.Address{}) {
|
|
r.admitERC20(t, h.key.Currency1.Address, 18) // seeds live on-chain code
|
|
}
|
|
if h.key.Currency0.Address != (common.Address{}) {
|
|
r.admitERC20(t, h.key.Currency0.Address, 18) // seeds live on-chain code
|
|
}
|
|
prev := installedAssetResolver.Load()
|
|
t.Cleanup(func() { installedAssetResolver.Store(prev) })
|
|
// Clear first. InstallAssetResolver REFUSES a re-install bound to a different
|
|
// identity, so installing on top of whatever is already there makes this harness —
|
|
// and therefore most of the suite — depend on what ran before it. Restoring `prev`
|
|
// on cleanup is not enough on its own: it fixes the exit, not the entry. Clearing
|
|
// on entry is what makes the harness hermetic in both directions.
|
|
installedAssetResolver.Store(nil)
|
|
if err := InstallAssetResolver(r, h.networkID, h.cChainID); err != nil {
|
|
t.Fatalf("installDefaultMarketResolver: %v", err)
|
|
}
|
|
}
|
|
|
|
// outToken is the harness pool's currency1 (the swap output for ZeroForOne).
|
|
func (h *settleHarness) outToken() common.Address { return h.key.Currency1.Address }
|
|
|
|
// outAssetID is the injective AssetID of the output token.
|
|
func (h *settleHarness) outAssetID() [32]byte { return assetID(h.key.Currency1) }
|
|
|
|
// inAssetID is the injective AssetID of the input (native) currency.
|
|
func (h *settleHarness) inAssetID() [32]byte { return assetID(h.key.Currency0) }
|
|
|
|
// wrapper returns the harness's contract.StateDB as the erc20Vault-capable wrapper.
|
|
func (h *settleHarness) wrapper() *contractStateDBWrapper {
|
|
return h.state.GetStateDB().(*contractStateDBWrapper)
|
|
}
|
|
|
|
// fundCallerNative seeds the caller's native balance so it can fund an order.
|
|
func (h *settleHarness) fundCallerNative(amount int64) {
|
|
h.state.stateDB.AddBalance(h.caller, uint256.NewInt(uint64(amount)))
|
|
}
|
|
|
|
// operator is the harness's DEX governance controller — the authority the mock
|
|
// AtomicState returns from GovernanceController(), the SOLE caller the governance-gated
|
|
// seed/fee/halt selectors accept. It is the per-network governance address the host
|
|
// supplies at runtime (testGovernance here), NOT a field on the SettleContract and NOT a
|
|
// hardcoded mnemonic-derivable EOA.
|
|
func (h *settleHarness) operator() common.Address { return h.state.governance }
|
|
|
|
// fundVaultOut seeds the 0x9999 vault's OUTPUT token into the SEAM RESERVE through the
|
|
// REAL operator-gated seedSeamReserve selector (FIX-4 — the production counterparty
|
|
// seed, not a test-only state poke), so a D->C settlement credit is backed (no mint).
|
|
func (h *settleHarness) fundVaultOut(amount int64) {
|
|
// The operator holds the token; the mock vault transferFrom debits the operator's
|
|
// balance into 0x9999 (no allowance model in the mock — honest balance check only).
|
|
h.wrapper().mintTestToken(h.outToken(), h.operator(), big.NewInt(amount))
|
|
data := make([]byte, 64)
|
|
copy(data[12:32], h.outToken().Bytes())
|
|
big.NewInt(amount).FillBytes(data[32:64])
|
|
if _, _, err := h.c.Run(h.state, h.operator(), poolManagerAddr9999,
|
|
prependSelector(SelectorSeedSeamReserve, data), 5_000_000, false); err != nil {
|
|
panic("fundVaultOut seed: " + err.Error())
|
|
}
|
|
}
|
|
|
|
// fundVaultNativeOut seeds the 0x9999 vault's NATIVE holdings into the SEAM RESERVE
|
|
// through the REAL seedSeamReserve selector. The host call frame moves msg.value into
|
|
// 0x9999 before Run (observed-delta), so we add the operator-delivered native first.
|
|
func (h *settleHarness) fundVaultNativeOut(amount int64) {
|
|
h.state.stateDB.AddBalance(poolManagerAddr9999, uint256.NewInt(uint64(amount))) // host frame moved msg.value
|
|
data := make([]byte, 64) // asset = address(0) (native), amount
|
|
big.NewInt(amount).FillBytes(data[32:64])
|
|
if _, _, err := h.c.Run(h.state, h.operator(), poolManagerAddr9999,
|
|
prependSelector(SelectorSeedSeamReserve, data), 5_000_000, false); err != nil {
|
|
panic("fundVaultNativeOut seed: " + err.Error())
|
|
}
|
|
}
|
|
|
|
// commitNativePosition drives a native LP COMMIT through the 0x9999 modifyLiquidity
|
|
// selector: it funds the caller, runs the ADD, flushes the staged C->D object, and
|
|
// returns the position record id (MakerOrderID) and the C->D commit object id
|
|
// (DerivePositionCommitID, read off the record). The commit moves `amount` from the
|
|
// caller's CSpendable balance into custody (DCommitted).
|
|
func (h *settleHarness) commitNativePosition(t testing.TB, tickLower, tickUpper int24, amount int64, salt [32]byte) (recordID, commitObjID ids.ID) {
|
|
t.Helper()
|
|
h.fundCallerNative(amount)
|
|
hookData := []byte{makerEnvelopeTag[0], makerEnvelopeTag[1], makerEnvelopeTag[2], makerEnvelopeTag[3], byte(MakerSideBid)}
|
|
args := buildModifyLiquidityArgs(h.key, tickLower, tickUpper, big.NewInt(amount), salt, hookData)
|
|
out, _, err := h.c.Run(h.state, h.caller, poolManagerAddr9999, prependSelector(SelectorModifyLiquidity, args), 5_000_000, false)
|
|
if err != nil {
|
|
t.Fatalf("commitNativePosition ADD: %v", err)
|
|
}
|
|
var rid [32]byte
|
|
copy(rid[:], out[0:32])
|
|
h.flushStaged(t)
|
|
db := newPoolStateAdapter(h.state)
|
|
commit := db.GetState(poolManagerAddr9999, orderSlot(rid, orderCommitObjSuffix))
|
|
return ids.ID(rid), ids.ID(commit)
|
|
}
|
|
|
|
// collectNative drives a native LP COLLECT/WITHDRAW through the 0x9999
|
|
// collectPosition selector: it consumes the D->C claim at outputID for
|
|
// `amount` of native against the named position record, crediting the caller out of
|
|
// custody. Returns the call error.
|
|
//
|
|
// The calldata now CARRIES the object (settle_import.go): there is no separate amount
|
|
// word, so `amount` no longer rides the wire — the credited value IS the object's
|
|
// amount. The bytes come from recordedObject, exactly as a keeper reads them off D.
|
|
func (h *settleHarness) collectNative(outputID ids.ID, amount uint64, positionID [32]byte) ([]byte, error) {
|
|
_ = amount // the amount is inside the object; the wire no longer restates it.
|
|
return h.collectCarrying(outputID, positionID, h.recordedObject(outputID))
|
|
}
|
|
|
|
// collectCarrying drives the SAME collectPosition selector with EXPLICIT object bytes.
|
|
// Carrying the object is the keeper's real degree of freedom now that execution never
|
|
// reads shared memory (settle_import.go), so it is also the attacker's: the redteam
|
|
// tests use this to supply bytes shared memory does NOT hold and then prove the
|
|
// resulting declaration cannot authenticate.
|
|
func (h *settleHarness) collectCarrying(outputID ids.ID, positionID [32]byte, object []byte) ([]byte, error) {
|
|
input := EncodeCollectPositionInput(outputID, [32]byte{}, positionID, object)
|
|
out, _, err := h.c.Run(h.state, h.caller, poolManagerAddr9999,
|
|
prependSelector(SelectorCollectPosition, input), 5_000_000, false)
|
|
return out, err
|
|
}
|
|
|
|
// flushStaged simulates the HOST's block-accept cross-domain commit using the
|
|
// DETERMINISTIC parent->current seq window (FlushAcceptedAtomicOps), the same model
|
|
// the production host uses. It tracks the harness's last-flushed seq as the "parent"
|
|
// boundary and the current state seq as the "accepted" boundary, applies that
|
|
// window to the C-Chain shared memory, and advances the boundary. No state mutation
|
|
// (the records are append-only consensus data) — exactly the production semantics.
|
|
func (h *settleHarness) flushStaged(t testing.TB) {
|
|
t.Helper()
|
|
// The staged ops live in the precompile's StateDB (the MockStateDB), which is the
|
|
// AtomicStateReader for the flush.
|
|
if err := FlushAcceptedAtomicOps(h.parentSeqState(), h.state.stateDB, h.cSM, nil); err != nil {
|
|
t.Fatalf("flushStaged: %v", err)
|
|
}
|
|
h.lastFlushed = ReadStagedAtomicSeq(h.state.stateDB)
|
|
}
|
|
|
|
// parentSeqState returns a read view whose staged seq is the harness's last-flushed
|
|
// boundary, so FlushAcceptedAtomicOps derives the window (lastFlushed, current].
|
|
func (h *settleHarness) parentSeqState() AtomicStateReader {
|
|
return &seqOnlyState{seq: h.lastFlushed}
|
|
}
|
|
|
|
// seqOnlyState is a minimal AtomicStateReader that reports a fixed staged seq (the
|
|
// parent boundary) and zero for every other slot — enough for ReadStagedAtomicSeq.
|
|
type seqOnlyState struct{ seq uint64 }
|
|
|
|
func (s *seqOnlyState) GetState(addr common.Address, key common.Hash) common.Hash {
|
|
if key == stageSeqKey {
|
|
var w common.Hash
|
|
putU64(w[24:32], s.seq)
|
|
return w
|
|
}
|
|
return common.Hash{}
|
|
}
|
|
|
|
// putDtoCObject simulates D's executeExport: it PUTs a funding claim into shared
|
|
// memory keyed by the C chain, so the C-side Import reads it via Get(dChainID). The
|
|
// recorded (beneficiary, asset, amount) is the value the C-side consume path binds.
|
|
func (h *settleHarness) putDtoCObject(t testing.TB, beneficiary common.Address, claimID ids.ID, asset [32]byte, amount uint64) {
|
|
t.Helper()
|
|
obj := encodeClaim(beneficiary, asset, amount)
|
|
reqs := map[ids.ID]*atomic.Requests{
|
|
h.cChainID: {PutRequests: []*atomic.Element{{
|
|
Key: claimID[:],
|
|
Value: obj,
|
|
Traits: [][]byte{beneficiary[:]},
|
|
}}},
|
|
}
|
|
// The D side applies to the C chain's partition (D is the source).
|
|
if err := h.dSM.Apply(reqs); err != nil {
|
|
t.Fatalf("putDtoCObjectRailSpent: %v", err)
|
|
}
|
|
}
|
|
|
|
// recordedObject reads back the D->C object BYTES a test PUT into shared memory for
|
|
// outputID — the same read a keeper performs off D when it builds the settle/collect
|
|
// transaction, whose calldata now CARRIES those bytes (settle_import.go). Empty when
|
|
// nothing was recorded at that key.
|
|
func (h *settleHarness) recordedObject(outputID ids.ID) []byte {
|
|
vals, err := h.cSM.Get(h.dChainID, [][]byte{outputID[:]})
|
|
if err != nil || len(vals) != 1 {
|
|
return nil
|
|
}
|
|
return vals[0]
|
|
}
|
|
|
|
// readCtoDObject simulates the dexvm's executeImport READ: it Gets the C->D object
|
|
// the precompile PUT (keyed by the D chain), readable by D via Get(cChainID).
|
|
func (h *settleHarness) readCtoDObject(t testing.TB, orderID ids.ID) ([]byte, bool) {
|
|
t.Helper()
|
|
vals, err := h.dSM.Get(h.cChainID, [][]byte{orderID[:]})
|
|
if err != nil || len(vals) != 1 || len(vals[0]) == 0 {
|
|
return nil, false
|
|
}
|
|
return vals[0], true
|
|
}
|
|
|
|
// nextTx advances the harness to a NEW transaction. A claim id is derived from
|
|
// (source, dest, sourceTx, index), so two crossings are distinct because the chain
|
|
// gives them distinct transactions — which is what a real host does and what this
|
|
// stands in for. Without it a test issuing two crossings is really issuing the same
|
|
// one twice, and the replay guard says so.
|
|
func (h *settleHarness) nextTx() {
|
|
var t ids.ID
|
|
copy(t[:], h.state.txID[:])
|
|
for i := len(t) - 1; i >= 0; i-- {
|
|
t[i]++
|
|
if t[i] != 0 {
|
|
break
|
|
}
|
|
}
|
|
h.state.txID = t
|
|
}
|
|
|
|
// crossCalldata builds a Phase-A swap calldata: the crossing that funds D. The
|
|
// hookData is empty because there is nothing left to say — a crossing moves value
|
|
// and creates no order, so it carries no deadline, no nonce and no operation.
|
|
func (h *settleHarness) crossCalldata() []byte {
|
|
return buildSwapCalldata(h.key, h.params, nil)
|
|
}
|
|
|
|
// settlementCalldata builds a Phase-B swap calldata that consumes a D->C claim,
|
|
// carrying the recorded object bytes exactly as a client reads them off D. There is
|
|
// no separate amount word: the credited value IS the object's amount.
|
|
func (h *settleHarness) settlementCalldata(claimID ids.ID, _ uint64) []byte {
|
|
return buildSwapCalldata(h.key, h.params, EncodeSettlementHookData(claimID, h.recordedObject(claimID)))
|
|
}
|
|
|
|
// buildSwapCalldata encodes a V4 swap call (UNCHANGED ABI) carrying hookData. The
|
|
// 4-byte selector is prepended by the caller (h.c.Run strips it); here we return
|
|
// the ABI args + hookData tail that Run sees as `data`.
|
|
func buildSwapCalldata(key PoolKey, params SwapParams, hookData []byte) []byte {
|
|
args := make([]byte, 288)
|
|
copy(args[0:160], EncodePoolKeyABI(key))
|
|
if params.ZeroForOne {
|
|
args[191] = 1
|
|
}
|
|
if params.AmountSpecified != nil {
|
|
// store two's-complement-free magnitude with a sign marker in the high bit is
|
|
// not needed; DecodeSwapInput reads the 32-byte word as a signed big.Int. We
|
|
// encode negative exact-input as the 256-bit two's complement.
|
|
v := params.AmountSpecified
|
|
if v.Sign() < 0 {
|
|
// two's complement in 256 bits
|
|
mod := new(big.Int).Lsh(big.NewInt(1), 256)
|
|
tc := new(big.Int).Add(mod, v)
|
|
tc.FillBytes(args[192:224])
|
|
} else {
|
|
v.FillBytes(args[192:224])
|
|
}
|
|
}
|
|
if params.SqrtPriceLimitX96 != nil {
|
|
params.SqrtPriceLimitX96.FillBytes(args[224:256])
|
|
}
|
|
binary.BigEndian.PutUint64(args[280:288], 288) // hookData offset
|
|
lenWord := make([]byte, 32)
|
|
binary.BigEndian.PutUint64(lenWord[24:32], uint64(len(hookData)))
|
|
padded := make([]byte, (len(hookData)+31)/32*32)
|
|
copy(padded, hookData)
|
|
return append(append(args, lenWord...), padded...)
|
|
}
|
|
|
|
// prependSelector builds calldata = 4-byte selector || data (used by the surface
|
|
// tests to call a 0x9999 selector directly). Mirrors the quarantined helper.
|
|
func prependSelector(sel uint32, data []byte) []byte {
|
|
out := make([]byte, 4+len(data))
|
|
binary.BigEndian.PutUint32(out[0:4], sel)
|
|
copy(out[4:], data)
|
|
return out
|
|
}
|
|
|
|
// leftPad32 left-pads b into a 32-byte word (ABI word helper for the surface
|
|
// tests). Mirrors the quarantined helper.
|
|
func leftPad32(b []byte) []byte {
|
|
out := make([]byte, 32)
|
|
copy(out[32-len(b):], b)
|
|
return out
|
|
}
|
|
|
|
// sqrtX96For builds a V4 SqrtPriceLimitX96 for a quote-per-base price. The funding
|
|
// rail never reads it — a crossing carries no price — but the V4 SwapParams tuple is
|
|
// unchanged, so the harness still fills the field the ABI defines.
|
|
func sqrtX96For(price float64) *big.Int {
|
|
s := new(big.Float).Sqrt(big.NewFloat(price))
|
|
s.Mul(s, new(big.Float).SetInt(new(big.Int).Lsh(big.NewInt(1), 96)))
|
|
out, _ := s.Int(nil)
|
|
return out
|
|
}
|