diff --git a/curvesim/pool/cryptoswap/pool.py b/curvesim/pool/cryptoswap/pool.py index 1f17ea5d6..d4cbb7a51 100644 --- a/curvesim/pool/cryptoswap/pool.py +++ b/curvesim/pool/cryptoswap/pool.py @@ -2,7 +2,7 @@ Mainly a module to house the `CryptoPool`, a cryptoswap implementation in Python. """ import time -from typing import List +from typing import Any, List, Tuple from curvesim.exceptions import CalculationError, CryptoPoolError, CurvesimValueError from curvesim.pool.base import Pool @@ -35,7 +35,7 @@ class CurveCryptoPool(Pool): "admin_fee", "ma_half_time", "price_scale", - "_price_oracle", + "cached_price_oracle", "last_prices", "last_prices_timestamp", "_block_timestamp", @@ -126,7 +126,7 @@ def __init__( self.admin_fee = admin_fee self.price_scale = initial_price - self._price_oracle = initial_price + self.cached_price_oracle = initial_price self.last_prices = initial_price self.ma_half_time = ma_half_time @@ -401,24 +401,21 @@ def _tweak_price( # noqa: complexity: 12 Also claims admin fees if appropriate (enough profit and price scale and oracle is close enough). """ - price_oracle: int = self._price_oracle + + old_price_oracle: int = self.cached_price_oracle last_prices: int = self.last_prices price_scale: int = self.price_scale - last_prices_timestamp: int = self.last_prices_timestamp - block_timestamp: int = self._block_timestamp p_new: int = 0 + + block_timestamp: int = self._block_timestamp n_coins: int = self.n - if last_prices_timestamp < block_timestamp: - # MA update required - ma_half_time: int = self.ma_half_time - alpha: int = _halfpow( - (block_timestamp - last_prices_timestamp) * 10**18 // ma_half_time - ) - price_oracle = ( - last_prices * (10**18 - alpha) + price_oracle * alpha - ) // 10**18 - self._price_oracle = price_oracle + # Calculate price oracle: + price_oracle = self.price_oracle_logic(self) + + if old_price_oracle != price_oracle: + # Price oracle has been updated: + self.cached_price_oracle = price_oracle self.last_prices_timestamp = block_timestamp D_unadjusted: int = new_D # Withdrawal methods know new D already @@ -941,7 +938,7 @@ def _calc_withdraw_one_coin( i: int, update_D: bool, calc_price: bool, - ) -> (int, int, int, List[int]): + ) -> Tuple[int, int, int, List[int]]: token_supply: int = self.tokens assert token_amount <= token_supply # dev: token amount more than supply assert i < self.n # dev: coin out of range @@ -1017,20 +1014,29 @@ def lp_price(self) -> int: Returns an LP token price approximating behavior as a constant-product AMM. """ return ( - 2 * self.virtual_price * _sqrt_int(self.internal_price_oracle()) // 10**18 + 2 + * self.virtual_price + * _sqrt_int(self.price_oracle_logic(self)) + // 10**18 ) - def internal_price_oracle(self) -> int: + @staticmethod + def price_oracle_logic(data_obj: Any[Pool]) -> int: """ Return the value of the EMA price oracle. + + Calculates an exponential moving average using the AMM's state. """ - price_oracle: int = self._price_oracle - last_prices_timestamp: int = self.last_prices_timestamp - block_timestamp: int = self._block_timestamp + # inputs from data object to calculate EMA: + price_oracle: int = data_obj.price_oracle + last_prices_timestamp: int = data_obj.last_prices_timestamp + ma_half_time: int = data_obj.ma_half_time + block_timestamp: int = data_obj.block_timestamp + last_prices: int = data_obj.last_prices + + # internal price oracle logic: if last_prices_timestamp < block_timestamp: - ma_half_time: int = self.ma_half_time - last_prices: int = self.last_prices alpha: int = _halfpow( (block_timestamp - last_prices_timestamp) * 10**18 // ma_half_time ) @@ -1045,7 +1051,7 @@ def price_oracle(self) -> int: Same as `internal_price_oracle`. Kept for compatability with the vyper interface. """ - return self.internal_price_oracle() + return self.price_oracle_logic(self) def get_virtual_price(self) -> int: """ diff --git a/test/unit/test_cryptopool.py b/test/unit/test_cryptopool.py index f96ff034a..19e99a392 100644 --- a/test/unit/test_cryptopool.py +++ b/test/unit/test_cryptopool.py @@ -75,7 +75,7 @@ def initialize_pool(vyper_cryptopool): price_oracle = vyper_cryptopool.eval("self._price_oracle") # pylint: disable-next=protected-access - pool._price_oracle = price_oracle + pool.cached_price_oracle = price_oracle last_prices = vyper_cryptopool.last_prices() last_prices_timestamp = vyper_cryptopool.last_prices_timestamp() @@ -99,7 +99,7 @@ def sync_ema_logic( """ # pylint: disable=protected-access price_oracle = vyper_cryptopool.eval("self._price_oracle") - pool._price_oracle = price_oracle + pool.cached_price_oracle = price_oracle # synchronize the times between the two pools and reset # last_prices and last_prices_timestamp @@ -369,12 +369,12 @@ def test_tweak_price( # ------- test no oracle update and no scale adjustment ------------- # assert pool.price_scale == vyper_cryptopool.price_scale() - assert pool._price_oracle == vyper_cryptopool.eval("self._price_oracle") + assert pool.cached_price_oracle == vyper_cryptopool.eval("self._price_oracle") old_scale = pool.price_scale - assert old_scale == pool._price_oracle + assert old_scale == pool.cached_price_oracle - old_oracle = pool._price_oracle + old_oracle = pool.cached_price_oracle pool._tweak_price(A, gamma, xp, last_price, 0) # pylint: disable=protected-access vyper_cryptopool.eval(f"self.tweak_price({A_gamma}, {xp}, {last_price}, 0)") @@ -383,7 +383,7 @@ def test_tweak_price( # no price adjustment since price oracle is same as price scale (`norm` is 0) assert pool.price_scale == old_scale # EMA price oracle won't update if price oracle and last price is the same - assert old_oracle == pool._price_oracle + assert old_oracle == pool.cached_price_oracle # ------- test oracle updates with no scale adjustment ------------- # @@ -392,7 +392,7 @@ def test_tweak_price( assert pool.virtual_price == vyper_cryptopool.virtual_price() assert pool.xcp_profit == vyper_cryptopool.xcp_profit() - old_oracle = pool._price_oracle + old_oracle = pool.cached_price_oracle old_scale = pool.price_scale old_virtual_price = pool.virtual_price @@ -401,23 +401,23 @@ def test_tweak_price( # check the pools are the same assert pool.price_scale == vyper_cryptopool.price_scale() - assert pool._price_oracle == vyper_cryptopool.eval("self._price_oracle") + assert pool.cached_price_oracle == vyper_cryptopool.eval("self._price_oracle") assert pool.virtual_price == vyper_cryptopool.virtual_price() assert pool.D == vyper_cryptopool.D() # check oracle updated # scale shouldn't change because no adjustment is possible # with no profit - assert pool._price_oracle != old_oracle + assert pool.cached_price_oracle != old_oracle assert pool.price_scale == old_scale assert pool.virtual_price == old_virtual_price # ------- test scale adjustment ----------------------- # sync_ema_logic(vyper_cryptopool, pool, last_price) - assert pool.price_scale != pool._price_oracle + assert pool.price_scale != pool.cached_price_oracle - old_oracle = pool._price_oracle + old_oracle = pool.cached_price_oracle old_scale = pool.price_scale old_virtual_price = pool.virtual_price @@ -428,7 +428,7 @@ def test_tweak_price( vyper_cryptopool.eval(f"self.tweak_price({A_gamma}, {xp}, 0, 0)") assert pool.price_scale == vyper_cryptopool.price_scale() - assert pool._price_oracle == vyper_cryptopool.eval("self._price_oracle") + assert pool.cached_price_oracle == vyper_cryptopool.eval("self._price_oracle") assert pool.D == vyper_cryptopool.D() assert pool.virtual_price == vyper_cryptopool.virtual_price() @@ -447,7 +447,7 @@ def test_tweak_price( vyper_cryptopool.eval(f"self.tweak_price({A_gamma}, {xp}, 0, 0)") assert pool.price_scale == vyper_cryptopool.price_scale() - assert pool._price_oracle == vyper_cryptopool.eval("self._price_oracle") + assert pool.cached_price_oracle == vyper_cryptopool.eval("self._price_oracle") assert pool.D == vyper_cryptopool.D() assert pool.virtual_price == vyper_cryptopool.virtual_price()