Skip to content

Commit

Permalink
feat(Python): Add ExitFee hook.
Browse files Browse the repository at this point in the history
  • Loading branch information
johngrantuk committed Aug 19, 2024
1 parent 8db27a3 commit e38ea1a
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 3 deletions.
89 changes: 89 additions & 0 deletions python/src/hooks/exit_fee_hook.py
Original file line number Diff line number Diff line change
@@ -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}
4 changes: 2 additions & 2 deletions python/src/vault.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)

Expand Down
83 changes: 83 additions & 0 deletions python/test/hooks/exit_fee.py
Original file line number Diff line number Diff line change
@@ -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
]
2 changes: 1 addition & 1 deletion typescript/test/hooks/beforeRemoveLiquidity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down

0 comments on commit e38ea1a

Please sign in to comment.