From e62a5cb23323131728dc1a884df8a891c50f4990 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Fri, 16 Aug 2024 16:11:20 +0100 Subject: [PATCH 1/8] test(Python): Hooks - after add liquidity hook. --- python/src/add_liquidity.py | 32 ++--- python/src/hooks/default_hook.py | 6 +- python/src/remove_liquidity.py | 2 +- python/src/swap.py | 2 +- python/src/vault.py | 2 +- python/test/hooks/after_add_liquidity.py | 149 +++++++++++++++++++++++ 6 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 python/test/hooks/after_add_liquidity.py diff --git a/python/src/add_liquidity.py b/python/src/add_liquidity.py index 5cb3d4a..21f872c 100644 --- a/python/src/add_liquidity.py +++ b/python/src/add_liquidity.py @@ -12,7 +12,7 @@ class Kind(Enum): - PROPORTIONAL = 0 + UNBALANCED = 0 SINGLE_TOKEN_EXACT_OUT = 1 @@ -29,7 +29,7 @@ def add_liquidity(add_liquidity_input, pool_state, pool_class, hook_class, hook_ ) updated_balances_live_scaled18 = pool_state["balancesLiveScaled18"][:] - if hook_class.shouldCallBeforeAddLiquidity: + if hook_class.should_call_before_add_liquidity: # Note - in SC balances and amounts are updated to reflect any rate change. # Daniel said we should not worry about this as any large rate changes # will mean something has gone wrong. @@ -47,7 +47,7 @@ def add_liquidity(add_liquidity_input, pool_state, pool_class, hook_class, hook_ for i, a in enumerate(hook_return["hookAdjustedBalancesScaled18"]): updated_balances_live_scaled18[i] = a - if add_liquidity_input["kind"] == Kind.PROPORTIONAL.value: + if add_liquidity_input["kind"] == Kind.UNBALANCED.value: amounts_in_scaled18 = max_amounts_in_scaled18 computed = compute_add_liquidity_unbalanced( updated_balances_live_scaled18, @@ -98,24 +98,24 @@ def add_liquidity(add_liquidity_input, pool_state, pool_class, hook_class, hook_ ) # Update the balances with the incoming amounts and subtract the swap fees - updated_balances_live_scaled18[i] += ( - amounts_in_scaled18[i] - aggregate_swap_fee_amount_scaled18 + updated_balances_live_scaled18[i] = ( + updated_balances_live_scaled18[i] + + amounts_in_scaled18[i] + - aggregate_swap_fee_amount_scaled18 ) - if hook_class.shouldCallAfterAddLiquidity: + if hook_class.should_call_after_add_liquidity: hook_return = hook_class.on_after_add_liquidity( - { - "kind": add_liquidity_input["kind"], - "amounts_in_scaled18": amounts_in_scaled18, - "amounts_in_raw": amounts_in_raw, - "bpt_amount_out": bpt_amount_out, - "balances_scaled_18": updated_balances_live_scaled18, - "hook_state": hook_state, - } + add_liquidity_input["kind"], + amounts_in_scaled18, + amounts_in_raw, + bpt_amount_out, + updated_balances_live_scaled18, + hook_state, ) if hook_return["success"] is False or len( - hook_return["hookAdjustedAmountsInRaw"] + hook_return["hook_adjusted_amounts_in_raw"] ) is not len(amounts_in_raw): raise SystemError( " AfterAddLiquidityHookFailed", @@ -124,7 +124,7 @@ def add_liquidity(add_liquidity_input, pool_state, pool_class, hook_class, hook_ ) # If hook adjusted amounts is not enabled, ignore amounts returned by the hook - if hook_class.enableHookAdjustedAmounts: + if hook_class.enable_hook_adjusted_amounts: for i, a in enumerate(hook_return["hook_adjusted_amounts_in_raw"]): amounts_in_raw[i] = a diff --git a/python/src/hooks/default_hook.py b/python/src/hooks/default_hook.py index c13ad15..cde1b63 100644 --- a/python/src/hooks/default_hook.py +++ b/python/src/hooks/default_hook.py @@ -2,11 +2,11 @@ class DefaultHook: shouldCallComputeDynamicSwapFee = False shouldCallBeforeSwap = False shouldCallAfterSwap = False - shouldCallBeforeAddLiquidity = False - shouldCallAfterAddLiquidity = False + should_call_before_add_liquidity = False + should_call_after_add_liquidity = False shouldCallBeforeRemoveLiquidity = False shouldCallAfterRemoveLiquidity = False - enableHookAdjustedAmounts = False + enable_hook_adjusted_amounts = False def on_before_add_liquidity(self): return False diff --git a/python/src/remove_liquidity.py b/python/src/remove_liquidity.py index 58300b4..1b6cc90 100644 --- a/python/src/remove_liquidity.py +++ b/python/src/remove_liquidity.py @@ -145,7 +145,7 @@ def remove_liquidity( ) # If hook adjusted amounts is not enabled, ignore amounts returned by the hook - if hook_class.enableHookAdjustedAmounts: + if hook_class.enable_hook_adjusted_amounts: for i, a in enumerate(hook_return["hook_adjusted_amounts_out_raw"]): amounts_out_raw[i] = a diff --git a/python/src/swap.py b/python/src/swap.py index 9ad90e5..ea06e5c 100644 --- a/python/src/swap.py +++ b/python/src/swap.py @@ -157,7 +157,7 @@ def swap(swap_input, pool_state, pool_class, hook_class, hook_state): "AfterAddSwapHookFailed", pool_state["poolType"], pool_state["hookType"] ) # If hook adjusted amounts is not enabled, ignore amount returned by the hook - if hook_class.enableHookAdjustedAmounts: + if hook_class.enable_hook_adjusted_amounts: amount_calculated_raw = hook_return["hook_adjusted_amount_calculated_raw"] return amount_calculated_raw diff --git a/python/src/vault.py b/python/src/vault.py index 923544c..5414bb7 100644 --- a/python/src/vault.py +++ b/python/src/vault.py @@ -63,4 +63,4 @@ def _get_hook(self, hook_name, hook_state): raise SystemError("Unsupported Hook Type:", hook_name) if hook_state is None: raise SystemError("No state for Hook:", hook_name) - return hook_class(hook_state) + return hook_class() diff --git a/python/test/hooks/after_add_liquidity.py b/python/test/hooks/after_add_liquidity.py new file mode 100644 index 0000000..4aed59c --- /dev/null +++ b/python/test/hooks/after_add_liquidity.py @@ -0,0 +1,149 @@ +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.add_liquidity import Kind + +# 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 +from src.hooks.default_hook import DefaultHook + +class CustomPool(Weighted): + def __init__(self, pool_state): + super().__init__(pool_state) + +class CustomHook: + 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 = True + self.should_call_before_remove_liquidity = False + self.should_call_after_remove_liquidity = False + 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, kind, amounts_in_scaled18, amounts_in_raw, bpt_amount_out, balances_scaled18, hook_state): + if not (isinstance(hook_state, dict) and hook_state is not None and 'expected_balances_live_scaled18' in hook_state): + raise ValueError('Unexpected hookState') + assert kind == add_liquidity_input['kind'] + assert bpt_amount_out == 146464294351915965 + assert amounts_in_scaled18 == add_liquidity_input['max_amounts_in_raw'] + assert amounts_in_raw == add_liquidity_input['max_amounts_in_raw'] + assert balances_scaled18 == hook_state['expected_balances_live_scaled18'] + return { + 'success': True, + 'hook_adjusted_amounts_in_raw': [ + amounts_in_raw[0] + 1, + amounts_in_raw[1] + 1, + ], + } + + def on_before_remove_liquidity(self): + return {'success': False, 'hook_adjusted_balances_scaled18': []} + + def on_after_remove_liquidity(self): + return {'success': False, '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} + +add_liquidity_input = { + "pool": '0xb2456a6f51530053bc41b0ee700fe6a2c37282e8', + "max_amounts_in_raw": [200000000000000000, 100000000000000000], + "min_bpt_amount_out_raw": 0, + "kind": Kind.UNBALANCED.value, +} + +pool = { + "poolType": "CustomPool", + "hookType": "CustomHook", + "chainId": "11155111", + "blockNumber": "5955145", + "poolAddress": "0xb2456a6f51530053bc41b0ee700fe6a2c37282e8", + "tokens": [ + "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + ], + "scalingFactors": [1000000000000000000, 1000000000000000000], + "weights": [500000000000000000, 500000000000000000], + "swapFee": 100000000000000000, + "balancesLiveScaled18": [1000000000000000000, 1000000000000000000], + "tokenRates": [1000000000000000000, 1000000000000000000], + "totalSupply": 1000000000000000000, + "aggregateSwapFee": 500000000000000000, +} + +vault = Vault( + custom_pool_classes={"CustomPool": CustomPool}, + custom_hook_classes={"CustomHook": CustomHook}, +) + +def test_hook_after_add_liquidity_no_fee(): + # aggregateSwapFee of 0 should not take any protocol fees from updated balances + # hook state is used to pass expected value to tests + input_hook_state = { + "expected_balances_live_scaled18": [ + 1200000000000000000, + 1100000000000000000, + ], + } + test = vault.add_liquidity( + add_liquidity_input, + { **pool, "aggregateSwapFee": 0 }, + hook_state=input_hook_state + ) + # Hook adds 1n to amountsIn + assert test["amounts_in_raw"] == [ + 200000000000000001, + 100000000000000001, + ] + assert test["bpt_amount_out_raw"] == 146464294351915965 + +def test_hook_after_add_liquidity_with_fee(): + # aggregateSwapFee of 50% should take half of remaining + # hook state is used to pass expected value to tests + # aggregate fee amount is 2554373534619714n which is deducted from amount in + input_hook_state = { + "expected_balances_live_scaled18": [ + 1197445626465380286, + 1100000000000000000, + ], + } + test = vault.add_liquidity( + add_liquidity_input, + pool, + hook_state=input_hook_state + ) + # Hook adds 1n to amountsIn + assert test["amounts_in_raw"] == [ + 200000000000000001, + 100000000000000001, + ] + assert test["bpt_amount_out_raw"] == 146464294351915965 + From 9174383322d4db0ec9e8f6b060eeb434bd74eeef Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 19 Aug 2024 11:42:03 +0100 Subject: [PATCH 2/8] test(Python): Hooks - after remove liquidity hook. --- python/src/hooks/default_hook.py | 4 +- python/src/remove_liquidity.py | 22 ++- python/test/hooks/after_remove_liquidity.py | 167 ++++++++++++++++++++ 3 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 python/test/hooks/after_remove_liquidity.py diff --git a/python/src/hooks/default_hook.py b/python/src/hooks/default_hook.py index cde1b63..f21199d 100644 --- a/python/src/hooks/default_hook.py +++ b/python/src/hooks/default_hook.py @@ -4,8 +4,8 @@ class DefaultHook: shouldCallAfterSwap = False should_call_before_add_liquidity = False should_call_after_add_liquidity = False - shouldCallBeforeRemoveLiquidity = False - shouldCallAfterRemoveLiquidity = False + should_call_before_remove_liquidity = False + should_call_after_remove_liquidity = False enable_hook_adjusted_amounts = False def on_before_add_liquidity(self): diff --git a/python/src/remove_liquidity.py b/python/src/remove_liquidity.py index 1b6cc90..20f8c58 100644 --- a/python/src/remove_liquidity.py +++ b/python/src/remove_liquidity.py @@ -36,7 +36,7 @@ def remove_liquidity( ) updated_balances_live_scaled18 = pool_state["balancesLiveScaled18"][:] - if hook_class.shouldCallBeforeRemoveLiquidity: + if hook_class.should_call_before_remove_liquidity: # Note - in SC balances and amounts are updated to reflect any rate change. # Daniel said we should not worry about this as any large rate changes # will mean something has gone wrong. @@ -119,24 +119,22 @@ def remove_liquidity( swap_fee_amounts_scaled18[i], pool_state["aggregateSwapFee"] ) - updated_balances_live_scaled18[i] -= ( + updated_balances_live_scaled18[i] = updated_balances_live_scaled18[i] - ( amounts_out_scaled18[i] + aggregate_swap_fee_amount_scaled18 ) - if hook_class.shouldCallAfterRemoveLiquidity: + if hook_class.should_call_after_remove_liquidity: hook_return = hook_class.on_after_remove_liquidity( - { - "kind": remove_liquidity_input["kind"], - "bpt_amount_in": bpt_amount_in, - "amountsOutScaled18": amounts_out_scaled18, - "amountsOutRaw": amounts_out_raw, - "updatedBalancesLiveScaled18": updated_balances_live_scaled18, - "hook_state": hook_state, - } + remove_liquidity_input["kind"], + bpt_amount_in, + amounts_out_scaled18, + amounts_out_raw, + updated_balances_live_scaled18, + hook_state, ) if hook_return["success"] is False or len( - hook_return["hookAdjustedAmountsOutRaw"] + hook_return["hook_adjusted_amounts_out_raw"] ) is not len(amounts_out_raw): raise SystemError( "AfterRemoveLiquidityHookFailed", diff --git a/python/test/hooks/after_remove_liquidity.py b/python/test/hooks/after_remove_liquidity.py new file mode 100644 index 0000000..bb71939 --- /dev/null +++ b/python/test/hooks/after_remove_liquidity.py @@ -0,0 +1,167 @@ +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 +from src.hooks.default_hook import DefaultHook + +class CustomPool(): + def __init__(self, pool_state): + self.pool_state = pool_state + + def on_swap(self, swap_params): + return 1 + + def compute_invariant(self, balances_live_scaled18): + return 1 + + def compute_balance( + self, + balances_live_scaled18, + token_in_index, + invariant_ratio, + ): + return 1 + +class CustomHook: + 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, kind, amounts_in_scaled18, amounts_in_raw, bpt_amount_out, balances_scaled18, hook_state): + 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 'expectedBalancesLiveScaled18' in hook_state): + raise ValueError('Unexpected hookState') + assert kind == remove_liquidity_input['kind'] + assert bpt_amount_in == remove_liquidity_input['max_bpt_amount_in_raw'] + assert amounts_out_scaled18 == [0, 909999999999999999] + assert amounts_out_raw == [0, 909999999999999999] + assert balances_scaled18 == hook_state['expectedBalancesLiveScaled18'] + return { + 'success': True, + 'hook_adjusted_amounts_out_raw': [0] * len(amounts_out_scaled18) + } + + 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} + +remove_liquidity_input = { + "pool": '0xb2456a6f51530053bc41b0ee700fe6a2c37282e8', + "min_amounts_out_raw": [0, 1], + "max_bpt_amount_in_raw": 100000000000000000, + "kind": RemoveKind.SINGLE_TOKEN_EXACT_IN.value, +} + +pool = { + "poolType": "CustomPool", + "hookType": "CustomHook", + "chainId": "11155111", + "blockNumber": "5955145", + "poolAddress": "0xb2456a6f51530053bc41b0ee700fe6a2c37282e8", + "tokens": [ + "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + ], + "scalingFactors": [1000000000000000000, 1000000000000000000], + "weights": [500000000000000000, 500000000000000000], + "swapFee": 100000000000000000, + "balancesLiveScaled18": [1000000000000000000, 1000000000000000000], + "tokenRates": [1000000000000000000, 1000000000000000000], + "totalSupply": 1000000000000000000, + "aggregateSwapFee": 500000000000000000, +} + +vault = Vault( + custom_pool_classes={"CustomPool": CustomPool}, + custom_hook_classes={"CustomHook": CustomHook}, +) + +def test_hook_after_remove_liquidity_no_fee(): + # aggregateSwapFee of 0 should not take any protocol fees from updated balances + # hook state is used to pass expected value to tests + # Original balance is 1 + # Amount out is 0.9099... + # Leaves 0.090000000000000001 + # Swap fee amount is: 0.09 which is all left in pool because aggregateFee is 0 + input_hook_state = { + "expectedBalancesLiveScaled18": [ + 1000000000000000000, + 90000000000000001, + ], + } + test = vault.remove_liquidity( + remove_liquidity_input, + { **pool, "aggregateSwapFee": 0 }, + hook_state=input_hook_state + ) + assert test["amounts_out_raw"] == [ + 0, + 0, + ] + assert test["bpt_amount_in_raw"] == remove_liquidity_input["max_bpt_amount_in_raw"] + + +def test_hook_after_add_liquidity_with_fee(): + # aggregateSwapFee of 50% should take half of remaining + # hook state is used to pass expected value to tests + # Original balance is 1 + # Amount out is 0.9099... + # Leaves 0.090000000000000001 + # Swap fee amount is: 0.09 + # Aggregate fee amount is 50% of swap fee: 0.045 + # Leaves 0.045000000000000001 in pool + input_hook_state = { + "expectedBalancesLiveScaled18": [ + 1000000000000000000, + 45000000000000001, + ], + } + test = vault.remove_liquidity( + remove_liquidity_input, + pool, + hook_state=input_hook_state + ) + assert test["amounts_out_raw"] == [ + 0, + 0, + ] + assert test["bpt_amount_in_raw"] == remove_liquidity_input["max_bpt_amount_in_raw"] \ No newline at end of file From 7e2d96199018bd2feae4104c3490fe7e3c9a7371 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 19 Aug 2024 12:21:41 +0100 Subject: [PATCH 3/8] test(Python): Hooks - after swap hook. --- python/src/hooks/default_hook.py | 6 +- python/src/swap.py | 14 +-- python/test/hooks/after_swap.py | 160 +++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 python/test/hooks/after_swap.py diff --git a/python/src/hooks/default_hook.py b/python/src/hooks/default_hook.py index f21199d..f12e54e 100644 --- a/python/src/hooks/default_hook.py +++ b/python/src/hooks/default_hook.py @@ -1,7 +1,7 @@ class DefaultHook: - shouldCallComputeDynamicSwapFee = False - shouldCallBeforeSwap = False - shouldCallAfterSwap = False + should_call_compute_dynamic_swap_fee = False + should_call_before_swap = False + should_call_after_swap = False should_call_before_add_liquidity = False should_call_after_add_liquidity = False should_call_before_remove_liquidity = False diff --git a/python/src/swap.py b/python/src/swap.py index ea06e5c..58a2708 100644 --- a/python/src/swap.py +++ b/python/src/swap.py @@ -39,7 +39,7 @@ def swap(swap_input, pool_state, pool_class, hook_class, hook_state): ) updated_balances_live_scaled18 = pool_state["balancesLiveScaled18"][:] - if hook_class.shouldCallBeforeSwap: + if hook_class.should_call_before_swap: # Note - in SC balances and amounts are updated to reflect any rate change. # Daniel said we should not worry about this as any large rate changes # will mean something has gone wrong. @@ -52,7 +52,7 @@ def swap(swap_input, pool_state, pool_class, hook_class, hook_state): updated_balances_live_scaled18[i] = a swap_fee = pool_state["swapFee"] - if hook_class.shouldCallComputeDynamicSwapFee: + if hook_class.should_call_compute_dynamic_swap_fee: hook_return = hook_class.onComputeDynamicSwapFee( swap_input, pool_state["swapFee"], @@ -115,7 +115,7 @@ def swap(swap_input, pool_state, pool_class, hook_class, hook_state): amount_given_scaled18, amount_calculated_scaled18 + aggregate_swap_fee_amount_scaled18, ) - if swap_input["swap_kind"] == SwapKind.GIVENIN + if swap_input["swap_kind"] == SwapKind.GIVENIN.value else ( amount_calculated_scaled18 - aggregate_swap_fee_amount_scaled18, amount_given_scaled18, @@ -125,20 +125,20 @@ def swap(swap_input, pool_state, pool_class, hook_class, hook_state): updated_balances_live_scaled18[input_index] += balance_in_increment updated_balances_live_scaled18[output_index] -= balance_out_decrement - if hook_class.shouldCallAfterSwap: - hook_return = hook_class.onAfterSwap( + if hook_class.should_call_after_swap: + hook_return = hook_class.on_after_swap( { "kind": swap_input["swap_kind"], "token_in": swap_input["token_in"], "token_out": swap_input["token_out"], "amount_in_scaled18": ( amount_given_scaled18 - if swap_input["swap_kind"] == SwapKind.GIVENIN + if swap_input["swap_kind"] == SwapKind.GIVENIN.value else amount_calculated_scaled18 ), "amount_out_scaled18": ( amount_calculated_scaled18 - if swap_input["swap_kind"] == SwapKind.GIVENIN + if swap_input["swap_kind"] == SwapKind.GIVENIN.value else amount_given_scaled18 ), "token_in_balance_scaled18": updated_balances_live_scaled18[ diff --git a/python/test/hooks/after_swap.py b/python/test/hooks/after_swap.py new file mode 100644 index 0000000..757729a --- /dev/null +++ b/python/test/hooks/after_swap.py @@ -0,0 +1,160 @@ +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.swap import SwapKind + +# 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 +from src.hooks.default_hook import DefaultHook + +pool = { + "poolType": "CustomPool", + "hookType": "CustomHook", + "chainId": "11155111", + "blockNumber": "5955145", + "poolAddress": "0xb2456a6f51530053bc41b0ee700fe6a2c37282e8", + "tokens": [ + "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + ], + "scalingFactors": [1000000000000000000, 1000000000000000000], + "weights": [500000000000000000, 500000000000000000], + "swapFee": 100000000000000000, + "balancesLiveScaled18": [1000000000000000000, 1000000000000000000], + "tokenRates": [1000000000000000000, 1000000000000000000], + "totalSupply": 1000000000000000000, + "aggregateSwapFee": 500000000000000000, +} + + +swap_input = { + "amount_raw": 1, + "swap_kind": SwapKind.GIVENIN.value, + "token_in": pool['tokens'][0], + "token_out": pool['tokens'][1], +} + +class CustomPool(): + def __init__(self, pool_state): + self.pool_state = pool_state + + def on_swap(self, swap_params): + return 100000000000 + + def compute_invariant(self, balances_live_scaled18): + return 1 + + def compute_balance( + self, + balances_live_scaled18, + token_in_index, + invariant_ratio, + ): + return 1 + +class CustomHook: + def __init__(self): + self.should_call_compute_dynamic_swap_fee = False + self.should_call_before_swap = False + self.should_call_after_swap = True + 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 = False + 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, kind, amounts_in_scaled18, amounts_in_raw, bpt_amount_out, balances_scaled18, hook_state): + 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): + return { + 'success': False, + 'hook_adjusted_amounts_out_raw': [] + } + + def on_before_swap(self): + return {'success': False, 'hook_adjusted_balances_scaled18': []} + + def on_after_swap(self, params): + hook_state = params['hook_state'] + token_in_balanceScaled18 = params['token_in_balance_scaled18'] + token_out_balance_scaled18 = params['token_out_balance_scaled18'] + if not (isinstance(hook_state, dict) and hook_state is not None and 'expectedBalancesLiveScaled18' in hook_state): + raise ValueError('Unexpected hookState') + assert params['kind'] == swap_input['swap_kind'] + + assert params['token_in'] == swap_input['token_in'] + assert params['token_out'] == swap_input['token_out'] + assert params['amount_in_scaled18'] == swap_input['amount_raw'] + assert params['amount_calculated_raw'] == 90000000000 + assert params['amount_calculated_scaled18'] == 90000000000 + assert params['amount_out_scaled18'] == 90000000000 + assert params['token_in_balance_scaled18'] == int(pool['balancesLiveScaled18'][0] + swap_input['amount_raw']) + assert [token_in_balanceScaled18, token_out_balance_scaled18] == hook_state['expectedBalancesLiveScaled18'] + return {'success': True, 'hook_adjusted_amount_calculated_raw': 1} + + def on_compute_dynamic_swap_fee(self): + return {'success': False, 'dynamic_swap_fee': 0} + +vault = Vault( + custom_pool_classes={"CustomPool": CustomPool}, + custom_hook_classes={"CustomHook": CustomHook}, +) + +def test_hook_after_remove_liquidity_no_fee(): + # aggregateSwapFee of 0 should not take any protocol fees from updated balances + # hook state is used to pass expected value to tests + # with aggregateFee = 0, balance out is just balance - calculated + input_hook_state = { + "expectedBalancesLiveScaled18": [ + pool['balancesLiveScaled18'][0] + swap_input['amount_raw'], + 999999910000000000, + ], + } + test = vault.swap( + swap_input, + { **pool, 'aggregateSwapFee': 0 }, + hook_state=input_hook_state + ) + assert test == 1 + + +def test_hook_after_add_liquidity_with_fee(): + # aggregateSwapFee of 50% should take half of remaining + # hook state is used to pass expected value to tests + # Aggregate fee amount is 50% of swap fee + input_hook_state = { + "expectedBalancesLiveScaled18": [ + pool['balancesLiveScaled18'][0] + swap_input['amount_raw'], + 999999905000000000, + ], + } + test = vault.swap( + swap_input, + pool, + hook_state=input_hook_state + ) + assert test == 1 \ No newline at end of file From 5c7bdf830805249f90645a303bb9b2ef796edf34 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 19 Aug 2024 14:05:47 +0100 Subject: [PATCH 4/8] test(Python): Hooks - before add liquidity hook. --- python/src/add_liquidity.py | 4 +- python/test/hooks/before_add_liquidity.py | 125 ++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 python/test/hooks/before_add_liquidity.py diff --git a/python/src/add_liquidity.py b/python/src/add_liquidity.py index 21f872c..d3711fa 100644 --- a/python/src/add_liquidity.py +++ b/python/src/add_liquidity.py @@ -35,7 +35,7 @@ def add_liquidity(add_liquidity_input, pool_state, pool_class, hook_class, hook_ # will mean something has gone wrong. # We do take into account and balance changes due # to hook using hookAdjustedBalancesScaled18. - hook_return = hook_class.onBeforeSwap( + hook_return = hook_class.on_before_add_liquidity( add_liquidity_input["kind"], add_liquidity_input["max_amounts_in_raw"], add_liquidity_input["min_bpt_amount_out_raw"], @@ -44,7 +44,7 @@ def add_liquidity(add_liquidity_input, pool_state, pool_class, hook_class, hook_ ) if hook_return["success"] is False: raise SystemError("BeforeAddLiquidityHookFailed") - for i, a in enumerate(hook_return["hookAdjustedBalancesScaled18"]): + for i, a in enumerate(hook_return["hook_adjusted_balances_scaled18"]): updated_balances_live_scaled18[i] = a if add_liquidity_input["kind"] == Kind.UNBALANCED.value: diff --git a/python/test/hooks/before_add_liquidity.py b/python/test/hooks/before_add_liquidity.py new file mode 100644 index 0000000..8f15418 --- /dev/null +++ b/python/test/hooks/before_add_liquidity.py @@ -0,0 +1,125 @@ +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.add_liquidity import Kind + +# 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 +from src.hooks.default_hook import DefaultHook + +class CustomPool(Weighted): + def __init__(self, pool_state): + super().__init__(pool_state) + +class CustomHook: + 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 = True + self.should_call_after_add_liquidity = False + self.should_call_before_remove_liquidity = False + self.should_call_after_remove_liquidity = False + self.enable_hook_adjusted_amounts = False + + def on_before_add_liquidity(self, kind, max_amounts_in_scaled18, min_bpt_amount_out, balances_scaled18, hook_state): + if not (isinstance(hook_state, dict) and hook_state is not None and 'balanceChange' in hook_state): + raise ValueError('Unexpected hookState') + assert kind == add_liquidity_input['kind'] + assert max_amounts_in_scaled18 == add_liquidity_input['max_amounts_in_raw'] + assert min_bpt_amount_out == add_liquidity_input['min_bpt_amount_out_raw'] + assert balances_scaled18 == pool['balancesLiveScaled18'] + + return { + 'success': True, + 'hook_adjusted_balances_scaled18': hook_state['balanceChange'], + } + + def on_after_add_liquidity(self, kind, amounts_in_scaled18, amounts_in_raw, bpt_amount_out, balances_scaled18, hook_state): + return { + 'success': True, + 'hook_adjusted_amounts_in_raw': [], + } + + def on_before_remove_liquidity(self): + return {'success': False, 'hook_adjusted_balances_scaled18': []} + + def on_after_remove_liquidity(self): + return {'success': False, '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} + +add_liquidity_input = { + "pool": '0xb2456a6f51530053bc41b0ee700fe6a2c37282e8', + "max_amounts_in_raw": [200000000000000000, 100000000000000000], + "min_bpt_amount_out_raw": 0, + "kind": Kind.UNBALANCED.value, +} + +pool = { + "poolType": "CustomPool", + "hookType": "CustomHook", + "chainId": "11155111", + "blockNumber": "5955145", + "poolAddress": "0xb2456a6f51530053bc41b0ee700fe6a2c37282e8", + "tokens": [ + "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + ], + "scalingFactors": [1000000000000000000, 1000000000000000000], + "weights": [500000000000000000, 500000000000000000], + "swapFee": 100000000000000000, + "balancesLiveScaled18": [2000000000000000000, 2000000000000000000], + "tokenRates": [1000000000000000000, 1000000000000000000], + "totalSupply": 1000000000000000000, + "aggregateSwapFee": 500000000000000000, +} + +vault = Vault( + custom_pool_classes={"CustomPool": CustomPool}, + custom_hook_classes={"CustomHook": CustomHook}, +) + +def test_hook_before_add_liquidity_no_fee(): + # should alter pool balances + # hook state is used to pass new balances which give expected bptAmount out + input_hook_state = { + "balanceChange": [ + 1000000000000000000, 1000000000000000000 + ], + } + test = vault.add_liquidity( + add_liquidity_input, + pool, + hook_state=input_hook_state + ) + # Hook adds 1n to amountsIn + assert test["amounts_in_raw"] == [ + 200000000000000000, + 100000000000000000, + ] + assert test["bpt_amount_out_raw"] == 146464294351915965 From 7471c966cd35321c8d32aa9463b19b0aca47422b Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 19 Aug 2024 14:18:12 +0100 Subject: [PATCH 5/8] test(Python): Hooks - before remove liquidity hook. --- python/src/remove_liquidity.py | 4 +- python/test/hooks/before_remove_liquidity.py | 136 +++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 python/test/hooks/before_remove_liquidity.py diff --git a/python/src/remove_liquidity.py b/python/src/remove_liquidity.py index 20f8c58..0a26537 100644 --- a/python/src/remove_liquidity.py +++ b/python/src/remove_liquidity.py @@ -42,7 +42,7 @@ def remove_liquidity( # will mean something has gone wrong. # We do take into account and balance changes due # to hook using hookAdjustedBalancesScaled18. - hook_return = hook_class.onBeforeRemoveLiquidity( + hook_return = hook_class.on_before_remove_liquidity( remove_liquidity_input["kind"], remove_liquidity_input["max_bpt_amount_in_raw"], remove_liquidity_input["min_amounts_out_raw"], @@ -52,7 +52,7 @@ def remove_liquidity( if hook_return["success"] is False: raise SystemError("BeforeRemoveLiquidityHookFailed") - for i, a in enumerate(hook_return["hookAdjustedBalancesScaled18"]): + for i, a in enumerate(hook_return["hook_adjusted_balances_scaled18"]): updated_balances_live_scaled18[i] = a if remove_liquidity_input["kind"] == RemoveKind.PROPORTIONAL.value: diff --git a/python/test/hooks/before_remove_liquidity.py b/python/test/hooks/before_remove_liquidity.py new file mode 100644 index 0000000..b888753 --- /dev/null +++ b/python/test/hooks/before_remove_liquidity.py @@ -0,0 +1,136 @@ +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 +from src.hooks.default_hook import DefaultHook + +remove_liquidity_input = { + "pool": '0xb2456a6f51530053bc41b0ee700fe6a2c37282e8', + "min_amounts_out_raw": [0, 1], + "max_bpt_amount_in_raw": 100000000000000000, + "kind": RemoveKind.SINGLE_TOKEN_EXACT_IN.value, +} + +class CustomPool(): + def __init__(self, pool_state): + self.pool_state = pool_state + + def on_swap(self, swap_params): + return 1 + + def compute_invariant(self, balances_live_scaled18): + return 1 + + def compute_balance( + self, + balances_live_scaled18, + token_in_index, + invariant_ratio, + ): + return 1 + +class CustomHook: + 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 = True + self.should_call_after_remove_liquidity = False + self.enable_hook_adjusted_amounts = False + + def on_before_add_liquidity(self): + return {'success': False, 'hook_adjusted_balances_scaled18': []} + + def on_after_add_liquidity(self, kind, amounts_in_scaled18, amounts_in_raw, bpt_amount_out, balances_scaled18, hook_state): + return { 'success': False, 'hook_adjusted_amounts_in_raw': [] }; + + def on_before_remove_liquidity(self, kind, max_bpt_amount_in, min_amounts_out_scaled18, balances_scaled18, hook_state): + if not (isinstance(hook_state, dict) and hook_state is not None and 'balanceChange' in hook_state): + raise ValueError('Unexpected hookState') + assert kind == remove_liquidity_input['kind'] + assert max_bpt_amount_in == remove_liquidity_input['max_bpt_amount_in_raw'] + assert min_amounts_out_scaled18 == remove_liquidity_input['min_amounts_out_raw'] + assert balances_scaled18 == pool['balancesLiveScaled18'] + + return {'success': True, 'hook_adjusted_balances_scaled18': hook_state['balanceChange']} + + def on_after_remove_liquidity(self, kind, bpt_amount_in, amounts_out_scaled18, amounts_out_raw, balances_scaled18, hook_state): + return { + 'success': False, + '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} + +pool = { + "poolType": "CustomPool", + "hookType": "CustomHook", + "chainId": "11155111", + "blockNumber": "5955145", + "poolAddress": "0xb2456a6f51530053bc41b0ee700fe6a2c37282e8", + "tokens": [ + "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + ], + "scalingFactors": [1000000000000000000, 1000000000000000000], + "weights": [500000000000000000, 500000000000000000], + "swapFee": 100000000000000000, + "balancesLiveScaled18": [2000000000000000000, 2000000000000000000], + "tokenRates": [1000000000000000000, 1000000000000000000], + "totalSupply": 1000000000000000000, + "aggregateSwapFee": 500000000000000000, +} + +vault = Vault( + custom_pool_classes={"CustomPool": CustomPool}, + custom_hook_classes={"CustomHook": CustomHook}, +) + +def test_hook_before_remove_liquidity(): + # should alter pool balances + # hook state is used to pass new balances which give expected result + input_hook_state = { + "balanceChange": [ + 1000000000000000000, + 1000000000000000000 + ], + } + test = vault.remove_liquidity( + remove_liquidity_input, + pool, + hook_state=input_hook_state + ) + assert test["bpt_amount_in_raw"] == remove_liquidity_input["max_bpt_amount_in_raw"] + assert test["amounts_out_raw"] == [ + 0, + 909999999999999999, + ] From 8db27a35c9bd6f4f51a413b4b5f2633a23da9dde Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 19 Aug 2024 14:26:41 +0100 Subject: [PATCH 6/8] test(Python): Hooks - before swap hook. --- python/src/swap.py | 6 +- python/test/hooks/after_swap.py | 4 +- python/test/hooks/before_swap.py | 118 +++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 python/test/hooks/before_swap.py diff --git a/python/src/swap.py b/python/src/swap.py index 58a2708..89984d8 100644 --- a/python/src/swap.py +++ b/python/src/swap.py @@ -45,10 +45,12 @@ def swap(swap_input, pool_state, pool_class, hook_class, hook_state): # will mean something has gone wrong. # We do take into account and balance changes due # to hook using hookAdjustedBalancesScaled18. - hook_return = hook_class.onBeforeSwap({**swap_input, "hook_state": hook_state}) + hook_return = hook_class.on_before_swap( + {**swap_input, "hook_state": hook_state} + ) if hook_return["success"] is False: raise SystemError("BeforeSwapHookFailed") - for i, a in enumerate(hook_return["hookAdjustedBalancesScaled18"]): + for i, a in enumerate(hook_return["hook_adjusted_balances_scaled18"]): updated_balances_live_scaled18[i] = a swap_fee = pool_state["swapFee"] diff --git a/python/test/hooks/after_swap.py b/python/test/hooks/after_swap.py index 757729a..8b45083 100644 --- a/python/test/hooks/after_swap.py +++ b/python/test/hooks/after_swap.py @@ -124,7 +124,7 @@ def on_compute_dynamic_swap_fee(self): custom_hook_classes={"CustomHook": CustomHook}, ) -def test_hook_after_remove_liquidity_no_fee(): +def test_hook_after_swap_no_fee(): # aggregateSwapFee of 0 should not take any protocol fees from updated balances # hook state is used to pass expected value to tests # with aggregateFee = 0, balance out is just balance - calculated @@ -142,7 +142,7 @@ def test_hook_after_remove_liquidity_no_fee(): assert test == 1 -def test_hook_after_add_liquidity_with_fee(): +def test_hook_after_swap_with_fee(): # aggregateSwapFee of 50% should take half of remaining # hook state is used to pass expected value to tests # Aggregate fee amount is 50% of swap fee diff --git a/python/test/hooks/before_swap.py b/python/test/hooks/before_swap.py new file mode 100644 index 0000000..de79338 --- /dev/null +++ b/python/test/hooks/before_swap.py @@ -0,0 +1,118 @@ +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.swap import SwapKind + +# 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 +from src.hooks.default_hook import DefaultHook + +pool = { + "poolType": "CustomPool", + "hookType": "CustomHook", + "chainId": "11155111", + "blockNumber": "5955145", + "poolAddress": "0xb2456a6f51530053bc41b0ee700fe6a2c37282e8", + "tokens": [ + "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", + "0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75", + ], + "scalingFactors": [1000000000000000000, 1000000000000000000], + "weights": [500000000000000000, 500000000000000000], + "swapFee": 100000000000000000, + "balancesLiveScaled18": [2000000000000000000, 2000000000000000000], + "tokenRates": [1000000000000000000, 1000000000000000000], + "totalSupply": 1000000000000000000, + "aggregateSwapFee": 500000000000000000, +} + + +swap_input = { + "amount_raw": 100000000, + "swap_kind": SwapKind.GIVENIN.value, + "token_in": pool['tokens'][0], + "token_out": pool['tokens'][1], +} + +class CustomPool(Weighted): + def __init__(self, pool_state): + super().__init__(pool_state) + +class CustomHook: + def __init__(self): + self.should_call_compute_dynamic_swap_fee = False + self.should_call_before_swap = True + 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 = False + self.enable_hook_adjusted_amounts = False + + def on_before_add_liquidity(self): + return {'success': False, 'hook_adjusted_balances_scaled18': []} + + def on_after_add_liquidity(self, kind, amounts_in_scaled18, amounts_in_raw, bpt_amount_out, balances_scaled18, hook_state): + 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): + return { + 'success': False, + 'hook_adjusted_amounts_out_raw': [] + } + + def on_before_swap(self, params): + hook_state = params['hook_state'] + if not (isinstance(hook_state, dict) and hook_state is not None and 'balanceChange' in hook_state): + raise ValueError('Unexpected hookState') + assert params['swap_kind'] == swap_input['swap_kind'] + assert params['token_in'] == swap_input['token_in'] + assert params['token_out'] == swap_input['token_out'] + assert params['amount_raw'] == swap_input['amount_raw'] + return {'success': True, 'hook_adjusted_balances_scaled18': hook_state['balanceChange']} + + def on_after_swap(self, params): + return {'success': True, 'hook_adjusted_amount_calculated_raw': 0} + + def on_compute_dynamic_swap_fee(self): + return {'success': False, 'dynamic_swap_fee': 0} + +vault = Vault( + custom_pool_classes={"CustomPool": CustomPool}, + custom_hook_classes={"CustomHook": CustomHook}, +) + +def test_before_swap(): + # should alter pool balances + # hook state is used to pass new balances which give expected swap result + input_hook_state = { + "balanceChange": [ + 1000000000000000000, 1000000000000000000 + ], + } + test = vault.swap( + swap_input, + pool, + hook_state=input_hook_state + ) + assert test == 89999999 \ No newline at end of file From e38ea1a8ad593bd48b31d50df50e8b4bcc9bc610 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 19 Aug 2024 14:53:00 +0100 Subject: [PATCH 7/8] 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 */ From 99a9b03e5f8d163221d05fccb6dfe710e594b498 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 19 Aug 2024 14:54:25 +0100 Subject: [PATCH 8/8] fix(Python): Remove relative imports. --- python/src/vault.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/python/src/vault.py b/python/src/vault.py index b44c9c5..64d509c 100644 --- a/python/src/vault.py +++ b/python/src/vault.py @@ -1,11 +1,12 @@ +from src.swap import swap +from src.add_liquidity import add_liquidity +from src.remove_liquidity import remove_liquidity +from src.pools.weighted import Weighted +from src.pools.buffer.erc4626_buffer_wrap_or_unwrap import erc4626_buffer_wrap_or_unwrap +from src.pools.stable import Stable +from src.hooks.default_hook import DefaultHook from src.hooks.exit_fee_hook import ExitFeeHook -from .swap import swap -from .add_liquidity import add_liquidity -from .remove_liquidity import remove_liquidity -from .pools.weighted import Weighted -from .pools.buffer.erc4626_buffer_wrap_or_unwrap import erc4626_buffer_wrap_or_unwrap -from .pools.stable import Stable -from .hooks.default_hook import DefaultHook + class Vault: def __init__(self, *, custom_pool_classes=None, custom_hook_classes=None):