1use std::any::Any;
4
5use alloy::primitives::{Address as AlloyAddress, U256};
6use balancer_maths_rust::{
7 common::{
8 maths::{div_up_fixed, mul_down_fixed, mul_up_fixed, pow_up_fixed},
9 pool_base::PoolBase,
10 types::{PoolState, SwapInput, SwapKind, SwapParams},
11 utils::{
12 compute_and_charge_aggregate_swap_fees_raw, to_raw_undo_rate_round_down,
13 to_scaled_18_apply_rate_round_down,
14 },
15 WAD as ONE_WAD_SCALED_18,
16 },
17 pools::{
18 quantamm::QuantAmmPool,
19 reclammv2::{compute_current_virtual_balances, compute_in_given_out, ReClammV2Pool},
20 stable::{self, StablePool},
21 weighted::{WeightedPool, MAX_IN_RATIO},
22 },
23 vault::swap::{swap as vault_swap, MINIMUM_TRADE_AMOUNT},
24 DefaultHook, PoolError,
25};
26use num_bigint::{BigUint, ToBigUint};
27use serde::{Deserialize, Serialize};
28use tycho_common::{
29 dto::ProtocolStateDelta,
30 models::token::Token,
31 simulation::{
32 errors::{SimulationError, TransitionError},
33 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
34 },
35 Bytes,
36};
37
38use crate::evm::{
39 engine_db::{create_engine, SHARED_TYCHO_DB},
40 protocol::{
41 balancer_v3::vm,
42 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
43 utils::add_fee_markup,
44 },
45};
46
47const WAD: f64 = 1e18;
49const SWAP_GAS: u64 = 210_000;
52const SPOT_PRICE_PROBE_DIVISOR: u64 = 1_000_000;
54const BLOCK_TIMESTAMP_ATTRIBUTE: &str = "block_timestamp";
56const MAX_VAULT_BALANCE: U256 = U256::from_limbs([u64::MAX, u64::MAX, 0, 0]);
59const MAX_TOKEN_OUT_RATIO: U256 = U256::from_limbs([990_000_000_000_000_000, 0, 0, 0]);
62const STABLE_MAX_IMBALANCE_RATIO: U256 = U256::from_limbs([10_000, 0, 0, 0]);
67
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
76pub struct BalancerV3State {
77 pool_address: Bytes,
79 tokens: Vec<Bytes>,
82 min_token_balances: Vec<U256>,
86 block_timestamp: u64,
89 state: PoolState,
91}
92
93impl BalancerV3State {
94 pub(super) fn new(
95 pool_address: Bytes,
96 tokens: Vec<Bytes>,
97 min_token_balances: Vec<U256>,
98 block_timestamp: u64,
99 state: PoolState,
100 ) -> Self {
101 Self { pool_address, tokens, min_token_balances, block_timestamp, state }
102 }
103
104 #[cfg(test)]
106 pub(super) fn token_addresses(&self) -> &[Bytes] {
107 &self.tokens
108 }
109
110 #[cfg(test)]
112 pub(super) fn raw_balances(&self) -> Vec<U256> {
113 let base = self.state.base();
114 (0..base.balances_live_scaled_18.len())
115 .map(|index| raw_balance(base, index).expect("a live balance must rescale to raw"))
116 .collect()
117 }
118
119 #[cfg(test)]
121 pub(super) fn state_balances(&self) -> &[U256] {
122 &self
123 .state
124 .base()
125 .balances_live_scaled_18
126 }
127
128 pub(super) fn token_index(&self, token: &Bytes) -> Result<usize, SimulationError> {
129 self.tokens
130 .iter()
131 .position(|candidate| candidate == token)
132 .ok_or_else(|| {
133 SimulationError::InvalidInput(
134 format!(
135 "token {token} is not registered in balancer_v3 pool {}",
136 self.pool_address
137 ),
138 None,
139 )
140 })
141 }
142
143 fn pool_impl(&self) -> Result<Box<dyn PoolBase>, PoolError> {
147 match &self.state {
148 PoolState::Weighted(state) => Ok(Box::new(WeightedPool::from(state.clone()))),
149 PoolState::Stable(state) => Ok(Box::new(StablePool::new(state.mutable.clone()))),
150 PoolState::ReClammV2(state) => Ok(Box::new(ReClammV2Pool::new(state.clone()))),
151 PoolState::QuantAmm(state) => {
154 QuantAmmPool::new(state.clone()).map(|pool| Box::new(pool) as Box<dyn PoolBase>)
155 }
156 other => Err(PoolError::UnsupportedPoolType(other.pool_type().to_string())),
157 }
158 }
159
160 fn vault_swap_exact_in(
164 &self,
165 amount_in: U256,
166 token_in: &Bytes,
167 token_out: &Bytes,
168 ) -> Result<U256, PoolError> {
169 let input = SwapInput {
170 amount_raw: amount_in,
171 swap_kind: SwapKind::GivenIn,
172 token_in: format!("0x{}", hex::encode(token_in)),
173 token_out: format!("0x{}", hex::encode(token_out)),
174 };
175 vault_swap(&input, &self.state, self.pool_impl()?.as_ref(), &DefaultHook::new(), None)
180 }
181
182 fn max_swap_amount_in(
190 &self,
191 index_in: usize,
192 index_out: usize,
193 ) -> Result<U256, SimulationError> {
194 let base = self.state.base();
195 let balances = &base.balances_live_scaled_18;
196 let maths_error = |e: PoolError| {
197 SimulationError::FatalError(format!(
198 "balancer_v3 swap limit failed for pool {}: {e:?}",
199 self.pool_address
200 ))
201 };
202
203 let max_in_scaled_18 = match &self.state {
204 PoolState::Weighted(state) => {
205 self.weighted_max_swap_amount_in(index_in, index_out, state.weights())?
206 }
207 PoolState::Stable(_) => self.stable_max_swap_amount_in(index_in, index_out)?,
208 PoolState::QuantAmm(state) => {
211 return self.quantamm_max_swap_amount_in(
212 index_in,
213 index_out,
214 &state.immutable.max_trade_size_ratio,
215 )
216 }
217 PoolState::ReClammV2(state) => {
218 let max_out_scaled_18 = mul_down_fixed(&MAX_TOKEN_OUT_RATIO, &balances[index_out])
219 .map_err(maths_error)?;
220 let mutable = &state.mutable;
221 let (virtual_balance_a, virtual_balance_b, _) = compute_current_virtual_balances(
225 &mutable.current_timestamp,
226 balances,
227 &mutable.last_virtual_balances[0],
228 &mutable.last_virtual_balances[1],
229 &mutable.daily_price_shift_base,
230 &mutable.last_timestamp,
231 &mutable.centeredness_margin,
232 &mutable.start_fourth_root_price_ratio,
233 &mutable.end_fourth_root_price_ratio,
234 &mutable.price_ratio_update_start_time,
235 &mutable.price_ratio_update_end_time,
236 )
237 .map_err(|e| {
238 SimulationError::RecoverableError(format!(
239 "balancer_v3 reCLAMM pool {} has no usable price range: {e:?}",
240 self.pool_address
241 ))
242 })?;
243 compute_in_given_out(
244 balances,
245 &virtual_balance_a,
246 &virtual_balance_b,
247 index_in,
248 index_out,
249 &max_out_scaled_18,
250 )
251 .map_err(|e| {
252 SimulationError::FatalError(format!(
253 "balancer_v3 swap limit failed for pool {}: {e}",
254 self.pool_address
255 ))
256 })?
257 }
258 other => {
259 return Err(SimulationError::FatalError(format!(
260 "balancer_v3 pool {} holds unsupported state `{}`",
261 self.pool_address,
262 other.pool_type()
263 )))
264 }
265 };
266
267 to_raw_undo_rate_round_down(
268 &max_in_scaled_18,
269 &base.scaling_factors[index_in],
270 &base.token_rates[index_in],
271 )
272 .map_err(maths_error)
273 }
274
275 fn quantamm_max_swap_amount_in(
285 &self,
286 index_in: usize,
287 index_out: usize,
288 max_trade_size_ratio: &U256,
289 ) -> Result<U256, SimulationError> {
290 let base = self.state.base();
291 let maths_error = |e: PoolError| {
292 SimulationError::FatalError(format!(
293 "balancer_v3 swap limit failed for pool {}: {e:?}",
294 self.pool_address
295 ))
296 };
297
298 let input_cap_scaled_18 =
299 mul_down_fixed(&base.balances_live_scaled_18[index_in], max_trade_size_ratio)
300 .map_err(maths_error)?;
301 let mut high = to_raw_undo_rate_round_down(
302 &input_cap_scaled_18,
303 &base.scaling_factors[index_in],
304 &base.token_rates[index_in],
305 )
306 .map_err(maths_error)?;
307
308 let (token_in, token_out) = (&self.tokens[index_in], &self.tokens[index_out]);
309 let accepted = |amount: &U256| {
310 self.vault_swap_exact_in(*amount, token_in, token_out)
311 .is_ok()
312 };
313 if accepted(&high) {
314 return Ok(high);
315 }
316
317 let mut low = U256::ZERO;
318 while high - low > U256::from(1) {
319 let mid = low + ((high - low) >> 1);
320 if accepted(&mid) {
321 low = mid;
322 } else {
323 high = mid;
324 }
325 }
326 Ok(low)
327 }
328
329 fn weighted_max_swap_amount_in(
338 &self,
339 index_in: usize,
340 index_out: usize,
341 weights: &[U256],
342 ) -> Result<U256, SimulationError> {
343 let base = self.state.base();
344 let balances = &base.balances_live_scaled_18;
345 let maths_error = |e: PoolError| {
346 SimulationError::FatalError(format!(
347 "balancer_v3 swap limit failed for pool {}: {e:?}",
348 self.pool_address
349 ))
350 };
351
352 let ratio_cap = mul_down_fixed(&balances[index_in], &MAX_IN_RATIO).map_err(maths_error)?;
353 let (Some(&min_in), Some(&min_out)) =
354 (self.min_token_balances.get(index_in), self.min_token_balances.get(index_out))
355 else {
356 return Ok(ratio_cap);
358 };
359
360 if balances[index_in] + U256::from(1) < min_in {
363 return Ok(U256::ZERO);
364 }
365 if min_out.is_zero() {
369 return Ok(ratio_cap);
370 }
371 let Some(target_out) = balances[index_out].checked_sub(min_out) else {
372 return Ok(U256::ZERO);
373 };
374 if target_out.is_zero() {
375 return Ok(U256::ZERO);
376 }
377
378 let min_balance_cap = match weighted_in_given_exact_out_unguarded(
379 &balances[index_in],
380 &weights[index_in],
381 &balances[index_out],
382 &weights[index_out],
383 &target_out,
384 ) {
385 Ok(cap) => cap,
386 Err(PoolError::MathOverflow) => return Ok(ratio_cap),
391 Err(e) => return Err(maths_error(e)),
392 };
393 Ok(ratio_cap.min(min_balance_cap))
394 }
395
396 pub(super) fn stable_max_swap_amount_in(
401 &self,
402 index_in: usize,
403 index_out: usize,
404 ) -> Result<U256, SimulationError> {
405 let balances = &self
406 .state
407 .base()
408 .balances_live_scaled_18;
409 let mut low = U256::ZERO;
410 let mut high = MAX_VAULT_BALANCE.saturating_sub(balances[index_in]);
411 if self.stable_swap_keeps_balance_valid(index_in, index_out, &high)? {
412 return Ok(high);
413 }
414 while high - low > U256::from(1) {
415 let mid = low + ((high - low) >> 1);
416 if self.stable_swap_keeps_balance_valid(index_in, index_out, &mid)? {
417 low = mid;
418 } else {
419 high = mid;
420 }
421 }
422 Ok(low)
423 }
424
425 pub(super) fn stable_swap_keeps_balance_valid(
431 &self,
432 index_in: usize,
433 index_out: usize,
434 amount_in_scaled_18: &U256,
435 ) -> Result<bool, SimulationError> {
436 let base = self.state.base();
437 let PoolState::Stable(state) = &self.state else {
438 return Err(SimulationError::FatalError(format!(
439 "balancer_v3 pool {} is not a stable pool",
440 self.pool_address
441 )));
442 };
443 let balances = &base.balances_live_scaled_18;
444 let maths_error = |e: PoolError| {
445 SimulationError::FatalError(format!(
446 "balancer_v3 stable limit probe failed for pool {}: {e:?}",
447 self.pool_address
448 ))
449 };
450
451 if balances.iter().any(U256::is_zero) {
455 return Ok(false);
456 }
457
458 let fee_scaled = mul_up_fixed(amount_in_scaled_18, &base.swap_fee).map_err(maths_error)?;
459 let Some(amount_in_after_fee) = amount_in_scaled_18.checked_sub(fee_scaled) else {
460 return Ok(false);
461 };
462 if amount_in_after_fee < MINIMUM_TRADE_AMOUNT {
463 return Ok(false);
464 }
465
466 let amp = &state.mutable.amp;
467 let invariant = stable::compute_invariant(amp, balances).map_err(maths_error)?;
468 let Ok(amount_out_scaled) = stable::compute_out_given_exact_in(
469 amp,
470 balances,
471 index_in,
472 index_out,
473 &amount_in_after_fee,
474 &invariant,
475 ) else {
476 return Ok(false);
477 };
478 let Some(new_balance_out) = balances[index_out].checked_sub(amount_out_scaled) else {
479 return Ok(false);
480 };
481 let new_balance_in = balances[index_in] + amount_in_after_fee;
482
483 let min_balance = balances
484 .iter()
485 .copied()
486 .min()
487 .unwrap_or_default()
488 .min(new_balance_out);
489 let max_balance = balances
490 .iter()
491 .copied()
492 .max()
493 .unwrap_or_default()
494 .max(new_balance_in);
495 if min_balance.is_zero() {
496 return Ok(false);
497 }
498 Ok(max_balance < STABLE_MAX_IMBALANCE_RATIO * min_balance)
499 }
500
501 fn with_swap_applied(
507 &self,
508 amount_in: U256,
509 amount_out: U256,
510 index_in: usize,
511 index_out: usize,
512 ) -> Result<Self, SimulationError> {
513 let base = self.state.base();
514 let maths_error = |e: balancer_maths_rust::PoolError| {
515 SimulationError::FatalError(format!("balancer_v3 balance update failed: {e:?}"))
516 };
517
518 let amount_in_scaled = to_scaled_18_apply_rate_round_down(
519 &amount_in,
520 &base.scaling_factors[index_in],
521 &base.token_rates[index_in],
522 )
523 .map_err(maths_error)?;
524 let amount_out_scaled = to_scaled_18_apply_rate_round_down(
525 &amount_out,
526 &base.scaling_factors[index_out],
527 &base.token_rates[index_out],
528 )
529 .map_err(maths_error)?;
530 let total_fee_scaled =
531 mul_up_fixed(&amount_in_scaled, &base.swap_fee).map_err(maths_error)?;
532 let protocol_fee_raw = compute_and_charge_aggregate_swap_fees_raw(
536 &total_fee_scaled,
537 &base.aggregate_swap_fee,
538 &base.scaling_factors,
539 &base.token_rates,
540 index_in,
541 )
542 .map_err(maths_error)?;
543 let protocol_fee_scaled = to_scaled_18_apply_rate_round_down(
544 &protocol_fee_raw,
545 &base.scaling_factors[index_in],
546 &base.token_rates[index_in],
547 )
548 .map_err(maths_error)?;
549
550 let mut balances = base.balances_live_scaled_18.clone();
551 balances[index_in] += amount_in_scaled - protocol_fee_scaled;
552 balances[index_out] = balances[index_out].saturating_sub(amount_out_scaled);
553
554 let mut updated = self.clone();
555 updated.set_balances(balances);
556 Ok(updated)
557 }
558
559 fn set_balances(&mut self, balances: Vec<U256>) {
560 match &mut self.state {
561 PoolState::Weighted(state) => state.base.balances_live_scaled_18 = balances,
562 PoolState::Stable(state) => state.base.balances_live_scaled_18 = balances,
563 PoolState::ReClammV2(state) => state.base.balances_live_scaled_18 = balances,
564 PoolState::QuantAmm(state) => state.base.balances_live_scaled_18 = balances,
565 _ => {}
566 }
567 }
568}
569
570#[typetag::serde]
571impl ProtocolSim for BalancerV3State {
572 fn fee(&self) -> f64 {
573 u256_to_f64(self.state.base().swap_fee)
574 .map(|fee| fee / WAD)
575 .unwrap_or(0.0)
576 }
577
578 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
579 let index_in = self.token_index(&base.address)?;
580 let index_out = self.token_index("e.address)?;
581 let pool_base = self.state.base();
582 let balances = &pool_base.balances_live_scaled_18;
583
584 let probe = (balances[index_in] / U256::from(SPOT_PRICE_PROBE_DIVISOR)).max(U256::from(1));
588 let probe_failed = |e: PoolError| {
589 SimulationError::RecoverableError(format!(
590 "balancer_v3 spot price probe failed for pool {}: {e:?}",
591 self.pool_address
592 ))
593 };
594 let out = self
595 .pool_impl()
596 .map_err(probe_failed)?
597 .on_swap(&SwapParams {
598 swap_kind: SwapKind::GivenIn,
599 token_in_index: index_in,
600 token_out_index: index_out,
601 amount_scaled_18: probe,
602 balances_live_scaled_18: balances.clone(),
603 })
604 .map_err(probe_failed)?;
605
606 let ratio = u256_to_f64(out)? / u256_to_f64(probe)?;
609 let rate_in = u256_to_f64(pool_base.token_rates[index_in])?;
610 let rate_out = u256_to_f64(pool_base.token_rates[index_out])?;
611 if rate_out == 0.0 {
612 return Err(SimulationError::RecoverableError(format!(
613 "balancer_v3 pool {} reports a zero rate for {}",
614 self.pool_address, quote.address
615 )));
616 }
617 Ok(add_fee_markup(ratio * rate_in / rate_out, self.fee()))
618 }
619
620 fn get_amount_out(
621 &self,
622 amount_in: BigUint,
623 token_in: &Token,
624 token_out: &Token,
625 ) -> Result<GetAmountOutResult, SimulationError> {
626 let index_in = self.token_index(&token_in.address)?;
627 let index_out = self.token_index(&token_out.address)?;
628 let amount_in = biguint_to_u256(&amount_in);
629 let amount_out = self
630 .vault_swap_exact_in(amount_in, &token_in.address, &token_out.address)
631 .map_err(|e| {
632 SimulationError::RecoverableError(format!(
633 "balancer_v3 swap failed for pool {}: {e:?}",
634 self.pool_address
635 ))
636 })?;
637 let new_state = self.with_swap_applied(amount_in, amount_out, index_in, index_out)?;
638
639 Ok(GetAmountOutResult::new(
640 u256_to_biguint(amount_out),
641 SWAP_GAS
642 .to_biguint()
643 .expect("u64 fits in BigUint"),
644 Box::new(new_state),
645 ))
646 }
647
648 fn get_limits(
649 &self,
650 sell_token: Bytes,
651 buy_token: Bytes,
652 ) -> Result<(BigUint, BigUint), SimulationError> {
653 let index_in = self.token_index(&sell_token)?;
654 let index_out = self.token_index(&buy_token)?;
655 let base = self.state.base();
656 if base.balances_live_scaled_18[index_in].is_zero() ||
657 base.balances_live_scaled_18[index_out].is_zero()
658 {
659 return Ok((BigUint::ZERO, BigUint::ZERO));
660 }
661
662 let max_in = self.max_swap_amount_in(index_in, index_out)?;
663 if max_in.is_zero() {
664 return Ok((BigUint::ZERO, BigUint::ZERO));
665 }
666 let max_out = match self.vault_swap_exact_in(max_in, &sell_token, &buy_token) {
669 Ok(amount_out) => amount_out,
670 Err(PoolError::TradeAmountTooSmall) => return Ok((BigUint::ZERO, BigUint::ZERO)),
671 Err(e) => {
672 return Err(SimulationError::RecoverableError(format!(
673 "balancer_v3 swap failed for pool {}: {e:?}",
674 self.pool_address
675 )))
676 }
677 };
678 Ok((u256_to_biguint(max_in), u256_to_biguint(max_out)))
679 }
680
681 fn delta_transition(
682 &mut self,
683 delta: ProtocolStateDelta,
684 _tokens: &std::collections::HashMap<Bytes, Token>,
685 _balances: &Balances,
686 ) -> Result<(), TransitionError> {
687 if let Some(timestamp) = delta
690 .updated_attributes
691 .get(BLOCK_TIMESTAMP_ATTRIBUTE)
692 .and_then(|raw| raw.as_ref().try_into().ok())
693 .map(u64::from_be_bytes)
694 {
695 self.block_timestamp = timestamp;
696 }
697
698 let engine = create_engine(SHARED_TYCHO_DB.clone(), false).expect("Infallible");
699 let pool = AlloyAddress::from_slice(self.pool_address.as_ref());
700 self.state = vm::refresh_pool_state(&engine, &pool, &self.state, self.block_timestamp)
701 .map_err(TransitionError::SimulationError)?;
702 Ok(())
703 }
704
705 fn clone_box(&self) -> Box<dyn ProtocolSim> {
706 Box::new(self.clone())
707 }
708
709 fn as_any(&self) -> &dyn Any {
710 self
711 }
712
713 fn as_any_mut(&mut self) -> &mut dyn Any {
714 self
715 }
716
717 fn eq(&self, other: &dyn ProtocolSim) -> bool {
718 other
719 .as_any()
720 .downcast_ref::<Self>()
721 .is_some_and(|other| self == other)
722 }
723}
724
725fn weighted_in_given_exact_out_unguarded(
731 balance_in: &U256,
732 weight_in: &U256,
733 balance_out: &U256,
734 weight_out: &U256,
735 amount_out: &U256,
736) -> Result<U256, PoolError> {
737 let base = div_up_fixed(balance_out, &(balance_out - amount_out))?;
738 let exponent = div_up_fixed(weight_out, weight_in)?;
739 let power = pow_up_fixed(&base, &exponent)?;
740 let ratio = power - ONE_WAD_SCALED_18;
741 mul_up_fixed(balance_in, &ratio)
742}
743
744#[cfg(test)]
746fn raw_balance(
747 base: &balancer_maths_rust::common::types::BasePoolState,
748 index: usize,
749) -> Result<U256, SimulationError> {
750 to_raw_undo_rate_round_down(
751 &base.balances_live_scaled_18[index],
752 &base.scaling_factors[index],
753 &base.token_rates[index],
754 )
755 .map_err(|e| SimulationError::FatalError(format!("balancer_v3 balance rescale failed: {e:?}")))
756}