balancer_maths_rust/hooks/stable_surge/
mod.rs1use crate::common::errors::PoolError;
4use crate::common::maths::{complement_fixed, div_down_fixed, mul_down_fixed};
5use crate::common::pool_base::PoolBase;
6use crate::common::types::SwapKind::GivenIn;
7use crate::common::types::{AddLiquidityKind, HookStateBase, RemoveLiquidityKind, SwapParams};
8use crate::hooks::types::{
9 AfterAddLiquidityResult, AfterRemoveLiquidityResult, AfterSwapParams, AfterSwapResult,
10 BeforeAddLiquidityResult, BeforeRemoveLiquidityResult, BeforeSwapResult, DynamicSwapFeeResult,
11 HookState,
12};
13use crate::hooks::{DefaultHook, HookBase, HookConfig};
14use crate::pools::stable::stable_data::StableMutable;
15use crate::pools::stable::StablePool;
16use num_bigint::BigInt;
17use num_traits::Zero;
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct StableSurgeHookState {
23 pub hook_type: String,
25 pub amp: BigInt,
27 pub surge_threshold_percentage: BigInt,
29 pub max_surge_fee_percentage: BigInt,
31}
32
33impl HookStateBase for StableSurgeHookState {
34 fn hook_type(&self) -> &str {
35 &self.hook_type
36 }
37}
38
39impl Default for StableSurgeHookState {
40 fn default() -> Self {
41 Self {
42 hook_type: "StableSurge".to_string(),
43 amp: BigInt::zero(),
44 surge_threshold_percentage: BigInt::zero(),
45 max_surge_fee_percentage: BigInt::zero(),
46 }
47 }
48}
49
50pub struct StableSurgeHook {
53 config: HookConfig,
54}
55
56impl StableSurgeHook {
57 pub fn new() -> Self {
58 let config = HookConfig {
59 should_call_compute_dynamic_swap_fee: true,
60 should_call_after_add_liquidity: true,
61 should_call_after_remove_liquidity: true,
62 ..Default::default()
63 };
64
65 Self { config }
66 }
67
68 fn get_surge_fee_percentage(
70 &self,
71 swap_params: &SwapParams,
72 surge_threshold_percentage: &BigInt,
73 max_surge_fee_percentage: &BigInt,
74 static_fee_percentage: &BigInt,
75 hook_state: &StableSurgeHookState,
76 ) -> Result<BigInt, PoolError> {
77 let stable_state = StableMutable {
79 amp: hook_state.amp.clone(),
80 };
81 let stable_pool = StablePool::new(stable_state);
82
83 let amount_calculated_scaled_18 = stable_pool.on_swap(swap_params)?;
85 let mut new_balances = swap_params.balances_live_scaled_18.clone();
86
87 if swap_params.swap_kind == GivenIn {
89 new_balances[swap_params.token_in_index] =
90 &new_balances[swap_params.token_in_index] + &swap_params.amount_scaled_18;
91 new_balances[swap_params.token_out_index] =
92 &new_balances[swap_params.token_out_index] - &amount_calculated_scaled_18;
93 } else {
94 new_balances[swap_params.token_in_index] =
95 &new_balances[swap_params.token_in_index] + &amount_calculated_scaled_18;
96 new_balances[swap_params.token_out_index] =
97 &new_balances[swap_params.token_out_index] - &swap_params.amount_scaled_18;
98 }
99
100 let new_total_imbalance = self.calculate_imbalance(&new_balances)?;
101
102 if new_total_imbalance.is_zero() {
104 return Ok(static_fee_percentage.clone());
105 }
106
107 let old_total_imbalance = self.calculate_imbalance(&swap_params.balances_live_scaled_18)?;
108
109 if new_total_imbalance <= old_total_imbalance
111 || new_total_imbalance <= *surge_threshold_percentage
112 {
113 return Ok(static_fee_percentage.clone());
114 }
115
116 let fee_difference = max_surge_fee_percentage - static_fee_percentage;
119 let imbalance_excess = &new_total_imbalance - surge_threshold_percentage;
120 let threshold_complement = complement_fixed(surge_threshold_percentage)?;
121
122 let surge_multiplier = div_down_fixed(&imbalance_excess, &threshold_complement)?;
123 let dynamic_fee_increase = mul_down_fixed(&fee_difference, &surge_multiplier)?;
124
125 Ok(static_fee_percentage + dynamic_fee_increase)
126 }
127
128 fn calculate_imbalance(&self, balances: &[BigInt]) -> Result<BigInt, PoolError> {
130 let median = self.find_median(balances);
131
132 let total_balance: BigInt = balances.iter().sum();
133 let total_diff: BigInt = balances
134 .iter()
135 .map(|balance| self.abs_sub(balance, &median))
136 .sum();
137
138 div_down_fixed(&total_diff, &total_balance)
139 }
140
141 fn find_median(&self, balances: &[BigInt]) -> BigInt {
143 let mut sorted_balances = balances.to_vec();
144 sorted_balances.sort();
145 let mid = sorted_balances.len() / 2;
146
147 if sorted_balances.len().is_multiple_of(2) {
148 (&sorted_balances[mid - 1] + &sorted_balances[mid]) / 2
149 } else {
150 sorted_balances[mid].clone()
151 }
152 }
153
154 fn abs_sub(&self, a: &BigInt, b: &BigInt) -> BigInt {
156 if a > b {
157 a - b
158 } else {
159 b - a
160 }
161 }
162
163 fn is_surging(
165 &self,
166 threshold_percentage: &BigInt,
167 current_balances: &[BigInt],
168 new_total_imbalance: &BigInt,
169 ) -> Result<bool, PoolError> {
170 if new_total_imbalance.is_zero() {
172 return Ok(false);
173 }
174
175 let old_total_imbalance = self.calculate_imbalance(current_balances)?;
176
177 Ok(
179 new_total_imbalance > &old_total_imbalance
180 && new_total_imbalance > threshold_percentage,
181 )
182 }
183}
184
185impl HookBase for StableSurgeHook {
186 fn hook_type(&self) -> &str {
187 "StableSurge"
188 }
189
190 fn config(&self) -> &HookConfig {
191 &self.config
192 }
193
194 fn on_compute_dynamic_swap_fee(
195 &self,
196 swap_params: &SwapParams,
197 static_swap_fee_percentage: &BigInt,
198 hook_state: &HookState,
199 ) -> DynamicSwapFeeResult {
200 match hook_state {
201 HookState::StableSurge(state) => {
202 match self.get_surge_fee_percentage(
203 swap_params,
204 &state.surge_threshold_percentage,
205 &state.max_surge_fee_percentage,
206 static_swap_fee_percentage,
207 state,
208 ) {
209 Ok(dynamic_swap_fee) => DynamicSwapFeeResult {
210 success: true,
211 dynamic_swap_fee,
212 },
213 Err(_) => DynamicSwapFeeResult {
214 success: false,
215 dynamic_swap_fee: static_swap_fee_percentage.clone(),
216 },
217 }
218 }
219 _ => DynamicSwapFeeResult {
220 success: false,
221 dynamic_swap_fee: static_swap_fee_percentage.clone(),
222 },
223 }
224 }
225
226 fn on_before_add_liquidity(
228 &self,
229 kind: AddLiquidityKind,
230 max_amounts_in_scaled_18: &[BigInt],
231 min_bpt_amount_out: &BigInt,
232 balances_scaled_18: &[BigInt],
233 hook_state: &HookState,
234 ) -> BeforeAddLiquidityResult {
235 DefaultHook::new().on_before_add_liquidity(
236 kind,
237 max_amounts_in_scaled_18,
238 min_bpt_amount_out,
239 balances_scaled_18,
240 hook_state,
241 )
242 }
243
244 fn on_after_add_liquidity(
245 &self,
246 _kind: AddLiquidityKind,
247 amounts_in_scaled_18: &[BigInt],
248 amounts_in_raw: &[BigInt],
249 _bpt_amount_out: &BigInt,
250 balances_scaled_18: &[BigInt],
251 hook_state: &HookState,
252 ) -> AfterAddLiquidityResult {
253 match hook_state {
254 HookState::StableSurge(state) => {
255 let mut old_balances_scaled_18 = vec![BigInt::zero(); balances_scaled_18.len()];
257 for i in 0..balances_scaled_18.len() {
258 old_balances_scaled_18[i] = &balances_scaled_18[i] - &amounts_in_scaled_18[i];
259 }
260
261 let new_total_imbalance = match self.calculate_imbalance(balances_scaled_18) {
262 Ok(imbalance) => imbalance,
263 Err(_) => {
264 return AfterAddLiquidityResult {
265 success: false,
266 hook_adjusted_amounts_in_raw: amounts_in_raw.to_vec(),
267 }
268 }
269 };
270
271 let is_surging = match self.is_surging(
272 &state.surge_threshold_percentage,
273 &old_balances_scaled_18,
274 &new_total_imbalance,
275 ) {
276 Ok(surging) => surging,
277 Err(_) => {
278 return AfterAddLiquidityResult {
279 success: false,
280 hook_adjusted_amounts_in_raw: amounts_in_raw.to_vec(),
281 }
282 }
283 };
284
285 AfterAddLiquidityResult {
287 success: !is_surging,
288 hook_adjusted_amounts_in_raw: amounts_in_raw.to_vec(),
289 }
290 }
291 _ => AfterAddLiquidityResult {
292 success: false,
293 hook_adjusted_amounts_in_raw: amounts_in_raw.to_vec(),
294 },
295 }
296 }
297
298 fn on_before_remove_liquidity(
299 &self,
300 kind: RemoveLiquidityKind,
301 max_bpt_amount_in: &BigInt,
302 min_amounts_out_scaled_18: &[BigInt],
303 balances_scaled_18: &[BigInt],
304 hook_state: &HookState,
305 ) -> BeforeRemoveLiquidityResult {
306 DefaultHook::new().on_before_remove_liquidity(
307 kind,
308 max_bpt_amount_in,
309 min_amounts_out_scaled_18,
310 balances_scaled_18,
311 hook_state,
312 )
313 }
314
315 fn on_after_remove_liquidity(
316 &self,
317 kind: RemoveLiquidityKind,
318 _bpt_amount_in: &BigInt,
319 amounts_out_scaled_18: &[BigInt],
320 amounts_out_raw: &[BigInt],
321 balances_scaled_18: &[BigInt],
322 hook_state: &HookState,
323 ) -> AfterRemoveLiquidityResult {
324 match hook_state {
325 HookState::StableSurge(state) => {
326 if kind == RemoveLiquidityKind::Proportional {
328 return AfterRemoveLiquidityResult {
329 success: true,
330 hook_adjusted_amounts_out_raw: amounts_out_raw.to_vec(),
331 };
332 }
333
334 let mut old_balances_scaled_18 = vec![BigInt::zero(); balances_scaled_18.len()];
336 for i in 0..balances_scaled_18.len() {
337 old_balances_scaled_18[i] = &balances_scaled_18[i] + &amounts_out_scaled_18[i];
338 }
339
340 let new_total_imbalance = match self.calculate_imbalance(balances_scaled_18) {
341 Ok(imbalance) => imbalance,
342 Err(_) => {
343 return AfterRemoveLiquidityResult {
344 success: false,
345 hook_adjusted_amounts_out_raw: amounts_out_raw.to_vec(),
346 }
347 }
348 };
349
350 let is_surging = match self.is_surging(
351 &state.surge_threshold_percentage,
352 &old_balances_scaled_18,
353 &new_total_imbalance,
354 ) {
355 Ok(surging) => surging,
356 Err(_) => {
357 return AfterRemoveLiquidityResult {
358 success: false,
359 hook_adjusted_amounts_out_raw: amounts_out_raw.to_vec(),
360 }
361 }
362 };
363
364 AfterRemoveLiquidityResult {
366 success: !is_surging,
367 hook_adjusted_amounts_out_raw: amounts_out_raw.to_vec(),
368 }
369 }
370 _ => AfterRemoveLiquidityResult {
371 success: false,
372 hook_adjusted_amounts_out_raw: amounts_out_raw.to_vec(),
373 },
374 }
375 }
376
377 fn on_before_swap(&self, swap_params: &SwapParams, hook_state: &HookState) -> BeforeSwapResult {
378 DefaultHook::new().on_before_swap(swap_params, hook_state)
379 }
380
381 fn on_after_swap(
382 &self,
383 after_swap_params: &AfterSwapParams,
384 hook_state: &HookState,
385 ) -> AfterSwapResult {
386 DefaultHook::new().on_after_swap(after_swap_params, hook_state)
387 }
388}
389
390impl Default for StableSurgeHook {
391 fn default() -> Self {
392 StableSurgeHook::new()
393 }
394}