Skip to main content

balancer_maths_rust/pools/stable/
stable_math.rs

1use crate::common::errors::PoolError;
2use crate::common::maths::div_up;
3use alloy_primitives::U256;
4
5/// Amplification precision
6pub const AMP_PRECISION: u64 = 1000;
7
8/// Invariant growth limit: non-proportional add cannot cause the invariant to increase by more than this ratio.
9pub const _MIN_INVARIANT_RATIO: u64 = 60e16 as u64; // 60%
10/// Invariant shrink limit: non-proportional remove cannot cause the invariant to decrease by less than this ratio.
11pub const _MAX_INVARIANT_RATIO: u64 = 500e16 as u64; // 500%
12
13/// Calculate the invariant for the stable swap curve.
14///
15/// Returns `Ok(0)` when every balance is zero (empty/uninitialized pool), matching the
16/// Solidity/TS/Python reference. A single zero balance among non-zero others makes the
17/// invariant undefined; those inputs return `Err(PoolError::StableZeroBalance)` instead of
18/// panicking on divide-by-zero (Solidity would revert with `Panic(0x12)`).
19pub fn compute_invariant(
20    amplification_parameter: &U256,
21    balances: &[U256],
22) -> Result<U256, PoolError> {
23    // Calculate the sum of balances
24    let total_balance: U256 = balances.iter().sum();
25    let num_tokens = balances.len() as u64;
26
27    if total_balance == U256::ZERO {
28        return Ok(U256::ZERO);
29    }
30
31    // Any individual zero balance would divide by zero in the D_P product below.
32    if balances.iter().any(|b| b.is_zero()) {
33        return Err(PoolError::StableZeroBalance);
34    }
35
36    // Initial invariant and amplification
37    let mut invariant = total_balance;
38    let amp_times_total = amplification_parameter * U256::from(num_tokens);
39
40    // Iteratively compute the invariant
41    for _ in 0..255 {
42        let mut d_p = invariant;
43
44        for balance in balances {
45            d_p = (d_p * invariant) / (balance * U256::from(num_tokens));
46        }
47
48        let prev_invariant = invariant;
49
50        let numerator = (amp_times_total * total_balance) / U256::from(AMP_PRECISION);
51        let numerator = numerator + (d_p * U256::from(num_tokens));
52        let numerator = numerator * invariant;
53
54        let amp_minus_precision = amp_times_total - U256::from(AMP_PRECISION);
55        let denominator = (amp_minus_precision * invariant) / U256::from(AMP_PRECISION);
56        let denominator = denominator + (U256::from(num_tokens + 1) * d_p);
57
58        invariant = numerator / denominator;
59
60        // Check for convergence
61        if invariant > prev_invariant {
62            if invariant - prev_invariant <= U256::ONE {
63                return Ok(invariant);
64            }
65        } else if prev_invariant - invariant <= U256::ONE {
66            return Ok(invariant);
67        }
68    }
69
70    Err(PoolError::StableInvariantDidntConverge)
71}
72
73/// Compute how many tokens can be taken out of a pool if `token_amount_in` are sent
74pub fn compute_out_given_exact_in(
75    amplification_parameter: &U256,
76    balances: &[U256],
77    token_index_in: usize,
78    token_index_out: usize,
79    token_amount_in: &U256,
80    invariant: &U256,
81) -> Result<U256, PoolError> {
82    let mut balances_copy = balances.to_vec();
83
84    // Add the token amount to the input balance
85    balances_copy[token_index_in] += token_amount_in;
86
87    // Calculate the final balance out
88    let final_balance_out = compute_balance(
89        amplification_parameter,
90        &balances_copy,
91        invariant,
92        token_index_out,
93    )?;
94
95    // Calculate and return the amount of tokens out, rounding down
96    Ok(balances_copy[token_index_out] - final_balance_out - U256::ONE)
97}
98
99/// Compute how many tokens must be sent to a pool to take out `token_amount_out`
100pub fn compute_in_given_exact_out(
101    amplification_parameter: &U256,
102    balances: &[U256],
103    token_index_in: usize,
104    token_index_out: usize,
105    token_amount_out: &U256,
106    invariant: &U256,
107) -> Result<U256, PoolError> {
108    if &balances[token_index_out] <= token_amount_out {
109        return Err(PoolError::TokenAmountOutIsGreaterThanBalance);
110    }
111
112    let mut balances_copy = balances.to_vec();
113
114    // Subtract the token amount from the output balance
115    balances_copy[token_index_out] -= token_amount_out;
116
117    // Calculate the final balance in
118    let final_balance_in = compute_balance(
119        amplification_parameter,
120        &balances_copy,
121        invariant,
122        token_index_in,
123    )?;
124
125    // Calculate and return the amount of tokens in, rounding up
126    Ok(final_balance_in - balances_copy[token_index_in] + U256::ONE)
127}
128
129/// Compute the balance of a token given the invariant.
130///
131/// Zero balances (or a zero invariant) make the stable balance equation undefined; return
132/// a typed error rather than panicking on divide-by-zero.
133pub fn compute_balance(
134    amplification_parameter: &U256,
135    balances: &[U256],
136    invariant: &U256,
137    token_index: usize,
138) -> Result<U256, PoolError> {
139    if invariant.is_zero() {
140        return Err(PoolError::ZeroInvariant);
141    }
142    if balances.iter().any(|b| b.is_zero()) {
143        return Err(PoolError::StableZeroBalance);
144    }
145
146    let num_tokens = balances.len() as u64;
147    let amp_times_total = amplification_parameter * U256::from(num_tokens);
148
149    // Calculate sum and P_D
150    let mut sum = balances[0];
151    let mut p_d = balances[0] * U256::from(num_tokens);
152
153    for balance in balances.iter().skip(1) {
154        p_d = (p_d * balance * U256::from(num_tokens)) / invariant;
155        sum += balance;
156    }
157
158    sum -= &balances[token_index];
159
160    // Calculate inv2 and c
161    let inv2 = invariant * invariant;
162    let c = div_up(
163        &(inv2 * U256::from(AMP_PRECISION)),
164        &(amp_times_total * p_d),
165    )? * balances[token_index];
166
167    let b = sum + (invariant * U256::from(AMP_PRECISION)) / amp_times_total;
168
169    // Initial approximation
170    let mut token_balance = div_up(&(inv2 + c), &(invariant + b))?;
171
172    // Iteratively solve for tokenBalance
173    for _i in 0..255 {
174        let prev_token_balance = token_balance;
175        token_balance = div_up(
176            &(token_balance * token_balance + c),
177            &(token_balance * U256::from(2) + b - invariant),
178        )?;
179
180        // Check for convergence
181        if token_balance > prev_token_balance {
182            if token_balance - prev_token_balance <= U256::ONE {
183                return Ok(token_balance);
184            }
185        } else if prev_token_balance - token_balance <= U256::ONE {
186            return Ok(token_balance);
187        }
188    }
189
190    Err(PoolError::StableInvariantDidntConverge)
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn compute_invariant_all_zero_balances_returns_zero() {
199        let result = compute_invariant(&U256::from(1000u64), &[U256::ZERO, U256::ZERO]);
200        assert_eq!(result, Ok(U256::ZERO));
201    }
202
203    #[test]
204    fn compute_invariant_partial_zero_balance_errors() {
205        // 3-token pool with one drained balance — previously panic'd on / 0.
206        let balances = [
207            U256::from(1_000_000u64),
208            U256::ZERO,
209            U256::from(1_000_000u64),
210        ];
211        let result = compute_invariant(&U256::from(1000u64), &balances);
212        assert!(matches!(result, Err(PoolError::StableZeroBalance)));
213    }
214
215    #[test]
216    fn compute_balance_zero_balance_errors() {
217        let balances = [
218            U256::from(1_000_000u64),
219            U256::ZERO,
220            U256::from(1_000_000u64),
221        ];
222        let result = compute_balance(
223            &U256::from(1000u64),
224            &balances,
225            &U256::from(2_000_000u64),
226            0,
227        );
228        assert!(matches!(result, Err(PoolError::StableZeroBalance)));
229    }
230
231    #[test]
232    fn compute_balance_zero_invariant_errors() {
233        let balances = [U256::from(1_000_000u64), U256::from(1_000_000u64)];
234        let result = compute_balance(&U256::from(1000u64), &balances, &U256::ZERO, 0);
235        assert!(matches!(result, Err(PoolError::ZeroInvariant)));
236    }
237}