From e38ea1a8ad593bd48b31d50df50e8b4bcc9bc610 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 19 Aug 2024 14:53:00 +0100 Subject: [PATCH] feat(Python): Add ExitFee hook. --- python/src/hooks/exit_fee_hook.py | 89 +++++++++++++++++++ python/src/vault.py | 4 +- python/test/hooks/exit_fee.py | 83 +++++++++++++++++ .../test/hooks/beforeRemoveLiquidity.test.ts | 2 +- 4 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 python/src/hooks/exit_fee_hook.py create mode 100644 python/test/hooks/exit_fee.py diff --git a/python/src/hooks/exit_fee_hook.py b/python/src/hooks/exit_fee_hook.py new file mode 100644 index 0000000..8c6bc62 --- /dev/null +++ b/python/src/hooks/exit_fee_hook.py @@ -0,0 +1,89 @@ +from src.remove_liquidity import RemoveKind +from src.maths import mul_down_fixed + + +# This hook implements the ExitFeeHookExample found in mono-repo: https://github.com/balancer/balancer-v3-monorepo/blob/c848c849cb44dc35f05d15858e4fba9f17e92d5e/pkg/pool-hooks/contracts/ExitFeeHookExample.sol +class ExitFeeHook: + def __init__(self): + self.should_call_compute_dynamic_swap_fee = False + self.should_call_before_swap = False + self.should_call_after_swap = False + self.should_call_before_add_liquidity = False + self.should_call_after_add_liquidity = False + self.should_call_before_remove_liquidity = False + self.should_call_after_remove_liquidity = True + self.enable_hook_adjusted_amounts = True + + def on_before_add_liquidity(self): + return {"success": False, "hook_adjusted_balances_scaled18": []} + + def on_after_add_liquidity( + self, + ): + return {"success": False, "hook_adjusted_amounts_in_raw": []} + + def on_before_remove_liquidity(self): + return {"success": False, "hook_adjusted_balances_scaled18": []} + + def on_after_remove_liquidity( + self, + kind, + _bpt_amount_in, + _amounts_out_scaled18, + amounts_out_raw, + _balances_scaled18, + hook_state, + ): + if not ( + isinstance(hook_state, dict) + and hook_state is not None + and "removeLiquidityHookFeePercentage" in hook_state + and "tokens" in hook_state + ): + raise ValueError("Unexpected hookState") + + # // Our current architecture only supports fees on tokens. Since we must always respect exact `amountsOut`, and + # // non-proportional remove liquidity operations would require taking fees in BPT, we only support proportional + # // removeLiquidity. + if kind != RemoveKind.PROPORTIONAL.value: + raise ValueError("ExitFeeHook: Unsupported RemoveKind: ", kind) + + accrued_fees = [0] * len(hook_state["tokens"]) + hook_adjusted_amounts_out_raw = amounts_out_raw[:] + if hook_state["removeLiquidityHookFeePercentage"] > 0: + # Charge fees proportional to amounts out of each token + + for i in range(len(amounts_out_raw)): + hook_fee = mul_down_fixed( + amounts_out_raw[i], + hook_state["removeLiquidityHookFeePercentage"], + ) + accrued_fees[i] = hook_fee + hook_adjusted_amounts_out_raw[i] -= hook_fee + # Fees don't need to be transferred to the hook, because donation will reinsert them in the vault + + # // In SC Hook Donates accrued fees back to LPs + # // _vault.addLiquidity( + # // AddLiquidityParams({ + # // pool: pool, + # // to: msg.sender, // It would mint BPTs to router, but it's a donation so no BPT is minted + # // maxAmountsIn: accruedFees, // Donate all accrued fees back to the pool (i.e. to the LPs) + # // minBptAmountOut: 0, // Donation does not return BPTs, any number above 0 will revert + # // kind: AddLiquidityKind.DONATION, + # // userData: bytes(''), // User data is not used by donation, so we can set to an empty string + # // }), + # // ); + + return { + "success": True, + "hook_adjusted_amounts_out_raw": hook_adjusted_amounts_out_raw, + } + + def on_before_swap(self): + return {"success": False, "hook_adjusted_balances_scaled18": []} + + def on_after_swap(self): + return {"success": False, "hook_adjusted_amount_calculated_raw": 0} + + def on_compute_dynamic_swap_fee(self): + return {"success": False, "dynamic_swap_fee": 0} diff --git a/python/src/vault.py b/python/src/vault.py index 5414bb7..b44c9c5 100644 --- a/python/src/vault.py +++ b/python/src/vault.py @@ -1,3 +1,4 @@ +from src.hooks.exit_fee_hook import ExitFeeHook from .swap import swap from .add_liquidity import add_liquidity from .remove_liquidity import remove_liquidity @@ -6,17 +7,16 @@ from .pools.stable import Stable from .hooks.default_hook import DefaultHook - class Vault: def __init__(self, *, custom_pool_classes=None, custom_hook_classes=None): self.pool_classes = { "Weighted": Weighted, "Stable": Stable, } + self.hook_classes = {"ExitFee": ExitFeeHook} if custom_pool_classes is not None: self.pool_classes.update(custom_pool_classes) - self.hook_classes = {} if custom_hook_classes is not None: self.hook_classes.update(custom_hook_classes) diff --git a/python/test/hooks/exit_fee.py b/python/test/hooks/exit_fee.py new file mode 100644 index 0000000..c96c855 --- /dev/null +++ b/python/test/hooks/exit_fee.py @@ -0,0 +1,83 @@ +import pytest +import sys +import os + +# Get the directory of the current file +current_file_dir = os.path.dirname(os.path.abspath(__file__)) +# Get the parent directory (one level up) +parent_dir = os.path.dirname(os.path.dirname(current_file_dir)) + +# Insert the parent directory at the start of sys.path +sys.path.insert(0, parent_dir) + +from src.pools.weighted import Weighted +from src.remove_liquidity import RemoveKind + +# Get the directory of the current file +current_file_dir = os.path.dirname(os.path.abspath(__file__)) +# Get the parent directory (one level up) +parent_dir = os.path.dirname(os.path.dirname(current_file_dir)) + +# Insert the parent directory at the start of sys.path +sys.path.insert(0, parent_dir) + +from src.vault import Vault + +remove_liquidity_input = { + "pool": '0x03722034317d8fb16845213bd3ce15439f9ce136', + "min_amounts_out_raw": [1, 1], + "max_bpt_amount_in_raw": 10000000000000, + "kind": RemoveKind.PROPORTIONAL.value, +} + +pool = { + "poolType": "Weighted", + "hookType": "ExitFee", + "chainId": "11155111", + "blockNumber": "5955145", + "poolAddress": "0x03722034317d8fb16845213bd3ce15439f9ce136", + "tokens": [ + "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + ], + "scalingFactors": [1000000000000000000, 1000000000000000000], + "weights": [500000000000000000, 500000000000000000], + "swapFee": 100000000000000000, + "balancesLiveScaled18": [5000000000000000, 5000000000000000000], + "tokenRates": [1000000000000000000, 1000000000000000000], + "totalSupply": 158113883008415798, + "aggregateSwapFee": 0, +} + +vault = Vault() + +def test_hook_exit_fee_no_fee(): + input_hook_state = { + 'removeLiquidityHookFeePercentage': 0, + 'tokens': pool['tokens'], + } + test = vault.remove_liquidity( + remove_liquidity_input, + pool, + hook_state=input_hook_state + ) + assert test["amounts_out_raw"] == [ + 316227766016, + 316227766016840 + ] + +def test_hook_exit_fee_with_fee(): + # 5% fee + input_hook_state = { + 'removeLiquidityHookFeePercentage': 50000000000000000, + 'tokens': pool['tokens'], + } + test = vault.remove_liquidity( + remove_liquidity_input, + pool, + hook_state=input_hook_state + ) + assert test["amounts_out_raw"] == [ + 300416377716, + 300416377715998 + ] diff --git a/typescript/test/hooks/beforeRemoveLiquidity.test.ts b/typescript/test/hooks/beforeRemoveLiquidity.test.ts index c629113..4387716 100644 --- a/typescript/test/hooks/beforeRemoveLiquidity.test.ts +++ b/typescript/test/hooks/beforeRemoveLiquidity.test.ts @@ -47,7 +47,7 @@ describe('hook - afterRemoveLiquidity', () => { }, }); - test('aggregateSwapFee of 50% should take half of remaining', () => { + test('should alter pool balances', () => { /* hook state is used to pass new balances which give expected result */