use alloy::primitives::{I256, U256};
use tycho_common::simulation::errors::SimulationError;
use super::{sqrt_price_math, FEE_PIPS_DENOMINATOR};
use crate::evm::protocol::{
safe_math::safe_add_u256,
utils::solidity_math::{mul_div, mul_div_rounding_up},
};
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SwapStepResult {
pub(crate) sqrt_price: U256,
pub(crate) amount_in_with_fee: U256,
pub(crate) amount_out: U256,
}
pub(crate) fn compute_swap_step(
sqrt_ratio_current: U256,
sqrt_ratio_target: U256,
liquidity: u128,
amount_remaining: I256,
fee_pips: u32,
) -> Result<SwapStepResult, SimulationError> {
let zero_for_one = sqrt_ratio_current >= sqrt_ratio_target;
let exact_in = amount_remaining >= I256::from_raw(U256::from(0u64));
let sqrt_ratio_next: U256;
let mut amount_in = U256::from(0u64);
let mut amount_out = U256::from(0u64);
if exact_in {
let amount_remaining_less_fee = mul_div(
amount_remaining.into_raw(),
U256::from(FEE_PIPS_DENOMINATOR - fee_pips),
U256::from(FEE_PIPS_DENOMINATOR),
)?;
amount_in = if zero_for_one {
sqrt_price_math::get_amount0_delta(
sqrt_ratio_target,
sqrt_ratio_current,
liquidity,
true,
)?
} else {
sqrt_price_math::get_amount1_delta(
sqrt_ratio_current,
sqrt_ratio_target,
liquidity,
true,
)?
};
if amount_remaining_less_fee >= amount_in {
sqrt_ratio_next = sqrt_ratio_target
} else {
sqrt_ratio_next = sqrt_price_math::get_next_sqrt_price_from_input(
sqrt_ratio_current,
liquidity,
amount_remaining_less_fee,
zero_for_one,
)?
}
} else {
amount_out = if zero_for_one {
sqrt_price_math::get_amount1_delta(
sqrt_ratio_target,
sqrt_ratio_current,
liquidity,
false,
)?
} else {
sqrt_price_math::get_amount0_delta(
sqrt_ratio_current,
sqrt_ratio_target,
liquidity,
false,
)?
};
if amount_remaining.abs().into_raw() > amount_out {
sqrt_ratio_next = sqrt_ratio_target;
} else {
sqrt_ratio_next = sqrt_price_math::get_next_sqrt_price_from_output(
sqrt_ratio_current,
liquidity,
amount_remaining.abs().into_raw(),
zero_for_one,
)?;
}
}
let max = sqrt_ratio_target == sqrt_ratio_next;
if zero_for_one {
if !exact_in {
amount_in = sqrt_price_math::get_amount0_delta(
sqrt_ratio_next,
sqrt_ratio_current,
liquidity,
true,
)?;
}
amount_out = if max && !exact_in {
amount_out
} else {
sqrt_price_math::get_amount1_delta(
sqrt_ratio_next,
sqrt_ratio_current,
liquidity,
false,
)?
}
} else {
if !exact_in {
amount_in = sqrt_price_math::get_amount1_delta(
sqrt_ratio_current,
sqrt_ratio_next,
liquidity,
true,
)?;
}
amount_out = if max && !exact_in {
amount_out
} else {
sqrt_price_math::get_amount0_delta(
sqrt_ratio_current,
sqrt_ratio_next,
liquidity,
false,
)?
};
}
if !exact_in && amount_out > amount_remaining.abs().into_raw() {
amount_out = amount_remaining.abs().into_raw();
}
let amount_in_with_fee = if exact_in && !max {
let amount_remaining = amount_remaining.into_raw();
debug_assert!(
if zero_for_one {
sqrt_price_math::get_amount0_delta(
sqrt_ratio_next,
sqrt_ratio_current,
liquidity,
true,
)
} else {
sqrt_price_math::get_amount1_delta(
sqrt_ratio_current,
sqrt_ratio_next,
liquidity,
true,
)
}
.is_ok_and(|amount_in| amount_in <= amount_remaining),
"a step stopping short of its target needs no more input than remains"
);
amount_remaining
} else {
let fee_amount = mul_div_rounding_up(
amount_in,
U256::from(fee_pips),
U256::from(FEE_PIPS_DENOMINATOR - fee_pips),
)?;
safe_add_u256(amount_in, fee_amount)?
};
Ok(SwapStepResult { sqrt_price: sqrt_ratio_next, amount_in_with_fee, amount_out })
}
#[cfg(test)]
mod tests {
use std::{ops::Neg, str::FromStr};
use super::*;
struct TestCase {
price: U256,
target: U256,
liquidity: u128,
remaining: I256,
fee: u32,
exp: (U256, U256, U256, U256),
}
#[test]
fn test_compute_swap_step() {
let cases = vec![
TestCase {
price: U256::from_str("1917240610156820439288675683655550").unwrap(),
target: U256::from_str("1919023616462402511535565081385034").unwrap(),
liquidity: 23130341825817804069u128,
remaining: I256::exp10(18),
fee: 500,
exp: (
U256::from_str("1917244033735642980420262835667387").unwrap(),
U256::from_str("999500000000000000").unwrap(),
U256::from_str("1706820897").unwrap(),
U256::from_str("500000000000000").unwrap(),
),
},
TestCase {
price: U256::from_str("1917240610156820439288675683655550").unwrap(),
target: U256::from_str("1919023616462402511535565081385034").unwrap(),
liquidity: 23130341825817804069u128,
remaining: I256::exp10(18).neg(),
fee: 500,
exp: (
U256::from_str("1919023616462402511535565081385034").unwrap(),
U256::from_str("520541484453545253034").unwrap(),
U256::from_str("888091216672").unwrap(),
U256::from_str("260400942698121688").unwrap(),
),
},
TestCase {
price: U256::from_str("1917240610156820439288675683655550").unwrap(),
target: U256::from_str("1908498483466244238266951834509291").unwrap(),
liquidity: 23130341825817804069u128,
remaining: I256::exp10(18).neg(),
fee: 500,
exp: (
U256::from_str("1917237184865352164019453920762266").unwrap(),
U256::from_str("1707680836").unwrap(),
U256::from_str("1000000000000000000").unwrap(),
U256::from_str("854268").unwrap(),
),
},
TestCase {
price: U256::from_str("1917240610156820439288675683655550").unwrap(),
target: U256::from_str("1908498483466244238266951834509291").unwrap(),
liquidity: 23130341825817804069u128,
remaining: I256::exp10(18),
fee: 500,
exp: (
U256::from_str("1908498483466244238266951834509291").unwrap(),
U256::from_str("4378348149175").unwrap(),
U256::from_str("2552228553845698906796").unwrap(),
U256::from_str("2190269210").unwrap(),
),
},
TestCase {
price: U256::from_str("1917240610156820439288675683655550").unwrap(),
target: U256::from_str("1908498483466244238266951834509291").unwrap(),
liquidity: 0u128,
remaining: I256::exp10(18),
fee: 500,
exp: (
U256::from_str("1908498483466244238266951834509291").unwrap(),
U256::ZERO,
U256::ZERO,
U256::ZERO,
),
},
];
for case in cases {
let res = compute_swap_step(
case.price,
case.target,
case.liquidity,
case.remaining,
case.fee,
)
.unwrap();
let (sqrt_price, amount_in, amount_out, fee_amount) = case.exp;
assert_eq!(
res,
SwapStepResult {
sqrt_price,
amount_in_with_fee: amount_in + fee_amount,
amount_out
}
);
}
}
#[test]
fn test_amount_in_with_fee_is_smallest_input_reaching_target() {
let price = U256::from_str("1917240610156820439288675683655550").unwrap();
let liquidity = 23130341825817804069u128;
let targets = [
U256::from_str("1908498483466244238266951834509291").unwrap(),
U256::from_str("1919023616462402511535565081385034").unwrap(),
];
for target in targets {
for fee in [100, 500, 3_000, 10_000, 999_999] {
let needed = compute_swap_step(price, target, liquidity, I256::exp10(30), fee)
.unwrap()
.amount_in_with_fee;
let at_needed = I256::from_raw(needed);
let below_needed = I256::from_raw(needed - U256::from(1u64));
let reached = compute_swap_step(price, target, liquidity, at_needed, fee).unwrap();
let short = compute_swap_step(price, target, liquidity, below_needed, fee).unwrap();
assert_eq!((reached.sqrt_price, reached.amount_in_with_fee), (target, needed));
assert_ne!(short.sqrt_price, target);
}
}
}
}