Skip to content

Commit

Permalink
curve style fees in stableswap (#137)
Browse files Browse the repository at this point in the history
* changed withdrawal fee for stableswap to match our runtime implementation
  • Loading branch information
jepidoptera authored and poliwop committed Jun 27, 2023
1 parent f33dc79 commit 890f330
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 67 deletions.
104 changes: 73 additions & 31 deletions hydradx/model/amm/stableswap_amm.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,44 +186,46 @@ def execute_swap(
return state.fail_transaction('Pool has insufficient liquidity.', agent)

new_agent = agent # .copy()
if tkn_buy not in new_agent.holdings:
new_agent.holdings[tkn_buy] = 0
new_agent.holdings[tkn_buy] += buy_quantity
new_agent.holdings[tkn_sell] -= sell_quantity
state.liquidity[tkn_buy] -= buy_quantity
state.liquidity[tkn_sell] += sell_quantity

return state, new_agent


def execute_remove_liquidity(
state: StableSwapPoolState,
agent: Agent,
shares_removed: float,
tkn_remove: str
):
if shares_removed > agent.holdings[state.unique_id]:
raise ValueError('Agent tried to remove more shares than it owns.')
elif shares_removed <= 0:
raise ValueError('Withdraw quantity must be > 0.')

share_fraction = shares_removed / state.shares

updated_d = state.d * (1 - share_fraction * (1 - state.trade_fee))
delta_tkn = state.calculate_y(
state.modified_balances(delta={}, omit=[tkn_remove]),
updated_d
) - state.liquidity[tkn_remove]

if delta_tkn >= state.liquidity[tkn_remove]:
return state.fail_transaction(f'Not enough liquidity in {tkn_remove}.', agent)

if tkn_remove not in agent.holdings:
agent.holdings[tkn_remove] = 0

state.shares -= shares_removed
agent.holdings[state.unique_id] -= shares_removed
state.liquidity[tkn_remove] += delta_tkn
agent.holdings[tkn_remove] -= delta_tkn # agent is receiving funds, because delta_tkn is a negative number
return state, agent
#
# def execute_remove_liquidity_old(
# state: StableSwapPoolState,
# agent: Agent,
# shares_removed: float,
# tkn_remove: str
# ):
# if shares_removed > agent.holdings[state.unique_id]:
# raise ValueError('Agent tried to remove more shares than it owns.')
# elif shares_removed <= 0:
# raise ValueError('Withdraw quantity must be > 0.')
#
# share_fraction = shares_removed / state.shares
#
# updated_d = state.d * (1 - share_fraction * (1 - state.trade_fee))
# delta_tkn = state.calculate_y(
# state.modified_balances(delta={}, omit=[tkn_remove]),
# updated_d
# ) - state.liquidity[tkn_remove]
#
# if delta_tkn >= state.liquidity[tkn_remove]:
# return state.fail_transaction(f'Not enough liquidity in {tkn_remove}.', agent)
#
# if tkn_remove not in agent.holdings:
# agent.holdings[tkn_remove] = 0
#
# state.shares -= shares_removed
# agent.holdings[state.unique_id] -= shares_removed
# state.liquidity[tkn_remove] += delta_tkn
# agent.holdings[tkn_remove] -= delta_tkn # agent is receiving funds, because delta_tkn is a negative number
# return state, agent


def execute_withdraw_asset(
Expand Down Expand Up @@ -260,6 +262,46 @@ def execute_withdraw_asset(
return state, agent


def execute_remove_liquidity(
state: StableSwapPoolState,
agent: Agent,
shares_removed: float,
tkn_remove: str,
):
# First, need to calculate
# * Get current D
# * Solve Eqn against y_i for D - _token_amount

_fee = state.trade_fee

initial_d = state.calculate_d()
reduced_d = initial_d - shares_removed * initial_d / state.shares

xp_reduced = copy.copy(state.liquidity)
xp_reduced.pop(tkn_remove)

reduced_y = state.calculate_y(state.modified_balances(omit=[tkn_remove]), reduced_d)
asset_reserve = state.liquidity[tkn_remove]

for tkn in state.asset_list:
if tkn == tkn_remove:
dx_expected = state.liquidity[tkn] * reduced_d / initial_d - reduced_y
asset_reserve -= _fee * dx_expected
else:
dx_expected = state.liquidity[tkn] - state.liquidity[tkn] * reduced_d / initial_d
xp_reduced[tkn] -= _fee * dx_expected

dy = asset_reserve - state.calculate_y(list(xp_reduced.values()), reduced_d)

agent.holdings[state.unique_id] -= shares_removed
state.shares -= shares_removed
state.liquidity[tkn_remove] -= dy
if tkn_remove not in agent.holdings:
agent.holdings[tkn_remove] = 0
agent.holdings[tkn_remove] += dy
return state, agent


def execute_add_liquidity(
state: StableSwapPoolState,
agent: Agent,
Expand Down
93 changes: 69 additions & 24 deletions hydradx/tests/test_stableswap.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,30 +70,33 @@ def test_round_trip_dy(initial_pool: StableSwapPoolState):
raise AssertionError('Round-trip calculation incorrect.')


@given(stableswap_config(precision=0.000000001))
def test_remove_asset(initial_pool: StableSwapPoolState):
initial_agent = Agent(
holdings={tkn: 0 for tkn in initial_pool.asset_list}
)
# agent holds all the shares
tkn_remove = initial_pool.asset_list[0]
pool_name = initial_pool.unique_id
delta_shares = min(initial_pool.shares / 2, 100)
initial_agent.holdings.update({initial_pool.unique_id: delta_shares + 1})
withdraw_shares_pool, withdraw_shares_agent = stableswap.remove_liquidity(
initial_pool, initial_agent, delta_shares, tkn_remove
)
delta_tkn = withdraw_shares_agent.holdings[tkn_remove] - initial_agent.holdings[tkn_remove]
withdraw_asset_pool, withdraw_asset_agent = stableswap.execute_withdraw_asset(
initial_pool.copy(), initial_agent.copy(), delta_tkn, tkn_remove
)
if (
withdraw_asset_agent.holdings[tkn_remove] != pytest.approx(withdraw_shares_agent.holdings[tkn_remove])
or withdraw_asset_agent.holdings[pool_name] != pytest.approx(withdraw_shares_agent.holdings[pool_name])
or withdraw_shares_pool.liquidity[tkn_remove] != pytest.approx(withdraw_asset_pool.liquidity[tkn_remove])
or withdraw_shares_pool.shares != pytest.approx(withdraw_asset_pool.shares)
):
raise AssertionError("Asset values don't match.")
# commented out because without further work, withdraw_asset and remove_liquidity are not equivalent.
# This is because withdraw_asset was written based remove_liquidity_old, which is not equivalent to remove_liquidity.
#
# @given(stableswap_config(precision=0.000000001))
# def test_remove_asset(initial_pool: StableSwapPoolState):
# initial_agent = Agent(
# holdings={tkn: 0 for tkn in initial_pool.asset_list}
# )
# # agent holds all the shares
# tkn_remove = initial_pool.asset_list[0]
# pool_name = initial_pool.unique_id
# delta_shares = min(initial_pool.shares / 2, 100)
# initial_agent.holdings.update({initial_pool.unique_id: delta_shares + 1})
# withdraw_shares_pool, withdraw_shares_agent = stableswap.remove_liquidity(
# initial_pool, initial_agent, delta_shares, tkn_remove
# )
# delta_tkn = withdraw_shares_agent.holdings[tkn_remove] - initial_agent.holdings[tkn_remove]
# withdraw_asset_pool, withdraw_asset_agent = stableswap.execute_withdraw_asset(
# initial_pool.copy(), initial_agent.copy(), delta_tkn, tkn_remove
# )
# if (
# withdraw_asset_agent.holdings[tkn_remove] != pytest.approx(withdraw_shares_agent.holdings[tkn_remove])
# or withdraw_asset_agent.holdings[pool_name] != pytest.approx(withdraw_shares_agent.holdings[pool_name])
# or withdraw_shares_pool.liquidity[tkn_remove] != pytest.approx(withdraw_asset_pool.liquidity[tkn_remove])
# or withdraw_shares_pool.shares != pytest.approx(withdraw_asset_pool.shares)
# ):
# raise AssertionError("Asset values don't match.")


@given(stableswap_config(precision=0.000000001))
Expand Down Expand Up @@ -197,3 +200,45 @@ def test_add_remove_liquidity(initial_pool: StableSwapPoolState):
raise AssertionError('Stableswap equation does not hold after remove liquidity operation.')
if remove_liquidity_agent.holdings[lp_tkn] != pytest.approx(lp.holdings[lp_tkn]):
raise AssertionError('LP did not get the same balance back when withdrawing liquidity.')


def test_curve_style_withdraw_fees():
initial_state = stableswap.StableSwapPoolState(
tokens={
'USDA': 1000000,
'USDB': 1000000,
'USDC': 1000000,
'USDD': 1000000,
}, amplification=100, trade_fee=0.003,
unique_id='test_pool'
)
initial_agent = Agent(
holdings={'USDA': 100000}
)
test_state, test_agent = stableswap.execute_add_liquidity(
state=initial_state.copy(),
agent=initial_agent.copy(),
quantity=initial_agent.holdings['USDA'],
tkn_add='USDA',
)

stable_state, stable_agent = stableswap.execute_remove_liquidity(
state=test_state.copy(),
agent=test_agent.copy(),
shares_removed=test_agent.holdings['test_pool'],
tkn_remove='USDB'
)
effective_fee_withdraw = 1 - stable_agent.holdings['USDB'] / initial_agent.holdings['USDA']

swap_state, swap_agent = stableswap.execute_swap(
initial_state.copy(),
initial_agent.copy(),
tkn_sell='USDA',
tkn_buy='USDB',
sell_quantity=initial_agent.holdings['USDA']
)
effective_fee_swap = 1 - swap_agent.holdings['USDB'] / initial_agent.holdings['USDA']

if effective_fee_withdraw <= effective_fee_swap:
raise AssertionError('Withdraw fee is not higher than swap fee.')

30 changes: 18 additions & 12 deletions hydradx/tests/test_stableswap_subpools.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,15 @@ def test_sell_omnipool_for_stable_swap(initial_state: oamm.OmnipoolState):
new_stable_pool.calculate_d() * stable_pool.shares
):
raise AssertionError("Shares/invariant ratio incorrect.")
if (
(new_stable_pool.shares - stable_pool.shares) * stable_pool.calculate_d()
* (1 - stable_pool.trade_fee) !=
pytest.approx(stable_pool.shares * (new_stable_pool.calculate_d() - stable_pool.calculate_d()))
):
raise AssertionError("Delta_shares * D * (1 - fee) did not yield expected result.")
# commented out because withdrawal fees and trade fees are no longer (close to) equivalent
# since the new execute_remove_liquidity function does not use the trade fee in the same way
#
# if (
# (new_stable_pool.shares - stable_pool.shares) * stable_pool.calculate_d()
# * (1 - stable_pool.trade_fee) !=
# pytest.approx(stable_pool.shares * (new_stable_pool.calculate_d() - stable_pool.calculate_d()))
# ):
# raise AssertionError("Delta_shares * D * (1 - fee) did not yield expected result.")
if (
new_state.liquidity[stable_shares] + stable_pool.shares !=
pytest.approx(new_stable_pool.shares + initial_state.liquidity[stable_shares])
Expand Down Expand Up @@ -357,12 +360,15 @@ def test_sell_LRNA_for_stableswap(initial_state: oamm.OmnipoolState):
pytest.approx(stable_pool.shares * new_stable_pool.calculate_d())
):
raise AssertionError("New_shares * D did not yield expected result.")
if (
(new_stable_pool.shares - stable_pool.shares) * stable_pool.calculate_d()
* (1 - stable_pool.trade_fee) !=
pytest.approx(stable_pool.shares * (new_stable_pool.calculate_d() - stable_pool.calculate_d()))
):
raise AssertionError("Delta_shares * D * (1 - fee) did not yield expected result.")
# commented out because withdrawal fees and trade fees are no longer (close to) equivalent
# since the new execute_remove_liquidity function does not use the trade fee in the same way
#
# if (
# (new_stable_pool.shares - stable_pool.shares) * stable_pool.calculate_d()
# * (1 - stable_pool.trade_fee) !=
# pytest.approx(stable_pool.shares * (new_stable_pool.calculate_d() - stable_pool.calculate_d()))
# ):
# raise AssertionError("Delta_shares * D * (1 - fee) did not yield expected result.")
if (
new_state.liquidity[stable_pool.unique_id] + stable_pool.shares
!= pytest.approx(new_stable_pool.shares + initial_state.liquidity[stable_pool.unique_id])
Expand Down

0 comments on commit 890f330

Please sign in to comment.