-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: stableswap (n=2,3) integration tests (#27)
* 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
1 parent
ad9fd1e
commit 148b847
Showing
8 changed files
with
620 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
Oops, something went wrong.