use std::any::Any;
use alloy::primitives::{Address as AlloyAddress, U256};
use balancer_maths_rust::{
common::{
maths::{div_up_fixed, mul_down_fixed, mul_up_fixed, pow_up_fixed},
pool_base::PoolBase,
types::{PoolState, SwapInput, SwapKind, SwapParams},
utils::{
compute_and_charge_aggregate_swap_fees_raw, to_raw_undo_rate_round_down,
to_scaled_18_apply_rate_round_down,
},
WAD as ONE_WAD_SCALED_18,
},
pools::{
quantamm::QuantAmmPool,
reclammv2::{compute_current_virtual_balances, compute_in_given_out, ReClammV2Pool},
stable::{self, StablePool},
weighted::{WeightedPool, MAX_IN_RATIO},
},
vault::swap::{swap as vault_swap, MINIMUM_TRADE_AMOUNT},
DefaultHook, PoolError,
};
use num_bigint::{BigUint, ToBigUint};
use serde::{Deserialize, Serialize};
use tycho_common::{
dto::ProtocolStateDelta,
models::token::Token,
simulation::{
errors::{SimulationError, TransitionError},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
use crate::evm::{
engine_db::{create_engine, SHARED_TYCHO_DB},
protocol::{
balancer_v3::vm,
u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
utils::add_fee_markup,
},
};
const WAD: f64 = 1e18;
const SWAP_GAS: u64 = 210_000;
const SPOT_PRICE_PROBE_DIVISOR: u64 = 1_000_000;
const BLOCK_TIMESTAMP_ATTRIBUTE: &str = "block_timestamp";
const MAX_VAULT_BALANCE: U256 = U256::from_limbs([u64::MAX, u64::MAX, 0, 0]);
const MAX_TOKEN_OUT_RATIO: U256 = U256::from_limbs([990_000_000_000_000_000, 0, 0, 0]);
const STABLE_MAX_IMBALANCE_RATIO: U256 = U256::from_limbs([10_000, 0, 0, 0]);
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BalancerV3State {
pool_address: Bytes,
tokens: Vec<Bytes>,
min_token_balances: Vec<U256>,
block_timestamp: u64,
state: PoolState,
}
impl BalancerV3State {
pub(super) fn new(
pool_address: Bytes,
tokens: Vec<Bytes>,
min_token_balances: Vec<U256>,
block_timestamp: u64,
state: PoolState,
) -> Self {
Self { pool_address, tokens, min_token_balances, block_timestamp, state }
}
#[cfg(test)]
pub(super) fn token_addresses(&self) -> &[Bytes] {
&self.tokens
}
#[cfg(test)]
pub(super) fn raw_balances(&self) -> Vec<U256> {
let base = self.state.base();
(0..base.balances_live_scaled_18.len())
.map(|index| raw_balance(base, index).expect("a live balance must rescale to raw"))
.collect()
}
#[cfg(test)]
pub(super) fn state_balances(&self) -> &[U256] {
&self
.state
.base()
.balances_live_scaled_18
}
pub(super) fn token_index(&self, token: &Bytes) -> Result<usize, SimulationError> {
self.tokens
.iter()
.position(|candidate| candidate == token)
.ok_or_else(|| {
SimulationError::InvalidInput(
format!(
"token {token} is not registered in balancer_v3 pool {}",
self.pool_address
),
None,
)
})
}
fn pool_impl(&self) -> Result<Box<dyn PoolBase>, PoolError> {
match &self.state {
PoolState::Weighted(state) => Ok(Box::new(WeightedPool::from(state.clone()))),
PoolState::Stable(state) => Ok(Box::new(StablePool::new(state.mutable.clone()))),
PoolState::ReClammV2(state) => Ok(Box::new(ReClammV2Pool::new(state.clone()))),
PoolState::QuantAmm(state) => {
QuantAmmPool::new(state.clone()).map(|pool| Box::new(pool) as Box<dyn PoolBase>)
}
other => Err(PoolError::UnsupportedPoolType(other.pool_type().to_string())),
}
}
fn vault_swap_exact_in(
&self,
amount_in: U256,
token_in: &Bytes,
token_out: &Bytes,
) -> Result<U256, PoolError> {
let input = SwapInput {
amount_raw: amount_in,
swap_kind: SwapKind::GivenIn,
token_in: format!("0x{}", hex::encode(token_in)),
token_out: format!("0x{}", hex::encode(token_out)),
};
vault_swap(&input, &self.state, self.pool_impl()?.as_ref(), &DefaultHook::new(), None)
}
fn max_swap_amount_in(
&self,
index_in: usize,
index_out: usize,
) -> Result<U256, SimulationError> {
let base = self.state.base();
let balances = &base.balances_live_scaled_18;
let maths_error = |e: PoolError| {
SimulationError::FatalError(format!(
"balancer_v3 swap limit failed for pool {}: {e:?}",
self.pool_address
))
};
let max_in_scaled_18 = match &self.state {
PoolState::Weighted(state) => {
self.weighted_max_swap_amount_in(index_in, index_out, state.weights())?
}
PoolState::Stable(_) => self.stable_max_swap_amount_in(index_in, index_out)?,
PoolState::QuantAmm(state) => {
return self.quantamm_max_swap_amount_in(
index_in,
index_out,
&state.immutable.max_trade_size_ratio,
)
}
PoolState::ReClammV2(state) => {
let max_out_scaled_18 = mul_down_fixed(&MAX_TOKEN_OUT_RATIO, &balances[index_out])
.map_err(maths_error)?;
let mutable = &state.mutable;
let (virtual_balance_a, virtual_balance_b, _) = compute_current_virtual_balances(
&mutable.current_timestamp,
balances,
&mutable.last_virtual_balances[0],
&mutable.last_virtual_balances[1],
&mutable.daily_price_shift_base,
&mutable.last_timestamp,
&mutable.centeredness_margin,
&mutable.start_fourth_root_price_ratio,
&mutable.end_fourth_root_price_ratio,
&mutable.price_ratio_update_start_time,
&mutable.price_ratio_update_end_time,
)
.map_err(|e| {
SimulationError::RecoverableError(format!(
"balancer_v3 reCLAMM pool {} has no usable price range: {e:?}",
self.pool_address
))
})?;
compute_in_given_out(
balances,
&virtual_balance_a,
&virtual_balance_b,
index_in,
index_out,
&max_out_scaled_18,
)
.map_err(|e| {
SimulationError::FatalError(format!(
"balancer_v3 swap limit failed for pool {}: {e}",
self.pool_address
))
})?
}
other => {
return Err(SimulationError::FatalError(format!(
"balancer_v3 pool {} holds unsupported state `{}`",
self.pool_address,
other.pool_type()
)))
}
};
to_raw_undo_rate_round_down(
&max_in_scaled_18,
&base.scaling_factors[index_in],
&base.token_rates[index_in],
)
.map_err(maths_error)
}
fn quantamm_max_swap_amount_in(
&self,
index_in: usize,
index_out: usize,
max_trade_size_ratio: &U256,
) -> Result<U256, SimulationError> {
let base = self.state.base();
let maths_error = |e: PoolError| {
SimulationError::FatalError(format!(
"balancer_v3 swap limit failed for pool {}: {e:?}",
self.pool_address
))
};
let input_cap_scaled_18 =
mul_down_fixed(&base.balances_live_scaled_18[index_in], max_trade_size_ratio)
.map_err(maths_error)?;
let mut high = to_raw_undo_rate_round_down(
&input_cap_scaled_18,
&base.scaling_factors[index_in],
&base.token_rates[index_in],
)
.map_err(maths_error)?;
let (token_in, token_out) = (&self.tokens[index_in], &self.tokens[index_out]);
let accepted = |amount: &U256| {
self.vault_swap_exact_in(*amount, token_in, token_out)
.is_ok()
};
if accepted(&high) {
return Ok(high);
}
let mut low = U256::ZERO;
while high - low > U256::from(1) {
let mid = low + ((high - low) >> 1);
if accepted(&mid) {
low = mid;
} else {
high = mid;
}
}
Ok(low)
}
fn weighted_max_swap_amount_in(
&self,
index_in: usize,
index_out: usize,
weights: &[U256],
) -> Result<U256, SimulationError> {
let base = self.state.base();
let balances = &base.balances_live_scaled_18;
let maths_error = |e: PoolError| {
SimulationError::FatalError(format!(
"balancer_v3 swap limit failed for pool {}: {e:?}",
self.pool_address
))
};
let ratio_cap = mul_down_fixed(&balances[index_in], &MAX_IN_RATIO).map_err(maths_error)?;
let (Some(&min_in), Some(&min_out)) =
(self.min_token_balances.get(index_in), self.min_token_balances.get(index_out))
else {
return Ok(ratio_cap);
};
if balances[index_in] + U256::from(1) < min_in {
return Ok(U256::ZERO);
}
if min_out.is_zero() {
return Ok(ratio_cap);
}
let Some(target_out) = balances[index_out].checked_sub(min_out) else {
return Ok(U256::ZERO);
};
if target_out.is_zero() {
return Ok(U256::ZERO);
}
let min_balance_cap = match weighted_in_given_exact_out_unguarded(
&balances[index_in],
&weights[index_in],
&balances[index_out],
&weights[index_out],
&target_out,
) {
Ok(cap) => cap,
Err(PoolError::MathOverflow) => return Ok(ratio_cap),
Err(e) => return Err(maths_error(e)),
};
Ok(ratio_cap.min(min_balance_cap))
}
pub(super) fn stable_max_swap_amount_in(
&self,
index_in: usize,
index_out: usize,
) -> Result<U256, SimulationError> {
let balances = &self
.state
.base()
.balances_live_scaled_18;
let mut low = U256::ZERO;
let mut high = MAX_VAULT_BALANCE.saturating_sub(balances[index_in]);
if self.stable_swap_keeps_balance_valid(index_in, index_out, &high)? {
return Ok(high);
}
while high - low > U256::from(1) {
let mid = low + ((high - low) >> 1);
if self.stable_swap_keeps_balance_valid(index_in, index_out, &mid)? {
low = mid;
} else {
high = mid;
}
}
Ok(low)
}
pub(super) fn stable_swap_keeps_balance_valid(
&self,
index_in: usize,
index_out: usize,
amount_in_scaled_18: &U256,
) -> Result<bool, SimulationError> {
let base = self.state.base();
let PoolState::Stable(state) = &self.state else {
return Err(SimulationError::FatalError(format!(
"balancer_v3 pool {} is not a stable pool",
self.pool_address
)));
};
let balances = &base.balances_live_scaled_18;
let maths_error = |e: PoolError| {
SimulationError::FatalError(format!(
"balancer_v3 stable limit probe failed for pool {}: {e:?}",
self.pool_address
))
};
if balances.iter().any(U256::is_zero) {
return Ok(false);
}
let fee_scaled = mul_up_fixed(amount_in_scaled_18, &base.swap_fee).map_err(maths_error)?;
let Some(amount_in_after_fee) = amount_in_scaled_18.checked_sub(fee_scaled) else {
return Ok(false);
};
if amount_in_after_fee < MINIMUM_TRADE_AMOUNT {
return Ok(false);
}
let amp = &state.mutable.amp;
let invariant = stable::compute_invariant(amp, balances).map_err(maths_error)?;
let Ok(amount_out_scaled) = stable::compute_out_given_exact_in(
amp,
balances,
index_in,
index_out,
&amount_in_after_fee,
&invariant,
) else {
return Ok(false);
};
let Some(new_balance_out) = balances[index_out].checked_sub(amount_out_scaled) else {
return Ok(false);
};
let new_balance_in = balances[index_in] + amount_in_after_fee;
let min_balance = balances
.iter()
.copied()
.min()
.unwrap_or_default()
.min(new_balance_out);
let max_balance = balances
.iter()
.copied()
.max()
.unwrap_or_default()
.max(new_balance_in);
if min_balance.is_zero() {
return Ok(false);
}
Ok(max_balance < STABLE_MAX_IMBALANCE_RATIO * min_balance)
}
fn with_swap_applied(
&self,
amount_in: U256,
amount_out: U256,
index_in: usize,
index_out: usize,
) -> Result<Self, SimulationError> {
let base = self.state.base();
let maths_error = |e: balancer_maths_rust::PoolError| {
SimulationError::FatalError(format!("balancer_v3 balance update failed: {e:?}"))
};
let amount_in_scaled = to_scaled_18_apply_rate_round_down(
&amount_in,
&base.scaling_factors[index_in],
&base.token_rates[index_in],
)
.map_err(maths_error)?;
let amount_out_scaled = to_scaled_18_apply_rate_round_down(
&amount_out,
&base.scaling_factors[index_out],
&base.token_rates[index_out],
)
.map_err(maths_error)?;
let total_fee_scaled =
mul_up_fixed(&amount_in_scaled, &base.swap_fee).map_err(maths_error)?;
let protocol_fee_raw = compute_and_charge_aggregate_swap_fees_raw(
&total_fee_scaled,
&base.aggregate_swap_fee,
&base.scaling_factors,
&base.token_rates,
index_in,
)
.map_err(maths_error)?;
let protocol_fee_scaled = to_scaled_18_apply_rate_round_down(
&protocol_fee_raw,
&base.scaling_factors[index_in],
&base.token_rates[index_in],
)
.map_err(maths_error)?;
let mut balances = base.balances_live_scaled_18.clone();
balances[index_in] += amount_in_scaled - protocol_fee_scaled;
balances[index_out] = balances[index_out].saturating_sub(amount_out_scaled);
let mut updated = self.clone();
updated.set_balances(balances);
Ok(updated)
}
fn set_balances(&mut self, balances: Vec<U256>) {
match &mut self.state {
PoolState::Weighted(state) => state.base.balances_live_scaled_18 = balances,
PoolState::Stable(state) => state.base.balances_live_scaled_18 = balances,
PoolState::ReClammV2(state) => state.base.balances_live_scaled_18 = balances,
PoolState::QuantAmm(state) => state.base.balances_live_scaled_18 = balances,
_ => {}
}
}
}
#[typetag::serde]
impl ProtocolSim for BalancerV3State {
fn fee(&self) -> f64 {
u256_to_f64(self.state.base().swap_fee)
.map(|fee| fee / WAD)
.unwrap_or(0.0)
}
fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
let index_in = self.token_index(&base.address)?;
let index_out = self.token_index("e.address)?;
let pool_base = self.state.base();
let balances = &pool_base.balances_live_scaled_18;
let probe = (balances[index_in] / U256::from(SPOT_PRICE_PROBE_DIVISOR)).max(U256::from(1));
let probe_failed = |e: PoolError| {
SimulationError::RecoverableError(format!(
"balancer_v3 spot price probe failed for pool {}: {e:?}",
self.pool_address
))
};
let out = self
.pool_impl()
.map_err(probe_failed)?
.on_swap(&SwapParams {
swap_kind: SwapKind::GivenIn,
token_in_index: index_in,
token_out_index: index_out,
amount_scaled_18: probe,
balances_live_scaled_18: balances.clone(),
})
.map_err(probe_failed)?;
let ratio = u256_to_f64(out)? / u256_to_f64(probe)?;
let rate_in = u256_to_f64(pool_base.token_rates[index_in])?;
let rate_out = u256_to_f64(pool_base.token_rates[index_out])?;
if rate_out == 0.0 {
return Err(SimulationError::RecoverableError(format!(
"balancer_v3 pool {} reports a zero rate for {}",
self.pool_address, quote.address
)));
}
Ok(add_fee_markup(ratio * rate_in / rate_out, self.fee()))
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
let index_in = self.token_index(&token_in.address)?;
let index_out = self.token_index(&token_out.address)?;
let amount_in = biguint_to_u256(&amount_in);
let amount_out = self
.vault_swap_exact_in(amount_in, &token_in.address, &token_out.address)
.map_err(|e| {
SimulationError::RecoverableError(format!(
"balancer_v3 swap failed for pool {}: {e:?}",
self.pool_address
))
})?;
let new_state = self.with_swap_applied(amount_in, amount_out, index_in, index_out)?;
Ok(GetAmountOutResult::new(
u256_to_biguint(amount_out),
SWAP_GAS
.to_biguint()
.expect("u64 fits in BigUint"),
Box::new(new_state),
))
}
fn get_limits(
&self,
sell_token: Bytes,
buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
let index_in = self.token_index(&sell_token)?;
let index_out = self.token_index(&buy_token)?;
let base = self.state.base();
if base.balances_live_scaled_18[index_in].is_zero() ||
base.balances_live_scaled_18[index_out].is_zero()
{
return Ok((BigUint::ZERO, BigUint::ZERO));
}
let max_in = self.max_swap_amount_in(index_in, index_out)?;
if max_in.is_zero() {
return Ok((BigUint::ZERO, BigUint::ZERO));
}
let max_out = match self.vault_swap_exact_in(max_in, &sell_token, &buy_token) {
Ok(amount_out) => amount_out,
Err(PoolError::TradeAmountTooSmall) => return Ok((BigUint::ZERO, BigUint::ZERO)),
Err(e) => {
return Err(SimulationError::RecoverableError(format!(
"balancer_v3 swap failed for pool {}: {e:?}",
self.pool_address
)))
}
};
Ok((u256_to_biguint(max_in), u256_to_biguint(max_out)))
}
fn delta_transition(
&mut self,
delta: ProtocolStateDelta,
_tokens: &std::collections::HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
if let Some(timestamp) = delta
.updated_attributes
.get(BLOCK_TIMESTAMP_ATTRIBUTE)
.and_then(|raw| raw.as_ref().try_into().ok())
.map(u64::from_be_bytes)
{
self.block_timestamp = timestamp;
}
let engine = create_engine(SHARED_TYCHO_DB.clone(), false).expect("Infallible");
let pool = AlloyAddress::from_slice(self.pool_address.as_ref());
self.state = vm::refresh_pool_state(&engine, &pool, &self.state, self.block_timestamp)
.map_err(TransitionError::SimulationError)?;
Ok(())
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn eq(&self, other: &dyn ProtocolSim) -> bool {
other
.as_any()
.downcast_ref::<Self>()
.is_some_and(|other| self == other)
}
}
fn weighted_in_given_exact_out_unguarded(
balance_in: &U256,
weight_in: &U256,
balance_out: &U256,
weight_out: &U256,
amount_out: &U256,
) -> Result<U256, PoolError> {
let base = div_up_fixed(balance_out, &(balance_out - amount_out))?;
let exponent = div_up_fixed(weight_out, weight_in)?;
let power = pow_up_fixed(&base, &exponent)?;
let ratio = power - ONE_WAD_SCALED_18;
mul_up_fixed(balance_in, &ratio)
}
#[cfg(test)]
fn raw_balance(
base: &balancer_maths_rust::common::types::BasePoolState,
index: usize,
) -> Result<U256, SimulationError> {
to_raw_undo_rate_round_down(
&base.balances_live_scaled_18[index],
&base.scaling_factors[index],
&base.token_rates[index],
)
.map_err(|e| SimulationError::FatalError(format!("balancer_v3 balance rescale failed: {e:?}")))
}