Files
precompile/dex/module.go
T
zeekay adb0feda16 precompile/dex: PoolManager → pure ingress + native lock-vault (kill the reserve); deposit/withdraw/balanceOf
Removed settleNativeLegs (the AMM-style native reserve). Native LUX is now lock/mint: real LUX
locked in 0x9010 vault, canonical balance minted in D-Chain available[caller][LUX]; withdraw burns
+ releases (refuses over-release). Added deposit/withdraw/balanceOf selectors + txHash-keyed
idempotency. Swap now BINDS caller identity into the submit frame (was zero → settlement fell back
to C-Chain) + InitializePool calls clob_open_market (markets were unbound → no-custody fallback).
ERC-20 deposit/withdraw return ErrERC20DepositUnsupported (explicit revert, NO fake reserve) pending
a real lock/mint Call() seam. Invariant balanceOf(0x9010)==Σavailable[LUX]+Σlocked[LUX].
2026-06-17 22:30:06 -07:00

1365 lines
48 KiB
Go

// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"encoding/binary"
"fmt"
"math/big"
"os"
"github.com/holiman/uint256"
"github.com/luxfi/crypto"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/tracing"
ethtypes "github.com/luxfi/geth/core/types"
"github.com/luxfi/precompile/contract"
"github.com/luxfi/precompile/modules"
"github.com/luxfi/precompile/precompileconfig"
)
var _ contract.Configurator = (*configurator)(nil)
var _ contract.StatefulPrecompiledContract = (*DEXContract)(nil)
// ConfigKey is the key used in json config files to specify this precompile config.
const ConfigKey = "dexConfig"
// Parsed addresses for LX precompiles (derived from string constants in types.go)
// See types.go for canonical LP-aligned address strings
var (
// Core AMM (LP-9010 series - Uniswap v4 style)
lxPoolAddr = common.HexToAddress(LXPoolAddress) // LP-9010 LXPool (v4 PoolManager)
lxOracleAddr = common.HexToAddress(LXOracleAddress) // LP-9011 LXOracle
lxRouterAddr = common.HexToAddress(LXRouterAddress) // LP-9012 LXRouter
lxHooksAddr = common.HexToAddress(LXHooksAddress) // LP-9013 LXHooks
lxFlashAddr = common.HexToAddress(LXFlashAddress) // LP-9014 LXFlash
// Trading & DeFi Extensions
lxBookAddr = common.HexToAddress(LXBookAddress) // LP-9020 LXBook (CLOB matcher)
lxVaultAddr = common.HexToAddress(LXVaultAddress) // LP-9030 LXVault
lxPriceAddr = common.HexToAddress(LXPriceAddress) // LP-9040 LXPrice
lxLendAddr = common.HexToAddress(LXLendAddress) // LP-9050 LXLend (lending pool)
lxLiquidAddr = common.HexToAddress(LXRepayerAddress) // LP-9060 LXLiquid (self-repaying loans)
)
// DEXPrecompile is the singleton instance.
//
// Backend strategy (decomplected — values, not places):
//
// - The precompile lives at ONE address (LP-9010) across every Lux-derived
// EVM (Lux C-Chain, Hanzo, Zoo, SPC, and white-label deployments such as
// downstream EVM). The ABI is identical for every chain.
//
// - The math/matching backend is parameterized. The default is the INERT
// engine (engine_inert.go): it performs no matching and reverts every call
// with ErrDEXBackendNotConfigured. V4 = CLOB, so the matcher lives ONLY on
// the d-chain, reached over ZAP — never embedded in the precompile. The
// venue makes 0x9010 live by calling SetBackend(NewZAPEngine(endpoint))
// from the EVM plugin main when dex-zap-endpoint is set; chains that do not
// enable it run an inert precompile that cleanly reverts.
//
// - Brand identity is a value the backend carries (Engine.Brand()). User-
// facing error strings produced by the precompile MUST come from the
// backend, never hard-coded here. This keeps a tenant surface free of
// the word "Lux" while still letting the OSS package be called "Lux DEX"
// on Lux-network deployments.
var DEXPrecompile = newDEXContract(newInertEngine())
// SetBackend swaps the DEX engine backing the singleton precompile.
//
// MUST be called from package main of the EVM plugin BEFORE the VM starts
// initializing precompiles (i.e. before any genesis Configure() call). Calling
// it after ANY pool-manager state has accumulated returns an error — backend
// swaps must not race with live state. The guard covers every per-pool map
// the precompile maintains (pools, poolStates, positions) so a future map
// added without updating the guard doesn't silently re-open the race.
//
// Logs the backend brand to stderr on success so operators see in the boot
// log which engine is wired (e.g. "Hanzo DEX" vs upstream OSS). RED V1.
//
// Threading: not safe for concurrent use. The host binary is the single caller
// during process startup.
func SetBackend(e Engine) error {
if e == nil {
return fmt.Errorf("dex: backend engine must not be nil")
}
pm := DEXPrecompile.poolManager
if len(pm.pools) != 0 || len(pm.poolStates) != 0 || len(pm.positions) != 0 {
return fmt.Errorf("dex: cannot swap backend after pool initialization")
}
pm.engine = e
// Surface the installed brand on the boot log. Stderr is the right sink
// for a process-lifecycle event that operators need to see even when
// structured logging hasn't been wired yet.
fmt.Fprintf(os.Stderr, "dex: precompile backend installed brand=%q\n", e.Brand())
return nil
}
// Backend returns the active engine. Useful for diagnostics and tests; do not
// use this to mutate engine state from outside the package.
func Backend() Engine {
return DEXPrecompile.poolManager.engine
}
// newDEXContract constructs the singleton with a non-nil engine. Separated
// from the var declaration so the construction path is testable and so
// reviewers can see the engine MUST be non-nil at init.
func newDEXContract(e Engine) *DEXContract {
if e == nil {
// Bug in caller — panic at init time, not at first swap.
panic("dex: DEXContract requires a non-nil engine")
}
return &DEXContract{
poolManager: NewPoolManager(e),
}
}
// Module is the precompile module (LXPool at LP-9010)
var Module = modules.Module{
ConfigKey: ConfigKey,
Address: lxPoolAddr,
Contract: DEXPrecompile,
Configurator: &configurator{},
}
// Method selectors: keccak256 of V4 function signatures (first 4 bytes).
// Verified against IPoolManager.sol at init time.
var (
SelectorInitialize uint32 = 0x6276CBBE // initialize((address,address,uint24,int24,address),uint160)
SelectorSwap uint32 = 0xF3CD914C // swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes)
SelectorModifyLiquidity uint32 = 0x5A6BCFDA // modifyLiquidity((address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)
SelectorDonate uint32 = 0x234266D7 // donate((address,address,uint24,int24,address),uint256,uint256,bytes)
SelectorUnlock uint32 = 0x48C89491 // unlock(bytes)
SelectorSettle uint32 = 0x11DA60B4 // settle()
SelectorSettleFor uint32 = 0x3DD45ADB // settleFor(address)
SelectorSync uint32 = 0xA5841194 // sync(address)
SelectorTake uint32 = 0x0B0D9C09 // take(address,address,uint256)
SelectorClear uint32 = 0x80F0B44C // clear(address,uint256)
SelectorMint uint32 = 0x156E29F6 // mint(address,uint256,uint256)
SelectorBurn uint32 = 0xF5298ACA // burn(address,uint256,uint256)
SelectorUpdateDynFee uint32 = 0x52759651 // updateDynamicLPFee((address,address,uint24,int24,address),uint24)
SelectorExtsload uint32 = 0x1E2EAEAF // extsload(bytes32)
SelectorExtsloadArray uint32 = 0xDBD035FF // extsload(bytes32[])
// CLOB custody selectors — the EVM ingress for funds in/out of the D-Chain
// ledger ("the money lives in the order book"). deposit LOCKS the asset in the
// 0x9010 vault (native LUX via msg.value) and MINTS the D-Chain available
// balance; withdraw BURNS the D-Chain balance and RELEASES the vault. The CLOB
// settles ONLY inside the D-Chain; these never fund a trade from 0x9010.
SelectorDeposit uint32 = 0x47E7EF24 // deposit(address,uint256) — asset, amount (msg.value==amount for native)
SelectorWithdraw uint32 = 0xF3FEF3A3 // withdraw(address,uint256) — asset, want
SelectorBalanceOf uint32 = 0xF7888AEC // balanceOf(address,address) — account, asset (read-only available)
// Admin pause/freeze selectors — computed in init() via keccak4.
SelectorPauseDEX uint32
SelectorResumeDEX uint32
SelectorPausePool uint32
SelectorResumePool uint32
SelectorFreezePool uint32
)
// keccak4 computes the first 4 bytes of keccak256(sig) as a uint32 selector.
func keccak4(sig string) uint32 {
hash := crypto.Keccak256([]byte(sig))
return binary.BigEndian.Uint32(hash[:4])
}
type configurator struct{}
func init() {
// Verify hardcoded selectors match keccak256 computation at startup.
verifySelector := func(name string, got uint32, sig string) {
want := keccak4(sig)
if got != want {
panic(fmt.Sprintf("dex: selector mismatch for %s: got 0x%08X, want 0x%08X", name, got, want))
}
}
verifySelector("initialize", SelectorInitialize, "initialize((address,address,uint24,int24,address),uint160)")
verifySelector("swap", SelectorSwap, "swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes)")
verifySelector("modifyLiquidity", SelectorModifyLiquidity, "modifyLiquidity((address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)")
verifySelector("donate", SelectorDonate, "donate((address,address,uint24,int24,address),uint256,uint256,bytes)")
verifySelector("unlock", SelectorUnlock, "unlock(bytes)")
verifySelector("settle", SelectorSettle, "settle()")
verifySelector("take", SelectorTake, "take(address,address,uint256)")
verifySelector("deposit", SelectorDeposit, "deposit(address,uint256)")
verifySelector("withdraw", SelectorWithdraw, "withdraw(address,uint256)")
verifySelector("balanceOf", SelectorBalanceOf, "balanceOf(address,address)")
// Compute admin selectors at init time (not compile-time constants).
SelectorPauseDEX = keccak4("pauseDEX()")
SelectorResumeDEX = keccak4("resumeDEX()")
SelectorPausePool = keccak4("pausePool(bytes32)")
SelectorResumePool = keccak4("resumePool(bytes32)")
SelectorFreezePool = keccak4("freezePool(bytes32)")
if err := modules.RegisterModule(Module); err != nil {
panic(err)
}
}
func (*configurator) MakeConfig() precompileconfig.Config {
return new(Config)
}
// compromisedDevControllers lists Ethereum addresses whose private keys are
// publicly distributed (Foundry/Anvil deterministic test accounts, Hardhat
// default mnemonic, etc.). Configuring any of these as the DEX protocol fee
// controller would let any caller pause / freeze the chain-wide AMM, so
// activation is refused. Stored as the lowercase hex representation produced
// by common.Address.Hex()-stripped-and-lowered for cheap membership testing.
//
// Sources:
// - Foundry/Anvil default mnemonic "test test test test test test test test
// test test test junk" (anvil --accounts 10) → accounts #0..#9.
// - Hardhat default mnemonic produces an overlapping set (same root).
//
// New entries should be appended whenever a new well-known development
// mnemonic is identified.
var compromisedDevControllers = map[common.Address]struct{}{
common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"): {}, // anvil #0
common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"): {}, // anvil #1
common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"): {}, // anvil #2
common.HexToAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"): {}, // anvil #3
common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"): {}, // anvil #4
common.HexToAddress("0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"): {}, // anvil #5
common.HexToAddress("0x976EA74026E726554dB657fA54763abd0C3a0aa9"): {}, // anvil #6
common.HexToAddress("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955"): {}, // anvil #7
common.HexToAddress("0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f"): {}, // anvil #8
common.HexToAddress("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720"): {}, // anvil #9
}
func (*configurator) Configure(
chainConfig precompileconfig.ChainConfig,
cfg precompileconfig.Config,
state contract.StateDB,
blockContext contract.ConfigurationBlockContext,
) error {
config, ok := cfg.(*Config)
if !ok {
return fmt.Errorf("expected config type %T, got %T: %v", &Config{}, cfg, cfg)
}
// Protocol fee controller is required. NO DEFAULT. A missing or zero
// controller would leave PauseDEX / ResumeDEX / FreezePool open to any
// caller; the operator MUST populate this explicitly in upgrade.json
// (typically a multisig or governance contract). Refusing activation
// surfaces the misconfiguration on the activation block rather than
// silently shipping an open-admin DEX.
if config.ProtocolFeeController == (common.Address{}) {
return ErrDEXNoProtocolFeeController
}
// Refuse known-compromised dev keys whose private keys are public
// knowledge (Foundry/Anvil, Hardhat default mnemonic). This catches the
// "I'll fix the placeholder before mainnet" footgun.
if _, bad := compromisedDevControllers[config.ProtocolFeeController]; bad {
return fmt.Errorf("%w: %s", ErrDEXCompromisedController, config.ProtocolFeeController.Hex())
}
DEXPrecompile.poolManager.protocolFeeController = config.ProtocolFeeController
return nil
}
// Config implements the precompileconfig.Config interface
type Config struct {
precompileconfig.Upgrade // Embedded for flat JSON structure
ProtocolFeeController common.Address `json:"protocolFeeController,omitempty"`
MaxPools uint64 `json:"maxPools,omitempty"`
EnableFlashLoans bool `json:"enableFlashLoans,omitempty"`
EnableHooks bool `json:"enableHooks,omitempty"`
}
func (c *Config) Key() string {
return ConfigKey
}
func (c *Config) Timestamp() *uint64 {
return c.Upgrade.Timestamp()
}
func (c *Config) IsDisabled() bool {
return c.Upgrade.Disable
}
func (c *Config) Equal(cfg precompileconfig.Config) bool {
other, ok := cfg.(*Config)
if !ok {
return false
}
return c.Upgrade.Equal(&other.Upgrade) &&
c.ProtocolFeeController == other.ProtocolFeeController &&
c.MaxPools == other.MaxPools &&
c.EnableFlashLoans == other.EnableFlashLoans &&
c.EnableHooks == other.EnableHooks
}
func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error {
return nil
}
// DEXContract implements the DEX precompile
type DEXContract struct {
poolManager *PoolManager
}
// Run executes the precompile
func (c *DEXContract) Run(
accessibleState contract.AccessibleState,
caller common.Address,
addr common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) (ret []byte, remainingGas uint64, err error) {
if len(input) < 4 {
return nil, suppliedGas, fmt.Errorf("input too short")
}
if accessibleState.GetBlockContext() == nil {
return nil, suppliedGas, fmt.Errorf("block context unavailable")
}
selector := binary.BigEndian.Uint32(input[:4])
data := input[4:]
switch selector {
case SelectorInitialize:
return c.runInitialize(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorSwap:
return c.runSwap(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorModifyLiquidity:
return c.runModifyLiquidity(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorDeposit:
return c.runDeposit(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorWithdraw:
return c.runWithdraw(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorBalanceOf:
return c.runBalanceOf(accessibleState, data, suppliedGas)
case SelectorDonate:
return c.runDonate(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorUnlock:
return c.runUnlock(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorTake:
return c.runTake(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorSettle, SelectorSettleFor:
return c.runSettle(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorSync:
return c.runSync(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorExtsload:
return c.runExtsload(accessibleState, data, suppliedGas)
case SelectorExtsloadArray:
return c.runExtsloadArray(accessibleState, data, suppliedGas)
case SelectorPauseDEX:
return c.runPauseDEX(accessibleState, caller, suppliedGas, readOnly)
case SelectorResumeDEX:
return c.runResumeDEX(accessibleState, caller, suppliedGas, readOnly)
case SelectorPausePool:
return c.runPausePool(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorResumePool:
return c.runResumePool(accessibleState, caller, data, suppliedGas, readOnly)
case SelectorFreezePool:
return c.runFreezePool(accessibleState, caller, data, suppliedGas, readOnly)
default:
return nil, suppliedGas, fmt.Errorf("unknown method selector: 0x%08x", selector)
}
}
func (c *DEXContract) runInitialize(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasPoolCreate {
return nil, 0, fmt.Errorf("out of gas")
}
// V4 ABI: PoolKey (5 slots = 160 bytes) + sqrtPriceX96 (32 bytes)
if len(input) < 192 {
return nil, suppliedGas - GasPoolCreate, fmt.Errorf("input too short for initialize")
}
key, err := DecodePoolKey(input[:160])
if err != nil {
return nil, suppliedGas - GasPoolCreate, err
}
sqrtPriceX96 := new(big.Int).SetBytes(input[160:192])
var hookData []byte
if len(input) > 192 {
hookData = decodeABIBytes(input, 192)
}
// Initialize pool
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
tick, err := c.poolManager.Initialize(stateAdapter, key, sqrtPriceX96, hookData)
if err != nil {
return nil, suppliedGas - GasPoolCreate, err
}
// Return tick as int24, sign-extended to int256 (32 bytes, two's complement).
// V4 ABI returns int24 as a full int256: negative values have 0xFF in high bytes.
result := bigIntTo32Bytes(big.NewInt(int64(tick)))
return result, suppliedGas - GasPoolCreate, nil
}
func (c *DEXContract) runSwap(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasSwap {
return nil, 0, fmt.Errorf("out of gas")
}
// Parse PoolKey and SwapParams from input
key, params, hookData, err := DecodeSwapInput(input)
if err != nil {
return nil, suppliedGas - GasSwap, err
}
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
delta, err := c.poolManager.Swap(stateAdapter, caller, key, params, hookData)
if err != nil {
return nil, suppliedGas - GasSwap, err
}
// NO C-CHAIN SETTLEMENT. A marketable order settles ENTIRELY inside the D-Chain
// ledger (the taker's locked spend moves to the maker, the maker's locked asset
// moves to the taker — dchain.settleFills, value-conserving by construction).
// The taker's funds were DEPOSITED into the D-Chain (deposit selector ->
// clob_deposit -> available) before this swap; the swap locked + spent them in
// the book. 0x9010 is a pure ingress adapter — it holds NO reserve, is NEVER a
// counterparty, and does NOT move value here. The returned BalanceDelta is the
// fills' net for the V4 ABI's caller, NOT an instruction to settle on C-Chain.
// (The former settleNativeLegs did a caller<->0x9010 native transfer = an
// AMM-style C-Chain reserve settlement; it left native LUX sitting in 0x9010
// "backing resting asks" — the exact reserve hazard the CLOB model forbids.)
// V4: Return BalanceDelta as single int256 (amount0 in upper 128 bits, amount1 in lower 128 bits)
result := PackBalanceDelta(delta.Amount0, delta.Amount1)
return result, suppliedGas - GasSwap, nil
}
func (c *DEXContract) runModifyLiquidity(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasAddLiquidity {
return nil, 0, fmt.Errorf("out of gas")
}
key, params, hookData, err := DecodeModifyLiquidityInput(input)
if err != nil {
return nil, suppliedGas - GasAddLiquidity, err
}
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
delta, feeDelta, err := c.poolManager.ModifyLiquidity(stateAdapter, caller, key, params, hookData)
if err != nil {
return nil, suppliedGas - GasAddLiquidity, err
}
// NO C-CHAIN SETTLEMENT. Placing a resting order LOCKS the maker's already-
// DEPOSITED D-Chain balance (available -> locked, inside the book); cancelling
// UNLOCKS it. The maker's funds never touch C-Chain here and 0x9010 holds no
// reserve backing the order — the resting order's funds live in the D-Chain
// ledger's locked[maker][asset], not in 0x9010. (Was settleNativeLegs, which
// moved native LUX caller<->0x9010 to "back" the resting ask = a reserve.)
// V4: Return two packed BalanceDeltas (callerDelta + feesAccrued), each 32 bytes
result := make([]byte, 64)
copy(result[0:32], PackBalanceDelta(delta.Amount0, delta.Amount1))
copy(result[32:64], PackBalanceDelta(feeDelta.Amount0, feeDelta.Amount1))
return result, suppliedGas - GasAddLiquidity, nil
}
// runDeposit is the EVM ingress for funds-IN: deposit(address asset, uint256
// amount). For native LUX (asset == address(0)) the caller MUST send msg.value ==
// amount; the EVM has already moved that value into the 0x9010 vault before this
// precompile runs, so the deposit LOCKS it there and MINTS the caller's available
// D-Chain balance. 0x9010 is a passive vault, never a trade counterparty.
//
// ABI: input = asset[32] (address, right-aligned) || amount[32] (uint256).
// Returns the deposited amount as uint256 (the credited available balance delta).
func (c *DEXContract) runDeposit(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasSettlement {
return nil, 0, fmt.Errorf("out of gas")
}
if len(input) < 64 {
return nil, suppliedGas - GasSettlement, fmt.Errorf("deposit: input too short")
}
asset := Currency{Address: common.BytesToAddress(input[12:32])}
amount := new(big.Int).SetBytes(input[32:64])
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
if err := c.poolManager.Deposit(stateAdapter, caller, asset, amount); err != nil {
return nil, suppliedGas - GasSettlement, err
}
out := make([]byte, 32)
amount.FillBytes(out)
return out, suppliedGas - GasSettlement, nil
}
// runWithdraw is the EVM ingress for funds-OUT: withdraw(address asset, uint256
// want). It BURNS up to `want` of the caller's available D-Chain balance (the
// ledger clamps to availability) and RELEASES exactly the realized amount from the
// 0x9010 vault back to the caller. Conserving: the vault never pays more than the
// ledger burned. Returns the realized amount as uint256.
func (c *DEXContract) runWithdraw(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasSettlement {
return nil, 0, fmt.Errorf("out of gas")
}
if len(input) < 64 {
return nil, suppliedGas - GasSettlement, fmt.Errorf("withdraw: input too short")
}
asset := Currency{Address: common.BytesToAddress(input[12:32])}
want := new(big.Int).SetBytes(input[32:64])
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
realized, err := c.poolManager.Withdraw(stateAdapter, caller, asset, want)
if err != nil {
return nil, suppliedGas - GasSettlement, err
}
out := make([]byte, 32)
realized.FillBytes(out)
return out, suppliedGas - GasSettlement, nil
}
// runBalanceOf is a read-only observation of an account's AVAILABLE D-Chain
// balance for an asset: balanceOf(address account, address asset). It forwards to
// the custody backend's Withdraw with want=0? No — a read must not mutate. It
// queries the D-Chain via the backend's read path. Returns the available balance
// as uint256. (The locked balance is queryable on the venue's clob_balance; this
// EVM view returns available, the spendable claim.)
func (c *DEXContract) runBalanceOf(
state contract.AccessibleState,
input []byte,
suppliedGas uint64,
) ([]byte, uint64, error) {
if suppliedGas < GasPoolLookup {
return nil, 0, fmt.Errorf("out of gas")
}
if len(input) < 64 {
return nil, suppliedGas - GasPoolLookup, fmt.Errorf("balanceOf: input too short")
}
account := common.BytesToAddress(input[12:32])
asset := Currency{Address: common.BytesToAddress(input[44:64])}
avail, err := c.poolManager.BalanceOf(account, asset)
if err != nil {
return nil, suppliedGas - GasPoolLookup, err
}
out := make([]byte, 32)
avail.FillBytes(out)
return out, suppliedGas - GasPoolLookup, nil
}
func (c *DEXContract) runTake(
_ contract.AccessibleState,
_ common.Address,
_ []byte,
suppliedGas uint64,
_ bool,
) ([]byte, uint64, error) {
return nil, suppliedGas, fmt.Errorf("take not supported: blockchain provides atomic settlement")
}
func (c *DEXContract) runSettle(
_ contract.AccessibleState,
_ common.Address,
_ []byte,
suppliedGas uint64,
_ bool,
) ([]byte, uint64, error) {
return nil, suppliedGas, fmt.Errorf("settle not supported: blockchain provides atomic settlement")
}
func (c *DEXContract) runUnlock(
_ contract.AccessibleState,
_ common.Address,
_ []byte,
suppliedGas uint64,
_ bool,
) ([]byte, uint64, error) {
// V4 unlock is used for flash accounting. In a precompile context the blockchain
// provides atomic settlement, so unlock is a no-op that returns empty bytes.
return []byte{}, suppliedGas, nil
}
func (c *DEXContract) runDonate(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
gasCost := GasSwap // donate uses similar gas to swap
if suppliedGas < gasCost {
return nil, 0, fmt.Errorf("out of gas")
}
// V4 ABI: PoolKey (160) + amount0 (32) + amount1 (32) + hookData
if len(input) < 224 {
return nil, suppliedGas - gasCost, fmt.Errorf("input too short for V4 donate")
}
key, err := DecodePoolKey(input[:160])
if err != nil {
return nil, suppliedGas - gasCost, err
}
amount0 := new(big.Int).SetBytes(input[160:192])
amount1 := new(big.Int).SetBytes(input[192:224])
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
var hookData []byte
if len(input) > 224 {
// Offset word for hookData (bytes arg) is at position 224 in the args buffer.
// The value at that position is the absolute offset from byte 0 of args.
hookData = decodeABIBytes(input, 224)
}
delta, err := c.poolManager.Donate(stateAdapter, caller, key, amount0, amount1, hookData)
if err != nil {
return nil, suppliedGas - gasCost, err
}
result := PackBalanceDelta(delta.Amount0, delta.Amount1)
return result, suppliedGas - gasCost, nil
}
func (c *DEXContract) runSync(
_ contract.AccessibleState,
_ common.Address,
_ []byte,
suppliedGas uint64,
_ bool,
) ([]byte, uint64, error) {
// V4 sync checkpoints ERC20 balances for flash accounting.
// Precompile uses direct state access, so sync is a no-op.
return []byte{}, suppliedGas, nil
}
func (c *DEXContract) runGetPool(
state contract.AccessibleState,
input []byte,
suppliedGas uint64,
) ([]byte, uint64, error) {
if suppliedGas < GasPoolLookup {
return nil, 0, fmt.Errorf("out of gas")
}
key, err := DecodePoolKey(input)
if err != nil {
return nil, suppliedGas - GasPoolLookup, err
}
poolId := key.ID()
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
pool := c.poolManager.getPool(stateAdapter, poolId)
if !pool.IsInitialized() {
return nil, suppliedGas - GasPoolLookup, fmt.Errorf("pool not found")
}
// Encode pool state
result := EncodePoolState(pool)
return result, suppliedGas - GasPoolLookup, nil
}
func (c *DEXContract) runGetPosition(
state contract.AccessibleState,
input []byte,
suppliedGas uint64,
) ([]byte, uint64, error) {
if suppliedGas < GasPoolLookup {
return nil, 0, fmt.Errorf("out of gas")
}
// Parse position key from input
// Position not found returns zeroes
result := make([]byte, 96) // liquidity (32) + feeGrowthInside0 (32) + feeGrowthInside1 (32)
return result, suppliedGas - GasPoolLookup, nil
}
// runExtsload reads a single bytes32 storage slot from the pool manager address.
// V4 ABI: extsload(bytes32) returns (bytes32)
// Input: 32 bytes (the storage slot key)
// Output: 32 bytes (the storage value)
func (c *DEXContract) runExtsload(
state contract.AccessibleState,
input []byte,
suppliedGas uint64,
) ([]byte, uint64, error) {
if suppliedGas < GasPoolLookup {
return nil, 0, fmt.Errorf("out of gas")
}
if len(input) < 32 {
return nil, suppliedGas - GasPoolLookup, fmt.Errorf("input too short for extsload: need 32 bytes, got %d", len(input))
}
slot := common.BytesToHash(input[0:32])
value := state.GetStateDB().GetState(lxPoolAddr, slot)
return value.Bytes(), suppliedGas - GasPoolLookup, nil
}
// runExtsloadArray reads multiple bytes32 storage slots from the pool manager address.
// V4 ABI: extsload(bytes32[]) returns (bytes32[])
// Input: ABI-encoded bytes32 array (offset word + length + slot0 + slot1 + ...)
// Output: ABI-encoded bytes32 array with the same number of values
//
// Gas: the precompile uses the canonical RequiredGas formula
// (extsloadArrayGas) as the single source of truth. Parse-time validation
// failures still charge the base fee — the dispatcher already deducted it,
// and the caller paid for the bounded work done before the error.
func (c *DEXContract) runExtsloadArray(
state contract.AccessibleState,
input []byte,
suppliedGas uint64,
) ([]byte, uint64, error) {
// Canonical gas: same formula RequiredGas advertises, computed once.
totalGas := extsloadArrayGas(input)
if suppliedGas < totalGas {
return nil, 0, fmt.Errorf("out of gas")
}
remaining := suppliedGas - totalGas
// ABI-encoded dynamic array: first 32 bytes = offset to array data
if len(input) < 64 {
return nil, remaining, fmt.Errorf("input too short for extsload array")
}
inputLen := uint64(len(input))
offsetBig := new(big.Int).SetBytes(input[0:32])
if offsetBig.BitLen() > 63 {
return nil, remaining, fmt.Errorf("extsload array: offset overflow")
}
offset := offsetBig.Uint64()
if offset > inputLen || inputLen-offset < 32 {
return nil, remaining, fmt.Errorf("extsload array: offset out of bounds")
}
countBig := new(big.Int).SetBytes(input[offset : offset+32])
if countBig.BitLen() > 63 {
return nil, remaining, fmt.Errorf("extsload array: count overflow")
}
count := countBig.Uint64()
const maxExtsloadSlots = 256
if count > maxExtsloadSlots {
return nil, remaining, fmt.Errorf("extsload array: count %d exceeds max %d", count, maxExtsloadSlots)
}
dataStart := offset + 32
if count > 0 && (dataStart > inputLen || inputLen-dataStart < count*32) {
return nil, remaining, fmt.Errorf("extsload array: data out of bounds")
}
// Build ABI-encoded return: offset (32) + count (32) + values (count * 32)
result := make([]byte, 64+count*32)
// Offset to array data = 32
result[31] = 0x20
// Array length
copy(result[32:64], input[offset:offset+32])
stateDB := state.GetStateDB()
for i := range count {
slotStart := dataStart + i*32
slot := common.BytesToHash(input[slotStart : slotStart+32])
value := stateDB.GetState(lxPoolAddr, slot)
copy(result[64+i*32:64+(i+1)*32], value.Bytes())
}
return result, remaining, nil
}
// --- Admin: Pause / Resume / Freeze handlers ---
func (c *DEXContract) runPauseDEX(
state contract.AccessibleState,
caller common.Address,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasAdmin {
return nil, 0, fmt.Errorf("out of gas")
}
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
if err := c.poolManager.PauseDEX(stateAdapter, caller); err != nil {
return nil, suppliedGas - GasAdmin, err
}
return nil, suppliedGas - GasAdmin, nil
}
func (c *DEXContract) runResumeDEX(
state contract.AccessibleState,
caller common.Address,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasAdmin {
return nil, 0, fmt.Errorf("out of gas")
}
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
if err := c.poolManager.ResumeDEX(stateAdapter, caller); err != nil {
return nil, suppliedGas - GasAdmin, err
}
return nil, suppliedGas - GasAdmin, nil
}
func (c *DEXContract) runPausePool(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasAdmin {
return nil, 0, fmt.Errorf("out of gas")
}
if len(input) < 32 {
return nil, suppliedGas - GasAdmin, fmt.Errorf("input too short for pool ID")
}
var poolId [32]byte
copy(poolId[:], input[:32])
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
if err := c.poolManager.PausePool(stateAdapter, caller, poolId); err != nil {
return nil, suppliedGas - GasAdmin, err
}
return nil, suppliedGas - GasAdmin, nil
}
func (c *DEXContract) runResumePool(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasAdmin {
return nil, 0, fmt.Errorf("out of gas")
}
if len(input) < 32 {
return nil, suppliedGas - GasAdmin, fmt.Errorf("input too short for pool ID")
}
var poolId [32]byte
copy(poolId[:], input[:32])
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
if err := c.poolManager.ResumePool(stateAdapter, caller, poolId); err != nil {
return nil, suppliedGas - GasAdmin, err
}
return nil, suppliedGas - GasAdmin, nil
}
func (c *DEXContract) runFreezePool(
state contract.AccessibleState,
caller common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
}
if suppliedGas < GasAdmin {
return nil, 0, fmt.Errorf("out of gas")
}
if len(input) < 32 {
return nil, suppliedGas - GasAdmin, fmt.Errorf("input too short for pool ID")
}
var poolId [32]byte
copy(poolId[:], input[:32])
stateAdapter := &poolStateAdapter{stateDB: state.GetStateDB(), blockNumber: state.GetBlockContext().Number().Uint64()}
if err := c.poolManager.FreezePool(stateAdapter, caller, poolId); err != nil {
return nil, suppliedGas - GasAdmin, err
}
return nil, suppliedGas - GasAdmin, nil
}
// RequiredGas returns the gas required for the precompile input.
//
// This is the canonical gas source for the DEX precompile — every run handler
// charges this exact value rather than re-deriving it inline. Putting the
// gas formula in one place means a tariff change only edits one switch.
//
// For dynamic-sized selectors (extsload(bytes32[])) the value is derived
// from the input: base cost + per-slot cost, capped at the maximum array
// length the loader will accept. Selectors with no input data fall back to
// their fixed tariff.
//
// RED V4 closes the divergence between this function and the previous inline
// charge in runExtsloadArray: now there is one source of truth.
func (c *DEXContract) RequiredGas(input []byte) uint64 {
if len(input) < 4 {
return GasSwap
}
selector := binary.BigEndian.Uint32(input[:4])
switch selector {
case SelectorInitialize:
return GasPoolCreate
case SelectorSwap, SelectorDonate:
return GasSwap
case SelectorModifyLiquidity:
return GasAddLiquidity
case SelectorDeposit, SelectorWithdraw:
return GasSettlement
case SelectorBalanceOf:
return GasPoolLookup
case SelectorTake:
return GasBalanceUpdate
case SelectorSettle, SelectorSettleFor:
return GasSettlement
case SelectorUnlock:
return GasFlashLoan
case SelectorSync:
return GasPoolLookup
case SelectorExtsload:
return GasPoolLookup
case SelectorExtsloadArray:
return extsloadArrayGas(input[4:])
case SelectorPauseDEX, SelectorResumeDEX, SelectorPausePool, SelectorResumePool, SelectorFreezePool:
return GasAdmin
default:
return GasSwap
}
}
// extsloadArrayGas computes the canonical gas charge for an extsload(bytes32[])
// call: GasPoolLookup base + GasPoolLookup per slot. Returns the base alone
// for malformed inputs — the Run path will surface the parse error and the
// caller pays the base fee for the dispatch.
//
// Inputs that would overflow uint64 are clamped to the maximum supported
// slot count (maxExtsloadSlots = 256). The Run path enforces the same cap,
// so the gas formula and the read budget agree.
func extsloadArrayGas(data []byte) uint64 {
if len(data) < 64 {
return GasPoolLookup
}
offsetBig := new(big.Int).SetBytes(data[0:32])
if offsetBig.BitLen() > 63 {
return GasPoolLookup
}
offset := offsetBig.Uint64()
dataLen := uint64(len(data))
if offset > dataLen || dataLen-offset < 32 {
return GasPoolLookup
}
countBig := new(big.Int).SetBytes(data[offset : offset+32])
if countBig.BitLen() > 63 {
return GasPoolLookup
}
count := countBig.Uint64()
const maxExtsloadSlots = 256
if count > maxExtsloadSlots {
count = maxExtsloadSlots
}
return GasPoolLookup + GasPoolLookup*count
}
// poolStateAdapter adapts contract.StateDB to dex.StateDB.
// blockNumber must be set from the execution context (AccessibleState.GetBlockContext().Number())
// since contract.StateDB does not expose block number.
type poolStateAdapter struct {
stateDB contract.StateDB
blockNumber uint64
}
func (a *poolStateAdapter) GetState(addr common.Address, key common.Hash) common.Hash {
return a.stateDB.GetState(addr, key)
}
func (a *poolStateAdapter) SetState(addr common.Address, key common.Hash, value common.Hash) {
a.stateDB.SetState(addr, key, value)
}
func (a *poolStateAdapter) GetBalance(addr common.Address) *uint256.Int {
return a.stateDB.GetBalance(addr)
}
func (a *poolStateAdapter) AddBalance(addr common.Address, amount *uint256.Int) {
a.stateDB.AddBalance(addr, amount, tracing.BalanceChangeUnspecified)
}
func (a *poolStateAdapter) SubBalance(addr common.Address, amount *uint256.Int) {
a.stateDB.SubBalance(addr, amount, tracing.BalanceChangeUnspecified)
}
func (a *poolStateAdapter) Exist(addr common.Address) bool {
return a.stateDB.Exist(addr)
}
func (a *poolStateAdapter) CreateAccount(addr common.Address) {
a.stateDB.CreateAccount(addr)
}
func (a *poolStateAdapter) GetBlockNumber() uint64 {
return a.blockNumber
}
func (a *poolStateAdapter) AddLog(log *ethtypes.Log) {
a.stateDB.AddLog(log)
}
// TxHash exposes the executing transaction hash (txIdentified seam) so the
// PoolManager can bind a marketable order to its EVM tx for replay-idempotency
// (RED H1). Sourced from the underlying geth StateDB.
func (a *poolStateAdapter) TxHash() common.Hash {
return a.stateDB.TxHash()
}
// Helper functions for encoding/decoding
func int24ToBytes(v int24) []byte {
b := make([]byte, 3)
b[0] = byte(v >> 16)
b[1] = byte(v >> 8)
b[2] = byte(v)
return b
}
// DecodePoolKey decodes a V4 ABI-encoded PoolKey from input bytes.
// V4 PoolKey is 5 slots x 32 bytes = 160 bytes:
//
// [0:32] currency0 (address, left-padded)
// [32:64] currency1 (address, left-padded)
// [64:96] fee (uint24, left-padded)
// [96:128] tickSpacing (int24, sign-extended to 32 bytes)
// [128:160] hooks (address, left-padded)
func DecodePoolKey(input []byte) (PoolKey, error) {
if len(input) < 160 {
return PoolKey{}, fmt.Errorf("input too short for V4 PoolKey: need 160 bytes, got %d", len(input))
}
key := PoolKey{}
key.Currency0 = Currency{Address: common.BytesToAddress(input[12:32])}
key.Currency1 = Currency{Address: common.BytesToAddress(input[44:64])}
key.Fee = uint24(new(big.Int).SetBytes(input[64:96]).Uint64())
// int24 sign extension from 32-byte ABI slot
tickVal := new(big.Int).SetBytes(input[96:128])
if input[96]&0x80 != 0 {
tickVal.Sub(tickVal, new(big.Int).Lsh(big.NewInt(1), 256))
}
key.TickSpacing = int32(tickVal.Int64())
key.Hooks = common.BytesToAddress(input[140:160])
return key, nil
}
// DecodeSwapInput decodes V4 ABI-encoded swap input.
// Layout after 4-byte selector:
//
// [0:160] PoolKey (5 slots)
// [160:192] zeroForOne (bool, 32 bytes)
// [192:224] amountSpecified (int256)
// [224:256] sqrtPriceLimitX96 (uint160, 32 bytes)
// [256:] hookData (ABI-encoded bytes: offset + length + data)
func DecodeSwapInput(input []byte) (PoolKey, SwapParams, []byte, error) {
if len(input) < 256 {
return PoolKey{}, SwapParams{}, nil, fmt.Errorf("input too short for V4 swap: need 256 bytes, got %d", len(input))
}
key, err := DecodePoolKey(input[:160])
if err != nil {
return PoolKey{}, SwapParams{}, nil, err
}
// V4: zeroForOne is a full 32-byte bool slot
zeroForOne := input[191] == 1
// V4: amountSpecified is signed int256.
// Negative = exact input, Positive = exact output.
amountSpecified := decodeSigned256(input[192:224])
params := SwapParams{
ZeroForOne: zeroForOne,
AmountSpecified: amountSpecified,
SqrtPriceLimitX96: new(big.Int).SetBytes(input[224:256]),
}
var hookData []byte
if len(input) > 256 {
// Offset word for hookData (bytes arg) is at position 256 in the args buffer.
// The value at that position is the absolute offset from byte 0 of args.
hookData = decodeABIBytes(input, 256)
}
return key, params, hookData, nil
}
// DecodeModifyLiquidityInput decodes V4 ABI-encoded modifyLiquidity input.
// Layout after 4-byte selector:
//
// [0:160] PoolKey (5 slots)
// [160:192] tickLower (int24, sign-extended to 32 bytes)
// [192:224] tickUpper (int24, sign-extended to 32 bytes)
// [224:256] liquidityDelta (int256)
// [256:288] salt (bytes32)
// [288:] hookData (ABI-encoded bytes)
func DecodeModifyLiquidityInput(input []byte) (PoolKey, ModifyLiquidityParams, []byte, error) {
if len(input) < 288 {
return PoolKey{}, ModifyLiquidityParams{}, nil, fmt.Errorf("input too short for V4 modifyLiquidity: need 288 bytes, got %d", len(input))
}
key, err := DecodePoolKey(input[:160])
if err != nil {
return PoolKey{}, ModifyLiquidityParams{}, nil, err
}
// tickLower: int24 sign-extended to 32 bytes
tickLowerVal := decodeSigned256(input[160:192])
// tickUpper: int24 sign-extended to 32 bytes
tickUpperVal := decodeSigned256(input[192:224])
params := ModifyLiquidityParams{
TickLower: int32(tickLowerVal.Int64()),
TickUpper: int32(tickUpperVal.Int64()),
LiquidityDelta: decodeSigned256(input[224:256]),
}
copy(params.Salt[:], input[256:288])
var hookData []byte
if len(input) > 288 {
// Offset word for hookData (bytes arg) is at position 288 in the args buffer.
hookData = decodeABIBytes(input, 288)
}
return key, params, hookData, nil
}
// EncodePoolState encodes pool state for return
func EncodePoolState(pool *Pool) []byte {
result := make([]byte, 160)
copy(result[0:32], bigIntTo32Bytes(pool.SqrtPriceX96))
binary.BigEndian.PutUint32(result[32:36], uint32(pool.Tick))
copy(result[64:96], bigIntTo32Bytes(pool.Liquidity))
copy(result[96:128], bigIntTo32Bytes(pool.FeeGrowth0X128))
copy(result[128:160], bigIntTo32Bytes(pool.FeeGrowth1X128))
return result
}
// bigIntTo32Bytes encodes a big.Int as a 32-byte ABI word.
// Positive values are zero-padded. Negative values use two's complement (2^256 + v).
// This prevents FillBytes from panicking on negative values and ensures correct
// ABI encoding for signed integer return values.
func bigIntTo32Bytes(v *big.Int) []byte {
result := make([]byte, 32)
if v == nil || v.Sign() == 0 {
return result
}
if v.Sign() > 0 {
v.FillBytes(result)
} else {
// Two's complement: 2^256 + v
tc := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(1), 256), v)
tc.FillBytes(result)
}
return result
}
// decodeSigned256 decodes a 32-byte big-endian two's complement int256.
// If the high bit is set, the value is negative: value = raw - 2^256.
func decodeSigned256(b []byte) *big.Int {
val := new(big.Int).SetBytes(b)
if len(b) > 0 && b[0]&0x80 != 0 {
val.Sub(val, new(big.Int).Lsh(big.NewInt(1), 256))
}
return val
}
// decodeInt24 decodes a 3-byte big-endian signed int24 value with sign extension.
// Values >= 2^23 (sign bit set) are negative: value = raw - 2^24.
func decodeInt24(b []byte) int24 {
if len(b) < 3 {
return 0
}
val := int32(b[0])<<16 | int32(b[1])<<8 | int32(b[2])
if val >= 1<<23 { // sign bit set
val -= 1 << 24
}
return int24(val)
}
// PackBalanceDelta packs amount0 and amount1 into V4 BalanceDelta format.
// V4 BalanceDelta is a single int256: amount0 in upper 128 bits, amount1 in lower 128 bits.
// This matches: toBalanceDelta(int128 _amount0, int128 _amount1) in BalanceDelta.sol
func PackBalanceDelta(amount0, amount1 *big.Int) []byte {
result := make([]byte, 32)
a0 := bigIntTo16Bytes(amount0) // signed int128
a1 := bigIntTo16Bytes(amount1) // signed int128
copy(result[0:16], a0)
copy(result[16:32], a1)
return result
}
// UnpackBalanceDelta unpacks a V4 BalanceDelta (single int256) into amount0 and amount1.
// amount0 = arithmetic right shift 128 bits, amount1 = sign-extend lower 128 bits.
func UnpackBalanceDelta(data []byte) (amount0, amount1 *big.Int) {
if len(data) < 32 {
return big.NewInt(0), big.NewInt(0)
}
// amount0: upper 128 bits (bytes 0-15), signed
a0 := new(big.Int).SetBytes(data[0:16])
if data[0]&0x80 != 0 {
a0.Sub(a0, new(big.Int).Lsh(big.NewInt(1), 128))
}
// amount1: lower 128 bits (bytes 16-31), signed
a1 := new(big.Int).SetBytes(data[16:32])
if data[16]&0x80 != 0 {
a1.Sub(a1, new(big.Int).Lsh(big.NewInt(1), 128))
}
return a0, a1
}
// bigIntTo16Bytes encodes a big.Int as a 16-byte signed int128 (two's complement).
func bigIntTo16Bytes(v *big.Int) []byte {
result := make([]byte, 16)
if v == nil || v.Sign() == 0 {
return result
}
if v.Sign() > 0 {
b := v.Bytes()
if len(b) > 16 {
b = b[len(b)-16:]
}
copy(result[16-len(b):], b)
} else {
// Two's complement: 2^128 + v
tc := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(1), 128), v)
b := tc.Bytes()
if len(b) > 16 {
b = b[len(b)-16:]
}
copy(result[16-len(b):], b)
}
return result
}
// decodeABIBytes decodes ABI-encoded dynamic bytes from a V4 calldata buffer.
//
// V4 ABI encoding uses ABSOLUTE offsets from byte 0 of the args buffer (after
// the 4-byte selector). The offset word at position offsetPos contains the
// absolute byte position where the length-prefixed data begins.
//
// Layout at absolute offset: [length (32 bytes)] [data (length bytes)]
//
// Parameters:
// - args: the full args buffer (input after 4-byte selector)
// - offsetPos: byte position within args where the 32-byte offset word lives
//
// Returns nil if the input is too short or malformed.
func decodeABIBytes(args []byte, offsetPos uint64) []byte {
argsLen := uint64(len(args))
if offsetPos+32 > argsLen {
return nil
}
// Read the absolute offset value from the offset word.
// Reject if the big.Int exceeds uint64 (truncation attack).
absOffsetBig := new(big.Int).SetBytes(args[offsetPos : offsetPos+32])
if absOffsetBig.BitLen() > 63 {
return nil // value exceeds int64 max — impossible valid offset
}
absOffset := absOffsetBig.Uint64()
// Offset must point forward (past the offset word) to prevent data confusion.
if absOffset < offsetPos+32 {
return nil
}
// Check absOffset+32 without overflow.
if absOffset > argsLen || argsLen-absOffset < 32 {
return nil
}
// At absOffset: 32 bytes length, then data.
lengthBig := new(big.Int).SetBytes(args[absOffset : absOffset+32])
if lengthBig.BitLen() > 63 {
return nil
}
length := lengthBig.Uint64()
dataStart := absOffset + 32
// Check dataStart+length without overflow.
if length == 0 {
return nil
}
if dataStart > argsLen || argsLen-dataStart < length {
return nil
}
return args[dataStart : dataStart+length]
}
// EncodePoolKeyABI encodes a PoolKey into V4 ABI format (5 slots x 32 bytes = 160 bytes).
func EncodePoolKeyABI(key PoolKey) []byte {
data := make([]byte, 160)
copy(data[12:32], key.Currency0.Address.Bytes())
copy(data[44:64], key.Currency1.Address.Bytes())
// fee as uint24 in last 3 bytes of slot
data[93] = byte(key.Fee >> 16)
data[94] = byte(key.Fee >> 8)
data[95] = byte(key.Fee)
// tickSpacing as int24 sign-extended to 32 bytes
if key.TickSpacing < 0 {
for i := 96; i < 125; i++ {
data[i] = 0xff
}
}
data[125] = byte(key.TickSpacing >> 16)
data[126] = byte(key.TickSpacing >> 8)
data[127] = byte(key.TickSpacing)
copy(data[140:160], key.Hooks.Bytes())
return data
}