diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index d9a63009..c629c4a9 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -102,7 +102,8 @@ def __init__(self, if last_oracle_values is None or 'price' not in last_oracle_values: self.oracles['price'] = Oracle(sma_equivalent_length=19, first_block=Block(self)) else: - self.oracles['price'] = Oracle(sma_equivalent_length=19, first_block=Block(self), last_values=last_oracle_values['price']) + self.oracles['price'] = Oracle(sma_equivalent_length=19, first_block=Block(self), + last_values=last_oracle_values['price']) if last_oracle_values is not None and oracles is not None: self.oracles.update({ name: Oracle(sma_equivalent_length=period, last_values=last_oracle_values[name] @@ -901,8 +902,14 @@ def execute_add_liquidity( ) -> tuple[OmnipoolState, Agent]: """Compute new state after liquidity addition""" + if quantity <= 0: + return state.fail_transaction('Quantity must be non-negative.', agent) + delta_Q = lrna_price(state, tkn_add) * quantity - if not (state.unique_id, tkn_add) in agent.holdings: + if (state.unique_id, tkn_add) in agent.holdings: + if agent.holdings[(state.unique_id, tkn_add)] != 0: + return state.fail_transaction(f'Agent already has liquidity in pool {tkn_add}.', agent) + else: agent.holdings[(state.unique_id, tkn_add)] = 0 if agent.holdings[tkn_add] < quantity: @@ -1326,4 +1333,4 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: if agent_holdings[tkn] > 0 and tkn not in prices: raise ValueError(f'Agent has holdings in {tkn} but no price was given.') - return value_assets(prices, agent_holdings) \ No newline at end of file + return value_assets(prices, agent_holdings) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 50245756..d1932106 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -1,6 +1,5 @@ import copy import math -import random import pytest from hypothesis import given, strategies as st, assume, settings @@ -13,7 +12,6 @@ from hydradx.model.amm.trade_strategies import constant_swaps, omnipool_arbitrage from hydradx.tests.strategies_omnipool import omnipool_reasonable_config, omnipool_config, assets_config - asset_price_strategy = st.floats(min_value=0.0001, max_value=100000) asset_price_bounded_strategy = st.floats(min_value=0.1, max_value=10) asset_number_strategy = st.integers(min_value=3, max_value=5) @@ -127,6 +125,40 @@ def test_add_liquidity(initial_state: oamm.OmnipoolState): raise AssertionError(f'legal transaction failed against weight limit in {i} ({new_state.fail})') +@settings(max_examples=1) +@given(omnipool_config(token_count=3, lrna_fee=0, asset_fee=0)) +def test_add_liquidity_with_existing_position_fails(initial_state: oamm.OmnipoolState): + old_state = initial_state + tkn = old_state.asset_list[0] + old_agent = Agent( + holdings={tkn: old_state.liquidity[tkn] / 10, (old_state.unique_id, tkn): old_state.shares[tkn] / 10} + ) + + delta_R = old_agent.holdings[tkn] + + new_state, new_agents = oamm.add_liquidity(old_state, old_agent, delta_R, tkn) + + if not new_state.fail: + raise AssertionError(f'Adding liquidity to an existing position should fail.') + + +@settings(max_examples=1) +@given(omnipool_config(token_count=3, lrna_fee=0, asset_fee=0)) +def test_add_liquidity_with_quantity_zero_should_fail(initial_state: oamm.OmnipoolState): + old_state = initial_state + tkn = old_state.asset_list[0] + old_agent = Agent( + holdings={tkn: old_state.liquidity[tkn] / 10, (old_state.unique_id, tkn): old_state.shares[tkn] / 10} + ) + + delta_R = 0 + + new_state, new_agents = oamm.add_liquidity(old_state, old_agent, delta_R, tkn) + + if not new_state.fail: + raise AssertionError(f'Adding liquidity with quantity zero should fail.') + + @given(omnipool_config(token_count=3, withdrawal_fee=False)) def test_remove_liquidity_no_fee(initial_state: oamm.OmnipoolState): i = initial_state.asset_list[2] @@ -1251,52 +1283,56 @@ def test_volatility_limit(omnipool: oamm.OmnipoolState): def test_LP_limits(omnipool: oamm.OmnipoolState, max_withdrawal_per_block, max_lp_per_block): omnipool.max_withdrawal_per_block = max_withdrawal_per_block omnipool.max_lp_per_block = max_lp_per_block - agent = Agent(holdings={'HDX': 10000000000}) + state = omnipool.copy() + initial_agent = Agent(holdings={'HDX': 10000000000}) + agent = initial_agent.copy() oamm.execute_add_liquidity( - state=omnipool, + state=state, agent=agent, tkn_add='HDX', - quantity=omnipool.liquidity['HDX'] * max_lp_per_block + quantity=state.liquidity['HDX'] * max_lp_per_block ) - if omnipool.fail: + if state.fail: raise AssertionError('Valid LP operation failed.') - omnipool.update() + state = omnipool.copy() + agent = initial_agent.copy() oamm.execute_add_liquidity( - state=omnipool, + state=state, agent=agent, tkn_add='HDX', - quantity=omnipool.liquidity['HDX'] * max_lp_per_block + 1 + quantity=state.liquidity['HDX'] * max_lp_per_block + 1 ) - if not omnipool.fail: + if not state.fail: raise AssertionError('Invalid LP operation succeeded.') - omnipool.update() + state = omnipool.copy() + agent = initial_agent.copy() # add liquidity again to test remove liquidity oamm.execute_add_liquidity( - state=omnipool, + state=state, agent=agent, tkn_add='HDX', - quantity=omnipool.liquidity['HDX'] * max_lp_per_block + quantity=state.liquidity['HDX'] * max_lp_per_block ) - if omnipool.fail: + if state.fail: raise AssertionError('Second LP operation failed.') withdraw_quantity = agent.holdings[('omnipool', 'HDX')] - total_shares = omnipool.shares['HDX'] + total_shares = state.shares['HDX'] oamm.execute_remove_liquidity( - state=omnipool, + state=state, agent=agent, tkn_remove='HDX', quantity=withdraw_quantity # agent.holdings[('omnipool', 'HDX')] ) - if withdraw_quantity / total_shares > max_withdrawal_per_block and not omnipool.fail: + if withdraw_quantity / total_shares > max_withdrawal_per_block and not state.fail: raise AssertionError('Agent was able to remove too much liquidity.') - omnipool.update() + state.update() oamm.execute_remove_liquidity( - state=omnipool, + state=state, agent=agent, tkn_remove='HDX', - quantity=omnipool.shares['HDX'] * max_withdrawal_per_block + quantity=state.shares['HDX'] * max_withdrawal_per_block ) - if omnipool.fail: + if state.fail: raise AssertionError('Agent was not able to remove liquidity.')