Skip to content

Commit

Permalink
Merge pull request #51 from curvefi/fix/view_contract_calc_token_amounts
Browse files Browse the repository at this point in the history
Update calc_token_amount calculations in view methods
  • Loading branch information
bout3fiddy authored Jun 22, 2024
2 parents b0269d3 + 0b308e0 commit ddc91d8
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 301 deletions.
240 changes: 136 additions & 104 deletions contracts/main/CurveStableSwapNGViews.vy
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
integrators
"""

from vyper.interfaces import ERC20Detailed

interface StableSwapNG:
def N_COINS() -> uint256: view
def BASE_POOL() -> address: view
Expand All @@ -20,22 +22,20 @@ interface StableSwapNG:
def A() -> uint256: view
def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: view
def totalSupply() -> uint256: view
def calc_token_amount(amounts: DynArray[uint256, MAX_COINS], deposit: bool) -> uint256: view
def offpeg_fee_multiplier() -> uint256: view

interface StableSwap2:
def calc_token_amount(amounts: uint256[2], deposit: bool) -> uint256: view

interface StableSwap3:
def calc_token_amount(amounts: uint256[3], deposit: bool) -> uint256: view

def coins(i: uint256) -> address: view

A_PRECISION: constant(uint256) = 100
MAX_COINS: constant(uint256) = 8
PRECISION: constant(uint256) = 10 ** 18
FEE_DENOMINATOR: constant(uint256) = 10 ** 10


VERSION: public(constant(String[8])) = "1.2.0"
# first version was: 0xe0B15824862f3222fdFeD99FeBD0f7e0EC26E1FA (ethereum mainnet)
# second version was: 0x13526206545e2DC7CcfBaF28dC88F440ce7AD3e0 (ethereum mainnet)


# ------------------------------ Public Getters ------------------------------


Expand Down Expand Up @@ -99,6 +99,7 @@ def get_dx_underlying(
BASE_N_COINS: uint256 = StableSwapNG(pool).BASE_N_COINS()
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
base_pool_has_static_fee: bool = self._has_static_fee(BASE_POOL)
base_pool_lp_token: address = StableSwapNG(pool).coins(1)

# CASE 1: Swap does not involve Metapool at all. In this case, we kindly ask the user
# to use the right pool for their swaps.
Expand All @@ -114,7 +115,7 @@ def get_dx_underlying(
if i == 0:
# Calculate LP tokens that are burnt to receive dy amount of base_j tokens.
lp_amount_burnt: uint256 = self._base_calc_token_amount(
dy, j - 1, BASE_N_COINS, BASE_POOL, False
dy, j - 1, BASE_N_COINS, BASE_POOL, base_pool_lp_token, False,
)
return self._get_dx(0, 1, lp_amount_burnt, pool, False, N_COINS)

Expand Down Expand Up @@ -152,6 +153,7 @@ def get_dy_underlying(
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
MAX_COIN: int128 = convert(N_COINS, int128) - 1
BASE_POOL: address = StableSwapNG(pool).BASE_POOL()
base_lp_token: address = StableSwapNG(pool).coins(1)

rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
Expand Down Expand Up @@ -182,7 +184,12 @@ def get_dy_underlying(
# i is from BasePool
base_n_coins: uint256 = StableSwapNG(pool).BASE_N_COINS()
x = self._base_calc_token_amount(
dx, base_i, base_n_coins, BASE_POOL, True
dx,
base_i,
base_n_coins,
BASE_POOL,
base_lp_token,
True,
) * rates[1] / PRECISION

# Adding number of pool tokens
Expand Down Expand Up @@ -225,83 +232,19 @@ def calc_token_amount(
) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@dev Only works for StableswapNG pools and not legacy versions
@param _amounts Amount of each coin being deposited
@param _is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
N_COINS: uint256 = StableSwapNG(pool).N_COINS()

rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
old_balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, old_balances, xp = self._get_rates_balances_xp(pool, N_COINS)

# Initial invariant
D0: uint256 = self.get_D(xp, amp, N_COINS)

total_supply: uint256 = StableSwapNG(pool).totalSupply()
new_balances: DynArray[uint256, MAX_COINS] = old_balances
for i in range(MAX_COINS):
if i == N_COINS:
break

amount: uint256 = _amounts[i]
if _is_deposit:
new_balances[i] += amount
else:
new_balances[i] -= amount

# Invariant after change
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp[idx] = rates[idx] * new_balances[idx] / PRECISION
D1: uint256 = self.get_D(xp, amp, N_COINS)

# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
if total_supply > 0:

# Only account for fees if we are not the first to deposit
base_fee: uint256 = StableSwapNG(pool).fee() * N_COINS / (4 * (N_COINS - 1))
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
_dynamic_fee_i: uint256 = 0
xs: uint256 = 0
ys: uint256 = (D0 + D1) / N_COINS

for i in range(MAX_COINS):
if i == N_COINS:
break

ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
new_balance: uint256 = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance

xs = rates[i] * (old_balances[i] + new_balance) / PRECISION
_dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee, fee_multiplier)
new_balances[i] -= _dynamic_fee_i * difference / FEE_DENOMINATOR

for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp[idx] = rates[idx] * new_balances[idx] / PRECISION

D2 = self.get_D(xp, amp, N_COINS)
else:
return D1 # Take the dust if there was any

diff: uint256 = 0
if _is_deposit:
diff = D2 - D0
else:
diff = D0 - D2
return diff * total_supply / D0
return self._calc_token_amount(
_amounts,
_is_deposit,
pool,
pool,
StableSwapNG(pool).N_COINS()
)


@view
Expand Down Expand Up @@ -449,36 +392,125 @@ def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256, _fee_multiplier: uin

@internal
@view
def _base_calc_token_amount(
dx: uint256,
base_i: int128,
base_n_coins: uint256,
base_pool: address,
is_deposit: bool
def _calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
pool: address,
pool_lp_token: address,
n_coins: uint256,
) -> uint256:
base_pool_is_ng: bool = raw_call(base_pool, method_id("D_ma_time()"), revert_on_failure=False, is_static_call=True)

if base_n_coins == 2 and not base_pool_is_ng:
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION

base_inputs: uint256[2] = empty(uint256[2])
base_inputs[base_i] = dx
return StableSwap2(base_pool).calc_token_amount(base_inputs, is_deposit)
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
old_balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])

elif base_n_coins == 3 and not base_pool_is_ng:
pool_is_ng: bool = raw_call(
pool,
method_id("D_ma_time()"),
revert_on_failure=False,
is_static_call=True
)
use_dynamic_fees: bool = True
if pool_is_ng:
rates, old_balances, xp = self._get_rates_balances_xp(pool, n_coins)
else:
use_dynamic_fees = False
for i in range(n_coins, bound=MAX_COINS):
rates.append(
10 ** (36 - convert(ERC20Detailed(StableSwapNG(pool).coins(i)).decimals(), uint256))
)
old_balances.append(StableSwapNG(pool).balances(i))
xp.append(rates[i] * old_balances[i] / PRECISION)

base_inputs: uint256[3] = empty(uint256[3])
base_inputs[base_i] = dx
return StableSwap3(base_pool).calc_token_amount(base_inputs, is_deposit)
# Initial invariant
D0: uint256 = self.get_D(xp, amp, n_coins)

else:
total_supply: uint256 = StableSwapNG(pool_lp_token).totalSupply()
new_balances: DynArray[uint256, MAX_COINS] = old_balances
for i in range(n_coins, bound=MAX_COINS):
amount: uint256 = _amounts[i]
if _is_deposit:
new_balances[i] += amount
else:
new_balances[i] -= amount

# Invariant after change
for idx in range(n_coins, bound=MAX_COINS):
xp[idx] = rates[idx] * new_balances[idx] / PRECISION
D1: uint256 = self.get_D(xp, amp, n_coins)

# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
fee_multiplier: uint256 = 0
_dynamic_fee_i: uint256 = 0
if total_supply > 0:

# Only account for fees if we are not the first to deposit
base_fee: uint256 = StableSwapNG(pool).fee() * n_coins / (4 * (n_coins - 1))
if use_dynamic_fees:
fee_multiplier = StableSwapNG(pool).offpeg_fee_multiplier()

xs: uint256 = 0
ys: uint256 = (D0 + D1) / n_coins

for i in range(n_coins, bound=MAX_COINS):

base_inputs: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
for i in range(base_n_coins, bound=MAX_COINS):
if i == convert(base_i, uint256):
base_inputs.append(dx)
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
new_balance: uint256 = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance

xs = rates[i] * (old_balances[i] + new_balance) / PRECISION

# use dynamic fees only if pool is NG
if use_dynamic_fees:
_dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee, fee_multiplier)
new_balances[i] -= _dynamic_fee_i * difference / FEE_DENOMINATOR
else:
base_inputs.append(0)
return StableSwapNG(base_pool).calc_token_amount(base_inputs, is_deposit)
new_balances[i] -= base_fee * difference / FEE_DENOMINATOR

for idx in range(n_coins, bound=MAX_COINS):
xp[idx] = rates[idx] * new_balances[idx] / PRECISION

D2 = self.get_D(xp, amp, n_coins)
else:
return D1 # Take the dust if there was any

diff: uint256 = 0
if _is_deposit:
diff = D2 - D0
else:
diff = D0 - D2
return diff * total_supply / D0


@internal
@view
def _base_calc_token_amount(
dx: uint256,
base_i: int128,
base_n_coins: uint256,
base_pool: address,
base_pool_lp_token: address,
is_deposit: bool,
) -> uint256:

base_inputs: DynArray[uint256, MAX_COINS] = [0, 0, 0, 0, 0, 0, 0, 0]
base_inputs[base_i] = dx

return self._calc_token_amount(
base_inputs,
is_deposit,
base_pool,
base_pool_lp_token,
base_n_coins
)


@internal
Expand Down
15 changes: 8 additions & 7 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ poetry = "1.5.1"
vyper = "0.3.10"
pycryptodome = "^3.18.0"
pre-commit = "^3.3.3"
titanoboa-zksync = {git = "https://github.com/DanielSchiavini/titanoboa-zksync.git", rev = "5f13b427df4b8832fcc16ec1f6d44460f1d04b49"}
titanoboa-zksync = {git = "https://github.com/DanielSchiavini/titanoboa-zksync.git"}

[tool.poetry.group.dev.dependencies]
black = "22.3.0"
Expand Down
Loading

0 comments on commit ddc91d8

Please sign in to comment.