wp-evm-v4-core 0.1.14

Pure data + quote + plan for v4-family DEXes — no async dependencies
Documentation
//! Pure data records for the v4 family.
//!
//! These are dumb structs with no methods. All operations on them live
//! in `quote`, `plan`, or `hydrate` modules.
//!
//! # Key differences from wp-evm-v3-core
//!
//! - **`PoolKey`** is re-exported from `wp-evm-v4-interfaces` (a `sol!`
//!   struct, not a tuple). It contains `currency0`, `currency1`,
//!   `fee: uint24`, `tickSpacing: int24`, and `hooks: address`.
//! - **`PoolId`** is computed from `PoolKey` via `keccak256(abi_encode(key))`.
//!   The v4 singleton PoolManager identifies pools by ID, not by address.
//! - **No `init_code_hash`** in `V4ProtocolConfig` — there is no factory
//!   and no pool contracts to CREATE2-derive.
//! - **Native ETH** is represented as `Currency(address(0))`, always
//!   as `currency0` when paired with an ERC20.

use alloy_primitives::{Address, B256, U256};
pub use wp_evm_base::types::PlanFragment;

// Re-export PoolKey from wp-evm-v4-interfaces. Pre-R17 this was an
// alias for `uniswap_v4_sdk::abi::PoolKey`; R17-pre Slice E flipped
// it to our own L0 interfaces crate, and R17-pre Slice F retired the
// SDK entirely. The on-wire encoding is unchanged — both definitions
// expand from the same Solidity struct
// `(address,address,uint24,int24,address)`.
pub use wp_evm_v4_interfaces::pool::PoolKey;

/// Snapshot of a v4 pool's on-chain state at some block.
///
/// Hydrated by `hydrate::pool_state`. Consumed by `quote::*` and `plan::*`.
/// The `pool_id` is derived from `pool_key` as
/// `keccak256(abi.encode(poolKey))` — see V4-core's [`PoolId.sol`][1].
///
/// [1]: https://github.com/Uniswap/v4-core/blob/main/src/types/PoolId.sol
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PoolState {
    pub pool_key: PoolKey,
    pub pool_id: B256,
    pub sqrt_price_x96: U256,
    pub tick: i32,
    pub liquidity: u128,
    /// Protocol fee from `getSlot0().protocolFee` (uint24, packed as
    /// two uint12 halves for 0→1 and 1→0 directions). Informational —
    /// not used by quote math.
    pub protocol_fee: u32,
    /// LP fee from slot0 (uint24). For static-fee pools, equals
    /// `pool_key.fee`. For dynamic-fee pools (see `DYNAMIC_FEE_FLAG`),
    /// represents the current computed fee — but dynamic-fee pools
    /// require hooks and are rejected by `quote::exact_in`.
    pub lp_fee: u32,
    /// Initialized ticks within a window around the current tick.
    /// Empty for minimal hydration; expanded in a future task.
    pub ticks: Vec<TickInfo>,
}

/// Tick info within a pool — same shape as wp-evm-v3-core's TickInfo.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TickInfo {
    pub tick: i32,
    pub liquidity_net: i128,
    pub liquidity_gross: u128,
}

/// Snapshot of a v4 LP position (as tracked by the PositionManager NFT).
///
/// Positions in V4 are keyed by `(owner, tickLower, tickUpper, salt)` at
/// the raw PoolManager level, but the PositionManager wraps them with
/// ERC721 token IDs — this struct reflects the PositionManager view.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PositionState {
    pub token_id: U256,
    pub owner: Address,
    pub pool_key: PoolKey,
    pub pool_id: B256,
    pub tick_lower: i32,
    pub tick_upper: i32,
    pub liquidity: u128,
    pub fees_owed_0: U256,
    pub fees_owed_1: U256,
}

/// Per-protocol configuration for a v4 deployment.
///
/// Adding a new v4 chain = creating a new protocol facade with a
/// `pub const CONFIG: V4ProtocolConfig = ...`. No family code changes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct V4ProtocolConfig {
    /// Singleton PoolManager. All pool state lives here.
    pub pool_manager: Address,
    /// ERC721 NFT wrapper for LP positions. Target for `plan::add_liquidity`,
    /// `plan::remove_liquidity`, `plan::collect_fees` via `modifyLiquidities`.
    pub position_manager: Address,
    /// UniversalRouter (V4-enabled). Target for `plan::swap_exact_in` via
    /// the V4 command byte. Deferred in this phase.
    pub universal_router: Address,
    /// StateView lens contract for efficient offchain reads.
    pub state_view: Address,
    /// V4Quoter for onchain simulation (optional — not currently used by
    /// `quote::*` which uses pure math, but held for future quoter
    /// delegation tests).
    pub quoter: Address,
    /// Canonical Permit2 contract for approvals and signature transfers.
    pub permit2: Address,
}

/// Parameters for an `exact_in` swap quote/plan.
///
/// Note: `fee` is NOT in params because it lives in `PoolKey` (for static
/// pools) or is read dynamically from `state.lp_fee` (for dynamic pools).
/// `hookData` is `bytes` and may be non-empty for pools with hooks, but
/// our Phase 5 MVP rejects hooked pools in `quote`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExactInParams {
    pub currency_in: Address,
    pub currency_out: Address,
    pub amount_in: U256,
    pub recipient: Address,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExactOutParams {
    pub currency_in: Address,
    pub currency_out: Address,
    pub amount_out: U256,
    pub recipient: Address,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddLiquidityParams {
    pub pool_key: PoolKey,
    pub tick_lower: i32,
    pub tick_upper: i32,
    pub liquidity: u128,
    pub recipient: Address,
    /// Arbitrary `bytes32` position salt for multiplexing multiple
    /// positions with the same range under one owner. `B256::ZERO`
    /// for the default position.
    pub salt: B256,
}

/// Parameters for removing liquidity from a v4 position.
///
/// `pool_key` is required because the V4 TAKE_PAIR action needs explicit
/// `currency0` / `currency1` addresses to deliver the burned tokens to.
/// Callers must look up the pool key either from a cached position
/// hydration or from their own records before constructing this.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoveLiquidityParams {
    pub pool_key: PoolKey, // required for TAKE_PAIR currency fields
    pub token_id: U256,
    pub liquidity: u128,
    /// Off-chain computed minimum token0 amount the caller expects back.
    /// `None` means no slippage protection (only safe for private mempools).
    pub amount0_min: Option<U256>,
    pub amount1_min: Option<U256>,
}

/// Parameters for *increasing* liquidity on an existing v4 position.
///
/// V4's `INCREASE_LIQUIDITY` action only takes `(tokenId, liquidity,
/// amount0Max, amount1Max, hookData)` — the PoolManager looks up the
/// pool by tokenId. But the V4Planner pairs that action with
/// `SETTLE_PAIR(currency0, currency1)` to settle deltas, which needs
/// the pool's actual currency addresses. So this caller-facing struct
/// carries the PoolKey explicitly — the CLI is expected to hydrate it
/// via `IPositionManager.getPoolAndPositionInfo(tokenId)` first.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IncreaseLiquidityCallerParams {
    pub token_id: U256,
    pub pool_key: PoolKey,
    pub liquidity: u128,
    pub amount0_max: U256,
    pub amount1_max: U256,
}

/// Parameters for collecting accrued fees from a v4 position.
///
/// V4 fee collection is expressed as `DECREASE_LIQUIDITY(liquidity=0) +
/// TAKE_PAIR` under the hood. The `TAKE_PAIR` action needs explicit
/// `currency0` / `currency1` addresses, which live in the `PoolKey` —
/// hence this struct carries the pool_key, same as `RemoveLiquidityParams`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CollectFeesParams {
    pub token_id: U256,
    pub recipient: Address,
    pub pool_key: PoolKey,
}

/// Output of a quote computation. Pure — no chain interaction needed
/// given a hydrated `PoolState`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Quote {
    pub amount_in: U256,
    pub amount_out: U256,
    pub sqrt_price_x96_after: U256,
    pub price_impact_bps: u16,
    /// The effective fee used for this quote (in pips). For static pools
    /// this matches `pool_key.fee`; for dynamic pools it's `state.lp_fee`.
    pub effective_fee_pips: u32,
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_primitives::{address, aliases::I24, aliases::U24, b256, U256};

    #[test]
    fn increase_liquidity_caller_params_carries_pool_key_and_amount_caps() {
        let pool_key = PoolKey {
            currency0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
            currency1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
            fee: U24::from(500u32),
            tickSpacing: I24::try_from(10).unwrap(),
            hooks: Address::ZERO,
        };
        let p = IncreaseLiquidityCallerParams {
            token_id: U256::from(42u64),
            pool_key: pool_key.clone(),
            liquidity: 1_000_000_000_000u128,
            amount0_max: U256::from(1_000_000u64),
            amount1_max: U256::from(2_000_000u64),
        };
        assert_eq!(p.token_id, U256::from(42u64));
        assert_eq!(p.pool_key, pool_key);
        assert_eq!(p.liquidity, 1_000_000_000_000u128);
        assert_eq!(p.amount0_max, U256::from(1_000_000u64));
        assert_eq!(p.amount1_max, U256::from(2_000_000u64));
    }

    #[test]
    fn pool_state_constructs() {
        let key = PoolKey {
            currency0: address!("0000000000000000000000000000000000000000"),
            currency1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
            fee: U24::from(3000u32),
            tickSpacing: I24::try_from(60i32).unwrap(),
            hooks: address!("0000000000000000000000000000000000000000"),
        };
        let s = PoolState {
            pool_key: key,
            pool_id: b256!("0000000000000000000000000000000000000000000000000000000000000001"),
            sqrt_price_x96: U256::from(1u64) << 96,
            tick: 0i32,
            liquidity: 0u128,
            protocol_fee: 0u32,
            lp_fee: 3000u32,
            ticks: vec![],
        };
        assert_eq!(s.lp_fee, 3000);
        assert_eq!(s.pool_key.fee, U24::from(3000u32));
    }
}