diff --git a/scripts/debug_tests.py b/scripts/debug_tests.py index 0ce3ab1..8efd838 100644 --- a/scripts/debug_tests.py +++ b/scripts/debug_tests.py @@ -11,7 +11,7 @@ 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 @@ -19,7 +19,7 @@ def main(): 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) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index e8ba328..98e076e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -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") @@ -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" @@ -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 @@ -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) @@ -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", diff --git a/tests/integration/curve_pools/conftest.py b/tests/integration/curve_pools/conftest.py index abc1499..1ba7df9 100644 --- a/tests/integration/curve_pools/conftest.py +++ b/tests/integration/curve_pools/conftest.py @@ -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") @@ -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] @@ -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( @@ -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, @@ -58,9 +79,7 @@ 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") @@ -68,7 +87,10 @@ def stableswap_pool(stableswap_factory, vault, dev_address, paired_tokens): 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"], @@ -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): ... diff --git a/tests/integration/curve_pools/test_stableswap.py b/tests/integration/curve_pools/test_stableswap.py index 097874a..ded1d08 100644 --- a/tests/integration/curve_pools/test_stableswap.py +++ b/tests/integration/curve_pools/test_stableswap.py @@ -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]