Skip to content

Commit

Permalink
test: stableswap integration
Browse files Browse the repository at this point in the history
  • Loading branch information
heswithme committed Oct 14, 2024
1 parent b0c9d65 commit b62e437
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 33 deletions.
4 changes: 2 additions & 2 deletions scripts/debug_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ def main():
# Pytest arguments
pytest_args = [
"-s", # Do not capture output, allowing you to see print statements and debug info
"tests/integration/test_stableswap.py", # Specific test to run
"tests/integration/curve_pools/test_stableswap.py::test_stableswap_pool_prices_with_vault_growth", # Specific test to run
# '--maxfail=1', # Stop after the firstD failure
"--tb=short", # Shorter traceback for easier reading
"-rA", # Show extra test summary info
]

if not is_debug_mode():
pass
pytest_args.append("-n=auto") # Automatically determine the number of workers
# pytest_args.append("-n=auto") # Automatically determine the number of workers

# Run pytest with the specified arguments
pytest.main(pytest_args)
Expand Down
14 changes: 8 additions & 6 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

boa.set_etherscan(api_key=os.getenv("ETHERSCAN_API_KEY"))
BOA_CACHE = False


@pytest.fixture(autouse=True, scope="module")
Expand All @@ -13,7 +14,7 @@ def better_traces(forked_env):
boa.from_etherscan(ab.vault_original, "vault_original")


@pytest.fixture(scope="module")
@pytest.fixture(scope="session")
def rpc_url():
return os.getenv("ETH_RPC_URL") or "https://rpc.ankr.com/eth"

Expand All @@ -22,9 +23,10 @@ def rpc_url():
def forked_env(rpc_url):
block_to_fork = 20928372
with boa.swap_env(boa.Env()):
boa.fork(url=rpc_url, block_identifier=block_to_fork)
# use this to disable caching
# boa.fork(url=rpc_url, block_identifier=block_to_fork, cache_file=None)
if BOA_CACHE:
boa.fork(url=rpc_url, block_identifier=block_to_fork)
else:
boa.fork(url=rpc_url, block_identifier=block_to_fork, cache_file=None)
boa.env.enable_fast_mode()
yield

Expand All @@ -45,7 +47,7 @@ def vault_factory():


@pytest.fixture(scope="module")
def fee_splitter(scope="module"):
def fee_splitter():
_factory = boa.load_vyi("tests/integration/interfaces/IFeeSplitter.vyi")
return _factory.at(ab.fee_splitter)

Expand Down Expand Up @@ -76,7 +78,7 @@ def vault(vault_factory):
return _vault


@pytest.fixture(scope="module")
@pytest.fixture(scope="function")
def rewards_handler(vault):
rh = boa.load(
"contracts/RewardsHandler.vy",
Expand Down
53 changes: 39 additions & 14 deletions tests/integration/curve_pools/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
import pytest


@pytest.fixture(scope="module")
def alice():
return boa.env.generate_address()


@pytest.fixture(scope="module")
def crvusd_init_balance():
return 1_000 * 10**18


@pytest.fixture(scope="module")
def stableswap_factory():
return boa.from_etherscan(ab.factory_stableswap_ng, "factory_stableswap_ng")
Expand All @@ -11,21 +21,33 @@ def stableswap_factory():
@pytest.fixture(scope="module")
def paired_tokens(request):
# This fixture is used to get upstream parametrization and populate the contracts
# Retrieve paired token combinations via request.param
# Retrieve paired token combination [token1, token2] via request.param
tokens_list = request.param
# update the dict with contracts
for token in tokens_list:
token["contract"] = boa.from_etherscan(token["address"], token["name"])
token["decimals"] = token["contract"].decimals()
return tokens_list


@pytest.fixture(scope="module")
def stableswap_pool(stableswap_factory, vault, dev_address, paired_tokens):
# Retrieve token addresses and asset types from request.param
pool_tokens = [
{"asset_type": 3, "name": "scrvusd", "address": vault.address, "contract": vault},
def pool_tokens(paired_tokens, vault):
# in any pool first is scrvusd, then one or two other tokens
return [
{
"name": "scrvusd",
"address": vault.address,
"contract": vault,
"asset_type": 3,
"decimals": 18,
},
*paired_tokens,
]


@pytest.fixture(scope="function")
def stableswap_pool(stableswap_factory, vault, dev_address, pool_tokens):
# Retrieve token addresses and asset types from request.param
coins = [token["address"] for token in pool_tokens]
asset_types = [token.get("asset_type") for token in pool_tokens]

Expand All @@ -34,8 +56,7 @@ def stableswap_pool(stableswap_factory, vault, dev_address, paired_tokens):
A, fee, ma_exp_time, implementation_idx = (2000, 1000000, 866, 0)
method_ids = [b""] * pool_size
oracles = ["0x0000000000000000000000000000000000000000"] * pool_size
OFFPEG_FEE_MULTIPLIER = 20000000000

offpeg_fee_mp = 20000000000
# deploy pool
with boa.env.prank(dev_address):
pool_address = stableswap_factory.deploy_plain_pool(
Expand All @@ -44,7 +65,7 @@ def stableswap_pool(stableswap_factory, vault, dev_address, paired_tokens):
coins,
A,
fee,
OFFPEG_FEE_MULTIPLIER,
offpeg_fee_mp,
ma_exp_time,
implementation_idx,
asset_types,
Expand All @@ -58,17 +79,18 @@ def stableswap_pool(stableswap_factory, vault, dev_address, paired_tokens):
dev_balances = []
for token in pool_tokens:
if token["asset_type"] == 0:
boa.deal(
token["contract"], dev_address, AMOUNT_STABLE * 10 ** token["contract"].decimals()
)
boa.deal(token["contract"], dev_address, AMOUNT_STABLE * 10 ** token["decimals"])
elif token["asset_type"] == 3:
underlying_token = token["contract"].asset()
underlying_contract = boa.from_etherscan(underlying_token, "token")
decimals = underlying_contract.decimals()
boa.deal(
underlying_contract,
dev_address,
AMOUNT_STABLE * 10**decimals,
AMOUNT_STABLE * 10**decimals
+ underlying_contract.balanceOf(
dev_address
), # in case of dai + sdai deal would overwrite, so we add the previous balance
)
underlying_contract.approve(
token["contract"],
Expand All @@ -79,13 +101,16 @@ def stableswap_pool(stableswap_factory, vault, dev_address, paired_tokens):
# Approve pool to spend vault tokens
token["contract"].approve(pool, 2**256 - 1, sender=dev_address)
dev_balances.append(token["contract"].balanceOf(dev_address))
# print(f"{[token["name"] for token in pool_tokens]}")
# print(f" vault balance: {vault.balanceOf(dev_address)/1e18}")
# print(f" vault supply: {vault.totalSupply()/1e18}")
pool.add_liquidity(dev_balances, 0, dev_address, sender=dev_address)
return pool


@pytest.fixture(scope="module")
@pytest.fixture(scope="function")
def twocrypto_pool(vault, pair_cryptocoin): ...


@pytest.fixture(scope="module")
@pytest.fixture(scope="function")
def tricrypto_pool(vault): ...
137 changes: 126 additions & 11 deletions tests/integration/curve_pools/test_stableswap.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,139 @@
import pytest
import boa
from utils import generate_list_combinations
import address_book as ab


N_COMBINATIONS = 1 # num of combinations in stableswap tests (>=36 => all combinations)
N_COMBINATIONS = 40 # num of combinations in stableswap tests (>=36 => all combinations)

# produce tokens for stableswap to pair against crvusd
paired_token_combinations = generate_list_combinations(ab.all_stables, [1, 2], randomize=True)
paired_token_combinations = generate_list_combinations(ab.all_stables, [1, 2], randomize=False)
tokens_subset = paired_token_combinations[0:N_COMBINATIONS]


@pytest.mark.parametrize("paired_tokens", tokens_subset, indirect=True)
def test_stableswap_pool_with_liquidity(stableswap_pool, paired_tokens):
def test_accrue_value(alice, dev_address, vault, crvusd, crvusd_init_balance):
# fund alice
assert crvusd.balanceOf(alice) == 0
boa.deal(crvusd, alice, crvusd_init_balance)
with boa.env.prank(alice):
crvusd.approve(vault, crvusd_init_balance)
vault.deposit(crvusd_init_balance, alice)

# basic boilerplate test to check vault functionality of rewards accrual without pools involved
alice_value_0 = vault.convertToAssets(vault.balanceOf(alice))

# deposit crvusd rewards into vault & time travel
boa.deal(crvusd, dev_address, crvusd_init_balance)
crvusd.transfer(vault, crvusd_init_balance, sender=dev_address)
vault.process_report(vault, sender=ab.dao_agent)
boa.env.time_travel(seconds=86_400 * 7)

# check alice's assets value increased
alice_value_1 = vault.convertToAssets(vault.balanceOf(alice))
print(f"alice_value_0: {alice_value_0}")
print(f"alice_value_1: {alice_value_1}")
assert alice_value_1 > alice_value_0
assert alice_value_1 == alice_value_0 + crvusd_init_balance


@pytest.mark.parametrize(
"paired_tokens",
tokens_subset,
indirect=True,
ids=[f"scrvusd+{'+'.join([token['name'] for token in tokens])}" for tokens in tokens_subset],
)
def test_stableswap_pool_liquidity(
stableswap_pool,
paired_tokens,
vault,
alice,
dev_address,
crvusd_init_balance,
crvusd,
):
# test where we check that value grows even when deposited as LP
n_coins = stableswap_pool.N_COINS()
# fund alice
assert crvusd.balanceOf(alice) == 0
boa.deal(crvusd, alice, crvusd_init_balance)
with boa.env.prank(alice):
crvusd.approve(vault, crvusd_init_balance)
vault.deposit(crvusd_init_balance, alice)

alice_value_0 = vault.convertToAssets(vault.balanceOf(alice))
alice_rate = alice_value_0 / vault.totalAssets()
# deposit single-sided scrvusd liq into pool (i.e. trade into equal parts)
add_liq_amounts = [0] * n_coins
add_liq_amounts[0] = vault.balanceOf(alice)
vault.approve(stableswap_pool, add_liq_amounts[0], sender=alice)
pool_shares = stableswap_pool.add_liquidity(add_liq_amounts, 0, alice, sender=alice)
assert stableswap_pool.balanceOf(alice) > 0

# now increase shares value by 5%
amt_reward = int(vault.totalAssets() * 0.05)
boa.deal(crvusd, dev_address, amt_reward)
crvusd.transfer(vault, amt_reward, sender=dev_address)
vault.process_report(vault, sender=ab.dao_agent)
boa.env.time_travel(seconds=86_400 * 7)

# remove liq (one-sided)
stableswap_pool.remove_liquidity_one_coin(pool_shares, 0, 0, alice, sender=alice)
alice_value_1 = vault.convertToAssets(vault.balanceOf(alice))
# print(f"alice_value_0: {alice_value_0}")
# print(f"alice_value_1: {alice_value_1}")
# print(f"pool_shares: {vault.convertToAssets(vault.balanceOf(stableswap_pool))}")

alice_expected_full_reward = alice_rate * amt_reward
# because we deposited LP, we only get 1/N_COINS of the reward (50% or 33% for 2, 3 coins)
# relative tolerance because of one-sided LP deposit & withdraw eat fees
# 5% relative tolerance to expected reward
assert alice_value_1 - alice_value_0 == pytest.approx(
alice_expected_full_reward / n_coins, rel=0.05
)


@pytest.mark.parametrize(
"paired_tokens",
tokens_subset,
indirect=True,
ids=[f"scrvusd+{'+'.join([token['name'] for token in tokens])}" for tokens in tokens_subset],
)
def test_stableswap_pool_prices_with_vault_growth(
stableswap_pool, pool_tokens, vault, dev_address, crvusd, paired_tokens
):
"""
Test deploying stableswap pool with different token combinations,
then adds liquidity to the pool and checks balances.
Test where vault shares grow (rewards airdropped), and we expect that pool prices change accordingly.
To balance the pool, dev removes liquidity in a balanced way at each iteration.
"""

# Check balances in the pool after adding liquidity
n_coins = stableswap_pool.N_COINS()
print(f"n_coins: {n_coins}")
for i in range(n_coins):
print(f"balance {i}: {stableswap_pool.balances(i)}")
growth_rate = 0.01 # Vault growth per iteration (1%)
airdropper = boa.env.generate_address()
decimals = [token["decimals"] for token in pool_tokens]
prev_dy = [stableswap_pool.get_dy(0, i, 10 ** decimals[0]) for i in range(1, n_coins)]
print(vault.totalAssets() / 1e18)
# Iteratively grow vault and adjust pool prices
for _ in range(10): # Run 10 iterations
# Step 1: Inflate vault by 1%
current_assets = vault.totalAssets()
amt_reward = int(current_assets * growth_rate)
boa.deal(crvusd, airdropper, amt_reward)
crvusd.transfer(vault, amt_reward, sender=airdropper)
vault.process_report(vault, sender=ab.dao_agent)
boa.env.time_travel(seconds=86_400 * 7)

# Step 2: Dev removes 5% of liquidity in a balanced way
stableswap_pool.remove_liquidity(
stableswap_pool.balanceOf(dev_address) // 20,
[0] * n_coins,
dev_address,
True,
sender=dev_address,
)
# Check pool prices after each iteration
cur_dy = [stableswap_pool.get_dy(0, i, 10 ** decimals[0]) for i in range(1, n_coins)]
for i in range(n_coins - 1):
assert cur_dy[i] > prev_dy[i] # important that in balanced pool dy increases
assert (
cur_dy[i] / prev_dy[i] - 1 == pytest.approx(growth_rate, rel=0.2)
) # price should grow along with vault (fees tolerated, we approx 1% growth with 20% tolerance)
prev_dy[i] = cur_dy[i]

0 comments on commit b62e437

Please sign in to comment.