#[cfg(feature = "wasm")]
use equanetwork_macros::wasm_expose;
use super::consts::{MAX_TOTAL_SKEW_PER_1M, PER_1M_DENOMINATOR};
use super::error::{
CoreError, AMOUNT_EXCEEDS_MAX_U64, ARITHMETIC_OVERFLOW, DIVISION_BY_ZERO,
INSUFFICIENT_LIQUIDITY, INVALID_FEE, INVALID_PRICE, INVALID_SKEW, MAX_SWAP_EXCEEDED,
};
use super::skew::{average_skew_per_m, SkewExponent};
use super::U128;
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn base_out_1_to_1(
amount_in: u64,
in_decimals: u8,
out_decimals: u8,
) -> Result<u64, CoreError> {
if amount_in == 0 {
return Ok(0);
}
let in_scale = 10u128
.checked_pow(in_decimals as u32)
.ok_or(ARITHMETIC_OVERFLOW)?;
let out_scale = 10u128
.checked_pow(out_decimals as u32)
.ok_or(ARITHMETIC_OVERFLOW)?;
let numerator = (amount_in as u128)
.checked_mul(out_scale)
.ok_or(ARITHMETIC_OVERFLOW)?;
let out = numerator.checked_div(in_scale).ok_or(DIVISION_BY_ZERO)?;
out.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn fair_out_with_prices(
amount_in: u64,
in_decimals: u8,
out_decimals: u8,
price_in_q64: U128,
price_out_q64: U128,
) -> Result<u64, CoreError> {
let base = base_out_1_to_1(amount_in, in_decimals, out_decimals)?;
let price_in: u128 = price_in_q64.into();
let price_out: u128 = price_out_q64.into();
if price_in == 0 || price_out == 0 {
return Err(INVALID_PRICE);
}
let numerator = (base as u128)
.checked_mul(price_in)
.ok_or(ARITHMETIC_OVERFLOW)?;
let out = numerator.checked_div(price_out).ok_or(DIVISION_BY_ZERO)?;
out.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn apply_swap_fee_per_1m(amount: u64, fee_per_1m: u32) -> Result<u64, CoreError> {
if fee_per_1m as u64 > PER_1M_DENOMINATOR {
return Err(INVALID_FEE);
}
let keep = PER_1M_DENOMINATOR.saturating_sub(fee_per_1m as u64);
let product = (amount as u128)
.checked_mul(keep as u128)
.ok_or(ARITHMETIC_OVERFLOW)?;
let out = product
.checked_div(PER_1M_DENOMINATOR as u128)
.ok_or(DIVISION_BY_ZERO)?;
out.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
#[allow(clippy::too_many_arguments)]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn vault_skew_contribution_per_1m(
balance_pre: u64,
balance_post: u64,
limit_lower: u64,
limit_upper: u64,
positive_skew_per_1m: u32,
negative_skew_per_1m: u32,
lower_skew_offset_bps: u16,
upper_skew_offset_bps: u16,
skew_exponent: SkewExponent,
) -> Result<i64, CoreError> {
average_skew_per_m(
balance_pre,
balance_post,
limit_lower,
limit_upper,
lower_skew_offset_bps,
upper_skew_offset_bps,
positive_skew_per_1m,
negative_skew_per_1m,
skew_exponent,
)
}
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn apply_total_skew_per_1m(fair_out: u64, total_skew_per_1m: i64) -> Result<u64, CoreError> {
let capped = total_skew_per_1m.clamp(-MAX_TOTAL_SKEW_PER_1M, MAX_TOTAL_SKEW_PER_1M);
let factor = (PER_1M_DENOMINATOR as i128)
.checked_sub(capped as i128)
.ok_or(ARITHMETIC_OVERFLOW)?;
if factor <= 0 {
return Err(INVALID_SKEW);
}
let product = (fair_out as u128)
.checked_mul(factor as u128)
.ok_or(ARITHMETIC_OVERFLOW)?;
let out = product
.checked_div(PER_1M_DENOMINATOR as u128)
.ok_or(DIVISION_BY_ZERO)?;
out.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub struct SwapQuote {
pub amount_in: u64,
pub fair_out: u64,
pub amount_out: u64,
pub total_skew_per_1m: i64,
pub partial: bool,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub struct VaultSwapLimits {
pub balance: u64,
pub limit_lower: u64,
pub limit_upper: u64,
pub max_swap_amount: u64,
pub swap_fee_per_1m: u32,
pub positive_skew_per_1m: u32,
pub negative_skew_per_1m: u32,
pub lower_skew_offset_bps: u16,
pub upper_skew_offset_bps: u16,
pub skew_exponent: SkewExponent,
pub decimals: u8,
}
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn quote_network_swap(
amount_in: u64,
input: &VaultSwapLimits,
output: &VaultSwapLimits,
price_in_q64: U128,
price_out_q64: U128,
allow_partial_fill: bool,
) -> Result<SwapQuote, CoreError> {
if amount_in == 0 {
return Err(INSUFFICIENT_LIQUIDITY);
}
let price_in: u128 = price_in_q64.into();
let price_out: u128 = price_out_q64.into();
let mut fill_in = amount_in;
if fill_in > input.max_swap_amount {
if !allow_partial_fill {
return Err(MAX_SWAP_EXCEEDED);
}
fill_in = input.max_swap_amount;
}
let input_headroom = input.limit_upper.saturating_sub(input.balance);
if fill_in > input_headroom {
if !allow_partial_fill {
return Err(INSUFFICIENT_LIQUIDITY);
}
fill_in = input_headroom;
}
for _ in 0..8 {
if fill_in == 0 {
return Err(INSUFFICIENT_LIQUIDITY);
}
let in_post = input.balance.saturating_add(fill_in);
let skew_in = vault_skew_contribution_per_1m(
input.balance,
in_post,
input.limit_lower,
input.limit_upper,
input.positive_skew_per_1m,
input.negative_skew_per_1m,
input.lower_skew_offset_bps,
input.upper_skew_offset_bps,
input.skew_exponent,
)?;
let priced_in = apply_swap_fee_per_1m(fill_in, input.swap_fee_per_1m)?;
if priced_in == 0 {
return Err(INSUFFICIENT_LIQUIDITY);
}
let fair = fair_out_with_prices(
priced_in,
input.decimals,
output.decimals,
U128::from(price_in),
U128::from(price_out),
)?;
let out_post_for_skew = output.balance.saturating_sub(fair.min(output.balance));
let skew_out = vault_skew_contribution_per_1m(
output.balance,
out_post_for_skew,
output.limit_lower,
output.limit_upper,
output.positive_skew_per_1m,
output.negative_skew_per_1m,
output.lower_skew_offset_bps,
output.upper_skew_offset_bps,
output.skew_exponent,
)?;
let total_skew = skew_in.saturating_add(skew_out);
let skewed_out = apply_total_skew_per_1m(fair, total_skew)?;
let exec_out = apply_swap_fee_per_1m(skewed_out, output.swap_fee_per_1m)?;
let spendable = output.balance.saturating_sub(output.limit_lower);
let mut max_out = core::cmp::min(spendable, output.max_swap_amount);
max_out = core::cmp::min(max_out, output.balance);
if exec_out > max_out {
if !allow_partial_fill {
return Err(if skewed_out > output.max_swap_amount {
MAX_SWAP_EXCEEDED
} else {
INSUFFICIENT_LIQUIDITY
});
}
if fair == 0 || exec_out == 0 {
return Err(INSUFFICIENT_LIQUIDITY);
}
let reduced = (fill_in as u128)
.checked_mul(max_out as u128)
.ok_or(ARITHMETIC_OVERFLOW)?
.checked_div(exec_out as u128)
.ok_or(DIVISION_BY_ZERO)?;
let reduced: u64 = reduced.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
if reduced >= fill_in || reduced == 0 {
return Err(INSUFFICIENT_LIQUIDITY);
}
fill_in = reduced;
continue;
}
let out_post = output.balance.saturating_sub(exec_out);
let skew_out = vault_skew_contribution_per_1m(
output.balance,
out_post,
output.limit_lower,
output.limit_upper,
output.positive_skew_per_1m,
output.negative_skew_per_1m,
output.lower_skew_offset_bps,
output.upper_skew_offset_bps,
output.skew_exponent,
)?;
let total_skew = skew_in.saturating_add(skew_out);
let skewed_out = apply_total_skew_per_1m(fair, total_skew)?;
let exec_out = apply_swap_fee_per_1m(skewed_out, output.swap_fee_per_1m)?;
if exec_out > max_out {
if !allow_partial_fill {
return Err(INSUFFICIENT_LIQUIDITY);
}
let reduced = (fill_in as u128)
.checked_mul(max_out as u128)
.ok_or(ARITHMETIC_OVERFLOW)?
.checked_div(exec_out as u128)
.ok_or(DIVISION_BY_ZERO)?;
let reduced: u64 = reduced.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
if reduced >= fill_in || reduced == 0 {
return Err(INSUFFICIENT_LIQUIDITY);
}
fill_in = reduced;
continue;
}
return Ok(SwapQuote {
amount_in: fill_in,
fair_out: fair,
amount_out: exec_out,
total_skew_per_1m: total_skew,
partial: fill_in < amount_in,
});
}
Err(INSUFFICIENT_LIQUIDITY)
}
#[cfg(test)]
mod tests {
use super::super::Q64_ONE;
use super::*;
#[test]
fn one_to_one_same_decimals() {
assert_eq!(base_out_1_to_1(1_000_000, 6, 6).unwrap(), 1_000_000);
}
#[test]
fn one_to_one_decimal_conversion() {
assert_eq!(base_out_1_to_1(1_000_000, 6, 8).unwrap(), 100_000_000);
assert_eq!(base_out_1_to_1(100_000_000, 8, 6).unwrap(), 1_000_000);
}
#[test]
fn cheaper_output_price_yields_more_out() {
let price_out = Q64_ONE * 998 / 1000;
let out = fair_out_with_prices(1_000_000, 6, 6, U128::from(Q64_ONE), U128::from(price_out))
.unwrap();
assert!(out > 1_000_000);
}
#[test]
fn swap_fee_retains_portion() {
let net = apply_swap_fee_per_1m(1_000_000, 1_000).unwrap();
assert_eq!(net, 999_000);
}
#[test]
fn skew_rebalance_negative() {
let skew = vault_skew_contribution_per_1m(
900,
600,
0,
1000,
1000,
1000,
0,
0,
SkewExponent::Linear,
)
.unwrap();
assert!(skew < 0);
}
}