Skip to content

Commit

Permalink
test: stableswap (n=2,3) integration tests (#27)
Browse files Browse the repository at this point in the history
* chore: add leveldb for caching

* test: update address book

* test: add interface for tests

* test: wip test

* test: running integration tests, wip

* ci: adding boa cache

* ci: fixing boa cache

* revert: rm boa cache from ci

* test: wip fixtures for pools

* feat: functioning pool fixture

* refactor: group pool tests

* test: stableswap integration

* ci: test matrix

* test: remove debug prints

* test: remove cryptoswaps

* chore: resolving reviews

* test: disable boa caching because does not allow parallel testing

* chore: remove prints

---------

Co-authored-by: Alberto <[email protected]>
  • Loading branch information
heswithme and AlbertoCentonze authored Oct 15, 2024
1 parent ad9fd1e commit 148b847
Show file tree
Hide file tree
Showing 8 changed files with 620 additions and 8 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ jobs:
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
folder:
- "tests/unitary"
- "tests/integration"
- "tests/hypothesis"

steps:
- uses: actions/checkout@v4
Expand All @@ -46,8 +51,8 @@ jobs:
run: uv sync --extra=dev

# Run tests with environment variables
- name: Run Tests
- name: Run Tests in ${{ matrix.folder }}
env:
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
run: uv run pytest -n auto
run: uv run pytest -n auto ${{ matrix.folder }}
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/unitary/twa", # 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
27 changes: 27 additions & 0 deletions tests/integration/address_book.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,30 @@
fee_splitter = "0x22556558419eed2d0a1af2e7fd60e63f3199aca3"
dao_agent = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968"
vault_original = "0xcA78AF7443f3F8FA0148b746Cb18FF67383CDF3f"

# curve factories
factory_stableswap_ng = "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"
factory_twocrypto_ng = "0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F"
factory_tricrypto_ng = "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963"

stables = [
{"name": "dai", "address": "0x6b175474e89094c44da98b954eedeac495271d0f", "asset_type": 0},
{"name": "usdt", "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", "asset_type": 0},
{"name": "usdc", "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "asset_type": 0},
{"name": "usde", "address": "0x4c9edd5852cd905f086c759e8383e09bff1e68b3", "asset_type": 0},
{"name": "frax", "address": "0x853d955acef822db058eb8505911ed77f175b99e", "asset_type": 0},
]

yield_stables = [
{"name": "sdai", "address": "0x83f20f44975d03b1b09e64809b757c47f942beea", "asset_type": 3},
{"name": "sfrax", "address": "0xa663b02cf0a4b149d2ad41910cb81e23e1c41c32", "asset_type": 3},
{"name": "susde", "address": "0x9d39a5de30e57443bff2a8307a4256c8797a3497", "asset_type": 3},
]
all_stables = [*stables, *yield_stables]

cryptos = {
"weth": {"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"},
"steth": {"address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84"},
"wbtc": {"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599"},
"tbtc": {"address": "0x18084fba666a33d37592fa2633fd49a74dd93a88"},
}
9 changes: 5 additions & 4 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import os

import address_book as ab
import boa
import pytest

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


@pytest.fixture(autouse=True)
Expand All @@ -23,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 Down
110 changes: 110 additions & 0 deletions tests/integration/curve_pools/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import address_book as ab
import boa
import pytest


@pytest.fixture()
def alice():
return boa.env.generate_address()


@pytest.fixture()
def dev_address():
return boa.env.generate_address()


@pytest.fixture()
def crvusd_init_balance():
return 1_000 * 10**18


@pytest.fixture()
def stableswap_factory():
return boa.from_etherscan(ab.factory_stableswap_ng, "factory_stableswap_ng")


@pytest.fixture()
def paired_tokens(request):
# This fixture is used to get upstream parametrization and populate the contracts
# 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()
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()
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]

pool_size = len(coins)
# pool parameters
A, fee, ma_exp_time, implementation_idx = (2000, 1000000, 866, 0)
method_ids = [b""] * pool_size
oracles = ["0x0000000000000000000000000000000000000000"] * pool_size
offpeg_fee_mp = 20000000000
# deploy pool
with boa.env.prank(dev_address):
pool_address = stableswap_factory.deploy_plain_pool(
"pool_name",
"POOL",
coins,
A,
fee,
offpeg_fee_mp,
ma_exp_time,
implementation_idx,
asset_types,
method_ids,
oracles,
)
pool_interface = boa.load_vyi("tests/integration/interfaces/CurveStableSwapNG.vyi")
pool = pool_interface.at(pool_address)
# fund dev with tokens (free-mint erc20s and deposit vaults)
AMOUNT_STABLE = 1_000_000
dev_balances = []
for token in pool_tokens:
if token["asset_type"] == 0:
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
+ underlying_contract.balanceOf(
dev_address
), # in case of dai + sdai deal would overwrite, so we add the previous balance
)
underlying_contract.approve(
token["contract"],
AMOUNT_STABLE * 10**decimals,
sender=dev_address,
)
token["contract"].deposit(AMOUNT_STABLE * 10**decimals, dev_address, sender=dev_address)
# Approve pool to spend vault tokens
token["contract"].approve(pool, 2**256 - 1, sender=dev_address)
dev_balances.append(token["contract"].balanceOf(dev_address))
pool.add_liquidity(dev_balances, 0, dev_address, sender=dev_address)
return pool
133 changes: 133 additions & 0 deletions tests/integration/curve_pools/test_stableswap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import pytest
import boa
from utils import generate_list_combinations
import address_book as ab


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=False)
tokens_subset = paired_token_combinations[0:N_COMBINATIONS]


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))
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))

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 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.
"""
n_coins = stableswap_pool.N_COINS()
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)]
# 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]
20 changes: 20 additions & 0 deletions tests/integration/curve_pools/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from itertools import combinations
import address_book as ab
import random


def generate_list_combinations(data_list, combo_sizes, randomize=False):
combos = []
for count in combo_sizes:
for combo in combinations(data_list, count):
combos.append(list(combo)) # Convert each combination to a list
if randomize:
random.shuffle(combos)
return combos


# test functionality if run as a script
if __name__ == "__main__":
combos = generate_list_combinations(ab.all_stables, [1, 2])
print(combos)
print(len(combos))
Loading

0 comments on commit 148b847

Please sign in to comment.