From 00361d7d31a7bfd21749130bb961259c7bc7e78b Mon Sep 17 00:00:00 2001 From: allt0ld Date: Mon, 11 Sep 2023 09:04:52 -0400 Subject: [PATCH 1/8] Implement `calcs.tricrypto_ng.lp_price` and use in `CurveCryptoPool` --- curvesim/pool/cryptoswap/calcs/tricrypto_ng.py | 7 +------ curvesim/pool/cryptoswap/pool.py | 10 ++++------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py b/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py index 36d10842a..fac958517 100644 --- a/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py +++ b/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py @@ -318,12 +318,7 @@ def lp_price(virtual_price, price_oracle) -> int: """ Returns an LP token price approximating behavior as a constant-product AMM. """ - # TODO: find/implement integer cube root function - # price_oracle = self.internal_price_oracle() - # return ( - # 3 * self.virtual_price * icbrt(price_oracle[0] * price_oracle[1]) - # ) // 10**24 - raise CalculationError("LP price calc doesn't support more than 3 coins") + return 3 * virtual_price * _cbrt(price_oracle[0] * price_oracle[1]) // 10**24 def get_p( diff --git a/curvesim/pool/cryptoswap/pool.py b/curvesim/pool/cryptoswap/pool.py index e16367ca4..f25a440e7 100644 --- a/curvesim/pool/cryptoswap/pool.py +++ b/curvesim/pool/cryptoswap/pool.py @@ -968,12 +968,10 @@ def lp_price(self) -> int: virtual_price = self.virtual_price price_oracle = self.internal_price_oracle() price = factory_2_coin.lp_price(virtual_price, price_oracle) - # TODO: find/implement integer cube root function - # elif self.n == 3: - # price_oracle = self.internal_price_oracle() - # price = ( - # 3 * self.virtual_price * icbrt(price_oracle[0] * price_oracle[1]) - # ) // 10**24 + elif self.n == 3: + virtual_price = self.virtual_price + price_oracle = self.internal_price_oracle() + price = tricrypto_ng.lp_price(virtual_price, price_oracle) else: raise CalculationError("LP price calc doesn't support more than 3 coins") From d487170ad9c0c54119ce7c4d30bab98b55f3707d Mon Sep 17 00:00:00 2001 From: allt0ld Date: Tue, 12 Sep 2023 11:56:06 -0400 Subject: [PATCH 2/8] Write `lp_price` docstring for all instances --- .../pool/cryptoswap/calcs/factory_2_coin.py | 22 +++++++++++++++---- .../pool/cryptoswap/calcs/tricrypto_ng.py | 19 ++++++++++++++-- curvesim/pool/cryptoswap/pool.py | 20 +++++++++++++---- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/curvesim/pool/cryptoswap/calcs/factory_2_coin.py b/curvesim/pool/cryptoswap/calcs/factory_2_coin.py index c57b99582..a23a1cc4a 100644 --- a/curvesim/pool/cryptoswap/calcs/factory_2_coin.py +++ b/curvesim/pool/cryptoswap/calcs/factory_2_coin.py @@ -60,12 +60,26 @@ def _sqrt_int(x: int) -> int: raise CalculationError("Did not converge") -def lp_price(virtual_price, price_oracle) -> int: +def lp_price(virtual_price: int, price_oracle: List[int]) -> int: """ - Returns an LP token price approximating behavior as a constant-product AMM. + Returns the price of an LP token in units of token 0. + + Derived from the equilibrium point of a constant-product AMM + that approximates Cryptoswap's behavior. + + Parameters + ---------- + virtual_price: int + Amount of XCP invariant per LP token in units of `D`. + price_oracle: List[int] + Price oracle value for the pool. + + Returns + ------- + int + Liquidity redeemable per LP token in units of token 0. """ - price_oracle = price_oracle[0] - return 2 * virtual_price * _sqrt_int(price_oracle) // 10**18 + return 2 * virtual_price * _sqrt_int(price_oracle[0]) // 10**18 # pylint: disable-next=too-many-locals diff --git a/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py b/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py index fac958517..d7fc8c808 100644 --- a/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py +++ b/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py @@ -314,9 +314,24 @@ def geometric_mean(x: List[int]) -> int: return _cbrt(prod) -def lp_price(virtual_price, price_oracle) -> int: +def lp_price(virtual_price: int, price_oracle: List[int]) -> int: """ - Returns an LP token price approximating behavior as a constant-product AMM. + Returns the price of an LP token in units of token 0. + + Derived from the equilibrium point of a constant-product AMM + that approximates Cryptoswap's behavior. + + Parameters + ---------- + virtual_price: int + Amount of XCP invariant per LP token in units of `D`. + price_oracle: List[int] + Price oracle value for the pool. + + Returns + ------- + int + Liquidity redeemable per LP token in units of token 0. """ return 3 * virtual_price * _cbrt(price_oracle[0] * price_oracle[1]) // 10**24 diff --git a/curvesim/pool/cryptoswap/pool.py b/curvesim/pool/cryptoswap/pool.py index f25a440e7..af2fab0dd 100644 --- a/curvesim/pool/cryptoswap/pool.py +++ b/curvesim/pool/cryptoswap/pool.py @@ -529,14 +529,14 @@ def get_y(self, i, j, x, xp): j: int index of coin; usually the "out"-token x: int - balance of i-th coin in units of D + balance of i-th coin in units of `D` xp: list of int - coin balances in units of D + coin balances in units of `D` Returns ------- int - The balance of the j-th coin, in units of D, for the other + The balance of the j-th coin, in units of `D`, for the other coin balances given. Note @@ -962,7 +962,19 @@ def calc_withdraw_one_coin(self, token_amount: int, i: int) -> int: def lp_price(self) -> int: """ - Returns an LP token price approximating behavior as a constant-product AMM. + Returns the price of an LP token in units of token 0. + + Derived from the equilibrium point of a constant-product AMM + that approximates Cryptoswap's behavior. + + Returns + ------- + int + Liquidity redeemable per LP token in units of token 0. + + Note + ---- + This is a "view" function; it doesn't change the state of the pool. """ if self.n == 2: virtual_price = self.virtual_price From 0a80190d3551c9643396210660c573c148b356f2 Mon Sep 17 00:00:00 2001 From: allt0ld Date: Thu, 14 Sep 2023 19:10:38 -0400 Subject: [PATCH 3/8] Update cryptoswap `price_oracle` and `lp_price` to 3 coins --- curvesim/pool/cryptoswap/calcs/__init__.py | 6 ++-- curvesim/pool/cryptoswap/pool.py | 28 +++++++++++++--- test/fixtures/pool.py | 8 +++++ test/unit/test_tricrypto.py | 39 +++++++++++++++++++++- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/curvesim/pool/cryptoswap/calcs/__init__.py b/curvesim/pool/cryptoswap/calcs/__init__.py index 9c62a5a0f..ca7c2a4ab 100644 --- a/curvesim/pool/cryptoswap/calcs/__init__.py +++ b/curvesim/pool/cryptoswap/calcs/__init__.py @@ -72,7 +72,9 @@ def get_y(A: int, gamma: int, xp: List[int], D: int, j: int) -> List[int]: return y_out -def get_alpha(ma_half_time, block_timestamp, last_prices_timestamp, n_coins): +def get_alpha( + ma_half_time: int, block_timestamp: int, last_prices_timestamp: int, n_coins: int +): if n_coins == 2: alpha: int = halfpow( (block_timestamp - last_prices_timestamp) * 10**18 // ma_half_time @@ -83,7 +85,7 @@ def get_alpha(ma_half_time, block_timestamp, last_prices_timestamp, n_coins): # # Note ln(2) = 0.693147... but the approx actually used is 694 / 1000. # - # CAUTION: neeed to be wary of off-by-one errors from integer division. + # CAUTION: need to be wary of off-by-one errors from integer division. ma_half_time = ceil(ma_half_time * 1000 / 694) alpha: int = tricrypto_ng.wad_exp( -1 * ((block_timestamp - last_prices_timestamp) * 10**18 // ma_half_time) diff --git a/curvesim/pool/cryptoswap/pool.py b/curvesim/pool/cryptoswap/pool.py index af2fab0dd..046cb35c9 100644 --- a/curvesim/pool/cryptoswap/pool.py +++ b/curvesim/pool/cryptoswap/pool.py @@ -16,8 +16,8 @@ get_alpha, get_p, get_y, - halfpow, newton_D, + tricrypto_ng, ) logger = get_logger(__name__) @@ -981,8 +981,9 @@ def lp_price(self) -> int: price_oracle = self.internal_price_oracle() price = factory_2_coin.lp_price(virtual_price, price_oracle) elif self.n == 3: + # 3-coin vyper contract uses cached packed oracle prices instead of internal_price_oracle() virtual_price = self.virtual_price - price_oracle = self.internal_price_oracle() + price_oracle = self._price_oracle price = tricrypto_ng.lp_price(virtual_price, price_oracle) else: raise CalculationError("LP price calc doesn't support more than 3 coins") @@ -998,11 +999,28 @@ def internal_price_oracle(self) -> List[int]: block_timestamp: int = self._block_timestamp if last_prices_timestamp < block_timestamp: + + if self.n == 2: + last_prices: List[int] = self.last_prices + elif self.n == 3: + # 3-coin vyper contract caps every "last price" that gets fed to the EMA + last_prices: List[int] = self.last_prices.copy() + price_scale: List[int] = self.price_scale + last_prices = [ + min(last_p, 2 * storage_p) + for last_p, storage_p in zip(last_prices, price_scale) + ] + else: + raise CalculationError( + "Price oracle calc doesn't support more than 3 coins" + ) + 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 + n_coins: int = self.n + alpha: int = get_alpha( + ma_half_time, block_timestamp, last_prices_timestamp, n_coins ) + return [ (last_p * (10**18 - alpha) + oracle_p * alpha) // 10**18 for last_p, oracle_p in zip(last_prices, price_oracle) diff --git a/test/fixtures/pool.py b/test/fixtures/pool.py index 0992ca066..e11cf1d86 100644 --- a/test/fixtures/pool.py +++ b/test/fixtures/pool.py @@ -216,6 +216,14 @@ def pack_3_uint64s(nums): return (nums[0] << 128) | (nums[1] << 64) | nums[0] +def unpack_prices(packed_prices): + mask = 2**128 - 1 + return [ + packed_prices & mask, + (packed_prices >> 128) & mask, + ] + + def pack_prices(prices): return (prices[1] << 128) | prices[0] diff --git a/test/unit/test_tricrypto.py b/test/unit/test_tricrypto.py index b1570877b..f45c82246 100644 --- a/test/unit/test_tricrypto.py +++ b/test/unit/test_tricrypto.py @@ -20,6 +20,7 @@ _newton_y, wad_exp, ) +from ..fixtures.pool import pack_prices, unpack_prices def initialize_pool(vyper_tricrypto): @@ -42,7 +43,11 @@ def initialize_pool(vyper_tricrypto): ma_half_time = vyper_tricrypto.ma_time() price_scale = [vyper_tricrypto.price_scale(i) for i in range(n_coins - 1)] - price_oracle = [vyper_tricrypto.price_oracle(i) for i in range(n_coins - 1)] + + # We load directly from contract storage and unpack instead of recalculating through price_oracle(i) + price_oracle_packed = vyper_tricrypto.eval("self.price_oracle_packed") + price_oracle = unpack_prices(price_oracle_packed) + last_prices = [vyper_tricrypto.last_prices(i) for i in range(n_coins - 1)] last_prices_timestamp = vyper_tricrypto.last_prices_timestamp() balances = [vyper_tricrypto.balances(i) for i in range(n_coins)] @@ -477,3 +482,35 @@ def test_calc_token_amount(vyper_tricrypto, x0_perc, x1_perc, x2_perc): assert abs(lp_amount - expected_lp_amount) < 2 assert pool.balances == expected_balances + + +@given( + st.lists(price, min_size=2, max_size=2), + st.lists(price, min_size=2, max_size=2), + st.integers(min_value=0, max_value=1000), +) +@settings( + suppress_health_check=[HealthCheck.function_scoped_fixture], + max_examples=5, + deadline=None, +) +def test_price_oracle(vyper_tricrypto, price_oracle, last_prices, time_delta): + """Test `price_oracle` and `lp_price` against vyper implementation.""" + n_coins = 3 + price_oracle_packed = pack_prices(price_oracle) + last_prices_packed = pack_prices(last_prices) + + vyper_tricrypto.eval(f"self.price_oracle_packed={price_oracle_packed}") + vyper_tricrypto.eval(f"self.last_prices_packed={last_prices_packed}") + vm_timestamp = boa.env.vm.state.timestamp + last_prices_timestamp = vm_timestamp - time_delta + vyper_tricrypto.eval(f"self.last_prices_timestamp={last_prices_timestamp}") + + pool = initialize_pool(vyper_tricrypto) + # pylint: disable-next=protected-access + pool._increment_timestamp(timestamp=vm_timestamp) + + assert pool.lp_price() == vyper_tricrypto.lp_price() + assert pool.price_oracle() == [ + vyper_tricrypto.price_oracle(i) for i in range(n_coins - 1) + ] From 9966315b9b4a4483cbf2b3044f07845641344af4 Mon Sep 17 00:00:00 2001 From: allt0ld Date: Thu, 14 Sep 2023 21:39:20 -0400 Subject: [PATCH 4/8] Log `lp_price` and Tricrypto oracle changes --- ...4_213622_philiplu97_tricrypto_lp_price.rst | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst diff --git a/changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst b/changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst new file mode 100644 index 000000000..6f69ed68d --- /dev/null +++ b/changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst @@ -0,0 +1,35 @@ +.. A new scriv changelog fragment. +.. +.. Uncomment the header that is right (remove the leading dots). +.. +.. Removed +.. ------- +.. +.. - A bullet item for the Removed category. +.. +Added +----- + +- Added 3-coin lp_price to CurveCryptoPool. +- Implemented Tricrypto_NG's oracle behavior in CurveCryptoPool for 3-coin pools. + +.. Changed +.. ------- +.. +.. - A bullet item for the Changed category. +.. +.. Deprecated +.. ---------- +.. +.. - A bullet item for the Deprecated category. +.. +.. Fixed +.. ----- +.. +.. - A bullet item for the Fixed category. +.. +.. Security +.. -------- +.. +.. - A bullet item for the Security category. +.. From d1306beb6e990f743793c9e0f12a7e13678dc274 Mon Sep 17 00:00:00 2001 From: allt0ld Date: Thu, 14 Sep 2023 21:55:44 -0400 Subject: [PATCH 5/8] Type hint `lp_price`, `internal_price_oracle`, and `get_alpha` --- curvesim/pool/cryptoswap/calcs/__init__.py | 2 +- curvesim/pool/cryptoswap/pool.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/curvesim/pool/cryptoswap/calcs/__init__.py b/curvesim/pool/cryptoswap/calcs/__init__.py index ca7c2a4ab..a0e9574e9 100644 --- a/curvesim/pool/cryptoswap/calcs/__init__.py +++ b/curvesim/pool/cryptoswap/calcs/__init__.py @@ -74,7 +74,7 @@ def get_y(A: int, gamma: int, xp: List[int], D: int, j: int) -> List[int]: def get_alpha( ma_half_time: int, block_timestamp: int, last_prices_timestamp: int, n_coins: int -): +) -> int: if n_coins == 2: alpha: int = halfpow( (block_timestamp - last_prices_timestamp) * 10**18 // ma_half_time diff --git a/curvesim/pool/cryptoswap/pool.py b/curvesim/pool/cryptoswap/pool.py index 046cb35c9..6cdecb4ff 100644 --- a/curvesim/pool/cryptoswap/pool.py +++ b/curvesim/pool/cryptoswap/pool.py @@ -977,14 +977,14 @@ def lp_price(self) -> int: This is a "view" function; it doesn't change the state of the pool. """ if self.n == 2: - virtual_price = self.virtual_price - price_oracle = self.internal_price_oracle() - price = factory_2_coin.lp_price(virtual_price, price_oracle) + virtual_price: int = self.virtual_price + price_oracle: List[int] = self.internal_price_oracle() + price: int = factory_2_coin.lp_price(virtual_price, price_oracle) elif self.n == 3: # 3-coin vyper contract uses cached packed oracle prices instead of internal_price_oracle() - virtual_price = self.virtual_price - price_oracle = self._price_oracle - price = tricrypto_ng.lp_price(virtual_price, price_oracle) + virtual_price: int = self.virtual_price + price_oracle: List[int] = self._price_oracle + price: int = tricrypto_ng.lp_price(virtual_price, price_oracle) else: raise CalculationError("LP price calc doesn't support more than 3 coins") @@ -994,7 +994,7 @@ def internal_price_oracle(self) -> List[int]: """ Return the value of the EMA price oracle. """ - price_oracle: int = self._price_oracle + price_oracle: List[int] = self._price_oracle last_prices_timestamp: int = self.last_prices_timestamp block_timestamp: int = self._block_timestamp From 349656aa1b30d38942af8a758de44753964a3f9a Mon Sep 17 00:00:00 2001 From: allt0ld Date: Fri, 15 Sep 2023 19:43:51 -0400 Subject: [PATCH 6/8] Remove commented lines from changelog --- ...4_213622_philiplu97_tricrypto_lp_price.rst | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst b/changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst index 6f69ed68d..a29928067 100644 --- a/changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst +++ b/changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst @@ -1,35 +1,5 @@ -.. A new scriv changelog fragment. -.. -.. Uncomment the header that is right (remove the leading dots). -.. -.. Removed -.. ------- -.. -.. - A bullet item for the Removed category. -.. Added ----- - Added 3-coin lp_price to CurveCryptoPool. - Implemented Tricrypto_NG's oracle behavior in CurveCryptoPool for 3-coin pools. - -.. Changed -.. ------- -.. -.. - A bullet item for the Changed category. -.. -.. Deprecated -.. ---------- -.. -.. - A bullet item for the Deprecated category. -.. -.. Fixed -.. ----- -.. -.. - A bullet item for the Fixed category. -.. -.. Security -.. -------- -.. -.. - A bullet item for the Security category. -.. From a2a0e5f1e599aa17cbe57d132eb6662baa98700f Mon Sep 17 00:00:00 2001 From: allt0ld Date: Fri, 15 Sep 2023 19:56:58 -0400 Subject: [PATCH 7/8] Fixup style --- curvesim/pool/cryptoswap/calcs/factory_2_coin.py | 4 +++- curvesim/pool/cryptoswap/calcs/tricrypto_ng.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/curvesim/pool/cryptoswap/calcs/factory_2_coin.py b/curvesim/pool/cryptoswap/calcs/factory_2_coin.py index a23a1cc4a..2ada0e339 100644 --- a/curvesim/pool/cryptoswap/calcs/factory_2_coin.py +++ b/curvesim/pool/cryptoswap/calcs/factory_2_coin.py @@ -79,7 +79,9 @@ def lp_price(virtual_price: int, price_oracle: List[int]) -> int: int Liquidity redeemable per LP token in units of token 0. """ - return 2 * virtual_price * _sqrt_int(price_oracle[0]) // 10**18 + price_oracle: int = price_oracle[0] + + return 2 * virtual_price * _sqrt_int(price_oracle) // 10**18 # pylint: disable-next=too-many-locals diff --git a/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py b/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py index d7fc8c808..5cb7edc57 100644 --- a/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py +++ b/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py @@ -333,7 +333,10 @@ def lp_price(virtual_price: int, price_oracle: List[int]) -> int: int Liquidity redeemable per LP token in units of token 0. """ - return 3 * virtual_price * _cbrt(price_oracle[0] * price_oracle[1]) // 10**24 + p_0: int = price_oracle[0] + p_1: int = price_oracle[1] + + return 3 * virtual_price * _cbrt(p_0 * p_1) // 10**24 def get_p( From 0362c6e51a334fc57b7716669c642326a459e5c4 Mon Sep 17 00:00:00 2001 From: allt0ld Date: Fri, 15 Sep 2023 20:03:35 -0400 Subject: [PATCH 8/8] Lint --- curvesim/pool/cryptoswap/calcs/factory_2_coin.py | 2 +- curvesim/pool/cryptoswap/calcs/tricrypto_ng.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/curvesim/pool/cryptoswap/calcs/factory_2_coin.py b/curvesim/pool/cryptoswap/calcs/factory_2_coin.py index 2ada0e339..e64fc2532 100644 --- a/curvesim/pool/cryptoswap/calcs/factory_2_coin.py +++ b/curvesim/pool/cryptoswap/calcs/factory_2_coin.py @@ -80,7 +80,7 @@ def lp_price(virtual_price: int, price_oracle: List[int]) -> int: Liquidity redeemable per LP token in units of token 0. """ price_oracle: int = price_oracle[0] - + return 2 * virtual_price * _sqrt_int(price_oracle) // 10**18 diff --git a/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py b/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py index 5cb7edc57..bb47a6937 100644 --- a/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py +++ b/curvesim/pool/cryptoswap/calcs/tricrypto_ng.py @@ -335,7 +335,7 @@ def lp_price(virtual_price: int, price_oracle: List[int]) -> int: """ p_0: int = price_oracle[0] p_1: int = price_oracle[1] - + return 3 * virtual_price * _cbrt(p_0 * p_1) // 10**24