use crate::data::{
AddLiquidityParams, CollectFeesParams, ExactInParams, PlanFragment, PoolState, Quote,
RemoveAndCollectParams, RemoveLiquidityParams,
};
use alloy_primitives::{aliases::U160, Address, U256};
use alloy_sol_types::{sol, SolCall};
use wp_evm_base::types::{Call, SlippageBps, TokenApproval};
use wp_evm_v3_core::plan::apply_slippage_min;
pub use wp_evm_v3_core::plan::{collectCall, decreaseLiquidityCall, increaseLiquidityCall};
sol! {
#[derive(Debug)]
struct AlgebraExactInputSingleParams {
address tokenIn;
address tokenOut;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 limitSqrtPrice;
}
function exactInputSingle(AlgebraExactInputSingleParams params)
external payable returns (uint256 amountOut);
}
pub fn swap_exact_in(
_state: &PoolState,
quote: &Quote,
params: &ExactInParams,
slippage: SlippageBps,
deadline: u64,
router: Address,
) -> PlanFragment {
let amount_out_min = apply_slippage_min(quote.amount_out, slippage);
let call_params = AlgebraExactInputSingleParams {
tokenIn: params.token_in,
tokenOut: params.token_out,
recipient: params.recipient,
deadline: U256::from(deadline),
amountIn: params.amount_in,
amountOutMinimum: amount_out_min,
limitSqrtPrice: U160::ZERO,
};
let calldata = 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,
}
}
sol! {
#[derive(Debug)]
struct AlgebraMintParams {
address token0;
address token1;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
function mint(AlgebraMintParams params) external payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
}
pub fn add_liquidity(
params: &AddLiquidityParams,
slippage: SlippageBps,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
let mint_params = AlgebraMintParams {
token0: params.token0,
token1: params.token1,
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)]
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 {
wp_evm_v3_core::plan::increase_liquidity(
token_id,
token0,
token1,
amount0_desired,
amount1_desired,
slippage,
deadline,
position_manager,
)
}
pub fn remove_liquidity(
params: &RemoveLiquidityParams,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
wp_evm_v3_core::plan::remove_liquidity(params, deadline, position_manager)
}
pub fn collect_fees(params: &CollectFeesParams, position_manager: Address) -> PlanFragment {
wp_evm_v3_core::plan::collect_fees(params, position_manager)
}
pub fn remove_liquidity_and_collect(
params: &RemoveAndCollectParams,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
wp_evm_v3_core::plan::remove_liquidity_and_collect(params, deadline, position_manager)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::AlgebraProtocolConfig;
use alloy_primitives::{address, b256, Address};
const TEST_CFG: AlgebraProtocolConfig = AlgebraProtocolConfig {
factory: address!("0x411b0fAcC3489691f28ad58c47006AF5E3Ab3A28"),
pool_deployer: address!("0x1111111111111111111111111111111111111112"),
router: address!("0xf5b509bB0909a69B1c207E495f687a596C168E12"),
position_mgr: address!("0x8eF88E4c7CfbbaC1C163f7eddd4B578792201de6"),
init_code_hash: b256!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
multicall: address!("0xcA11bde05977b3631167028862bE2a173976CA11"),
quoter: None,
farming_center: None,
};
fn dummy_pool_state() -> PoolState {
PoolState {
token0: address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"), token1: address!("0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"), fee: 500,
tick_spacing: 60,
sqrt_price_x96: U256::from(1u64) << 96,
liquidity: 0,
tick: 0,
ticks: vec![],
}
}
fn dummy_quote(amount_in: U256, amount_out: U256) -> Quote {
Quote {
amount_in,
amount_out,
sqrt_price_x96_after: U256::from(1u64) << 96,
price_impact_bps: 0,
effective_fee_pips: 500,
}
}
#[test]
fn selector_is_0xbc651188() {
let expected: [u8; 4] = [0xbc, 0x65, 0x11, 0x88];
assert_eq!(
exactInputSingleCall::SELECTOR,
expected,
"AlgebraV1.9 selector mismatch — check that ExactInputSingleParams has 7 fields and no fee"
);
}
#[test]
fn calldata_decodes_without_fee_field() {
let s = dummy_pool_state();
let token_in = s.token0;
let token_out = s.token1;
let amount_in = U256::from(1_000_000u64);
let q = dummy_quote(amount_in, U256::from(500_000_000_000_000u64));
let p = ExactInParams {
token_in,
token_out,
amount_in,
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), 9_999_999_999, TEST_CFG.router);
assert_eq!(frag.calls.len(), 1);
let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata)
.expect("calldata should round-trip through abi_decode");
assert_eq!(decoded.params.tokenIn, token_in);
assert_eq!(decoded.params.tokenOut, token_out);
assert_eq!(decoded.params.recipient, p.recipient);
assert_eq!(decoded.params.amountIn, amount_in);
assert_eq!(decoded.params.limitSqrtPrice, U160::ZERO);
let expected_min = U256::from(497_500_000_000_000u64);
assert_eq!(decoded.params.amountOutMinimum, expected_min);
}
#[test]
fn swap_exact_in_calldata_round_trips_all_fields() {
let s = dummy_pool_state();
let token_in = s.token0;
let token_out = s.token1;
let amount_in = U256::from(1_000_000u64);
let amount_out = U256::from(500_000_000_000_000u64);
let q = dummy_quote(amount_in, amount_out);
let recipient = address!("0000000000000000000000000000000000000099");
let p = ExactInParams { token_in, token_out, amount_in, recipient };
let deadline = 1_700_000_000u64;
let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), deadline, TEST_CFG.router);
let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata)
.expect("calldata should decode ok");
let dp = decoded.params;
assert_eq!(dp.tokenIn, token_in);
assert_eq!(dp.tokenOut, token_out);
assert_eq!(dp.recipient, recipient);
assert_eq!(dp.deadline, U256::from(deadline));
assert_eq!(dp.amountIn, amount_in);
let expected_min = amount_out * U256::from(9950u64) / U256::from(10000u64);
assert_eq!(dp.amountOutMinimum, expected_min);
assert_eq!(dp.limitSqrtPrice, U160::ZERO);
}
#[test]
fn plan_targets_router_with_correct_approval() {
let s = dummy_pool_state();
let amount_in = U256::from(1_000_000u64);
let q = dummy_quote(amount_in, U256::from(500_000_000_000_000u64));
let p = ExactInParams {
token_in: s.token0,
token_out: s.token1,
amount_in,
recipient: address!("0x0000000000000000000000000000000000000099"),
};
let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), 9_999_999_999, TEST_CFG.router);
assert_eq!(frag.calls[0].target, TEST_CFG.router);
assert_eq!(frag.approvals.len(), 1);
assert_eq!(frag.approvals[0].token, s.token0);
assert_eq!(frag.approvals[0].spender, TEST_CFG.router);
assert_eq!(frag.approvals[0].min_amount, amount_in);
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn algebra_mint_selector_differs_from_uniswap_v3() {
let uniswap_v3_mint: [u8; 4] = [0x88, 0x31, 0x64, 0x56];
assert_ne!(
mintCall::SELECTOR,
uniswap_v3_mint,
"Algebra mint selector should differ from Uniswap V3 because Algebra MintParams has no fee field"
);
}
#[test]
fn plan_add_liquidity_targets_position_manager() {
let p = AddLiquidityParams {
token0: address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"),
token1: address!("0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"),
tick_lower: -887_272,
tick_upper: 887_272,
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 add_liquidity_calldata_round_trips_all_fields() {
let p = AddLiquidityParams {
token0: address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"),
token1: address!("0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"),
tick_lower: -887_272,
tick_upper: 887_272,
amount0_desired: U256::from(1_000_000u64),
amount1_desired: U256::from(500_000_000_000_000u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
};
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.tickLower, alloy_primitives::aliases::I24::try_from(-887_272i32).unwrap());
assert_eq!(mp.tickUpper, alloy_primitives::aliases::I24::try_from(887_272i32).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!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174");
let token1 = address!("0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619");
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.approvals.len(), 2);
assert_eq!(frag.approvals[0].token, token0);
assert_eq!(frag.approvals[1].token, token1);
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn increase_liquidity_delegation_matches_v3_core_bytewise() {
let token0 = address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174");
let token1 = address!("0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619");
let token_id = U256::from(987_654u64);
let amount0 = U256::from(2_000_000u64);
let amount1 = U256::from(750_000_000_000_000u64);
let slippage = SlippageBps::new(100);
let deadline = 1_700_000_000u64;
let ours = increase_liquidity(
token_id,
token0,
token1,
amount0,
amount1,
slippage,
deadline,
TEST_CFG.position_mgr,
);
let core = wp_evm_v3_core::plan::increase_liquidity(
token_id,
token0,
token1,
amount0,
amount1,
slippage,
deadline,
TEST_CFG.position_mgr,
);
assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
assert_eq!(ours.calls[0].target, core.calls[0].target);
assert_eq!(ours.approvals, core.approvals);
}
#[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 remove_liquidity_delegation_matches_v3_core_bytewise() {
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 ours = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
let core = wp_evm_v3_core::plan::remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
assert_eq!(ours.calls[0].target, core.calls[0].target);
}
#[test]
fn remove_liquidity_default_mins_delegation_matches_v3_core_bytewise() {
let p = RemoveLiquidityParams {
token_id: U256::from(42u64),
liquidity: 1_000_000_000_000u128,
amount0_min: None,
amount1_min: None,
};
let ours = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
let core = wp_evm_v3_core::plan::remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
assert_eq!(ours.calls[0].target, core.calls[0].target);
}
#[test]
fn plan_collect_fees_targets_position_manager_no_approvals() {
let p = CollectFeesParams {
token_id: U256::from(42u64),
recipient: address!("0x0000000000000000000000000000000000000099"),
token0: Address::ZERO,
token1: Address::ZERO,
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 collect_fees_delegation_matches_v3_core_bytewise() {
let p = CollectFeesParams {
token_id: U256::from(42u64),
recipient: address!("0000000000000000000000000000000000000099"),
token0: Address::ZERO,
token1: Address::ZERO,
caller: Address::ZERO,
};
let ours = collect_fees(&p, TEST_CFG.position_mgr);
let core = wp_evm_v3_core::plan::collect_fees(&p, TEST_CFG.position_mgr);
assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
assert_eq!(ours.calls[0].target, core.calls[0].target);
}
fn fixture_remove_and_collect_params() -> RemoveAndCollectParams {
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,
burn: false,
}
}
#[test]
fn plan_remove_liquidity_and_collect_targets_nfpm_with_outer_multicall() {
let frag = remove_liquidity_and_collect(
&fixture_remove_and_collect_params(),
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 selector must be multicall(bytes[])"
);
assert!(frag.approvals.is_empty());
assert_eq!(frag.value, U256::ZERO);
}
#[test]
fn remove_liquidity_and_collect_delegation_matches_v3_core_bytewise() {
let p = fixture_remove_and_collect_params();
let ours = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
let core = wp_evm_v3_core::plan::remove_liquidity_and_collect(
&p,
9_999_999_999,
TEST_CFG.position_mgr,
);
assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
assert_eq!(ours.calls[0].target, core.calls[0].target);
}
#[test]
fn remove_liquidity_and_collect_default_mins_delegation_matches_v3_core_bytewise() {
let mut p = fixture_remove_and_collect_params();
p.amount0_min = None;
p.amount1_min = None;
let ours = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
let core = wp_evm_v3_core::plan::remove_liquidity_and_collect(
&p,
9_999_999_999,
TEST_CFG.position_mgr,
);
assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
assert_eq!(ours.calls[0].target, core.calls[0].target);
}
}