diff --git a/.github/workflows/test-python.yaml b/.github/workflows/test-python.yaml index c6220fc..132cfad 100644 --- a/.github/workflows/test-python.yaml +++ b/.github/workflows/test-python.yaml @@ -58,10 +58,10 @@ jobs: - name: Create env file for Brownie pm run: | - touch ~/.brownie/packages/overlay-market/v1-core@1.0.0-beta.2/.env - echo WEB3_INFURA_PROJECT_ID=${{ secrets.WEB3_INFURA_PROJECT_ID }} >> ~/.brownie/packages/overlay-market/v1-core@1.0.0-beta.2/.env - echo ETHERSCAN_TOKEN=${{ secrets.ETHERSCAN_TOKEN }} >> ~/.brownie/packages/overlay-market/v1-core@1.0.0-beta.2/.env - cat ~/.brownie/packages/overlay-market/v1-core@1.0.0-beta.2/.env + touch ~/.brownie/packages/overlay-market/v1-core@1.0.0-beta.4/.env + echo WEB3_INFURA_PROJECT_ID=${{ secrets.WEB3_INFURA_PROJECT_ID }} >> ~/.brownie/packages/overlay-market/v1-core@1.0.0-beta.4/.env + echo ETHERSCAN_TOKEN=${{ secrets.ETHERSCAN_TOKEN }} >> ~/.brownie/packages/overlay-market/v1-core@1.0.0-beta.4/.env + cat ~/.brownie/packages/overlay-market/v1-core@1.0.0-beta.4/.env - name: Run Tests run: brownie test -vv -s --gas diff --git a/brownie-config.yaml b/brownie-config.yaml index 47061ac..27fa9e6 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -11,7 +11,7 @@ dotenv: .env # require OpenZepplin, Uniswap Contracts dependencies: - OpenZeppelin/openzeppelin-contracts@4.5.0 - - overlay-market/v1-core@1.0.0-beta.2 + - overlay-market/v1-core@1.0.0-beta.4 - Uniswap/v3-core@1.0.0 - Uniswap/v3-periphery@1.0.0 @@ -23,6 +23,6 @@ compiler: runs: 800 remappings: - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.5.0" - - "@overlay/v1-core=overlay-market/v1-core@1.0.0-beta.2" + - "@overlay/v1-core=overlay-market/v1-core@1.0.0-beta.4" - "@uniswap/v3-core=Uniswap/v3-core@1.0.0" - "@uniswap/v3-periphery=Uniswap/v3-periphery@1.0.0" diff --git a/contracts/state/OverlayV1EstimateState.sol b/contracts/state/OverlayV1EstimateState.sol index 7e31e97..805dfe5 100644 --- a/contracts/state/OverlayV1EstimateState.sol +++ b/contracts/state/OverlayV1EstimateState.sol @@ -6,6 +6,7 @@ import "@overlay/v1-core/contracts/libraries/FixedPoint.sol"; import "@overlay/v1-core/contracts/libraries/Oracle.sol"; import "@overlay/v1-core/contracts/libraries/Position.sol"; import "@overlay/v1-core/contracts/libraries/Risk.sol"; +import "@overlay/v1-core/contracts/libraries/Tick.sol"; import "../interfaces/state/IOverlayV1EstimateState.sol"; @@ -19,6 +20,7 @@ abstract contract OverlayV1EstimateState is OverlayV1PriceState, OverlayV1OIState { + using FixedCast for uint256; using FixedPoint for uint256; using Position for Position.Info; @@ -37,18 +39,31 @@ abstract contract OverlayV1EstimateState is uint256 oi = _oiFromNotional(data, notional); uint256 fractionOfCapOi = _fractionOfCapOi(market, data, oi); + // get the attributes needed to calculate position oiShares: + // oiLong/Short, oiLongShares/oiShortShares + (uint256 oiLong, uint256 oiShort) = _ois(market); + uint256 oiShares = Position.calcOiShares( + oi, + isLong ? oiLong : oiShort, + isLong ? market.oiLongShares() : market.oiShortShares() + ); + // prices uint256 midPrice = _mid(data); uint256 price = isLong ? _ask(market, data, fractionOfCapOi) : _bid(market, data, fractionOfCapOi); + + // TODO: test position_ = Position.Info({ - notional: uint96(notional), - debt: uint96(debt), - entryToMidRatio: Position.calcEntryToMidRatio(price, midPrice), + notionalInitial: uint96(notional), + debtInitial: uint96(debt), + midTick: Tick.priceToTick(midPrice), + entryTick: Tick.priceToTick(price), isLong: isLong, liquidated: false, - oiShares: oi + oiShares: uint240(oiShares), + fractionRemaining: FixedPoint.ONE.toUint16Fixed() }); } @@ -57,7 +72,7 @@ abstract contract OverlayV1EstimateState is uint256 fraction = FixedPoint.ONE; // debt estimate is simply initial debt of position - debt_ = position.debtCurrent(fraction); + debt_ = Position.debtInitial(position, fraction); } function _costEstimate(Position.Info memory position) internal view returns (uint256 cost_) { @@ -84,7 +99,7 @@ abstract contract OverlayV1EstimateState is uint256 maintenanceMarginFraction = market.params( uint256(Risk.Parameters.MaintenanceMarginFraction) ); - uint256 q = position.notionalInitial(FixedPoint.ONE); + uint256 q = Position.notionalInitial(position, FixedPoint.ONE); maintenanceMargin_ = q.mulUp(maintenanceMarginFraction); } diff --git a/contracts/state/OverlayV1PositionState.sol b/contracts/state/OverlayV1PositionState.sol index 811b0ae..ae9b804 100644 --- a/contracts/state/OverlayV1PositionState.sol +++ b/contracts/state/OverlayV1PositionState.sol @@ -31,22 +31,26 @@ abstract contract OverlayV1PositionState is ) internal view returns (Position.Info memory position_) { bytes32 key = keccak256(abi.encodePacked(owner, id)); ( - uint96 notional, - uint96 debt, - uint48 entryToMidRatio, + uint96 notionalInitial, + uint96 debtInitial, + int24 midTick, + int24 entryTick, bool isLong, bool liquidated, - uint256 oiShares + uint240 oiShares, + uint16 fractionRemaining ) = market.positions(key); // assemble the position info struct position_ = Position.Info({ - notional: notional, - debt: debt, - entryToMidRatio: entryToMidRatio, + notionalInitial: notionalInitial, + debtInitial: debtInitial, + midTick: midTick, + entryTick: entryTick, isLong: isLong, liquidated: liquidated, - oiShares: oiShares + oiShares: oiShares, + fractionRemaining: fractionRemaining }); } @@ -56,7 +60,7 @@ abstract contract OverlayV1PositionState is uint256 fraction = FixedPoint.ONE; // return the debt - debt_ = position.debtCurrent(fraction); + debt_ = Position.debtInitial(position, fraction); } /// @dev current cost basis of individual position @@ -101,9 +105,9 @@ abstract contract OverlayV1PositionState is uint256 fraction = FixedPoint.ONE; // get attributes needed to calculate current collateral amount: - // notionalInitial, debtCurrent, oiInitial, oiCurrent - uint256 q = position.notionalInitial(fraction); - uint256 d = position.debtCurrent(fraction); + // notionalInitial, debtInitial, oiInitial, oiCurrent + uint256 q = Position.notionalInitial(position, fraction); + uint256 d = Position.debtInitial(position, fraction); uint256 oiInitial = position.oiInitial(fraction); // calculate oiCurrent from aggregate oi values @@ -305,7 +309,7 @@ abstract contract OverlayV1PositionState is uint256 maintenanceMarginFraction = market.params( uint256(Risk.Parameters.MaintenanceMarginFraction) ); - uint256 q = position.notionalInitial(FixedPoint.ONE); + uint256 q = Position.notionalInitial(position, FixedPoint.ONE); maintenanceMargin_ = q.mulUp(maintenanceMarginFraction); } diff --git a/tests/feedisperser/conftest.py b/tests/feedisperser/conftest.py index 9fb7442..1b5c24e 100644 --- a/tests/feedisperser/conftest.py +++ b/tests/feedisperser/conftest.py @@ -4,7 +4,7 @@ @pytest.fixture(scope="module") def ovl_v1_core(pm): - return pm("overlay-market/v1-core@1.0.0-beta.2") + return pm("overlay-market/v1-core@1.0.0-beta.4") @pytest.fixture(scope="module") @@ -43,14 +43,22 @@ def governor_role(): @pytest.fixture(scope="module", params=[8000000]) -def create_token(ovl_v1_core, gov, alice, bob, governor_role, request): +def create_token(ovl_v1_core, gov, alice, bob, minter_role, governor_role, + request): sup = request.param def create_token(supply=sup): ovl = ovl_v1_core.OverlayV1Token tok = gov.deploy(ovl) + + # grant governor role tok.grantRole(governor_role, gov, {"from": gov}) + + # mint the token then renounce minter role + tok.grantRole(minter_role, gov, {"from": gov}) tok.mint(gov, supply * 10 ** tok.decimals(), {"from": gov}) + tok.renounceRole(minter_role, gov, {"from": gov}) + tok.transfer(alice, (supply/2) * 10 ** tok.decimals(), {"from": gov}) tok.transfer(bob, (supply/2) * 10 ** tok.decimals(), {"from": gov}) return tok diff --git a/tests/state/conftest.py b/tests/state/conftest.py index a2c8495..2963f79 100644 --- a/tests/state/conftest.py +++ b/tests/state/conftest.py @@ -4,7 +4,7 @@ @pytest.fixture(scope="module") def ovl_v1_core(pm): - return pm("overlay-market/v1-core@1.0.0-beta.2") + return pm("overlay-market/v1-core@1.0.0-beta.4") @pytest.fixture(scope="module") @@ -32,21 +32,6 @@ def fee_recipient(accounts): yield accounts[4] -@pytest.fixture(scope="module", params=[8000000]) -def create_token(ovl_v1_core, gov, alice, bob, request): - sup = request.param - - def create_token(supply=sup): - ovl = ovl_v1_core.OverlayV1Token - tok = gov.deploy(ovl) - tok.mint(gov, supply * 10 ** tok.decimals(), {"from": gov}) - tok.transfer(alice, (supply/2) * 10 ** tok.decimals(), {"from": gov}) - tok.transfer(bob, (supply/2) * 10 ** tok.decimals(), {"from": gov}) - return tok - - yield create_token - - @pytest.fixture(scope="module") def minter_role(): yield web3.solidityKeccak(['string'], ["MINTER"]) @@ -62,6 +47,26 @@ def governor_role(): yield web3.solidityKeccak(['string'], ["GOVERNOR"]) +@pytest.fixture(scope="module", params=[8000000]) +def create_token(ovl_v1_core, gov, alice, bob, minter_role, request): + sup = request.param + + def create_token(supply=sup): + ovl = ovl_v1_core.OverlayV1Token + tok = gov.deploy(ovl) + + # mint the token then renounce minter role + tok.grantRole(minter_role, gov, {"from": gov}) + tok.mint(gov, supply * 10 ** tok.decimals(), {"from": gov}) + tok.renounceRole(minter_role, gov, {"from": gov}) + + tok.transfer(alice, (supply/2) * 10 ** tok.decimals(), {"from": gov}) + tok.transfer(bob, (supply/2) * 10 ** tok.decimals(), {"from": gov}) + return tok + + yield create_token + + @pytest.fixture(scope="module") def ovl(create_token): yield create_token() diff --git a/tests/state/test_estimate.py b/tests/state/test_estimate.py index 23dc775..1a7b400 100644 --- a/tests/state/test_estimate.py +++ b/tests/state/test_estimate.py @@ -2,7 +2,7 @@ from brownie.test import given, strategy from decimal import Decimal -from .utils import calculate_mid_ratio, RiskParameter +from .utils import price_to_tick, RiskParameter @given( @@ -21,9 +21,18 @@ def test_position_estimate(state, market, feed, alice, ovl, leverage, is_long): # NOTE: state.mid() tested in test_price.py mid_price = state.mid(market) - # calculate the oi + # oi total and oi total shares on side + # NOTE: state.ois() tested in test_oi.py + oi_long, oi_short = state.ois(market) + oi_total = oi_long if is_long else oi_short + oi_total_shares = market.oiLongShares() \ + if is_long else market.oiShortShares() + + # calculate the oi and oi shares # NOTE: state.fractionOfCapOi() tested in test_oi.py oi = int(Decimal(notional) * Decimal(1e18) / Decimal(mid_price)) + oi_shares = int(Decimal(oi) * Decimal(oi_total_shares) + / Decimal(oi_total)) if oi_total > 0 else oi fraction_oi = state.fractionOfCapOi(market, oi) # get the entry price @@ -35,13 +44,16 @@ def test_position_estimate(state, market, feed, alice, ovl, leverage, is_long): expect_notional = notional expect_debt = expect_notional - collateral expect_is_long = is_long - expect_mid_ratio = calculate_mid_ratio(entry_price, mid_price) + expect_mid_tick = price_to_tick(mid_price) + expect_entry_tick = price_to_tick(entry_price) expect_liquidated = False - expect_oi_shares = oi + expect_oi_shares = oi_shares + expect_fraction_remaining = 10000 # check market position is same as position returned from state - expect = (expect_notional, expect_debt, expect_mid_ratio, expect_is_long, - expect_liquidated, expect_oi_shares) + expect = (expect_notional, expect_debt, expect_mid_tick, expect_entry_tick, + expect_is_long, expect_liquidated, expect_oi_shares, + expect_fraction_remaining) actual = state.positionEstimate(market, collateral, leverage, is_long) assert expect == actual @@ -93,13 +105,13 @@ def test_oi_estimate(state, market, feed, alice, ovl, is_long): mid_price = state.mid(market) # calculate the oi - # NOTE: state.fractionOfCapOi() tested in test_oi.py oi = int(Decimal(notional) * Decimal(1e18) / Decimal(mid_price)) # check position's oi is same as position returned from state + # NOTE: rel 1e-4 given tick precision to 1bps expect = oi actual = state.oiEstimate(market, collateral, leverage, is_long) - assert expect == actual + assert expect == approx(actual, rel=1e-4) @given(is_long=strategy('bool')) @@ -166,7 +178,8 @@ def test_liquidation_price_estimate(state, market, feed, alice, ovl, is_long): if is_long else entry_price + dp # check position's maintenance is same as position returned from state + # NOTE: rel 1e-4 given tick precision to 1bps expect = int(expect_liquidation_price) actual = int(state.liquidationPriceEstimate( market, collateral, leverage, is_long)) - assert expect == approx(actual) + assert expect == approx(actual, rel=1e-4) diff --git a/tests/state/test_position.py b/tests/state/test_position.py index 30fb6d5..d2f58f7 100644 --- a/tests/state/test_position.py +++ b/tests/state/test_position.py @@ -6,7 +6,7 @@ from .utils import ( get_position_key, - position_entry_price, + tick_to_price, RiskParameter ) @@ -62,7 +62,7 @@ def test_debt(state, market, feed, ovl, alice): pos_key = get_position_key(alice.address, pos_id) # check market position debt same as state queried position debt - (_, expect_debt, _, _, _, _) = market.positions(pos_key) + (_, expect_debt, _, _, _, _, _, _) = market.positions(pos_key) actual_debt = state.debt(market, alice.address, pos_id) assert expect_debt == actual_debt @@ -88,7 +88,7 @@ def test_cost(state, market, feed, ovl, alice): pos_key = get_position_key(alice.address, pos_id) # check market position debt same as state queried position debt - (expect_notional_initial, expect_debt, _, _, + (expect_notional_initial, expect_debt, _, _, _, _, _, _) = market.positions(pos_key) expect_cost = expect_notional_initial - expect_debt actual_cost = state.cost(market, alice.address, pos_id) @@ -116,7 +116,7 @@ def test_oi(state, market, feed, ovl, alice): pos_key = get_position_key(alice.address, pos_id) # check market position oi same as state query oi - (_, _, _, _, _, expect_oi_shares) = market.positions(pos_key) + (_, _, _, _, _, _, expect_oi_shares, _) = market.positions(pos_key) expect_oi_long_shares = market.oiLongShares() # NOTE: ois() tests in test_oi.py @@ -166,19 +166,27 @@ def test_collateral(state, market, feed, ovl, alice): pos_key = get_position_key(alice.address, pos_id) # get market position oi - (expect_notional_initial, expect_debt, _, _, _, - expect_oi_shares) = market.positions(pos_key) + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = market.positions(pos_key) expect_oi_long_shares = market.oiLongShares() # NOTE: ois() tests in test_oi.py actual_oi_long, _ = state.ois(market) + # get the entry and mid prices + expect_mid_price = tick_to_price(expect_mid_tick) + expect_mid_price = int(expect_mid_price) + + expect_entry_price = tick_to_price(expect_entry_tick) + expect_entry_price = int(expect_entry_price) + # calculate the expected collateral amount: Q * (OI(t) / OI(0)) - D expect_oi = int( Decimal(actual_oi_long) * Decimal(expect_oi_shares) / Decimal(expect_oi_long_shares) ) - expect_oi_initial = expect_oi_shares + expect_oi_initial = Decimal( + expect_notional_initial) * Decimal(1e18) / Decimal(expect_mid_price) expect_collateral = int(expect_notional_initial * (expect_oi / expect_oi_initial) - expect_debt) @@ -198,7 +206,8 @@ def test_collateral(state, market, feed, ovl, alice): Decimal(actual_oi_long) * Decimal(expect_oi_shares) / Decimal(expect_oi_long_shares) ) - expect_oi_initial = expect_oi_shares + expect_oi_initial = Decimal( + expect_notional_initial) * Decimal(1e18) / Decimal(expect_mid_price) expect_collateral = int(expect_notional_initial * (expect_oi / expect_oi_initial) - expect_debt) @@ -229,8 +238,8 @@ def test_value(state, market, feed, ovl, alice, is_long): # get market position oi pos = market.positions(pos_key) - (expect_notional_initial, expect_debt, expect_mid_ratio, _, _, - expect_oi_shares) = pos + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = pos expect_oi_tot_shares_on_side = market.oiLongShares() if is_long \ else market.oiShortShares() @@ -238,18 +247,22 @@ def test_value(state, market, feed, ovl, alice, is_long): actual_oi_long, actual_oi_short = state.ois(market) actual_oi_tot_on_side = actual_oi_long if is_long else actual_oi_short + # get the entry and mid prices + expect_mid_price = tick_to_price(expect_mid_tick) + expect_mid_price = int(expect_mid_price) + + expect_entry_price = tick_to_price(expect_entry_tick) + expect_entry_price = int(expect_entry_price) + # calculate the expected value of position # V(t) = N(t) +/- OI(t) * [P(t) - P(0)] - expect_oi_initial = expect_oi_shares + expect_oi_initial = Decimal( + expect_notional_initial) * Decimal(1e18) / Decimal(expect_mid_price) expect_oi = int( Decimal(actual_oi_tot_on_side) * Decimal(expect_oi_shares) / Decimal(expect_oi_tot_shares_on_side) ) - # get the entry price - expect_entry_price = position_entry_price(pos) - expect_entry_price = int(expect_entry_price) - # NOTE: fractionOfCapOi tests in test_oi.py frac_cap_oi = state.fractionOfCapOi(market, expect_oi) @@ -294,8 +307,8 @@ def test_notional(state, market, feed, ovl, alice, is_long): # get market position oi pos = market.positions(pos_key) - (expect_notional_initial, expect_debt, expect_mid_ratio, _, _, - expect_oi_shares) = pos + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = pos expect_oi_tot_shares_on_side = market.oiLongShares() if is_long \ else market.oiShortShares() @@ -303,18 +316,22 @@ def test_notional(state, market, feed, ovl, alice, is_long): actual_oi_long, actual_oi_short = state.ois(market) actual_oi_tot_on_side = actual_oi_long if is_long else actual_oi_short + # get the entry and mid prices + expect_mid_price = tick_to_price(expect_mid_tick) + expect_mid_price = int(expect_mid_price) + + expect_entry_price = tick_to_price(expect_entry_tick) + expect_entry_price = int(expect_entry_price) + # calculate the expected value of position # V(t) = N(t) + D +/- OI(t) * [P(t) - P(0)] - expect_oi_initial = expect_oi_shares + expect_oi_initial = Decimal( + expect_notional_initial) * Decimal(1e18) / Decimal(expect_mid_price) expect_oi = int( Decimal(actual_oi_tot_on_side) * Decimal(expect_oi_shares) / Decimal(expect_oi_tot_shares_on_side) ) - # get the entry price - expect_entry_price = position_entry_price(pos) - expect_entry_price = int(expect_entry_price) - # NOTE: fractionOfCapOi tests in test_oi.py frac_cap_oi = state.fractionOfCapOi(market, expect_oi) @@ -360,8 +377,8 @@ def test_trading_fee(state, market, feed, ovl, alice, is_long): # get market position oi pos = market.positions(pos_key) - (expect_notional_initial, expect_debt, expect_mid_ratio, _, _, - expect_oi_shares) = pos + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = pos expect_oi_tot_shares_on_side = market.oiLongShares() if is_long \ else market.oiShortShares() @@ -369,18 +386,22 @@ def test_trading_fee(state, market, feed, ovl, alice, is_long): actual_oi_long, actual_oi_short = state.ois(market) actual_oi_tot_on_side = actual_oi_long if is_long else actual_oi_short + # get the entry and mid prices + expect_mid_price = tick_to_price(expect_mid_tick) + expect_mid_price = int(expect_mid_price) + + expect_entry_price = tick_to_price(expect_entry_tick) + expect_entry_price = int(expect_entry_price) + # calculate the expected value of position # V(t) = N(t) + D +/- OI(t) * [P(t) - P(0)] - expect_oi_initial = expect_oi_shares + expect_oi_initial = Decimal( + expect_notional_initial) * Decimal(1e18) / Decimal(expect_mid_price) expect_oi = int( Decimal(actual_oi_tot_on_side) * Decimal(expect_oi_shares) / Decimal(expect_oi_tot_shares_on_side) ) - # get the entry price - expect_entry_price = position_entry_price(pos) - expect_entry_price = int(expect_entry_price) - # NOTE: fractionOfCapOi tests in test_oi.py frac_cap_oi = state.fractionOfCapOi(market, expect_oi) @@ -435,8 +456,8 @@ def test_liquidatable(state, mock_market, mock_feed, ovl, alice, is_long): # get the market position pos = mock_market.positions(pos_key) - (expect_notional_initial, expect_debt, expect_mid_ratio, _, _, - expect_oi_shares) = pos + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = pos # check not liquidatable expect_liquidatable = False @@ -502,8 +523,8 @@ def test_liquidation_fee(state, mock_market, mock_feed, ovl, alice, is_long): # get market position oi pos = mock_market.positions(pos_key) - (expect_notional_initial, expect_debt, expect_mid_ratio, _, _, - expect_oi_shares) = pos + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = pos expect_oi_tot_shares_on_side = mock_market.oiLongShares() if is_long \ else mock_market.oiShortShares() @@ -511,18 +532,22 @@ def test_liquidation_fee(state, mock_market, mock_feed, ovl, alice, is_long): actual_oi_long, actual_oi_short = state.ois(mock_market) actual_oi_tot_on_side = actual_oi_long if is_long else actual_oi_short + # get the entry and mid prices + expect_mid_price = tick_to_price(expect_mid_tick) + expect_mid_price = int(expect_mid_price) + + expect_entry_price = tick_to_price(expect_entry_tick) + expect_entry_price = int(expect_entry_price) + # calculate the expected value of position # V(t) = N(t) +/- OI(t) * [P(t) - P(0)] - expect_oi_initial = expect_oi_shares + expect_oi_initial = Decimal( + expect_notional_initial) * Decimal(1e18) / Decimal(expect_mid_price) expect_oi = int( Decimal(actual_oi_tot_on_side) * Decimal(expect_oi_shares) / Decimal(expect_oi_tot_shares_on_side) ) - # get the entry price - expect_entry_price = position_entry_price(pos) - expect_entry_price = int(expect_entry_price) - # mid used for liquidation exit (manipulation resistant) # NOTE: mid tests in test_price.py expect_exit_price = state.mid(mock_market) @@ -570,8 +595,8 @@ def test_maintenance_margin(state, market, feed, ovl, alice): # get market position oi pos = market.positions(pos_key) - (expect_notional_initial, expect_debt, expect_mid_ratio, _, _, - expect_oi_shares) = pos + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = pos # calculate the expected maintenance margin: MM * Q expect_maintenance_margin_fraction = market.params( @@ -620,8 +645,8 @@ def test_margin_excess_before_liquidation(state, mock_market, mock_feed, # get market position oi pos = mock_market.positions(pos_key) - (expect_notional_initial, expect_debt, expect_mid_ratio, _, _, - expect_oi_shares) = pos + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = pos expect_oi_tot_shares_on_side = mock_market.oiLongShares() if is_long \ else mock_market.oiShortShares() @@ -629,18 +654,22 @@ def test_margin_excess_before_liquidation(state, mock_market, mock_feed, actual_oi_long, actual_oi_short = state.ois(mock_market) actual_oi_tot_on_side = actual_oi_long if is_long else actual_oi_short + # get the entry and mid prices + expect_mid_price = tick_to_price(expect_mid_tick) + expect_mid_price = int(expect_mid_price) + + expect_entry_price = tick_to_price(expect_entry_tick) + expect_entry_price = int(expect_entry_price) + # calculate the expected value of position # V(t) = N(t) +/- OI(t) * [P(t) - P(0)] - expect_oi_initial = expect_oi_shares + expect_oi_initial = Decimal( + expect_notional_initial) * Decimal(1e18) / Decimal(expect_mid_price) expect_oi = int( Decimal(actual_oi_tot_on_side) * Decimal(expect_oi_shares) / Decimal(expect_oi_tot_shares_on_side) ) - # get the entry price - expect_entry_price = position_entry_price(pos) - expect_entry_price = int(expect_entry_price) - # mid used for liquidation exit (manipulation resistant) # NOTE: mid tests in test_price.py expect_exit_price = state.mid(mock_market) @@ -672,9 +701,7 @@ def test_margin_excess_before_liquidation(state, mock_market, mock_feed, expect_excess = expect_value - expect_maintenance_margin - expect_liq_fee actual_excess = int(state.marginExcessBeforeLiquidation( mock_market, alice.address, pos_id)) - - # TODO: why rel tol needed here? - assert expect_excess == approx(actual_excess, rel=1e-3) + assert expect_excess == approx(actual_excess, rel=1e-4) # repeat the same when excess < 0 # set price to just beyond liquidation price @@ -715,9 +742,7 @@ def test_margin_excess_before_liquidation(state, mock_market, mock_feed, expect_excess = expect_value - expect_maintenance_margin - expect_liq_fee actual_excess = int(state.marginExcessBeforeLiquidation( mock_market, alice.address, pos_id)) - - # TODO: why rel tol needed here? - assert expect_excess == approx(actual_excess, rel=1e-3) + assert expect_excess == approx(actual_excess, rel=1e-4) @given(is_long=strategy('bool')) @@ -742,8 +767,8 @@ def test_liquidation_price(state, market, feed, ovl, alice, is_long): # get market position oi pos = market.positions(pos_key) - (expect_notional_initial, expect_debt, expect_mid_ratio, _, _, - expect_oi_shares) = pos + (expect_notional_initial, expect_debt, expect_mid_tick, expect_entry_tick, + _, _, expect_oi_shares, _) = pos expect_oi_tot_shares_on_side = market.oiLongShares() if is_long \ else market.oiShortShares() @@ -751,18 +776,22 @@ def test_liquidation_price(state, market, feed, ovl, alice, is_long): actual_oi_long, actual_oi_short = state.ois(market) actual_oi_tot_on_side = actual_oi_long if is_long else actual_oi_short + # get the entry and mid prices + expect_mid_price = tick_to_price(expect_mid_tick) + expect_mid_price = int(expect_mid_price) + + expect_entry_price = tick_to_price(expect_entry_tick) + expect_entry_price = int(expect_entry_price) + # calculate the expected value of position # V(t) = N(t) + D +/- OI(t) * [P(t) - P(0)] - expect_oi_initial = expect_oi_shares + expect_oi_initial = Decimal( + expect_notional_initial) * Decimal(1e18) / Decimal(expect_mid_price) expect_oi = int( Decimal(actual_oi_tot_on_side) * Decimal(expect_oi_shares) / Decimal(expect_oi_tot_shares_on_side) ) - # get the entry price - expect_entry_price = position_entry_price(pos) - expect_entry_price = int(expect_entry_price) - # calculate the collateral backing the position expect_collateral = Decimal(expect_notional_initial * (expect_oi / expect_oi_initial)-expect_debt) diff --git a/tests/state/utils.py b/tests/state/utils.py index 5093cd4..9b648a9 100644 --- a/tests/state/utils.py +++ b/tests/state/utils.py @@ -2,6 +2,7 @@ from decimal import Decimal from enum import Enum from hexbytes import HexBytes +from math import log from typing import Any @@ -42,28 +43,20 @@ def get_position_key(owner: str, id: int) -> HexBytes: return web3.solidityKeccak(['address', 'uint256'], [owner, id]) -def calculate_mid_ratio(entry_price: int, mid_price: int) -> int: +def tick_to_price(tick: int) -> int: """ - Returns mid ratio from entry price and mid price - - NOTE: mid_ratio is uint48 format and mid, entry prices - are int FixedPoint format + Returns the price associated with a given tick + price = 1.0001 ** tick """ - # NOTE: mid_ratio "ONE" is 1e14 given uint48 - mid_ratio = int(Decimal(entry_price) * Decimal(1e14) / Decimal(mid_price)) - return mid_ratio + return int((Decimal(1.0001) ** Decimal(tick)) * Decimal(1e18)) -def position_entry_price(position: Any) -> float: +def price_to_tick(price: int) -> int: """ - Returns the position entry price from an individual position tuple. - - entry = ratio * mid = ratio * (notional / oi) + Returns the tick associated with a given price + price = 1.0001 ** tick """ - (notional, debt, ratio, is_long, liquidated, oi_shares) = position - ratio_fixed_point = Decimal(ratio) * Decimal(1e4) - entry = float(ratio_fixed_point * Decimal(notional) / Decimal(oi_shares)) - return entry + return int(log(Decimal(price) / Decimal(1e18)) / log(Decimal(1.0001))) def transform_snapshot(snapshot: Any, timestamp: int, window: int,