wp_evm_v3_core/data.rs
1//! Pure data records for the v3 family.
2//!
3//! These are dumb structs with no methods. All operations on them live
4//! in `quote`, `plan`, or `hydrate` modules.
5
6use alloy_primitives::{Address, B256, U256};
7
8/// Snapshot of a v3 pool's on-chain state at some block.
9///
10/// Hydrated by `hydrate::pool_state`. Consumed by `quote::*` and `plan::*`.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct PoolState {
13 pub token0: Address,
14 pub token1: Address,
15 pub fee: u32,
16 pub tick_spacing: i32,
17 pub sqrt_price_x96: U256,
18 pub liquidity: u128,
19 /// Current tick as reported by the pool's `slot0()` at hydration time.
20 ///
21 /// **Informational only.** Quote functions derive `tick_current` from
22 /// `sqrt_price_x96` via the SDK's tick math; this field is not consumed
23 /// by `quote::*` or `plan::*`. Stored for display and debugging.
24 pub tick: i32,
25 /// Initialized ticks within a window around the current tick. The
26 /// window size is determined at hydrate time.
27 ///
28 /// **Sorted ascending by `tick`.** Callers must populate via
29 /// `wp-evm-v3-provider::populate_ticks` (or equivalent) before passing
30 /// the `PoolState` to `quote::*`. The native swap loop
31 /// (`crate::swap::swap`) relies on this ordering for O(log n) tick
32 /// search.
33 pub ticks: Vec<TickInfo>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct TickInfo {
38 pub tick: i32,
39 pub liquidity_net: i128,
40 pub liquidity_gross: u128,
41}
42
43/// Snapshot of a v3 LP position.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct PositionState {
46 pub token_id: U256,
47 pub owner: Address,
48 pub token0: Address,
49 pub token1: Address,
50 pub fee: u32,
51 pub tick_lower: i32,
52 pub tick_upper: i32,
53 pub liquidity: u128,
54 pub fees_owed_0: U256,
55 pub fees_owed_1: U256,
56}
57
58/// Per-protocol configuration baked into protocol facade crates.
59///
60/// Adding a new v3-fork DEX = creating a new protocol facade with a
61/// `pub const CONFIG: V3ProtocolConfig = ...`. No family code changes.
62///
63/// Uniswap V3 SwapRouter ABI version. Determines whether the
64/// `exactInputSingle` / `exactOutputSingle` params struct carries a
65/// `deadline: uint256` field.
66///
67/// - `V1`: original SwapRouter (with deadline). Selector for
68/// `exactInputSingle`: `0x414bf389`. Runtime size ~12,070 bytes.
69/// - `V02`: SwapRouter02 (no `deadline`; checks tx-level deadline
70/// externally via `Multicall` deadline wrapper if needed). Selector
71/// for `exactInputSingle`: `0x04e45aaf`. Runtime size ~24,497 bytes.
72///
73/// All 8 V3-family routers in waterpump-evm scanned 2026-05-12 — see
74/// R26-future-1 spec PR #200 §4 Slice 2 Task 6 amendment table for
75/// per-facade-chain mapping. Base + Avalanche Uniswap V3 = V02; all
76/// other 6 facade-chain combos = V1.
77#[derive(Copy, Clone, Debug, PartialEq, Eq)]
78pub enum SwapRouterKind {
79 /// Canonical Uniswap V3 SwapRouter (with `deadline` in params).
80 V1,
81 /// SwapRouter02 (no `deadline` in params).
82 V02,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct V3ProtocolConfig {
87 pub factory: Address,
88 /// CREATE2 caller for pool deployment. `None` means the factory
89 /// itself is the deployer (Uniswap V3 pattern). `Some(addr)` routes
90 /// CREATE2 derivation through a separate `PoolDeployer` contract
91 /// (PancakeSwap V3 pattern; analogous to Algebra's `AlgebraPoolDeployer`).
92 pub pool_deployer: Option<Address>,
93 pub router: Address,
94 /// SwapRouter ABI version — determines `exactInputSingle` /
95 /// `exactOutputSingle` encoding shape (V1 includes `deadline`,
96 /// V02 doesn't). See `SwapRouterKind` doc-comment for selector
97 /// + runtime-size scan provenance.
98 pub swap_router_kind: SwapRouterKind,
99 pub position_mgr: Address,
100 pub init_code_hash: B256,
101 pub fee_tiers: &'static [u32],
102 pub multicall: Address,
103 /// Canonical QuoterV2 deployment for this protocol, when one exists.
104 /// `None` means the facade has no on-chain parity oracle registered.
105 /// `wp-evm-v3-provider::quote_online` returns an error if called with
106 /// `None`.
107 pub quoter: Option<Address>,
108}
109
110/// Parameters for an `exact_in` swap quote/plan.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct ExactInParams {
113 pub token_in: Address,
114 pub token_out: Address,
115 pub amount_in: U256,
116 pub recipient: Address,
117}
118
119/// Parameters for an `exact_out` swap quote/plan.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct ExactOutParams {
122 pub token_in: Address,
123 pub token_out: Address,
124 pub amount_out: U256,
125 pub recipient: Address,
126}
127
128/// Output of a quote computation. Pure — no chain interaction needed
129/// to produce one given a `PoolState`.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct Quote {
132 pub amount_in: U256,
133 pub amount_out: U256,
134 pub sqrt_price_x96_after: U256,
135 pub price_impact_bps: u16,
136}
137
138/// Parameters for an `add_liquidity` plan.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct AddLiquidityParams {
141 pub token0: Address,
142 pub token1: Address,
143 pub fee: u32,
144 pub tick_lower: i32,
145 pub tick_upper: i32,
146 pub amount0_desired: U256,
147 pub amount1_desired: U256,
148 pub recipient: Address,
149}
150
151/// Parameters for a `remove_liquidity` plan.
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct RemoveLiquidityParams {
154 pub token_id: U256,
155 pub liquidity: u128,
156 /// Minimum `token0` amount the caller expects back, computed off-chain
157 /// from a prior quote against the current pool state. `None` means
158 /// no slippage protection (zero) — only safe for private mempools or
159 /// when the caller has other guarantees.
160 pub amount0_min: Option<U256>,
161 /// Same for `token1`.
162 pub amount1_min: Option<U256>,
163}
164
165/// Parameters for a `collect_fees` plan.
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct CollectFeesParams {
168 pub token_id: U256,
169 pub recipient: Address,
170 /// Pool's `token0` address — caller-supplied (typically fetched
171 /// via NFPM.positions(tokenId) before constructing this struct).
172 /// Used by facade-layer native unwrap to compose `sweepToken` for
173 /// the non-native side when `recipient == Address::ZERO`. Ignored
174 /// for non-native recipient cases.
175 pub token0: Address,
176 /// Same for token1.
177 pub token1: Address,
178 /// Caller's address (msg.sender). When `recipient == ZERO` (native
179 /// unwrap sentinel), used as the final `unwrapWETH9.recipient` and
180 /// `sweepToken.recipient`. Ignored for non-native recipient cases.
181 pub caller: Address,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct RemoveAndCollectParams {
186 /// NFPM position id — single source of truth (no risk of mismatch
187 /// between separate remove + collect param structs).
188 pub token_id: U256,
189 // -- decrease side --
190 pub liquidity: u128,
191 pub amount0_min: Option<U256>,
192 pub amount1_min: Option<U256>,
193 // -- collect side --
194 /// Recipient for the collected tokens. Pass `Address::ZERO` to
195 /// trigger native unwrap (wrapped-native side → unwrapped to caller;
196 /// non-native side → swept to caller as ERC20).
197 pub recipient: Address,
198 /// Pool's `token0` — caller fetches via NFPM.positions(tokenId)
199 /// before constructing. Used by facade-layer native unwrap to
200 /// determine which side is wrapped-native and which side to sweep.
201 pub token0: Address,
202 /// Same for token1.
203 pub token1: Address,
204 /// Caller's `msg.sender`. When `recipient == ZERO`, used as final
205 /// `unwrapWETH9.recipient` and `sweepToken.recipient`. Ignored for
206 /// non-native cases.
207 pub caller: Address,
208}
209
210/// Re-export of `wp_evm_base::types::PlanFragment`.
211///
212/// Canonical single definition lives in `waterpump-base`. All family crates
213/// re-export from there so the Phase 5 composer can aggregate fragments from
214/// multiple families into one `Vec<PlanFragment>`.
215pub use wp_evm_base::types::PlanFragment;
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use alloy_primitives::{address, U256};
221
222 #[test]
223 fn pool_state_constructs() {
224 let s = PoolState {
225 token0: address!("0x0000000000000000000000000000000000000001"),
226 token1: address!("0x0000000000000000000000000000000000000002"),
227 fee: 3000,
228 tick_spacing: 60,
229 sqrt_price_x96: U256::from(1u64) << 96,
230 liquidity: 0u128,
231 tick: 0i32,
232 ticks: vec![],
233 };
234 assert_eq!(s.fee, 3000);
235 }
236
237 #[test]
238 fn plan_fragment_default_is_empty() {
239 let f = PlanFragment::default();
240 assert!(f.calls.is_empty());
241 assert!(f.approvals.is_empty());
242 assert_eq!(f.value, U256::ZERO);
243 }
244}