use crate::data::{
AddLiquidityParams, CollectFeesParams, ExactInParams, ExactOutParams, PlanFragment, PoolState,
Quote, RemoveAndCollectParams, RemoveLiquidityParams, SwapRouterKind,
};
use alloy_primitives::Address;
use alloy_primitives::{aliases::U160, U256};
use alloy_sol_types::{sol, SolCall};
use wp_evm_base::types::{Call, SlippageBps, TokenApproval};
use wp_evm_v3_interfaces::periphery::router::IPeripheryRouter;
sol! {
#[derive(Debug)]
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams params)
external payable returns (uint256 amountOut);
}
sol! {
#[derive(Debug)]
struct ExactInputSingleParamsV02 {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
interface ISwapRouter02 {
function exactInputSingle(ExactInputSingleParamsV02 params)
external payable returns (uint256 amountOut);
}
}
sol! {
#[derive(Debug)]
struct ExactOutputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountOut;
uint256 amountInMaximum;
uint160 sqrtPriceLimitX96;
}
function exactOutputSingle(ExactOutputSingleParams params)
external payable returns (uint256 amountIn);
}
sol! {
#[derive(Debug)]
struct ExactOutputSingleParamsV02 {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountOut;
uint256 amountInMaximum;
uint160 sqrtPriceLimitX96;
}
interface ISwapRouter02ExactOut {
function exactOutputSingle(ExactOutputSingleParamsV02 params)
external payable returns (uint256 amountIn);
}
}
sol! {
#[derive(Debug)]
struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
function mint(MintParams params) external payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
#[derive(Debug)]
struct IncreaseLiquidityParams {
uint256 tokenId;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function increaseLiquidity(IncreaseLiquidityParams params) external payable
returns (uint128 liquidity, uint256 amount0, uint256 amount1);
#[derive(Debug)]
struct DecreaseLiquidityParams {
uint256 tokenId;
uint128 liquidity;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function decreaseLiquidity(DecreaseLiquidityParams params) external payable
returns (uint256 amount0, uint256 amount1);
#[derive(Debug)]
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
function collect(CollectParams params) external payable
returns (uint256 amount0, uint256 amount1);
}
pub fn swap_exact_in(
state: &PoolState,
quote: &Quote,
params: &ExactInParams,
slippage: SlippageBps,
deadline: u64,
router: Address,
kind: SwapRouterKind,
) -> PlanFragment {
swap_exact_in_with_fee_fn(state, quote, params, slippage, deadline, router, kind, |s| s.fee)
}
#[allow(
clippy::too_many_arguments,
reason = "public planner API threads router ABI kind plus fee injection"
)]
pub fn swap_exact_in_with_fee_fn<F>(
state: &PoolState,
quote: &Quote,
params: &ExactInParams,
slippage: SlippageBps,
deadline: u64,
router: Address,
kind: SwapRouterKind,
fee_fn: F,
) -> PlanFragment
where
F: Fn(&PoolState) -> u32,
{
let effective_fee = fee_fn(state);
let amount_out_min = apply_slippage_min(quote.amount_out, slippage);
let fee_u24 = alloy_primitives::aliases::U24::from(effective_fee);
let calldata = match kind {
SwapRouterKind::V1 => {
let call_params = ExactInputSingleParams {
tokenIn: params.token_in,
tokenOut: params.token_out,
fee: fee_u24,
recipient: params.recipient,
deadline: U256::from(deadline),
amountIn: params.amount_in,
amountOutMinimum: amount_out_min,
sqrtPriceLimitX96: U160::ZERO,
};
exactInputSingleCall { params: call_params }.abi_encode().into()
}
SwapRouterKind::V02 => {
let call_params = ExactInputSingleParamsV02 {
tokenIn: params.token_in,
tokenOut: params.token_out,
fee: fee_u24,
recipient: params.recipient,
amountIn: params.amount_in,
amountOutMinimum: amount_out_min,
sqrtPriceLimitX96: U160::ZERO,
};
ISwapRouter02::exactInputSingleCall { params: call_params }.abi_encode().into()
}
};
PlanFragment {
calls: vec![Call { target: router, calldata, value: U256::ZERO }],
approvals: vec![TokenApproval {
token: params.token_in,
spender: router,
min_amount: params.amount_in,
}],
value: U256::ZERO,
}
}
pub fn swap_exact_out(
state: &PoolState,
quote: &Quote,
params: &ExactOutParams,
slippage: SlippageBps,
deadline: u64,
router: Address,
kind: SwapRouterKind,
) -> PlanFragment {
swap_exact_out_with_fee_fn(state, quote, params, slippage, deadline, router, kind, |s| s.fee)
}
#[allow(
clippy::too_many_arguments,
reason = "public planner API threads router ABI kind plus fee injection"
)]
pub fn swap_exact_out_with_fee_fn<F>(
state: &PoolState,
quote: &Quote,
params: &ExactOutParams,
slippage: SlippageBps,
deadline: u64,
router: Address,
kind: SwapRouterKind,
fee_fn: F,
) -> PlanFragment
where
F: Fn(&PoolState) -> u32,
{
let effective_fee = fee_fn(state);
let amount_in_max = apply_slippage_max(quote.amount_in, slippage);
let fee_u24 = alloy_primitives::aliases::U24::from(effective_fee);
let calldata = match kind {
SwapRouterKind::V1 => {
let call_params = ExactOutputSingleParams {
tokenIn: params.token_in,
tokenOut: params.token_out,
fee: fee_u24,
recipient: params.recipient,
deadline: U256::from(deadline),
amountOut: params.amount_out,
amountInMaximum: amount_in_max,
sqrtPriceLimitX96: U160::ZERO,
};
exactOutputSingleCall { params: call_params }.abi_encode().into()
}
SwapRouterKind::V02 => {
let call_params = ExactOutputSingleParamsV02 {
tokenIn: params.token_in,
tokenOut: params.token_out,
fee: fee_u24,
recipient: params.recipient,
amountOut: params.amount_out,
amountInMaximum: amount_in_max,
sqrtPriceLimitX96: U160::ZERO,
};
ISwapRouter02ExactOut::exactOutputSingleCall { params: call_params }.abi_encode().into()
}
};
PlanFragment {
calls: vec![Call { target: router, calldata, value: U256::ZERO }],
approvals: vec![TokenApproval {
token: params.token_in,
spender: router,
min_amount: amount_in_max,
}],
value: U256::ZERO,
}
}
pub fn apply_slippage_min(quoted: U256, slippage: SlippageBps) -> U256 {
let bps = U256::from(slippage.as_bps());
let denom = U256::from(10_000u64);
quoted * (denom - bps) / denom
}
pub fn apply_slippage_max(quoted: U256, slippage: SlippageBps) -> U256 {
let bps = U256::from(slippage.as_bps());
let denom = U256::from(10_000u64);
quoted * (denom + bps) / denom
}
pub fn add_liquidity(
params: &AddLiquidityParams,
slippage: SlippageBps,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
let mint_params = MintParams {
token0: params.token0,
token1: params.token1,
fee: alloy_primitives::aliases::U24::from(params.fee),
tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
.expect("tick_lower within i24 range"),
tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
.expect("tick_upper within i24 range"),
amount0Desired: params.amount0_desired,
amount1Desired: params.amount1_desired,
amount0Min: apply_slippage_min(params.amount0_desired, slippage),
amount1Min: apply_slippage_min(params.amount1_desired, slippage),
recipient: params.recipient,
deadline: U256::from(deadline),
};
let calldata = mintCall { params: mint_params }.abi_encode().into();
PlanFragment {
calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
approvals: vec![
TokenApproval {
token: params.token0,
spender: position_manager,
min_amount: params.amount0_desired,
},
TokenApproval {
token: params.token1,
spender: position_manager,
min_amount: params.amount1_desired,
},
],
value: U256::ZERO,
}
}
#[allow(
clippy::too_many_arguments,
reason = "public planner API mirrors NFPM increaseLiquidity parameters"
)]
pub fn increase_liquidity(
token_id: U256,
token0: Address,
token1: Address,
amount0_desired: U256,
amount1_desired: U256,
slippage: SlippageBps,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
let inc_params = IncreaseLiquidityParams {
tokenId: token_id,
amount0Desired: amount0_desired,
amount1Desired: amount1_desired,
amount0Min: apply_slippage_min(amount0_desired, slippage),
amount1Min: apply_slippage_min(amount1_desired, slippage),
deadline: U256::from(deadline),
};
let calldata = increaseLiquidityCall { params: inc_params }.abi_encode().into();
PlanFragment {
calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
approvals: vec![
TokenApproval { token: token0, spender: position_manager, min_amount: amount0_desired },
TokenApproval { token: token1, spender: position_manager, min_amount: amount1_desired },
],
value: U256::ZERO,
}
}
pub fn remove_liquidity(
params: &RemoveLiquidityParams,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
let dec = DecreaseLiquidityParams {
tokenId: params.token_id,
liquidity: params.liquidity,
amount0Min: params.amount0_min.unwrap_or(U256::ZERO),
amount1Min: params.amount1_min.unwrap_or(U256::ZERO),
deadline: U256::from(deadline),
};
let calldata = decreaseLiquidityCall { params: dec }.abi_encode().into();
PlanFragment {
calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
approvals: vec![],
value: U256::ZERO,
}
}
pub fn remove_liquidity_and_collect(
params: &RemoveAndCollectParams,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
let dec_params = DecreaseLiquidityParams {
tokenId: params.token_id,
liquidity: params.liquidity,
amount0Min: params.amount0_min.unwrap_or(U256::ZERO),
amount1Min: params.amount1_min.unwrap_or(U256::ZERO),
deadline: U256::from(deadline),
};
let decrease_calldata: alloy_primitives::Bytes =
decreaseLiquidityCall { params: dec_params }.abi_encode().into();
let collect_params = CollectParams {
tokenId: params.token_id,
recipient: params.recipient,
amount0Max: u128::MAX,
amount1Max: u128::MAX,
};
let collect_calldata: alloy_primitives::Bytes =
collectCall { params: collect_params }.abi_encode().into();
let multicall_data = vec![decrease_calldata, collect_calldata];
let multicall_calldata =
IPeripheryRouter::multicallCall { data: multicall_data }.abi_encode().into();
PlanFragment {
calls: vec![Call {
target: position_manager,
value: U256::ZERO,
calldata: multicall_calldata,
}],
approvals: vec![],
value: U256::ZERO,
}
}
pub fn collect_fees(params: &CollectFeesParams, position_manager: Address) -> PlanFragment {
let coll = CollectParams {
tokenId: params.token_id,
recipient: params.recipient,
amount0Max: u128::MAX,
amount1Max: u128::MAX,
};
let calldata = collectCall { params: coll }.abi_encode().into();
PlanFragment {
calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
approvals: vec![],
value: U256::ZERO,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::{
AddLiquidityParams, CollectFeesParams, RemoveAndCollectParams, RemoveLiquidityParams,
SwapRouterKind, V3ProtocolConfig,
};
use alloy_primitives::{address, b256, Address};
const TEST_CFG: V3ProtocolConfig = V3ProtocolConfig {
factory: address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
pool_deployer: None,
router: address!("0xE592427A0AEce92De3Edee1F18E0157C05861564"),
swap_router_kind: SwapRouterKind::V1,
position_mgr: address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
init_code_hash: b256!("0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"),
fee_tiers: &[100, 500, 3000, 10000],
multicall: address!("0xcA11bde05977b3631167028862bE2a173976CA11"),
quoter: None,
};
fn dummy_pool_state(t0: Address, t1: Address) -> PoolState {
PoolState {
token0: t0,
token1: t1,
fee: 3000,
tick_spacing: 60,
sqrt_price_x96: U256::from(1u64) << 96,
liquidity: 0,
tick: 0,
ticks: vec![],
}
}
fn fixture_exact_in_params() -> ExactInParams {
ExactInParams {
token_in: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
token_out: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
amount_in: U256::from(1_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
}
}
fn fixture_exact_out_params() -> ExactOutParams {
ExactOutParams {
token_in: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
token_out: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
amount_out: U256::from(500_000_000_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
}
}
fn fixture_quote(state: &PoolState) -> Quote {
Quote {
amount_in: U256::from(1_000_000u64),
amount_out: U256::from(500_000_000_000_000u64),
sqrt_price_x96_after: state.sqrt_price_x96,
price_impact_bps: 0,
}
}
#[test]
fn plan_swap_emits_one_call_with_router_target() {
let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let s = dummy_pool_state(token_in, token_out);
let q = Quote {
amount_in: U256::from(1_000_000u64),
amount_out: U256::from(500_000_000_000_000u64),
sqrt_price_x96_after: s.sqrt_price_x96,
price_impact_bps: 0,
};
let p = ExactInParams {
token_in,
token_out,
amount_in: q.amount_in,
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let frag = swap_exact_in(
&s,
&q,
&p,
SlippageBps::new(50),
9_999_999_999,
TEST_CFG.router,
SwapRouterKind::V1,
);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, TEST_CFG.router);
assert_eq!(frag.approvals.len(), 1);
assert_eq!(frag.approvals[0].token, token_in);
assert_eq!(frag.approvals[0].spender, TEST_CFG.router);
assert_eq!(frag.approvals[0].min_amount, q.amount_in);
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn swap_exact_in_v1_emits_v1_selector() {
let params = fixture_exact_in_params();
let state = dummy_pool_state(params.token_in, params.token_out);
let quote = fixture_quote(&state);
let plan = swap_exact_in(
&state,
"e,
¶ms,
SlippageBps::new(50),
0,
TEST_CFG.router,
SwapRouterKind::V1,
);
assert_eq!(
&plan.calls[0].calldata[..4],
&[0x41, 0x4b, 0xf3, 0x89],
"V1 selector must be 0x414bf389"
);
}
#[test]
fn swap_exact_in_v02_emits_v02_selector() {
let params = fixture_exact_in_params();
let state = dummy_pool_state(params.token_in, params.token_out);
let quote = fixture_quote(&state);
let plan = swap_exact_in(
&state,
"e,
¶ms,
SlippageBps::new(50),
0,
TEST_CFG.router,
SwapRouterKind::V02,
);
assert_eq!(
&plan.calls[0].calldata[..4],
&[0x04, 0xe4, 0x5a, 0xaf],
"V02 selector must be 0x04e45aaf"
);
}
#[test]
fn swap_exact_in_v1_includes_deadline() {
let params = fixture_exact_in_params();
let state = dummy_pool_state(params.token_in, params.token_out);
let quote = fixture_quote(&state);
let v1_plan = swap_exact_in(
&state,
"e,
¶ms,
SlippageBps::new(50),
1_700_000_000,
TEST_CFG.router,
SwapRouterKind::V1,
);
let v02_plan = swap_exact_in(
&state,
"e,
¶ms,
SlippageBps::new(50),
1_700_000_000,
TEST_CFG.router,
SwapRouterKind::V02,
);
assert_eq!(
v1_plan.calls[0].calldata.len(),
v02_plan.calls[0].calldata.len() + 32,
"V1 calldata must be 32 bytes longer than V02 (deadline field)"
);
}
#[test]
fn swap_exact_out_v1_emits_v1_selector() {
let params = fixture_exact_out_params();
let state = dummy_pool_state(params.token_in, params.token_out);
let quote = fixture_quote(&state);
let plan = swap_exact_out(
&state,
"e,
¶ms,
SlippageBps::new(50),
0,
TEST_CFG.router,
SwapRouterKind::V1,
);
assert_eq!(
&plan.calls[0].calldata[..4],
&[0xdb, 0x3e, 0x21, 0x98],
"V1 selector must be 0xdb3e2198"
);
}
#[test]
fn swap_exact_out_v02_emits_v02_selector() {
let params = fixture_exact_out_params();
let state = dummy_pool_state(params.token_in, params.token_out);
let quote = fixture_quote(&state);
let plan = swap_exact_out(
&state,
"e,
¶ms,
SlippageBps::new(50),
0,
TEST_CFG.router,
SwapRouterKind::V02,
);
assert_eq!(
&plan.calls[0].calldata[..4],
&[0x50, 0x23, 0xb4, 0xdf],
"V02 selector must be 0x5023b4df"
);
}
#[test]
fn swap_exact_out_v1_includes_deadline() {
let params = fixture_exact_out_params();
let state = dummy_pool_state(params.token_in, params.token_out);
let quote = fixture_quote(&state);
let v1_plan = swap_exact_out(
&state,
"e,
¶ms,
SlippageBps::new(50),
1_700_000_000,
TEST_CFG.router,
SwapRouterKind::V1,
);
let v02_plan = swap_exact_out(
&state,
"e,
¶ms,
SlippageBps::new(50),
1_700_000_000,
TEST_CFG.router,
SwapRouterKind::V02,
);
assert_eq!(
v1_plan.calls[0].calldata.len(),
v02_plan.calls[0].calldata.len() + 32,
"V1 calldata must be 32 bytes longer than V02 (deadline field)"
);
}
#[test]
fn swap_exact_out_approval_min_amount_is_amount_in_max() {
let params = fixture_exact_out_params();
let state = dummy_pool_state(params.token_in, params.token_out);
let quote = Quote {
amount_in: U256::from(1_000_000u64),
amount_out: params.amount_out,
sqrt_price_x96_after: state.sqrt_price_x96,
price_impact_bps: 0,
};
let plan = swap_exact_out(
&state,
"e,
¶ms,
SlippageBps::new(50),
0,
TEST_CFG.router,
SwapRouterKind::V1,
);
assert_eq!(
plan.approvals[0].min_amount,
U256::from(1_005_000u64),
"approval min_amount must equal amount_in_max (worst case)"
);
}
#[test]
fn slippage_min_50bps_on_1eth() {
let out =
apply_slippage_min(U256::from(1_000_000_000_000_000_000u64), SlippageBps::new(50));
assert_eq!(out, U256::from(995_000_000_000_000_000u64));
}
#[test]
fn slippage_max_50bps_on_1eth() {
let out =
apply_slippage_max(U256::from(1_000_000_000_000_000_000u64), SlippageBps::new(50));
assert_eq!(out, U256::from(1_005_000_000_000_000_000u64));
}
#[test]
fn plan_add_liquidity_targets_position_manager() {
let p = AddLiquidityParams {
token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
fee: 3000,
tick_lower: -201_000,
tick_upper: -198_960,
amount0_desired: U256::from(1_000_000u64),
amount1_desired: U256::from(500_000_000_000_000u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let frag = add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, TEST_CFG.position_mgr);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
assert_eq!(frag.approvals.len(), 2);
assert_eq!(frag.approvals[0].token, p.token0);
assert_eq!(frag.approvals[0].spender, TEST_CFG.position_mgr);
assert_eq!(frag.approvals[1].token, p.token1);
assert_eq!(frag.approvals[1].spender, TEST_CFG.position_mgr);
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn plan_remove_liquidity_targets_position_manager_no_approvals() {
let p = RemoveLiquidityParams {
token_id: U256::from(42u64),
liquidity: 1_000_000_000_000u128,
amount0_min: None,
amount1_min: None,
};
let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
assert!(frag.approvals.is_empty());
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn plan_remove_liquidity_passes_min_amounts_when_supplied() {
let p = RemoveLiquidityParams {
token_id: U256::from(42u64),
liquidity: 1_000_000_000_000u128,
amount0_min: Some(U256::from(500_000u64)),
amount1_min: Some(U256::from(1_000_000_000u64)),
};
let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
let decoded =
decreaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
assert_eq!(decoded.params.amount0Min, U256::from(500_000u64));
assert_eq!(decoded.params.amount1Min, U256::from(1_000_000_000u64));
}
#[test]
fn plan_remove_liquidity_defaults_missing_mins_to_zero() {
let p = RemoveLiquidityParams {
token_id: U256::from(42u64),
liquidity: 1_000_000_000_000u128,
amount0_min: None,
amount1_min: None,
};
let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
let decoded =
decreaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
assert_eq!(decoded.params.amount0Min, U256::ZERO);
assert_eq!(decoded.params.amount1Min, U256::ZERO);
}
#[test]
fn slippage_min_50bps_on_100k_weth() {
let quoted: U256 = U256::from(10u64).pow(U256::from(23u64));
let out = apply_slippage_min(quoted, SlippageBps::new(50));
let expected: U256 = quoted * U256::from(9950u64) / U256::from(10_000u64);
assert_eq!(out, expected);
assert!(out > U256::from(u64::MAX));
}
#[test]
fn plan_collect_fees_targets_position_manager_no_approvals() {
let p = CollectFeesParams {
token_id: U256::from(42u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
token0: address!("0x0000000000000000000000000000000000000001"),
token1: address!("0x0000000000000000000000000000000000000002"),
caller: Address::ZERO,
};
let frag = collect_fees(&p, TEST_CFG.position_mgr);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
assert!(frag.approvals.is_empty());
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn plan_collect_fees_calldata_round_trips_all_fields() {
let p = CollectFeesParams {
token_id: U256::from(42u64),
recipient: address!("0000000000000000000000000000000000000099"),
token0: address!("0000000000000000000000000000000000000001"),
token1: address!("0000000000000000000000000000000000000002"),
caller: Address::ZERO,
};
let frag = collect_fees(&p, TEST_CFG.position_mgr);
let decoded = collectCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
assert_eq!(decoded.params.tokenId, p.token_id);
assert_eq!(decoded.params.recipient, p.recipient);
assert_eq!(decoded.params.amount0Max, u128::MAX);
assert_eq!(decoded.params.amount1Max, u128::MAX);
}
#[test]
fn plan_remove_liquidity_and_collect_targets_nfpm_with_multicall_outer() {
let p = RemoveAndCollectParams {
token_id: U256::from(1u64),
liquidity: 1000u128,
amount0_min: Some(U256::from(99u64)),
amount1_min: Some(U256::from(199u64)),
recipient: address!("0000000000000000000000000000000000000099"),
token0: address!("0000000000000000000000000000000000000001"),
token1: address!("0000000000000000000000000000000000000002"),
caller: Address::ZERO,
};
let frag = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
assert_eq!(
&frag.calls[0].calldata[..4],
&[0xac, 0x96, 0x50, 0xd8],
"outer call selector must be multicall"
);
assert!(frag.approvals.is_empty());
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn plan_remove_liquidity_and_collect_inner_decoded_correctly() {
let p = RemoveAndCollectParams {
token_id: U256::from(42u64),
liquidity: 1_000_000_000_000u128,
amount0_min: Some(U256::from(500_000u64)),
amount1_min: Some(U256::from(1_000_000_000u64)),
recipient: address!("0000000000000000000000000000000000000099"),
token0: address!("0000000000000000000000000000000000000001"),
token1: address!("0000000000000000000000000000000000000002"),
caller: Address::ZERO,
};
let frag = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata)
.expect("decode outer multicall");
assert_eq!(outer.data.len(), 2);
assert_eq!(&outer.data[0][..4], decreaseLiquidityCall::SELECTOR.as_slice());
assert_eq!(&outer.data[1][..4], collectCall::SELECTOR.as_slice());
let decrease = decreaseLiquidityCall::abi_decode(&outer.data[0]).expect("decode decrease");
assert_eq!(decrease.params.tokenId, p.token_id);
assert_eq!(decrease.params.liquidity, p.liquidity);
assert_eq!(decrease.params.amount0Min, U256::from(500_000u64));
assert_eq!(decrease.params.amount1Min, U256::from(1_000_000_000u64));
assert_eq!(decrease.params.deadline, U256::from(9_999_999_999u64));
let collect = collectCall::abi_decode(&outer.data[1]).expect("decode collect");
assert_eq!(collect.params.tokenId, p.token_id);
assert_eq!(collect.params.recipient, p.recipient);
assert_eq!(collect.params.amount0Max, u128::MAX);
assert_eq!(collect.params.amount1Max, u128::MAX);
}
#[test]
fn plan_remove_liquidity_and_collect_defaults_missing_mins_to_zero() {
let p = RemoveAndCollectParams {
token_id: U256::from(42u64),
liquidity: 1_000_000_000_000u128,
amount0_min: None,
amount1_min: None,
recipient: address!("0000000000000000000000000000000000000099"),
token0: address!("0000000000000000000000000000000000000001"),
token1: address!("0000000000000000000000000000000000000002"),
caller: Address::ZERO,
};
let frag = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata)
.expect("decode outer multicall");
let decrease = decreaseLiquidityCall::abi_decode(&outer.data[0]).expect("decode decrease");
assert_eq!(decrease.params.amount0Min, U256::ZERO);
assert_eq!(decrease.params.amount1Min, U256::ZERO);
}
#[test]
fn swap_exact_in_calldata_round_trips_all_fields() {
let token_in = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token_out = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let s = dummy_pool_state(token_in, token_out); let q = Quote {
amount_in: U256::from(1_000_000u64),
amount_out: U256::from(500_000_000_000_000u64),
sqrt_price_x96_after: s.sqrt_price_x96,
price_impact_bps: 0,
};
let recipient = address!("0000000000000000000000000000000000000099");
let p = ExactInParams { token_in, token_out, amount_in: q.amount_in, recipient };
let deadline = 1_700_000_000u64;
let frag = swap_exact_in(
&s,
&q,
&p,
SlippageBps::new(50),
deadline,
TEST_CFG.router,
SwapRouterKind::V1,
);
let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
let params = decoded.params;
assert_eq!(params.tokenIn, token_in);
assert_eq!(params.tokenOut, token_out);
assert_eq!(params.fee, alloy_primitives::aliases::U24::from(3000u32));
assert_eq!(params.recipient, recipient);
assert_eq!(params.deadline, U256::from(deadline));
assert_eq!(params.amountIn, q.amount_in);
let expected_min = q.amount_out * U256::from(9950u64) / U256::from(10000u64);
assert_eq!(params.amountOutMinimum, expected_min);
assert_eq!(params.sqrtPriceLimitX96, alloy_primitives::aliases::U160::ZERO);
}
#[test]
fn add_liquidity_calldata_round_trips_all_fields() {
let p = AddLiquidityParams {
token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
fee: 3000,
tick_lower: -201_000,
tick_upper: -198_960,
amount0_desired: U256::from(1_000_000u64),
amount1_desired: U256::from(500_000_000_000_000u64),
recipient: address!("0000000000000000000000000000000000000099"),
};
let deadline = 1_700_000_000u64;
let frag = add_liquidity(&p, SlippageBps::new(100), deadline, TEST_CFG.position_mgr);
let decoded = mintCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
let mp = decoded.params;
assert_eq!(mp.token0, p.token0);
assert_eq!(mp.token1, p.token1);
assert_eq!(mp.fee, alloy_primitives::aliases::U24::from(3000u32));
assert_eq!(mp.tickLower, alloy_primitives::aliases::I24::try_from(-201_000i32).unwrap());
assert_eq!(mp.tickUpper, alloy_primitives::aliases::I24::try_from(-198_960i32).unwrap());
assert_eq!(mp.amount0Desired, p.amount0_desired);
assert_eq!(mp.amount1Desired, p.amount1_desired);
let expected_min0 = p.amount0_desired * U256::from(9900u64) / U256::from(10000u64);
assert_eq!(mp.amount0Min, expected_min0);
let expected_min1 = p.amount1_desired * U256::from(9900u64) / U256::from(10000u64);
assert_eq!(mp.amount1Min, expected_min1);
assert_eq!(mp.recipient, p.recipient);
assert_eq!(mp.deadline, U256::from(deadline));
}
#[test]
fn plan_increase_liquidity_targets_position_manager_two_approvals() {
let token0 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token1 = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let frag = increase_liquidity(
U256::from(123_456u64),
token0,
token1,
U256::from(1_000_000u64),
U256::from(500_000_000_000_000u64),
SlippageBps::new(50),
9_999_999_999,
TEST_CFG.position_mgr,
);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
assert_eq!(frag.calls[0].value, U256::ZERO);
assert_eq!(frag.approvals.len(), 2);
assert_eq!(frag.approvals[0].token, token0);
assert_eq!(frag.approvals[0].spender, TEST_CFG.position_mgr);
assert_eq!(frag.approvals[0].min_amount, U256::from(1_000_000u64));
assert_eq!(frag.approvals[1].token, token1);
assert_eq!(frag.approvals[1].spender, TEST_CFG.position_mgr);
assert_eq!(frag.approvals[1].min_amount, U256::from(500_000_000_000_000u64));
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn increase_liquidity_calldata_round_trips_all_fields() {
let token0 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token1 = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let token_id = U256::from(987_654u64);
let amount0 = U256::from(2_000_000u64);
let amount1 = U256::from(750_000_000_000_000u64);
let deadline = 1_700_000_000u64;
let frag = increase_liquidity(
token_id,
token0,
token1,
amount0,
amount1,
SlippageBps::new(100),
deadline,
TEST_CFG.position_mgr,
);
let decoded =
increaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
let ip = decoded.params;
assert_eq!(ip.tokenId, token_id);
assert_eq!(ip.amount0Desired, amount0);
assert_eq!(ip.amount1Desired, amount1);
let expected_min0 = amount0 * U256::from(9900u64) / U256::from(10000u64);
assert_eq!(ip.amount0Min, expected_min0);
let expected_min1 = amount1 * U256::from(9900u64) / U256::from(10000u64);
assert_eq!(ip.amount1Min, expected_min1);
assert_eq!(ip.deadline, U256::from(deadline));
}
#[test]
fn increase_liquidity_selector_matches_v3_canonical() {
assert_eq!(increaseLiquidityCall::SELECTOR, [0x21, 0x9f, 0x5d, 0x17]);
}
#[test]
fn swap_exact_in_with_fee_fn_injects_fee_into_calldata() {
let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let s = dummy_pool_state(token_in, token_out); let q = Quote {
amount_in: U256::from(1_000_000u64),
amount_out: U256::from(500_000_000_000_000u64),
sqrt_price_x96_after: s.sqrt_price_x96,
price_impact_bps: 0,
};
let p = ExactInParams {
token_in,
token_out,
amount_in: q.amount_in,
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let frag = swap_exact_in_with_fee_fn(
&s,
&q,
&p,
SlippageBps::new(50),
9_999_999_999,
TEST_CFG.router,
SwapRouterKind::V1,
|_| 500,
);
let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
assert_eq!(decoded.params.fee, alloy_primitives::aliases::U24::from(500u32));
}
#[test]
fn slippage_min_zero_bps_returns_full_amount() {
let out = apply_slippage_min(U256::from(1_000_000u64), SlippageBps::new(0));
assert_eq!(out, U256::from(1_000_000u64));
}
#[test]
fn slippage_min_10000_bps_returns_zero() {
let out = apply_slippage_min(U256::from(1_000_000u64), SlippageBps::new(10_000));
assert_eq!(out, U256::ZERO);
}
#[test]
fn slippage_min_zero_quoted_returns_zero() {
let out = apply_slippage_min(U256::ZERO, SlippageBps::new(50));
assert_eq!(out, U256::ZERO);
}
#[test]
fn slippage_min_max_u256_does_not_overflow() {
let out = apply_slippage_min(U256::MAX, SlippageBps::new(50));
assert!(out > U256::ZERO, "expected nonzero result for MAX input");
assert!(out < U256::MAX, "expected result < MAX (slippage was applied)");
}
}