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