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

Meta Calculator: implementation of bulk get_dy and get_dx #15

Open
wants to merge 13 commits into
base: meta-calc
Choose a base branch
from
136 changes: 110 additions & 26 deletions contracts/CurveCalcMeta.vy
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface CurveBase:


MAX_COINS: constant(int128) = 8
INPUT_SIZE: constant(int128) = 100
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed INPUT_SIZE to 80 as the contract size was exceeding the bytecode size limit. Was there a particular reason for setting it to 100?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some number. Doesn't matter how big it is I think as long as it fits, but should be big enough. That's for DEX aggregators to do this on-chain (where INPUT_SIZE is the maximum they do)

INPUT_SIZE: constant(int128) = 80
FEE_DENOMINATOR: constant(uint256) = 10 ** 10
A_PRECISION: constant(uint256) = 100

Expand Down Expand Up @@ -206,7 +206,7 @@ def get_dy(n_coins: uint256, balances: uint256[MAX_COINS], _amp: uint256, fee: u
rates: uint256[MAX_COINS], precisions: uint256[MAX_COINS],
i: int128, j: int128, dx: uint256[INPUT_SIZE]) -> uint256[INPUT_SIZE]:
"""
@notice Bulk-calculate amount of of coin j given in exchange for coin i
@notice Bulk-calculate amount of coin j given in exchange for coin i
@param n_coins Number of coins in the pool
@param balances Array with coin balances
@param _amp Amplification coefficient (unused because it uses no precision)
Expand Down Expand Up @@ -246,15 +246,17 @@ def get_dy(n_coins: uint256, balances: uint256[MAX_COINS], _amp: uint256, fee: u
_base_pool: address = self.base_pool
xp_base: uint256[MAX_COINS] = empty(uint256[MAX_COINS])
v_price: uint256 = CurveBase(_base_pool).base_virtual_price()
ratesp_base: uint256[MAX_COINS] = ratesp
for k in range(BASE_N_COINS):
xp_base[k] = xp[N_COINS+k-1]
ratesp_base[k] = ratesp[N_COINS+k-1]
xp[N_COINS-1] = Curve(self.meta_pool).balances(N_COINS-1) * v_price / 10**18
ratesp[N_COINS-1] = v_price
xp[N_COINS] = 0
ratesp[N_COINS] = 0
amp_base: uint256 = CurveBase(_base_pool).A_precise()
D_base_0: uint256 = self.get_D(BASE_N_COINS, xp_base, amp_base)
D: uint256 = self.get_D(n_coins, xp, amp)
D: uint256 = self.get_D(N_COINS, xp, amp)
base_fee: uint256 = CurveBase(_base_pool).fee()
base_supply: uint256 = 0
if i == 0 or j == 0:
Expand All @@ -278,10 +280,9 @@ def get_dy(n_coins: uint256, balances: uint256[MAX_COINS], _amp: uint256, fee: u
D1: uint256 = D_base_0 - _dy * D_base_0 / base_supply
xp_reduced: uint256[MAX_COINS] = xp_base
_y: uint256 = self.get_y_D(BASE_N_COINS, amp_base, j-1, xp_base, D1)
dy_0: uint256 = (xp_base[j-1] - _y) / precisions[j] # wo fee
for l in range(BASE_N_COINS):
dx_expected: uint256 = xp_base[l]
if k == j-1:
if l == j-1:
dx_expected = dx_expected*D1/D_base_0 - _y
else:
dx_expected = dx_expected - dx_expected*D1/D_base_0
Expand All @@ -292,66 +293,149 @@ def get_dy(n_coins: uint256, balances: uint256[MAX_COINS], _amp: uint256, fee: u
elif j == 0:
# deposit to base pool (calc_token_amount)
new_balances: uint256[MAX_COINS] = xp_base
new_balances[i+1] += dx[k] * ratesp[i+1] / 10**18
new_balances[i-1] += dx[k] * ratesp_base[i-1] / 10**18
# invariant after deposit
D1: uint256 = self.get_D(BASE_N_COINS, new_balances, amp_base)
# take fees into account
for l in range(BASE_N_COINS):
ideal_balance: uint256 = D1 * xp_base[i+1] / D_base_0
ideal_balance: uint256 = D1 * xp_base[l] / D_base_0
difference: uint256 = 0
if ideal_balance > new_balances[i+1]:
difference = ideal_balance - new_balances[i+1]
if ideal_balance > new_balances[l]:
difference = ideal_balance - new_balances[l]
else:
difference = new_balances[i+1] - ideal_balance
new_balances[i+1] -= base_fee * difference / FEE_DENOMINATOR
difference = new_balances[l] - ideal_balance
new_balances[l] -= base_fee * difference / FEE_DENOMINATOR
D2: uint256 = self.get_D(BASE_N_COINS, new_balances, amp_base)
dx_meta: uint256 = base_supply * (D2 - D_base_0) / D_base_0
# swap dx_meta to coin j
x_after_trade: uint256 = dx_meta * v_price / 10**18 + xp[1]
dy[k] = self.get_y(D, N_COINS, xp, amp, 1, 0, x_after_trade)
dy[k] = (xp[0] - dy[k] - 1) * 10**18 / ratesp[0]
dy[k] -= dy[k] * fee / FEE_DENOMINATOR

else:
x_after_trade: uint256 = dx[k] * ratesp[i] / 10**18 + xp[i]
x_after_trade: uint256 = dx[k] * ratesp_base[i-1] / 10**18 + xp_base[i-1]
dy[k] = self.get_y(D_base_0, BASE_N_COINS, xp_base, amp_base, i-1, j-1, x_after_trade)
dy[k] = (xp[j] - dy[k] - 1) * 10**18 / ratesp[j]
dy[k] = (xp_base[j-1] - dy[k] - 1) * 10**18 / ratesp_base[j-1]
dy[k] -= dy[k] * base_fee / FEE_DENOMINATOR

else:
raise "Unsupported pool size"

return dy


@view
@external
def get_dx(n_coins: uint256, balances: uint256[MAX_COINS], amp: uint256, fee: uint256,
def get_dx(n_coins: uint256, balances: uint256[MAX_COINS], _amp: uint256, fee: uint256,
rates: uint256[MAX_COINS], precisions: uint256[MAX_COINS],
i: int128, j: int128, dy: uint256) -> uint256:
i: int128, j: int128, dy: uint256[INPUT_SIZE]) -> uint256[INPUT_SIZE]:
"""
@notice Calculate amount of of coin i taken when exchanging for coin j
@notice Bulk-calculate amount of coin i taken when exchanging for coin j
@param n_coins Number of coins in the pool
@param balances Array with coin balances
@param amp Amplification coefficient
@param _amp Amplification coefficient
@param fee Pool's fee at 1e10 basis
@param rates Array with rates for "lent out" tokens
@param precisions Precision multipliers to get the coin to 1e18 basis
@param i Index of the changed coin (trade in)
@param j Index of the other changed coin (trade out)
@param dy Amount of coin j (trade out)
@return Amount of coin i (trade in)
@param dy Array of values of coin j (trade out)
@return Array of values of coin i (trade in)
"""

xp: uint256[MAX_COINS] = balances
ratesp: uint256[MAX_COINS] = precisions
for k in range(MAX_COINS):
xp[k] = xp[k] * rates[k] * precisions[k] / 10 ** 18
ratesp[k] *= rates[k]
D: uint256 = self.get_D(n_coins, xp, amp)
dx: uint256[INPUT_SIZE] = dy
amp: uint256 = Curve(self.meta_pool).A_precise()

if n_coins == N_COINS:
# Metapool with the pool token
D: uint256 = self.get_D(n_coins, xp, amp)
for k in range(INPUT_SIZE):
if dy[k] == 0:
break
else:
y_after_trade: uint256 = xp[j] - dy[k] * ratesp[j] / 10 ** 18 * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
dx[k] = self.get_y(D, n_coins, xp, amp, j, i, y_after_trade)
dx[k] = (dx[k] - xp[i]) * 10 ** 18 / ratesp[i]

elif n_coins == N_COINS + BASE_N_COINS - 1:
_base_pool: address = self.base_pool
xp_base: uint256[MAX_COINS] = empty(uint256[MAX_COINS])
v_price: uint256 = CurveBase(_base_pool).base_virtual_price()
ratesp_base: uint256[MAX_COINS] = ratesp
for k in range(BASE_N_COINS):
xp_base[k] = xp[N_COINS+k-1]
ratesp_base[k] = ratesp[N_COINS+k-1]
xp[N_COINS-1] = Curve(self.meta_pool).balances(N_COINS-1) * v_price / 10**18
ratesp[N_COINS-1] = v_price
xp[N_COINS] = 0
ratesp[N_COINS] = 0
amp_base: uint256 = CurveBase(_base_pool).A_precise()
D_base_0: uint256 = self.get_D(BASE_N_COINS, xp_base, amp_base)
D: uint256 = self.get_D(N_COINS, xp, amp)
base_fee: uint256 = CurveBase(_base_pool).fee()
base_supply: uint256 = 0
if i == 0 or j == 0:
base_fee = base_fee * BASE_N_COINS / (4 * (BASE_N_COINS - 1))
base_supply = ERC20(self.base_token).totalSupply()
# ... -> 0 - swap inside, withdraw from base
# 0 -> ... - withdraw from base, swap inside
# both i, j >= 1 - swap in base

y_after_trade: uint256 = xp[j] - dy * ratesp[j] / 10 ** 18 * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
x: uint256 = self.get_y(D, n_coins, xp, amp, j, i, y_after_trade)
dx: uint256 = (x - xp[i]) * 10 ** 18 / ratesp[i]
for k in range(INPUT_SIZE):
if dy[k] == 0:
break
else:
if j == 0:
y_after_trade: uint256 = xp[j] - dy[k] * ratesp[j] / 10**18 * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
_dx: uint256 = self.get_y(D, N_COINS, xp, amp, 0, 1, y_after_trade)
_dx = (_dx - xp[1] - 1) * 10**18 / ratesp[1]
D1: uint256 = D_base_0 + _dx * D_base_0 / base_supply
new_balances: uint256[MAX_COINS] = xp_base
_y: uint256 = self.get_y_D(
BASE_N_COINS, amp_base, i-1, xp_base, D1)
for l in range(BASE_N_COINS):
dx_expected: uint256 = xp_base[l]
if l == i-1:
dx_expected = _y - dx_expected*D1/D_base_0
else:
dx_expected = dx_expected*D1/D_base_0 - dx_expected
new_balances[l] -= base_fee * \
dx_expected / FEE_DENOMINATOR
dx[k] = self.get_y_D(BASE_N_COINS, amp_base,
i-1, new_balances, D1) - new_balances[i-1]
dx[k] = (dx[k] - 1) / precisions[i]
elif i == 0:
# # coin j-1 from base pool is withdrawn
new_balances: uint256[MAX_COINS] = xp_base
new_balances[j-1] -= dy[k] * ratesp_base[j-1] / 10**18
# # invariant after withdrawal
D1: uint256 = self.get_D(BASE_N_COINS, new_balances, amp_base)
# take fees into account
for l in range(BASE_N_COINS):
ideal_balance: uint256 = D1 * xp_base[l] / D_base_0
difference: uint256 = 0
if ideal_balance > new_balances[l]:
difference = ideal_balance - new_balances[l]
else:
difference = new_balances[l] - ideal_balance
new_balances[l] -= base_fee * difference / FEE_DENOMINATOR
D2: uint256 = self.get_D(BASE_N_COINS, new_balances, amp_base)
dx_meta: uint256 = base_supply * (D_base_0 - D2) / D_base_0
# swap dx_meta to coin i
y_after_trade: uint256 = xp[1] - dx_meta * v_price / 10**18 * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
dx[k] = self.get_y(D, N_COINS, xp, amp, 1, 0, y_after_trade)
dx[k] = (dx[k] - xp[0] - 1) * 10**18 / ratesp[0]
else:
# swap in base pool
y_after_trade: uint256 = xp_base[j-1] - dy[k] * ratesp_base[j-1] / 10**18 * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
dx[k] = self.get_y(D_base_0, BASE_N_COINS, xp_base, amp_base, j-1, i-1, y_after_trade)
dx[k] = (dx[k] - xp_base[i-1] - 1) * 10 ** 18 / ratesp_base[i-1]
else:
raise "Unsupported pool size"

return dx

2 changes: 1 addition & 1 deletion contracts/testing/ERC20.vy
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ symbol: public(String[32])
decimals: public(uint256)
balanceOf: public(HashMap[address, uint256])
allowances: HashMap[address, HashMap[address, uint256]]
total_supply: uint256
total_supply: public(uint256)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You actually don't need this public because totalSupply is already public



@external
Expand Down
6 changes: 6 additions & 0 deletions contracts/testing/MetaPoolMock.vy
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ def get_dy(i: int128, j: int128, dx: uint256) -> uint256:
return self._get_dy(self.coin_list[i], self.coin_list[j], dx)


@external
@view
def A_precise() -> uint256:
return self.A
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, in real pools A_precise is larger than A by a multiplier A_PRECISION = 100



@external
@view
def get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256:
Expand Down
15 changes: 15 additions & 0 deletions contracts/testing/PoolMockV1.vy
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256):
self._exchange(msg.sender, _from, self.underlying_coin_list[j], dx, min_dy)


@external
@view
def A_precise() -> uint256:
return self.A


# testing functions

@external
Expand Down Expand Up @@ -199,6 +205,12 @@ def _set_fees_and_owner(
self.future_owner = _future_owner


@view
@external
def base_virtual_price() -> uint256:
return self.get_virtual_price


@external
def _set_balances(_new_balances: uint256[4]):
self._balances = _new_balances
Expand All @@ -208,6 +220,9 @@ def _set_balances(_new_balances: uint256[4]):
def _set_virtual_price(_value: uint256):
self.get_virtual_price = _value

@external
def _set_fee(_fee: uint256):
self.fee = _fee

@external
@payable
Expand Down
14 changes: 14 additions & 0 deletions contracts/testing/PoolMockV2.vy
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256):
self._exchange(msg.sender, _from, self.underlying_coin_list[j], dx, min_dy)


@external
@view
def A_precise() -> uint256:
return self.A

# testing functions

@external
Expand Down Expand Up @@ -204,10 +209,19 @@ def _set_balances(_new_balances: uint256[4]):
self._balances = _new_balances


@view
@external
def base_virtual_price() -> uint256:
return self.get_virtual_price


@external
def _set_virtual_price(_value: uint256):
self.get_virtual_price = _value

@external
def _set_fee(_fee: uint256):
self.fee = _fee

@external
@payable
Expand Down
32 changes: 23 additions & 9 deletions tests/local/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def pytest_addoption(parser):

def pytest_configure(config):
# add custom markers
config.addinivalue_line("markers", "once: only run this test once (no parametrization)")
config.addinivalue_line(
"markers", "once: only run this test once (no parametrization)")
config.addinivalue_line("markers", "params: test parametrization filters")
config.addinivalue_line(
"markers",
Expand Down Expand Up @@ -91,7 +92,8 @@ def pytest_collection_modifyitems(config, items):
seen[path].add(item.obj)

# hacky magic to ensure the correct number of tests is shown in collection report
config.pluginmanager.get_plugin("terminalreporter")._numcollected = len(items)
config.pluginmanager.get_plugin(
"terminalreporter")._numcollected = len(items)


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -167,7 +169,8 @@ def _underlying_coins(_underlying_decimals, alice):
deployers = [ERC20, ERC20NoReturn, ERC20ReturnFalse]
coins = []
for i, (deployer, decimals) in enumerate(zip(deployers, _underlying_decimals)):
contract = deployer.deploy(f"Test Token {i}", f"TST{i}", decimals, {"from": alice})
contract = deployer.deploy(
f"Test Token {i}", f"TST{i}", decimals, {"from": alice})
coins.append(contract)
coins.append("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE")

Expand All @@ -194,7 +197,8 @@ def _meta_coins(_meta_decimals, alice):
deployers = [ERC20, ERC20NoReturn, ERC20ReturnFalse]
coins = []
for i, (deployer, decimals) in enumerate(zip(deployers, _meta_decimals)):
contract = deployer.deploy(f"MetaTest Token {i}", f"MT{i}", decimals, {"from": alice})
contract = deployer.deploy(
f"MetaTest Token {i}", f"MT{i}", decimals, {"from": alice})
coins.append(contract)

return coins
Expand Down Expand Up @@ -267,10 +271,12 @@ def swap(PoolMockV1, PoolMockV2, alice, underlying_coins, is_v1):
deployer = PoolMockV2

n_coins = len(underlying_coins)
underlying_coins = underlying_coins + [ZERO_ADDRESS] * (4 - len(underlying_coins))
underlying_coins = underlying_coins + \
[ZERO_ADDRESS] * (4 - len(underlying_coins))

contract = deployer.deploy(
n_coins, underlying_coins, [ZERO_ADDRESS] * 4, 70, 4000000, {"from": alice}
n_coins, underlying_coins, [ZERO_ADDRESS] *
4, 70, 4000000, {"from": alice}
)
return contract

Expand All @@ -284,7 +290,8 @@ def lending_swap(PoolMockV1, PoolMockV2, alice, wrapped_coins, underlying_coins,

n_coins = len(underlying_coins)
wrapped_coins = wrapped_coins + [ZERO_ADDRESS] * (4 - len(wrapped_coins))
underlying_coins = underlying_coins + [ZERO_ADDRESS] * (4 - len(underlying_coins))
underlying_coins = underlying_coins + \
[ZERO_ADDRESS] * (4 - len(underlying_coins))

contract = deployer.deploy(
n_coins, wrapped_coins, underlying_coins, 70, 4000000, {"from": alice}
Expand All @@ -295,9 +302,11 @@ def lending_swap(PoolMockV1, PoolMockV2, alice, wrapped_coins, underlying_coins,
@pytest.fixture(scope="module")
def meta_swap(MetaPoolMock, alice, swap, meta_coins, underlying_coins, n_metacoins, n_coins):
meta_coins = meta_coins + [ZERO_ADDRESS] * (4 - len(meta_coins))
underlying_coins = underlying_coins + [ZERO_ADDRESS] * (4 - len(underlying_coins))
underlying_coins = underlying_coins + \
[ZERO_ADDRESS] * (4 - len(underlying_coins))
return MetaPoolMock.deploy(
n_metacoins, n_coins, swap, meta_coins, underlying_coins, 70, 4000000, {"from": alice}
n_metacoins, n_coins, swap, meta_coins, underlying_coins, 70, 4000000, {
"from": alice}
)


Expand All @@ -313,3 +322,8 @@ def liquidity_gauge_meta(LiquidityGaugeMock, alice, gauge_controller, meta_lp_to
gauge = LiquidityGaugeMock.deploy(meta_lp_token, {'from': alice})
gauge_controller._set_gauge_type(gauge, 2, {'from': alice})
yield gauge


@pytest.fixture(scope="module")
def calculatorMeta(CurveCalcMeta, swap, meta_lp_token, meta_swap, alice):
yield CurveCalcMeta.deploy(swap, meta_lp_token, meta_swap, {'from': alice})
Loading