balancer_maths_rust/hooks/exit_fee/
mod.rs

1//! Exit fee hook implementation
2
3use crate::common::maths::mul_down_fixed;
4use crate::common::types::{HookStateBase, RemoveLiquidityKind};
5use crate::hooks::types::{AfterRemoveLiquidityResult, HookState};
6use crate::hooks::{DefaultHook, HookBase, HookConfig};
7use alloy_primitives::U256;
8use serde::{Deserialize, Serialize};
9
10/// Exit fee hook state
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct ExitFeeHookState {
13    /// Hook type
14    pub hook_type: String,
15    /// Token addresses
16    pub tokens: Vec<String>,
17    /// Remove liquidity hook fee percentage (scaled 18)
18    pub remove_liquidity_hook_fee_percentage: U256,
19}
20
21impl HookStateBase for ExitFeeHookState {
22    fn hook_type(&self) -> &str {
23        &self.hook_type
24    }
25}
26
27impl Default for ExitFeeHookState {
28    fn default() -> Self {
29        Self {
30            hook_type: "ExitFee".to_string(),
31            tokens: vec![],
32            remove_liquidity_hook_fee_percentage: U256::ZERO,
33        }
34    }
35}
36
37/// Exit fee hook implementation
38/// This hook implements the ExitFeeHookExample found in mono-repo: https://github.com/balancer/balancer-v3-monorepo/blob/c848c849cb44dc35f05d15858e4fba9f17e92d5f/pkg/pool-hooks/contracts/ExitFeeHookExample.sol
39pub struct ExitFeeHook {
40    config: HookConfig,
41}
42
43impl ExitFeeHook {
44    pub fn new() -> Self {
45        let config = HookConfig {
46            should_call_after_remove_liquidity: true,
47            enable_hook_adjusted_amounts: true,
48            ..Default::default()
49        };
50
51        Self { config }
52    }
53}
54
55impl HookBase for ExitFeeHook {
56    fn hook_type(&self) -> &str {
57        "ExitFee"
58    }
59
60    fn config(&self) -> &HookConfig {
61        &self.config
62    }
63
64    fn on_after_remove_liquidity(
65        &self,
66        kind: RemoveLiquidityKind,
67        _bpt_amount_in: &U256,
68        _amounts_out_scaled_18: &[U256],
69        amounts_out_raw: &[U256],
70        _balances_scaled_18: &[U256],
71        hook_state: &HookState,
72    ) -> AfterRemoveLiquidityResult {
73        match hook_state {
74            HookState::ExitFee(state) => {
75                // Our current architecture only supports fees on tokens. Since we must always respect exact `amountsOut`, and
76                // non-proportional remove liquidity operations would require taking fees in BPT, we only support proportional
77                // removeLiquidity.
78                if kind != RemoveLiquidityKind::Proportional {
79                    return AfterRemoveLiquidityResult {
80                        success: false,
81                        hook_adjusted_amounts_out_raw: amounts_out_raw.to_vec(),
82                    };
83                }
84
85                let mut accrued_fees = vec![U256::ZERO; state.tokens.len()];
86                let mut hook_adjusted_amounts_out_raw = amounts_out_raw.to_vec();
87
88                if state.remove_liquidity_hook_fee_percentage > U256::ZERO {
89                    // Charge fees proportional to amounts out of each token
90                    for i in 0..amounts_out_raw.len() {
91                        let hook_fee = mul_down_fixed(
92                            &amounts_out_raw[i],
93                            &state.remove_liquidity_hook_fee_percentage,
94                        )
95                        .unwrap_or(U256::ZERO);
96
97                        accrued_fees[i] = hook_fee;
98                        hook_adjusted_amounts_out_raw[i] -= hook_fee;
99                        // Fees don't need to be transferred to the hook, because donation will reinsert them in the vault
100                    }
101
102                    // In SC Hook Donates accrued fees back to LPs
103                    // _vault.addLiquidity(
104                    //     AddLiquidityParams({
105                    //         pool: pool,
106                    //         to: msg.sender, // It would mint BPTs to router, but it's a donation so no BPT is minted
107                    //         maxAmountsIn: accruedFees, // Donate all accrued fees back to the pool (i.e. to the LPs)
108                    //         minBptAmountOut: 0, // Donation does not return BPTs, any number above 0 will revert
109                    //         kind: AddLiquidityKind.DONATION,
110                    //         userData: bytes(''), // User data is not used by donation, so we can set to an empty string
111                    //     }),
112                    // );
113                }
114
115                AfterRemoveLiquidityResult {
116                    success: true,
117                    hook_adjusted_amounts_out_raw,
118                }
119            }
120            _ => AfterRemoveLiquidityResult {
121                success: false,
122                hook_adjusted_amounts_out_raw: amounts_out_raw.to_vec(),
123            },
124        }
125    }
126
127    // Delegate all other methods to DefaultHook
128    fn on_before_add_liquidity(
129        &self,
130        kind: crate::common::types::AddLiquidityKind,
131        max_amounts_in_scaled_18: &[U256],
132        min_bpt_amount_out: &U256,
133        balances_scaled_18: &[U256],
134        hook_state: &HookState,
135    ) -> crate::hooks::types::BeforeAddLiquidityResult {
136        DefaultHook::new().on_before_add_liquidity(
137            kind,
138            max_amounts_in_scaled_18,
139            min_bpt_amount_out,
140            balances_scaled_18,
141            hook_state,
142        )
143    }
144
145    fn on_after_add_liquidity(
146        &self,
147        kind: crate::common::types::AddLiquidityKind,
148        amounts_in_scaled_18: &[U256],
149        amounts_in_raw: &[U256],
150        bpt_amount_out: &U256,
151        balances_scaled_18: &[U256],
152        hook_state: &HookState,
153    ) -> crate::hooks::types::AfterAddLiquidityResult {
154        DefaultHook::new().on_after_add_liquidity(
155            kind,
156            amounts_in_scaled_18,
157            amounts_in_raw,
158            bpt_amount_out,
159            balances_scaled_18,
160            hook_state,
161        )
162    }
163
164    fn on_before_remove_liquidity(
165        &self,
166        kind: crate::common::types::RemoveLiquidityKind,
167        max_bpt_amount_in: &U256,
168        min_amounts_out_scaled_18: &[U256],
169        balances_scaled_18: &[U256],
170        hook_state: &HookState,
171    ) -> crate::hooks::types::BeforeRemoveLiquidityResult {
172        DefaultHook::new().on_before_remove_liquidity(
173            kind,
174            max_bpt_amount_in,
175            min_amounts_out_scaled_18,
176            balances_scaled_18,
177            hook_state,
178        )
179    }
180
181    fn on_before_swap(
182        &self,
183        swap_params: &crate::common::types::SwapParams,
184        hook_state: &HookState,
185    ) -> crate::hooks::types::BeforeSwapResult {
186        DefaultHook::new().on_before_swap(swap_params, hook_state)
187    }
188
189    fn on_after_swap(
190        &self,
191        after_swap_params: &crate::hooks::types::AfterSwapParams,
192        hook_state: &HookState,
193    ) -> crate::hooks::types::AfterSwapResult {
194        DefaultHook::new().on_after_swap(after_swap_params, hook_state)
195    }
196
197    fn on_compute_dynamic_swap_fee(
198        &self,
199        swap_params: &crate::common::types::SwapParams,
200        static_swap_fee_percentage: &U256,
201        hook_state: &HookState,
202    ) -> crate::hooks::types::DynamicSwapFeeResult {
203        DefaultHook::new().on_compute_dynamic_swap_fee(
204            swap_params,
205            static_swap_fee_percentage,
206            hook_state,
207        )
208    }
209}
210
211impl Default for ExitFeeHook {
212    fn default() -> Self {
213        ExitFeeHook::new()
214    }
215}