Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tricrypto price oracle and lp price #256

Merged
merged 8 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.d/20230914_213622_philiplu97_tricrypto_lp_price.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Added
-----

- Added 3-coin lp_price to CurveCryptoPool.
- Implemented Tricrypto_NG's oracle behavior in CurveCryptoPool for 3-coin pools.
6 changes: 4 additions & 2 deletions curvesim/pool/cryptoswap/calcs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
) -> int:
if n_coins == 2:
alpha: int = halfpow(
(block_timestamp - last_prices_timestamp) * 10**18 // ma_half_time
Expand All @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions curvesim/pool/cryptoswap/calcs/factory_2_coin.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,27 @@ 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]
allt0ld marked this conversation as resolved.
Show resolved Hide resolved
price_oracle: int = price_oracle[0]

return 2 * virtual_price * _sqrt_int(price_oracle) // 10**18


Expand Down
29 changes: 21 additions & 8 deletions curvesim/pool/cryptoswap/calcs/tricrypto_ng.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,16 +314,29 @@ 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.
"""
# 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")
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(
Expand Down
64 changes: 46 additions & 18 deletions curvesim/pool/cryptoswap/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
get_alpha,
get_p,
get_y,
halfpow,
newton_D,
tricrypto_ng,
)

logger = get_logger(__name__)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -962,18 +962,29 @@ 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
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
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: 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")

Expand All @@ -983,16 +994,33 @@ 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
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)
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
39 changes: 38 additions & 1 deletion test/unit/test_tricrypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
_newton_y,
wad_exp,
)
from ..fixtures.pool import pack_prices, unpack_prices


def initialize_pool(vyper_tricrypto):
Expand All @@ -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)]
Expand Down Expand Up @@ -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)
]
Loading