code/pallets/swap/src/pallet/balancer.rs
// Balancer swap
//
// Unlike uniswap v2 or v3, it allows adding liquidity disproportionally to price. This is
// achieved by introducing the weights w1 and w2 so that w1 + w2 = 1. In these formulas x
// means base currency (alpha) and y means quote currency (tao). The w1 weight in the code
// below is referred as weight_base, and w2 as weight_quote. Because of the w1 + w2 = 1
// constraint, only weight_quote is stored, and weight_base is always calculated.
//
// The formulas used for pool operation are following:
//
// Price: p = (w1*y) / (w2*x)
//
// Reserve deltas / (or -1 * payouts) in swaps are computed by:
//
// if ∆x is given (sell) ∆y = y * ((x / (x+∆x))^(w1/w2) - 1)
// if ∆y is given (buy) ∆x = x * ((y / (y+∆y))^(w2/w1) - 1)
//
// When swaps are executing the orders with slippage control, we need to know what amount
// we can swap before the price reaches the limit value of p':
//
// If p' < p (sell): ∆x = x * ((p / p')^w2 - 1)
// If p' < p (buy): ∆y = y * ((p' / p)^w1 - 1)
//
// In order to initialize weights with existing reserve values and price:
//
// w1 = px / (px + y)
// w2 = y / (px + y)
//
// Weights are adjusted when some amounts are added to the reserves. This prevents price
// from changing.
//
// new_w1 = p * (x + ∆x) / (p * (x + ∆x) + y + ∆y)
// new_w2 = (y + ∆y) / (p * (x + ∆x) + y + ∆y)
//
// Weights are limited to stay within [0.1, 0.9] range to avoid precision issues in exponentiation.
// Practically, these limitations will not be achieved, but if they are, the swap will not allow injection
// that will push the weights out of this interval because we prefer chain and swap stability over success
// of a single injection. Currently, we only allow the protocol to inject disproportionally to price, and
// the amount of disproportion will not cause weigths to get far from 0.5.
//
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::pallet_prelude::*;
use safe_bigmath::*;
use safe_math::*;
use sp_arithmetic::Perquintill;
use sp_core::U256;
use sp_runtime::Saturating;
use sp_std::ops::Neg;
use substrate_fixed::types::U64F64;
use subtensor_macros::freeze_struct;
/// Balancer implements all high complexity math for swap operations such as:
/// - Swapping x for y, which includes limit orders
/// - Adding and removing liquidity (including unbalanced)
///
/// Notation used in this file:
/// - x: Base reserve (alplha reserve)
/// - y: Quote reserve (tao reserve)
/// - ∆x: Alpha paid in/out
/// - ∆y: Tao paid in/out
/// - w1: Base weight (a.k.a weight_base)
/// - w2: Quote weight (a.k.a weight_quote)
#[freeze_struct("33a4fb0774da77c7")]
#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct Balancer {
quote: Perquintill,
}
/// Accuracy matches to 18 decimal digits used to represent weights
pub const ACCURACY: u64 = 1_000_000_000_000_000_000_u64;
/// Lower imit of weights is 0.01
pub const MIN_WEIGHT: Perquintill = Perquintill::from_parts(ACCURACY / 100);
/// 1.0 in Perquintill
pub const ONE: Perquintill = Perquintill::from_parts(ACCURACY);
#[derive(Debug)]
pub enum BalancerError {
/// The provided weight value is out of range
InvalidValue,
}
impl Default for Balancer {
/// The default value of weights is 0.5 for pool initialization
fn default() -> Self {
Self {
quote: Perquintill::from_rational(1u128, 2u128),
}
}
}
impl Balancer {
/// Creates a new instance of balancer with a given quote weight
pub fn new(quote: Perquintill) -> Result<Self, BalancerError> {
if Self::check_constraints(quote) {
Ok(Balancer { quote })
} else {
Err(BalancerError::InvalidValue)
}
}
/// Constraints limit balancer weights within certain range of values:
/// - Both weights are above minimum
/// - Sum of weights is equal to 1.0
fn check_constraints(quote: Perquintill) -> bool {
let base = ONE.saturating_sub(quote);
(base >= MIN_WEIGHT) && (quote >= MIN_WEIGHT)
}
/// We store quote weight as Perquintill
pub fn get_quote_weight(&self) -> Perquintill {
self.quote
}
/// Base weight is calculated as 1.0 - quote_weight
pub fn get_base_weight(&self) -> Perquintill {
ONE.saturating_sub(self.quote)
}
/// Sets quote currency weight in the balancer.
/// Because sum of weights is always 1.0, there is no need to
/// store base currency weight
pub fn set_quote_weight(&mut self, new_value: Perquintill) -> Result<(), BalancerError> {
if Self::check_constraints(new_value) {
self.quote = new_value;
Ok(())
} else {
Err(BalancerError::InvalidValue)
}
}
/// If base_quote is true, calculate (x / (x + ∆x))^(weight_base / weight_quote),
/// otherwise, calculate (x / (x + ∆x))^(weight_quote / weight_base)
///
/// Here we use SafeInt from bigmath crate for high-precision exponentiation,
/// which exposes the function pow_ratio_scaled.
///
/// Note: ∆x may be negative
fn exp_scaled(&self, x: u64, dx: i128, base_quote: bool) -> U64F64 {
let x_plus_dx = if dx >= 0 {
x.saturating_add(dx as u64)
} else {
x.saturating_sub(dx.neg() as u64)
};
if x_plus_dx == 0 {
return U64F64::saturating_from_num(0);
}
let w1: u128 = self.get_base_weight().deconstruct() as u128;
let w2: u128 = self.get_quote_weight().deconstruct() as u128;
let precision = 256;
let x_safe = SafeInt::from(x);
let w1_safe = SafeInt::from(w1);
let w2_safe = SafeInt::from(w2);
let perquintill_scale = SafeInt::from(ACCURACY as u128);
let denominator = SafeInt::from(x_plus_dx);
log::debug!("x = {:?}", x);
log::debug!("dx = {:?}", dx);
log::debug!("x_safe = {:?}", x_safe);
log::debug!("denominator = {:?}", denominator);
log::debug!("w1_safe = {:?}", w1_safe);
log::debug!("w2_safe = {:?}", w2_safe);
log::debug!("precision = {:?}", precision);
log::debug!("perquintill_scale = {:?}", perquintill_scale);
let maybe_result_safe_int = if base_quote {
SafeInt::pow_ratio_scaled(
&x_safe,
&denominator,
&w1_safe,
&w2_safe,
precision,
&perquintill_scale,
)
} else {
SafeInt::pow_ratio_scaled(
&x_safe,
&denominator,
&w2_safe,
&w1_safe,
precision,
&perquintill_scale,
)
};
if let Some(result_safe_int) = maybe_result_safe_int
&& let Some(result_u64) = result_safe_int.to_u64()
{
let result = U64F64::saturating_from_num(result_u64)
.safe_div(U64F64::saturating_from_num(ACCURACY));
return if dx >= 0 {
result.min(U64F64::from_num(1))
} else {
result
};
}
U64F64::saturating_from_num(0)
}
/// Calculates exponent of (x / (x + ∆x)) ^ (w_base/w_quote)
/// This method is used in sell swaps
/// (∆x is given by user, ∆y is paid out by the pool)
pub fn exp_base_quote(&self, x: u64, dx: u64) -> U64F64 {
self.exp_scaled(x, dx as i128, true)
}
/// Calculates exponent of (y / (y + ∆y)) ^ (w_quote/w_base)
/// This method is used in buy swaps
/// (∆y is given by user, ∆x is paid out by the pool)
pub fn exp_quote_base(&self, y: u64, dy: u64) -> U64F64 {
self.exp_scaled(y, dy as i128, false)
}
/// Calculates price as (w1/w2) * (y/x), where
/// - w1 is base weight
/// - w2 is quote weight
/// - x is base reserve
/// - y is quote reserve
pub fn calculate_price(&self, x: u64, y: u64) -> U64F64 {
let w2_fixed = U64F64::saturating_from_num(self.get_quote_weight().deconstruct());
let w1_fixed = U64F64::saturating_from_num(self.get_base_weight().deconstruct());
let x_fixed = U64F64::saturating_from_num(x);
let y_fixed = U64F64::saturating_from_num(y);
w1_fixed
.safe_div(w2_fixed)
.saturating_mul(y_fixed.safe_div(x_fixed))
}
/// Multiply a u128 value by a Perquintill with u128 result rounded to the
/// nearest integer
fn mul_perquintill_round(p: Perquintill, value: u128) -> u128 {
let parts = p.deconstruct() as u128;
let acc = ACCURACY as u128;
let num = U256::from(value).saturating_mul(U256::from(parts));
let den = U256::from(acc);
// Add 0.5 before integer division to achieve rounding to the nearest
// integer
let zero = U256::from(0);
let res = num
.saturating_add(den.checked_div(U256::from(2u8)).unwrap_or(zero))
.checked_div(den)
.unwrap_or(zero);
res.min(U256::from(u128::MAX))
.try_into()
.unwrap_or_default()
}
/// When liquidity is added to balancer swap, it may be added with arbitrary proportion,
/// not necessarily in the proportion of price, like with uniswap v2 or v3. In order to
/// stay within balancer pool invariant, the weights need to be updated. Invariant:
///
/// L = x ^ weight_base * y ^ weight_quote
///
/// Note that weights must remain within the proper range (both be above MIN_WEIGHT),
/// so only reasonably small disproportions of updates are appropriate.
pub fn update_weights_for_added_liquidity(
&mut self,
tao_reserve: u64,
alpha_reserve: u64,
tao_delta: u64,
alpha_delta: u64,
) -> Result<(), BalancerError> {
// Calculate new to-be reserves (do not update here)
let tao_reserve_u128 = u64::from(tao_reserve) as u128;
let alpha_reserve_u128 = u64::from(alpha_reserve) as u128;
let tao_delta_u128 = u64::from(tao_delta) as u128;
let alpha_delta_u128 = u64::from(alpha_delta) as u128;
let new_tao_reserve_u128 = tao_reserve_u128.saturating_add(tao_delta_u128);
let new_alpha_reserve_u128 = alpha_reserve_u128.saturating_add(alpha_delta_u128);
// Calculate new weights
let quantity_1: u128 = Self::mul_perquintill_round(
self.get_base_weight(),
tao_reserve_u128.saturating_mul(new_alpha_reserve_u128),
);
let quantity_2: u128 = Self::mul_perquintill_round(
self.get_quote_weight(),
alpha_reserve_u128.saturating_mul(new_tao_reserve_u128),
);
let q_sum = quantity_1.saturating_add(quantity_2);
// Calculate new reserve weights
let new_reserve_weight = if q_sum != 0 {
// Both TAO and Alpha are non-zero, normal case
Perquintill::from_rational(quantity_2, q_sum)
} else {
// Either TAO or Alpha reserve were and/or remain zero => Initialize weights to 0.5
Perquintill::from_rational(1u128, 2u128)
};
self.set_quote_weight(new_reserve_weight)
}
/// Calculates quote delta needed to reach the price up when byuing
/// This method is needed for limit orders.
///
/// Formula is:
/// ∆y = y * ((price_new / price)^weight_base - 1)
/// price_new >= price
pub fn calculate_quote_delta_in(
&self,
current_price: U64F64,
target_price: U64F64,
reserve: u64,
) -> u64 {
let base_numerator: u128 = target_price.to_bits();
let base_denominator: u128 = current_price.to_bits();
let w1_fixed: u128 = self.get_base_weight().deconstruct() as u128;
let scale: u128 = 10u128.pow(18);
let maybe_exp_result = SafeInt::pow_ratio_scaled(
&SafeInt::from(base_numerator),
&SafeInt::from(base_denominator),
&SafeInt::from(w1_fixed),
&SafeInt::from(ACCURACY),
1024,
&SafeInt::from(scale),
);
if let Some(exp_result_safe_int) = maybe_exp_result {
let reserve_fixed = U64F64::saturating_from_num(reserve);
let one = U64F64::saturating_from_num(1);
let scale_fixed = U64F64::saturating_from_num(scale);
let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() {
U64F64::saturating_from_num(exp_result_u64)
} else if u64::MAX < exp_result_safe_int {
U64F64::saturating_from_num(u64::MAX)
} else {
U64F64::saturating_from_num(0)
};
reserve_fixed
.saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one))
.saturating_to_num::<u64>()
} else {
0u64
}
}
/// Calculates base delta needed to reach the price down when selling
/// This method is needed for limit orders.
///
/// Formula is:
/// ∆x = x * ((price / price_new)^weight_quote - 1)
/// price_new <= price
pub fn calculate_base_delta_in(
&self,
current_price: U64F64,
target_price: U64F64,
reserve: u64,
) -> u64 {
let base_numerator: u128 = current_price.to_bits();
let base_denominator: u128 = target_price.to_bits();
let w2_fixed: u128 = self.get_quote_weight().deconstruct() as u128;
let scale: u128 = 10u128.pow(18);
let maybe_exp_result = SafeInt::pow_ratio_scaled(
&SafeInt::from(base_numerator),
&SafeInt::from(base_denominator),
&SafeInt::from(w2_fixed),
&SafeInt::from(ACCURACY),
1024,
&SafeInt::from(scale),
);
if let Some(exp_result_safe_int) = maybe_exp_result {
let one = U64F64::saturating_from_num(1);
let scale_fixed = U64F64::saturating_from_num(scale);
let reserve_fixed = U64F64::saturating_from_num(reserve);
let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() {
U64F64::saturating_from_num(exp_result_u64)
} else if u64::MAX < exp_result_safe_int {
U64F64::saturating_from_num(u64::MAX)
} else {
U64F64::saturating_from_num(0)
};
reserve_fixed
.saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one))
.saturating_to_num::<u64>()
} else {
0u64
}
}
/// Calculates amount of Alpha that needs to be sold to get a given amount of TAO
pub fn get_base_needed_for_quote(
&self,
tao_reserve: u64,
alpha_reserve: u64,
delta_tao: u64,
) -> u64 {
let e = self.exp_scaled(tao_reserve, (delta_tao as i128).neg(), false);
let one = U64F64::from_num(1);
let alpha_reserve_fixed = U64F64::from_num(alpha_reserve);
// e > 1 in this case
alpha_reserve_fixed
.saturating_mul(e.saturating_sub(one))
.saturating_to_num::<u64>()
}
}
// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests --nocapture
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
#[cfg(feature = "std")]
mod tests {
use crate::pallet::Balancer;
use crate::pallet::balancer::*;
use approx::assert_abs_diff_eq;
use sp_arithmetic::Perquintill;
use std::panic::{AssertUnwindSafe, catch_unwind};
// Helper: convert Perquintill to f64 for comparison
fn perquintill_to_f64(p: Perquintill) -> f64 {
let parts = p.deconstruct() as f64;
parts / ACCURACY as f64
}
// Helper: convert U64F64 to f64 for comparison
fn f(v: U64F64) -> f64 {
v.to_num::<f64>()
}
fn assert_no_panic<R, F>(label: &str, f: F) -> R
where
F: FnOnce() -> R,
{
catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|_| panic!("{label} panicked"))
}
#[test]
fn test_balancer_rejects_invalid_boundary_weights_without_panicking() {
[
Perquintill::zero(),
Perquintill::from_parts(1),
MIN_WEIGHT.saturating_sub(Perquintill::from_parts(1)),
ONE.saturating_sub(MIN_WEIGHT)
.saturating_add(Perquintill::from_parts(1)),
ONE,
]
.into_iter()
.for_each(|quote| {
assert_no_panic("Balancer::new invalid boundary weight", || {
assert!(Balancer::new(quote).is_err());
});
});
let mut balancer = Balancer::default();
assert_no_panic("Balancer::set_quote_weight invalid boundary weight", || {
assert!(balancer.set_quote_weight(Perquintill::zero()).is_err());
});
assert_eq!(
balancer.get_quote_weight(),
Perquintill::from_rational(1u128, 2u128)
);
}
#[test]
fn test_balancer_extreme_exp_inputs_do_not_panic() {
let weights = [
MIN_WEIGHT,
Perquintill::from_rational(1u128, 2u128),
ONE.saturating_sub(MIN_WEIGHT),
];
let inputs = [
(0u64, 0u64),
(0u64, 1u64),
(1u64, 0u64),
(1u64, 1u64),
(1u64, u64::MAX),
(u64::MAX, 0u64),
(u64::MAX, 1u64),
(u64::MAX, u64::MAX),
];
for quote in weights {
let balancer = Balancer::new(quote).unwrap();
for (reserve, delta) in inputs {
assert_no_panic("exp_base_quote extreme input", || {
let _ = balancer.exp_base_quote(reserve, delta);
});
assert_no_panic("exp_quote_base extreme input", || {
let _ = balancer.exp_quote_base(reserve, delta);
});
assert_no_panic("exp_scaled negative extreme input", || {
let _ = balancer.exp_scaled(reserve, -(delta as i128), true);
let _ = balancer.exp_scaled(reserve, -(delta as i128), false);
});
}
}
}
#[test]
fn test_balancer_price_and_limit_delta_corner_cases_do_not_panic() {
let balancer = Balancer::new(MIN_WEIGHT).unwrap();
let prices = [
U64F64::from_num(0),
U64F64::from_num(1),
U64F64::from_num(u64::MAX),
];
let reserves = [0u64, 1u64, u64::MAX];
for x in reserves {
for y in reserves {
assert_no_panic("calculate_price corner reserves", || {
let _ = balancer.calculate_price(x, y);
});
}
}
for current_price in prices {
for target_price in prices {
for reserve in reserves {
assert_no_panic("calculate_quote_delta_in corner input", || {
let _ =
balancer.calculate_quote_delta_in(current_price, target_price, reserve);
});
assert_no_panic("calculate_base_delta_in corner input", || {
let _ =
balancer.calculate_base_delta_in(current_price, target_price, reserve);
});
}
}
}
}
#[test]
fn test_balancer_liquidity_weight_update_extremes_do_not_panic() {
let inputs = [
(0u64, 0u64, 0u64, 0u64),
(0u64, 0u64, u64::MAX, u64::MAX),
(0u64, u64::MAX, u64::MAX, 0u64),
(u64::MAX, 0u64, 0u64, u64::MAX),
(u64::MAX, u64::MAX, u64::MAX, u64::MAX),
(1u64, u64::MAX, u64::MAX, 1u64),
(u64::MAX, 1u64, 1u64, u64::MAX),
];
for (tao_reserve, alpha_reserve, tao_delta, alpha_delta) in inputs {
let mut balancer = Balancer::default();
assert_no_panic("update_weights_for_added_liquidity extreme input", || {
let _ = balancer.update_weights_for_added_liquidity(
tao_reserve,
alpha_reserve,
tao_delta,
alpha_delta,
);
});
}
}
#[test]
fn test_balancer_base_needed_for_quote_extremes_do_not_panic() {
let balancer = Balancer::new(ONE.saturating_sub(MIN_WEIGHT)).unwrap();
let inputs = [
(0u64, 0u64, 0u64),
(0u64, 1u64, 1u64),
(1u64, 0u64, 1u64),
(1u64, 1u64, 0u64),
(1u64, 1u64, 1u64),
(1u64, 1u64, u64::MAX),
(u64::MAX, u64::MAX, 0u64),
(u64::MAX, u64::MAX, u64::MAX),
];
for (tao_reserve, alpha_reserve, delta_tao) in inputs {
assert_no_panic("get_base_needed_for_quote extreme input", || {
let _ = balancer.get_base_needed_for_quote(tao_reserve, alpha_reserve, delta_tao);
});
}
}
#[test]
fn test_safe_bigmath_pow_ratio_internal_paths_do_not_panic() {
let base_num = SafeInt::from(999_999_937u64);
let base_den = SafeInt::from(1_000_000_003u64);
let scale = SafeInt::from(1_000_000u64);
let cases = [
// Exact integer/root path with exponent values at the safe-bigmath threshold.
(
SafeInt::from(1024u32),
SafeInt::one(),
"exact max numerator",
),
(
SafeInt::from(999u32),
SafeInt::from(1024u32),
"exact root denominator",
),
// One step over the threshold forces the fixed-point ln/exp fallback path.
(SafeInt::from(1025u32), SafeInt::one(), "fallback numerator"),
(
SafeInt::from(999u32),
SafeInt::from(1025u32),
"fallback denominator",
),
// GCD reduction should route this back to the exact path.
(
SafeInt::from(2048u32),
SafeInt::from(4096u32),
"gcd reduced",
),
];
for (exp_num, exp_den, label) in cases {
let result = assert_no_panic(label, || {
SafeInt::pow_ratio_scaled(&base_num, &base_den, &exp_num, &exp_den, 64, &scale)
});
assert!(result.is_some(), "{label} should produce a result");
}
}
#[test]
fn test_balancer_near_equal_weights_with_tiny_delta_do_not_panic() {
let weights = [
Perquintill::from_parts(500_000_000_500_000_000),
Perquintill::from_parts(499_999_999_500_000_000),
Perquintill::from_parts(500_000_000_000_500_000),
Perquintill::from_parts(499_999_999_999_500_000),
];
let reserve = 21_000_000_000_000_000u64;
let tiny_deltas = [1u64, 100u64, 100_000u64];
for quote in weights {
let balancer = Balancer::new(quote).unwrap();
for delta in tiny_deltas {
assert_no_panic("near-equal exp_base_quote tiny delta", || {
let e = balancer.exp_base_quote(reserve, delta);
assert!(e <= U64F64::from_num(1));
assert!(e > U64F64::from_num(0));
});
assert_no_panic("near-equal exp_quote_base tiny delta", || {
let e = balancer.exp_quote_base(reserve, delta);
assert!(e <= U64F64::from_num(1));
assert!(e > U64F64::from_num(0));
});
}
}
}
#[test]
fn test_balancer_log_normalization_reserve_shapes_do_not_panic() {
let balancer = Balancer::new(Perquintill::from_parts(500_000_000_500_000_000)).unwrap();
let reserves = [
(1u64 << 42) - 1,
1u64 << 42,
(1u64 << 42) + 1,
((1u64 << 42) + (1u64 << 41)) - 1,
(1u64 << 42) + (1u64 << 41),
((1u64 << 42) + (1u64 << 41)) + 1,
];
for reserve in reserves {
for delta in [1u64, reserve / 1_000, reserve / 2] {
assert_no_panic("log-normalization exp_base_quote", || {
let e = balancer.exp_base_quote(reserve, delta);
assert!(e <= U64F64::from_num(1));
});
assert_no_panic("log-normalization exp_quote_base", || {
let e = balancer.exp_quote_base(reserve, delta);
assert!(e <= U64F64::from_num(1));
});
}
}
}
#[test]
fn test_perquintill_power() {
const PRECISION: u32 = 4096;
const PERQUINTILL: u128 = ACCURACY as u128;
let x = SafeInt::from(21_000_000_000_000_000u64);
let delta = SafeInt::from(7_000_000_000_000_000u64);
let w1 = SafeInt::from(600_000_000_000_000_000u128);
let w2 = SafeInt::from(400_000_000_000_000_000u128);
let denominator = &x + δ
assert_eq!(w1.clone() + w2.clone(), SafeInt::from(PERQUINTILL));
let perquintill_result = SafeInt::pow_ratio_scaled(
&x,
&denominator,
&w1,
&w2,
PRECISION,
&SafeInt::from(PERQUINTILL),
)
.expect("perquintill integer result");
assert_eq!(
perquintill_result,
SafeInt::from(649_519_052_838_328_985u128)
);
let readable = safe_bigmath::SafeDec::<18>::from_raw(perquintill_result);
assert_eq!(format!("{}", readable), "0.649519052838328985");
}
/// Validate realistic values that can be calculated with f64 precision
#[test]
fn test_exp_base_quote_happy_path() {
// Outer test cases: w_quote
[
Perquintill::from_rational(500_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(500_000_000_001_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(499_999_999_999_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(500_000_000_100_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(500_000_001_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(500_000_010_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(500_000_100_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(500_001_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(500_010_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(500_100_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(501_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(510_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(100_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(100_000_000_001_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(200_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(300_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(400_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(600_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(700_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(800_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(899_999_999_999_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(900_000_000_000_u128, 1_000_000_000_000_u128),
Perquintill::from_rational(
102_337_248_363_782_924_u128,
1_000_000_000_000_000_000_u128,
),
]
.into_iter()
.for_each(|w_quote| {
// Inner test cases: y, x, ∆x
[
(1_000_u64, 1_000_u64, 0_u64),
(1_000_u64, 1_000_u64, 1_u64),
(1_500_u64, 1_000_u64, 1_u64),
(
1_000_000_000_000_u64,
100_000_000_000_000_u64,
100_000_000_u64,
),
(
1_000_000_000_000_u64,
100_000_000_000_000_u64,
100_000_000_u64,
),
(
100_000_000_000_u64,
100_000_000_000_000_u64,
100_000_000_u64,
),
(100_000_000_000_u64, 100_000_000_000_000_u64, 1_000_000_u64),
(
100_000_000_000_u64,
100_000_000_000_000_u64,
1_000_000_000_000_u64,
),
(
1_000_000_000_u64,
100_000_000_000_000_u64,
1_000_000_000_000_u64,
),
(
1_000_000_u64,
100_000_000_000_000_u64,
1_000_000_000_000_u64,
),
(1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_000_u64),
(1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_u64),
(1_000_u64, 100_000_000_000_000_u64, 1_000_000_u64),
(1_000_u64, 100_000_000_000_000_u64, 1_000_u64),
(1_000_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64),
(10_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64),
// Extreme values of ∆x for small x
(1_000_000_000_u64, 4_000_000_000_u64, 1_000_000_000_000_u64),
(1_000_000_000_000_u64, 1_000_u64, 1_000_000_000_000_u64),
(
5_628_038_062_729_553_u64,
400_775_553_u64,
14_446_633_907_665_582_u64,
),
(
5_600_000_000_000_000_u64,
400_000_000_u64,
14_000_000_000_000_000_u64,
),
]
.into_iter()
.for_each(|(y, x, dx)| {
let bal = Balancer::new(w_quote).unwrap();
let e1 = bal.exp_base_quote(x, dx);
let e2 = bal.exp_quote_base(x, dx);
let one = U64F64::from_num(1);
let y_fixed = U64F64::from_num(y);
let dy1 = y_fixed * (one - e1);
let dy2 = y_fixed * (one - e2);
if dx > x.saturating_mul(1_000) {
assert!(e1 <= one);
assert!(e2 <= one);
return;
}
let w1 = perquintill_to_f64(bal.get_base_weight());
let w2 = perquintill_to_f64(bal.get_quote_weight());
let e1_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2);
let dy1_expected = y as f64 * (1. - e1_expected);
let e2_expected = (x as f64 / (x as f64 + dx as f64)).powf(w2 / w1);
let dy2_expected = y as f64 * (1. - e2_expected);
// Start tolerance with 0.001 rao
let mut eps1 = 0.001;
let mut eps2 = 0.001;
// If swapping more than 100k tao/alpha, relax tolerance to 1.0 rao
if dy1_expected > 100_000_000_000_000_f64 {
eps1 = 1.0;
}
if dy2_expected > 100_000_000_000_000_f64 {
eps2 = 1.0;
}
assert_abs_diff_eq!(f(dy1), dy1_expected, epsilon = eps1);
assert_abs_diff_eq!(f(dy2), dy2_expected, epsilon = eps2);
})
});
}
/// This test exercises practical application edge cases of exp_base_quote
/// The practical formula where this function is used:
/// ∆y = y * (exp_base_quote(x, ∆x) - 1)
///
/// The test validates that two different sets of parameters produce (sensibly)
/// different results
///
#[test]
fn test_exp_base_quote_dy_precision() {
// Test cases: y, x1, ∆x1, w_quote1, x2, ∆x2, w_quote2
// Realized dy1 should be greater than dy2
[
(
1_000_000_000_u64,
21_000_000_000_000_000_u64,
21_000_000_000_u64,
Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128),
21_000_000_000_000_000_u64,
21_000_000_000_u64,
Perquintill::from_rational(1_000_000_000_001_u128, 2_000_000_000_000_u128),
),
(
1_000_000_000_u64,
21_000_000_000_000_000_u64,
21_000_000_000_u64,
Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_001_u128),
21_000_000_000_000_000_u64,
21_000_000_000_u64,
Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128),
),
(
1_000_000_000_u64,
21_000_000_000_000_000_u64,
2_u64,
Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128),
21_000_000_000_000_000_u64,
1_u64,
Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128),
),
(
1_000_000_000_u64,
21_000_000_000_000_000_u64,
1_u64,
Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128),
21_000_000_000_000_000_u64,
1_u64,
Perquintill::from_rational(1_010_000_000_000_u128, 2_000_000_000_000_u128),
),
(
1_000_000_000_u64,
21_000_000_000_000_000_u64,
1_u64,
Perquintill::from_rational(1_000_000_000_000_u128, 2_010_000_000_000_u128),
21_000_000_000_000_000_u64,
1_u64,
Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128),
),
]
.into_iter()
.for_each(|(y, x1, dx1, w_quote1, x2, dx2, w_quote2)| {
let bal1 = Balancer::new(w_quote1).unwrap();
let bal2 = Balancer::new(w_quote2).unwrap();
let exp1 = bal1.exp_base_quote(x1, dx1);
let exp2 = bal2.exp_base_quote(x2, dx2);
let one = U64F64::from_num(1);
let y_fixed = U64F64::from_num(y);
let dy1 = y_fixed * (one - exp1);
let dy2 = y_fixed * (one - exp2);
assert!(dy1 > dy2);
let zero = U64F64::from_num(0);
assert!(dy1 != zero);
assert!(dy2 != zero);
})
}
/// Test the broad range of w_quote values, usually should be ignored
#[ignore]
#[test]
fn test_exp_quote_broad_range() {
let y = 1_000_000_000_000_u64;
let x = 100_000_000_000_000_u64;
let dx = 10_000_000_u64;
let mut prev = U64F64::from_num(1_000_000_000);
let mut last_progress = 0.;
let start = 100_000_000_000_u128;
let stop = 900_000_000_000_u128;
for num in (start..=stop).step_by(1000_usize) {
let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128);
let bal = Balancer::new(w_quote).unwrap();
let e = bal.exp_base_quote(x, dx);
let one = U64F64::from_num(1);
let dy = U64F64::from_num(y) * (one - e);
let progress = (num as f64 - start as f64) / (stop as f64 - start as f64);
if progress - last_progress >= 0.0001 {
// Replace with println for real-time progress
log::debug!("progress = {:?}%", progress * 100.);
log::debug!("dy = {:?}", dy);
last_progress = progress;
}
assert!(dy != U64F64::from_num(0));
assert!(dy <= prev);
prev = dy;
}
}
// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_quote_fuzzy --include-ignored --exact --nocapture
#[ignore]
#[test]
fn test_exp_quote_fuzzy() {
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
const ITERATIONS: usize = 1_000_000_000;
let counter = Arc::new(AtomicUsize::new(0));
(0..ITERATIONS)
.into_par_iter()
.for_each(|i| {
// Each iteration gets its own deterministic RNG.
// Seed depends on i, so runs are reproducible.
let mut rng = StdRng::seed_from_u64(42 + i as u64);
let max_supply: u64 = 21_000_000_000_000_000;
let full_range = true;
let x: u64 = rng.gen_range(1_000..=max_supply); // Alpha reserve
let y: u64 = if full_range {
// TAO reserve (allow huge prices)
rng.gen_range(1_000..=max_supply)
} else {
// TAO reserve (limit prices with 0-1000)
rng.gen_range(1_000..x.saturating_mul(1000).min(max_supply))
};
let dx: u64 = if full_range {
// Alhpa sold (allow huge values)
rng.gen_range(1_000..=21_000_000_000_000_000)
} else {
// Alhpa sold (do not sell more than 100% of what's in alpha reserve)
rng.gen_range(1_000..=x)
};
let w_numerator: u64 = rng.gen_range(ACCURACY / 10..=ACCURACY / 10 * 9);
let w_quote = Perquintill::from_rational(w_numerator, ACCURACY);
let bal = Balancer::new(w_quote).unwrap();
let e = bal.exp_base_quote(x, dx);
let one = U64F64::from_num(1);
let dy = U64F64::from_num(y) * (one - e);
// Calculate expected in f64 and approx-assert
let w1 = perquintill_to_f64(bal.get_base_weight());
let w2 = perquintill_to_f64(bal.get_quote_weight());
let e_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2);
let dy_expected = y as f64 * (1. - e_expected);
let actual = dy.to_num::<f64>();
let eps = (dy_expected / 1_000_000.).clamp(1.0, 1000.0);
assert!(
(actual - dy_expected).abs() <= eps,
"dy mismatch:\n actual: {}\n expected: {}\n eps: {}\nParameters:\n x: {}\n y: {}\n dx: {}\n w_numerator: {}\n",
actual, dy_expected, eps, x, y, dx, w_numerator,
);
// Assert that we aren't giving out more than reserve y
assert!(dy <= y, "dy = {},\ny = {}", dy, y,);
// Print progress
let done = counter.fetch_add(1, Ordering::Relaxed) + 1;
if done % 10_000_000 == 0 {
let progress = done as f64 / ITERATIONS as f64 * 100.0;
// Replace with println for real-time progress
log::debug!("progress = {progress:.4}%");
}
});
}
#[test]
fn test_calculate_quote_delta_in() {
let num = 250_000_000_000_u128; // w1 = 0.75
let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128);
let bal = Balancer::new(w_quote).unwrap();
let current_price: U64F64 = U64F64::from_num(0.1);
let target_price: U64F64 = U64F64::from_num(0.2);
let tao_reserve: u64 = 1_000_000_000;
let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve);
// ∆y = y•[(p'/p)^w1 - 1]
let dy_expected = tao_reserve as f64
* ((target_price.to_num::<f64>() / current_price.to_num::<f64>()).powf(0.75) - 1.0);
assert_eq!(dy, dy_expected as u64,);
}
#[test]
fn test_calculate_base_delta_in() {
let num = 250_000_000_000_u128; // w2 = 0.25
let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128);
let bal = Balancer::new(w_quote).unwrap();
let current_price: U64F64 = U64F64::from_num(0.2);
let target_price: U64F64 = U64F64::from_num(0.1);
let alpha_reserve: u64 = 1_000_000_000;
let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve);
// ∆x = x•[(p/p')^w2 - 1]
let dx_expected = alpha_reserve as f64
* ((current_price.to_num::<f64>() / target_price.to_num::<f64>()).powf(0.25) - 1.0);
assert_eq!(dx, dx_expected as u64,);
}
#[test]
fn test_calculate_quote_delta_in_impossible() {
let num = 250_000_000_000_u128; // w1 = 0.75
let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128);
let bal = Balancer::new(w_quote).unwrap();
// Impossible price (lower)
let current_price: U64F64 = U64F64::from_num(0.1);
let target_price: U64F64 = U64F64::from_num(0.05);
let tao_reserve: u64 = 1_000_000_000;
let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve);
let dy_expected = 0u64;
assert_eq!(dy, dy_expected);
}
#[test]
fn test_calculate_base_delta_in_impossible() {
let num = 250_000_000_000_u128; // w2 = 0.25
let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128);
let bal = Balancer::new(w_quote).unwrap();
// Impossible price (higher)
let current_price: U64F64 = U64F64::from_num(0.1);
let target_price: U64F64 = U64F64::from_num(0.2);
let alpha_reserve: u64 = 1_000_000_000;
let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve);
let dx_expected = 0u64;
assert_eq!(dx, dx_expected);
}
#[test]
fn test_calculate_delta_in_reverse_swap() {
let num = 500_000_000_000_u128;
let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128);
let bal = Balancer::new(w_quote).unwrap();
let current_price: U64F64 = U64F64::from_num(0.1);
let target_price: U64F64 = U64F64::from_num(0.2);
let tao_reserve: u64 = 1_000_000_000;
// Here is the simple case of w1 = w2 = 0.5, so alpha = tao / price
let alpha_reserve: u64 = (tao_reserve as f64 / current_price.to_num::<f64>()) as u64;
let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve);
let dx = alpha_reserve as f64
* (1.0
- (tao_reserve as f64 / (tao_reserve as f64 + dy as f64))
.powf(num as f64 / (1_000_000_000_000 - num) as f64));
// Verify that buying with dy will in fact bring the price to target_price
let actual_price = bal.calculate_price(alpha_reserve - dx as u64, tao_reserve + dy);
assert_abs_diff_eq!(
actual_price.to_num::<f64>(),
target_price.to_num::<f64>(),
epsilon = target_price.to_num::<f64>() / 1_000_000_000.
);
}
#[test]
fn test_mul_round_zero_and_one() {
let v = 1_000_000u128;
// p = 0 -> always 0
assert_eq!(Balancer::mul_perquintill_round(Perquintill::zero(), v), 0);
// p = 1 -> identity
assert_eq!(Balancer::mul_perquintill_round(Perquintill::one(), v), v);
}
#[test]
fn test_mul_round_half_behaviour() {
// p = 1/2
let p = Perquintill::from_rational(1u128, 2u128);
// Check rounding around .5 boundaries
// value * 1/2, rounded to nearest
assert_eq!(Balancer::mul_perquintill_round(p, 0), 0); // 0.0 -> 0
assert_eq!(Balancer::mul_perquintill_round(p, 1), 1); // 0.5 -> 1 (round up)
assert_eq!(Balancer::mul_perquintill_round(p, 2), 1); // 1.0 -> 1
assert_eq!(Balancer::mul_perquintill_round(p, 3), 2); // 1.5 -> 2
assert_eq!(Balancer::mul_perquintill_round(p, 4), 2); // 2.0 -> 2
assert_eq!(Balancer::mul_perquintill_round(p, 5), 3); // 2.5 -> 3
assert_eq!(Balancer::mul_perquintill_round(p, 1023), 512); // 511.5 -> 512
assert_eq!(Balancer::mul_perquintill_round(p, 1025), 513); // 512.5 -> 513
}
#[test]
fn test_mul_round_third_behaviour() {
// p = 1/3
let p = Perquintill::from_rational(1u128, 3u128);
// value * 1/3, rounded to nearest
assert_eq!(Balancer::mul_perquintill_round(p, 3), 1); // 1.0 -> 1
assert_eq!(Balancer::mul_perquintill_round(p, 4), 1); // 1.333... -> 1
assert_eq!(Balancer::mul_perquintill_round(p, 5), 2); // 1.666... -> 2
assert_eq!(Balancer::mul_perquintill_round(p, 6), 2); // 2.0 -> 2
}
#[test]
fn test_mul_round_large_values_simple_rational() {
// p = 7/10 (exact in perquintill: 0.7)
let p = Perquintill::from_rational(7u128, 10u128);
let v: u128 = 1_000_000_000_000_000_000;
let res = Balancer::mul_perquintill_round(p, v);
// Expected = round(0.7 * v) with pure integer math:
// round(v * 7 / 10) = (v*7 + 10/2) / 10
let expected = (v.saturating_mul(7) + 10 / 2) / 10;
assert_eq!(res, expected);
}
#[test]
fn test_mul_round_max_value_with_one() {
let v = u128::MAX;
let p = ONE;
// For p = 1, result must be exactly value, and must not overflow
let res = Balancer::mul_perquintill_round(p, v);
assert_eq!(res, v);
}
#[test]
fn test_price_with_equal_weights_is_y_over_x() {
// quote = 0.5, base = 0.5 -> w1 / w2 = 1, so price = y/x
let quote = Perquintill::from_rational(1u128, 2u128);
let bal = Balancer::new(quote).unwrap();
let x = 2u64;
let y = 5u64;
let price = bal.calculate_price(x, y);
let price_f = f(price);
let expected_f = (y as f64) / (x as f64);
assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-12);
}
#[test]
fn test_price_scales_with_weight_ratio_two_to_one() {
// Assume base = 1 - quote.
// quote = 1/3 -> base = 2/3, so w1 / w2 = 2.
// Then price = 2 * (y/x).
let quote = Perquintill::from_rational(1u128, 3u128);
let bal = Balancer::new(quote).unwrap();
let x = 4u64;
let y = 10u64;
let price_f = f(bal.calculate_price(x, y));
let expected_f = 2.0 * (y as f64 / x as f64);
assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-10);
}
#[test]
fn test_price_is_zero_when_y_is_zero() {
// If y = 0, y/x = 0 so price must be 0 regardless of weights (for x > 0).
let quote = Perquintill::from_rational(3u128, 10u128); // 0.3
let bal = Balancer::new(quote).unwrap();
let x = 10u64;
let y = 0u64;
let price_f = f(bal.calculate_price(x, y));
assert_abs_diff_eq!(price_f, 0.0, epsilon = 0.0);
}
#[test]
fn test_price_invariant_when_scaling_x_and_y_with_equal_weights() {
// For equal weights, price(x, y) == price(kx, ky).
let quote = Perquintill::from_rational(1u128, 2u128); // 0.5
let bal = Balancer::new(quote).unwrap();
let x1 = 3u64;
let y1 = 7u64;
let k = 10u64;
let x2 = x1 * k;
let y2 = y1 * k;
let p1 = f(bal.calculate_price(x1, y1));
let p2 = f(bal.calculate_price(x2, y2));
assert_abs_diff_eq!(p1, p2, epsilon = 1e-12);
}
#[test]
fn test_price_matches_formula_for_general_quote() {
// General check: price = (w1 / w2) * (y/x),
// where w1 = base_weight, w2 = quote_weight.
// Here we assume get_base_weight = 1 - quote.
let quote = Perquintill::from_rational(2u128, 5u128); // 0.4
let bal = Balancer::new(quote).unwrap();
let x = 9u64;
let y = 25u64;
let price_f = f(bal.calculate_price(x, y));
let base = Perquintill::one() - quote;
let w1 = base.deconstruct() as f64;
let w2 = quote.deconstruct() as f64;
let expected_f = (w1 / w2) * (y as f64 / x as f64);
assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9);
}
#[test]
fn test_price_high_values_non_equal_weights() {
// Non-equal weights, high x and y (up to 21e15)
let quote = Perquintill::from_rational(3u128, 10u128); // 0.3
let bal = Balancer::new(quote).unwrap();
let x: u64 = 21_000_000_000_000_000;
let y: u64 = 15_000_000_000_000_000;
let price = bal.calculate_price(x, y);
let price_f = f(price);
// Expected: (w1 / w2) * (y / x), using Balancer's actual weights
let w1 = bal.get_base_weight().deconstruct() as f64;
let w2 = bal.get_quote_weight().deconstruct() as f64;
let expected_f = (w1 / w2) * (y as f64 / x as f64);
assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9);
}
// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_scaled --exact --nocapture
#[test]
fn test_exp_scaled() {
[
// base_weight_numerator, base_weight_denominator, reserve, d_reserve, base_quote
(5_u64, 10_u64, 100000_u64, 100_u64, true, 0.999000999000999),
(1_u64, 4_u64, 500000_u64, 5000_u64, true, 0.970590147927644),
(3_u64, 4_u64, 200000_u64, 2000_u64, false, 0.970590147927644),
(
9_u64,
10_u64,
13513642_u64,
1673_u64,
false,
0.998886481979889,
),
(
773_u64,
1000_u64,
7_000_000_000_u64,
10_000_u64,
true,
0.999999580484586,
),
]
.into_iter()
.map(|v| {
(
Perquintill::from_rational(v.0, v.1),
v.2,
v.3,
v.4,
U64F64::from_num(v.5),
)
})
.for_each(|(quote_weight, reserve, d_reserve, base_quote, expected)| {
let balancer = Balancer::new(quote_weight).unwrap();
let result = balancer.exp_scaled(reserve, d_reserve as i128, base_quote);
assert_abs_diff_eq!(
result.to_num::<f64>(),
expected.to_num::<f64>(),
epsilon = 0.000000001
);
});
}
// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_base_needed_for_quote --exact --nocapture
#[test]
fn test_base_needed_for_quote() {
let num = 250_000_000_000_u128; // w1 = 0.75
let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128);
let bal = Balancer::new(w_quote).unwrap();
let tao_reserve: u64 = 1_000_000_000;
let alpha_reserve: u64 = 1_000_000_000;
let tao_delta: u64 = 1_123_432; // typical fee range
let dx = bal.get_base_needed_for_quote(tao_reserve, alpha_reserve, tao_delta);
// ∆x = x•[(y/(y+∆y))^(w2/w1) - 1]
let dx_expected = tao_reserve as f64
* ((tao_reserve as f64 / ((tao_reserve - tao_delta) as f64)).powf(0.25 / 0.75) - 1.0);
assert_eq!(dx, dx_expected as u64,);
}
}