use crate::arithmetic::{fp_div, fp_div_ceil, fp_mul};
use crate::constants::SCALE;
use crate::error::SolMathError;
use crate::hp::pow_fixed_hp;
use crate::mul_div::mul_div_ceil_u128;
pub fn token_to_fp(raw_amount: u64, token_decimals: u8) -> Result<u128, SolMathError> {
let amount = raw_amount as u128;
let decimals = token_decimals as u32;
if decimals > 38 {
return Err(SolMathError::DomainError);
}
if decimals <= 12 {
amount
.checked_mul(10u128.pow(12 - decimals))
.ok_or(SolMathError::Overflow)
} else {
Ok(amount / 10u128.pow(decimals - 12))
}
}
pub fn fp_to_token_floor(fp_amount: u128, token_decimals: u8) -> Result<u64, SolMathError> {
let decimals = token_decimals as u32;
if decimals > 38 {
return Err(SolMathError::DomainError);
}
let raw = if decimals <= 12 {
fp_amount / 10u128.pow(12 - decimals)
} else {
fp_amount
.checked_mul(10u128.pow(decimals - 12))
.ok_or(SolMathError::Overflow)?
};
if raw > u64::MAX as u128 {
return Err(SolMathError::Overflow);
}
Ok(raw as u64)
}
pub fn fp_to_token_ceil(fp_amount: u128, token_decimals: u8) -> Result<u64, SolMathError> {
let decimals = token_decimals as u32;
if decimals > 38 {
return Err(SolMathError::DomainError);
}
let divisor = if decimals <= 12 {
10u128.pow(12 - decimals)
} else {
let raw = fp_amount
.checked_mul(10u128.pow(decimals - 12))
.ok_or(SolMathError::Overflow)?;
if raw > u64::MAX as u128 {
return Err(SolMathError::Overflow);
}
return Ok(raw as u64);
};
let raw = fp_amount
.checked_add(divisor - 1)
.ok_or(SolMathError::Overflow)?
/ divisor;
if raw > u64::MAX as u128 {
return Err(SolMathError::Overflow);
}
Ok(raw as u64)
}
pub fn weighted_pool_swap(
balance_in: u128,
balance_out: u128,
weight_in: u128,
weight_out: u128,
amount_in: u128,
fee_rate: u128,
) -> Result<(u128, u128), SolMathError> {
if weight_out == 0 {
return Err(SolMathError::DivisionByZero);
}
if weight_in == 0 {
return Err(SolMathError::DomainError);
}
if balance_in == 0 || balance_out == 0 {
return Err(SolMathError::DomainError);
}
if fee_rate > SCALE {
return Err(SolMathError::DomainError);
}
if amount_in == 0 {
return Ok((0, 0));
}
let denominator = balance_in
.checked_add(amount_in)
.ok_or(SolMathError::Overflow)?;
let min_balance = denominator / 100 + u128::from(denominator % 100 != 0);
if balance_in < min_balance {
return Err(SolMathError::DomainError);
}
let weight_q = weight_in / weight_out;
if weight_q > 20 || (weight_q == 20 && weight_in % weight_out != 0) {
return Err(SolMathError::DomainError);
}
let ratio = fp_div_ceil(balance_in, denominator)?;
let weight_ratio = fp_div(weight_in, weight_out)?;
if ratio < SCALE / 100 || weight_ratio > 20 * SCALE {
return Err(SolMathError::DomainError);
}
let power = pow_fixed_hp(ratio, weight_ratio)?;
if power > SCALE {
return Err(SolMathError::Overflow);
}
let power_up = power.checked_add(5).unwrap_or(SCALE).min(SCALE);
let one_minus_power = SCALE - power_up;
let gross_out = fp_mul(balance_out, one_minus_power)?.min(balance_out.saturating_sub(1));
let fee = mul_div_ceil_u128(gross_out, fee_rate, SCALE)?;
if fee > gross_out {
return Err(SolMathError::Overflow);
}
let net_out = gross_out - fee;
Ok((net_out, fee))
}
#[cfg(test)]
mod adversarial_tests {
use super::*;
#[test]
fn swap_rounding_never_exceeds_exact_equal_weight_output() {
let (out, fee) =
weighted_pool_swap(SCALE, 3 * SCALE, SCALE / 2, SCALE / 2, 2 * SCALE, 0).unwrap();
assert_eq!(fee, 0);
assert!(out <= 2 * SCALE);
}
#[test]
fn rejects_outside_certified_power_domain() {
assert_eq!(
weighted_pool_swap(SCALE, SCALE, 21 * SCALE, SCALE, SCALE, 0),
Err(SolMathError::DomainError)
);
assert_eq!(
weighted_pool_swap(SCALE, SCALE, SCALE, SCALE, 100 * SCALE, 0),
Err(SolMathError::DomainError)
);
}
#[test]
fn exact_domain_checks_cannot_be_bypassed_by_fixed_rounding() {
assert_eq!(
weighted_pool_swap(100_000_000, SCALE, SCALE, SCALE, 9_900_000_001, 0,),
Err(SolMathError::DomainError)
);
assert_eq!(
weighted_pool_swap(
SCALE,
SCALE,
20_000_000_000_000_000_001,
1_000_000_000_000_000_000,
SCALE,
0,
),
Err(SolMathError::DomainError)
);
}
}