use crate::data::{
CollectFeesParams, ExactInParams, GaugeClaim, GaugeEarnedGrid, PlanFragment, PoolState, Quote,
RamsesAddLiquidityParams, RemoveAndCollectParams, RemoveLiquidityParams,
};
use alloy_primitives::{Address, U256};
use alloy_sol_types::{sol, SolCall};
use wp_evm_base::types::{Call, SlippageBps, TokenApproval};
use wp_evm_ramses_interfaces::gauge::IRamsesVoter;
use wp_evm_v3_core::plan::apply_slippage_min;
use wp_evm_velodrome_interfaces::gauge::IVelodromeCLGauge;
sol! {
#[derive(Debug)]
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
int24 tickSpacing;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams params)
external payable returns (uint256 amountOut);
}
pub(crate) mod shadow_nfpm {
use alloy_sol_types::sol;
sol! {
#[derive(Debug)]
struct ShadowMintParams {
address token0;
address token1;
int24 tickSpacing;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
function mint(ShadowMintParams params) external payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
}
}
pub(crate) mod slipstream_nfpm {
use alloy_sol_types::sol;
sol! {
#[derive(Debug)]
struct SlipstreamMintParams {
address token0;
address token1;
int24 tickSpacing;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
uint160 sqrtPriceX96;
}
function mint(SlipstreamMintParams params) external payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
}
}
use shadow_nfpm::{mintCall as shadowMintCall, ShadowMintParams};
use slipstream_nfpm::{mintCall as slipstreamMintCall, SlipstreamMintParams};
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 = ExactInputSingleParams {
tokenIn: params.token_in,
tokenOut: params.token_out,
tickSpacing: alloy_primitives::aliases::I24::try_from(state.tick_spacing)
.expect("tick_spacing fits in i24"),
recipient: params.recipient,
deadline: U256::from(deadline),
amountIn: params.amount_in,
amountOutMinimum: amount_out_min,
sqrtPriceLimitX96: alloy_primitives::aliases::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,
}
}
pub fn add_liquidity(
params: &RamsesAddLiquidityParams,
slippage: SlippageBps,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
let mint_params = ShadowMintParams {
token0: params.token0,
token1: params.token1,
tickSpacing: alloy_primitives::aliases::I24::try_from(params.tick_spacing)
.expect("tick_spacing fits in i24"),
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 = shadowMintCall { 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,
}
}
pub fn add_liquidity_slipstream(
params: &RamsesAddLiquidityParams,
slippage: SlippageBps,
deadline: u64,
position_manager: Address,
) -> PlanFragment {
let mint_params = SlipstreamMintParams {
token0: params.token0,
token1: params.token1,
tickSpacing: alloy_primitives::aliases::I24::try_from(params.tick_spacing)
.expect("tick_spacing fits in i24"),
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),
sqrtPriceX96: alloy_primitives::aliases::U160::ZERO,
};
let calldata = slipstreamMintCall { 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: alloy_primitives::U256,
token0: alloy_primitives::Address,
token1: alloy_primitives::Address,
amount0_desired: alloy_primitives::U256,
amount1_desired: alloy_primitives::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 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)
}
pub fn collect_fees(params: &CollectFeesParams, position_manager: Address) -> PlanFragment {
wp_evm_v3_core::plan::collect_fees(params, position_manager)
}
pub fn claim_cl_gauge_rewards(voter: Address, claims: &[GaugeClaim]) -> PlanFragment {
let gauges: Vec<Address> = claims.iter().map(|c| c.gauge).collect();
let tokens: Vec<Vec<Address>> = claims.iter().map(|c| c.reward_tokens.clone()).collect();
let nfp_token_ids: Vec<Vec<U256>> = claims.iter().map(|c| c.token_ids.clone()).collect();
let calldata = IRamsesVoter::claimClGaugeRewardsCall {
_gauges: gauges,
_tokens: tokens,
_nfpTokenIds: nfp_token_ids,
}
.abi_encode()
.into();
PlanFragment {
calls: vec![Call { target: voter, calldata, value: U256::ZERO }],
approvals: vec![],
value: U256::ZERO,
}
}
pub fn claim_gauge(gauge: Address, token_id: U256) -> PlanFragment {
let calldata = IVelodromeCLGauge::getRewardCall { tokenId: token_id }.abi_encode().into();
PlanFragment {
calls: vec![Call { target: gauge, calldata, value: U256::ZERO }],
approvals: vec![],
value: U256::ZERO,
}
}
pub fn build_gauge_claims(grids: &[GaugeEarnedGrid]) -> Vec<GaugeClaim> {
let mut out = Vec::new();
for g in grids {
let kept_tokens: Vec<usize> = (0..g.reward_tokens.len())
.filter(|&t| (0..g.token_ids.len()).any(|p| g.earned[t][p] > U256::ZERO))
.collect();
let kept_positions: Vec<usize> = (0..g.token_ids.len())
.filter(|&p| (0..g.reward_tokens.len()).any(|t| g.earned[t][p] > U256::ZERO))
.collect();
if kept_tokens.is_empty() || kept_positions.is_empty() {
continue;
}
out.push(GaugeClaim {
gauge: g.gauge,
reward_tokens: kept_tokens.iter().map(|&t| g.reward_tokens[t]).collect(),
token_ids: kept_positions.iter().map(|&p| g.token_ids[p]).collect(),
});
}
out
}
pub fn claim_cl_gauge_rewards_from_grids(
voter: Address,
grids: &[GaugeEarnedGrid],
) -> Option<PlanFragment> {
let claims = build_gauge_claims(grids);
if claims.is_empty() {
None
} else {
Some(claim_cl_gauge_rewards(voter, &claims))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{address, Address};
const ROUTER: Address = address!("0x2222222222222222222222222222222222222222");
const POSITION_MANAGER: Address = address!("0x3333333333333333333333333333333333333333");
fn dummy_pool_state(t0: Address, t1: Address) -> PoolState {
PoolState {
token0: t0,
token1: t1,
fee: 0,
tick_spacing: 50,
sqrt_price_x96: U256::from(1u64) << 96,
liquidity: 0,
tick: 0,
ticks: vec![],
}
}
#[test]
fn plan_swap_emits_ramses_selector() {
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, ROUTER);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, ROUTER);
assert_eq!(&frag.calls[0].calldata[..4], &[0xa0, 0x26, 0x38, 0x3e]);
}
#[test]
fn plan_swap_encodes_tick_spacing_not_fee() {
let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let mut s = dummy_pool_state(token_in, token_out);
s.tick_spacing = 200;
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, ROUTER);
let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
assert_eq!(
decoded.params.tickSpacing,
alloy_primitives::aliases::I24::try_from(200i32).unwrap()
);
}
#[test]
fn add_liquidity_targets_position_manager() {
let p = RamsesAddLiquidityParams {
token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
tick_spacing: 50,
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, POSITION_MANAGER);
assert_eq!(frag.calls[0].target, POSITION_MANAGER);
assert_eq!(frag.approvals.len(), 2);
}
#[test]
fn remove_liquidity_and_collect_targets_position_manager_with_outer_multicall() {
use alloy_sol_types::SolValue;
let p = RemoveAndCollectParams {
token_id: U256::from(1u64),
liquidity: 1_000u128,
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, POSITION_MANAGER);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, POSITION_MANAGER);
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);
let (inner,): (Vec<alloy_primitives::Bytes>,) =
<(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
.expect("decode outer multicall params");
assert_eq!(inner.len(), 2);
assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
}
#[test]
fn remove_liquidity_and_collect_delegation_matches_v3_core_bytewise() {
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 ours = remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
let core =
wp_evm_v3_core::plan::remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
assert_eq!(ours.calls[0].target, core.calls[0].target);
}
#[test]
fn claim_cl_gauge_rewards_targets_voter_with_ordered_arrays() {
use alloy_sol_types::SolCall;
let voter = address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D");
let claims = vec![
GaugeClaim {
gauge: address!("1111111111111111111111111111111111111111"),
reward_tokens: vec![
address!("aaaa000000000000000000000000000000000000"),
address!("bbbb000000000000000000000000000000000000"),
],
token_ids: vec![U256::from(10u64), U256::from(11u64)],
},
GaugeClaim {
gauge: address!("2222222222222222222222222222222222222222"),
reward_tokens: vec![address!("cccc000000000000000000000000000000000000")],
token_ids: vec![U256::from(20u64)],
},
];
let frag = claim_cl_gauge_rewards(voter, &claims);
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, voter);
assert!(frag.approvals.is_empty());
assert_eq!(frag.value, U256::ZERO);
assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
let decoded =
wp_evm_ramses_interfaces::gauge::IRamsesVoter::claimClGaugeRewardsCall::abi_decode(
&frag.calls[0].calldata,
)
.expect("decode");
assert_eq!(decoded._gauges, vec![claims[0].gauge, claims[1].gauge]);
assert_eq!(decoded._tokens[0], claims[0].reward_tokens);
assert_eq!(decoded._nfpTokenIds[1], claims[1].token_ids);
let reference = wp_evm_ramses_interfaces::gauge::IRamsesVoter::claimClGaugeRewardsCall {
_gauges: vec![
address!("1111111111111111111111111111111111111111"),
address!("2222222222222222222222222222222222222222"),
],
_tokens: vec![
vec![
address!("aaaa000000000000000000000000000000000000"),
address!("bbbb000000000000000000000000000000000000"),
],
vec![address!("cccc000000000000000000000000000000000000")],
],
_nfpTokenIds: vec![vec![U256::from(10u64), U256::from(11u64)], vec![U256::from(20u64)]],
}
.abi_encode();
assert_eq!(&frag.calls[0].calldata[..], &reference[..]);
}
#[test]
fn claim_gauge_targets_gauge_with_get_reward_selector() {
let gauge = address!("4444444444444444444444444444444444444444");
let frag = claim_gauge(gauge, U256::from(42u64));
assert_eq!(frag.calls.len(), 1);
assert_eq!(frag.calls[0].target, gauge);
assert!(frag.approvals.is_empty());
assert_eq!(frag.value, U256::ZERO);
assert_eq!(&frag.calls[0].calldata[..4], &[0x1c, 0x4b, 0x77, 0x4b]);
}
#[test]
fn claim_cl_gauge_rewards_from_grids_none_when_empty_or_all_zero() {
let voter = Address::from([9u8; 20]);
let zero = grid(0x11, 1, &[1], vec![vec![0]]);
assert!(claim_cl_gauge_rewards_from_grids(voter, &[zero]).is_none());
assert!(claim_cl_gauge_rewards_from_grids(voter, &[]).is_none());
}
#[test]
fn claim_cl_gauge_rewards_from_grids_some_targets_voter_when_nonzero() {
let voter = address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D");
let g = grid(0x11, 1, &[1], vec![vec![7]]);
let frag = claim_cl_gauge_rewards_from_grids(voter, &[g]).expect("nonzero -> Some");
assert_eq!(frag.calls[0].target, voter);
assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
}
fn grid(
gauge_byte: u8,
tokens: usize,
ids: &[u64],
earned: Vec<Vec<u64>>,
) -> crate::data::GaugeEarnedGrid {
crate::data::GaugeEarnedGrid {
gauge: Address::from([gauge_byte; 20]),
reward_tokens: (0..tokens).map(|i| Address::from([i as u8 + 1; 20])).collect(),
token_ids: ids.iter().map(|&i| U256::from(i)).collect(),
earned: earned.into_iter().map(|r| r.into_iter().map(U256::from).collect()).collect(),
}
}
#[test]
fn build_gauge_claims_drops_all_zero_token_and_position() {
let g = grid(0x11, 2, &[100, 101], vec![vec![0, 5], vec![0, 0]]);
let claims = build_gauge_claims(&[g]);
assert_eq!(claims.len(), 1);
assert_eq!(claims[0].reward_tokens, vec![Address::from([1u8; 20])]);
assert_eq!(claims[0].token_ids, vec![U256::from(101u64)]);
}
#[test]
fn build_gauge_claims_drops_fully_empty_gauge() {
let g = grid(0x22, 2, &[1, 2], vec![vec![0, 0], vec![0, 0]]);
assert!(build_gauge_claims(&[g]).is_empty());
}
#[test]
fn build_gauge_claims_keeps_multiple_gauges() {
let a = grid(0x11, 1, &[1], vec![vec![9]]);
let b = grid(0x22, 1, &[2], vec![vec![0]]); let c = grid(0x33, 1, &[3], vec![vec![3]]);
let claims = build_gauge_claims(&[a, b, c]);
assert_eq!(claims.len(), 2);
assert_eq!(claims[0].gauge, Address::from([0x11u8; 20]));
assert_eq!(claims[1].gauge, Address::from([0x33u8; 20]));
}
#[test]
fn build_gauge_claims_empty_input_empty_output() {
assert!(build_gauge_claims(&[]).is_empty());
}
}