diff --git a/contracts/main/CurveStableSwapMetaNG.vy b/contracts/main/CurveStableSwapMetaNG.vy index 5c1e567c..d3b1e47a 100644 --- a/contracts/main/CurveStableSwapMetaNG.vy +++ b/contracts/main/CurveStableSwapMetaNG.vy @@ -89,6 +89,12 @@ interface StableSwap2: interface StableSwap3: def add_liquidity(amounts: uint256[3], min_mint_amount: uint256): nonpayable +interface StableSwapNG: + def add_liquidity( + amounts: DynArray[uint256, MAX_COINS], + min_mint_amount: uint256 + ) -> uint256: nonpayable + interface StableSwap: def remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_amount: uint256): nonpayable def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256): nonpayable @@ -201,6 +207,7 @@ N_COINS_128: constant(int128) = 2 PRECISION: constant(uint256) = 10 ** 18 BASE_POOL: public(immutable(address)) +BASE_POOL_IS_NG: immutable(bool) BASE_N_COINS: public(immutable(uint256)) BASE_COINS: public(immutable(DynArray[address, MAX_COINS])) @@ -326,10 +333,11 @@ def __init__( Calculated as: keccak(text=event_signature.replace(" ", ""))[:4] @param _oracles Array of rate oracle addresses. """ - assert len(_base_coins) <= 3 # dev: implementation does not support base pool with more than 3 coins - # The following reverts if BASE_POOL is an NG implementaion. - assert not raw_call(_base_pool, method_id("D_ma_time()"), revert_on_failure=False) + BASE_POOL_IS_NG = raw_call(_base_pool, method_id("D_ma_time()"), revert_on_failure=False) + + if not BASE_POOL_IS_NG: + assert len(_base_coins) <= 3 # dev: implementation does not support old gen base pool with more than 3 coins math = Math(_math_implementation) BASE_POOL = _base_pool @@ -1179,6 +1187,16 @@ def _exchange( @internal def _meta_add_liquidity(dx: uint256, base_i: int128) -> uint256: + if BASE_POOL_IS_NG: + + base_inputs: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + for i in range(BASE_N_COINS, bound=MAX_COINS): + if i == convert(base_i, uint256): + base_inputs.append(dx) + else: + base_inputs.append(0) + return StableSwapNG(BASE_POOL).add_liquidity(base_inputs, 0) + coin_i: address = coins[MAX_METAPOOL_COIN_INDEX] x: uint256 = ERC20(coin_i).balanceOf(self) diff --git a/contracts/main/CurveStableSwapNG.vy b/contracts/main/CurveStableSwapNG.vy index b3ad05bd..f4f32c10 100644 --- a/contracts/main/CurveStableSwapNG.vy +++ b/contracts/main/CurveStableSwapNG.vy @@ -1091,7 +1091,7 @@ def get_D(_xp: DynArray[uint256, MAX_COINS], _amp: uint256) -> uint256: D_P: uint256 = D for x in _xp: - D_P *= D / x + D_P = D_P * D / x D_P /= pow_mod256(N_COINS, N_COINS) Dprev: uint256 = D diff --git a/contracts/main/CurveStableSwapNGViews.vy b/contracts/main/CurveStableSwapNGViews.vy index 183355dc..6ff702dd 100644 --- a/contracts/main/CurveStableSwapNGViews.vy +++ b/contracts/main/CurveStableSwapNGViews.vy @@ -471,7 +471,13 @@ def _base_calc_token_amount( else: - raise "base_n_coins > 3 not supported yet." + base_inputs: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) + for i in range(base_n_coins, bound=MAX_COINS): + if i == convert(base_i, uint256): + base_inputs.append(dx) + else: + base_inputs.append(0) + return StableSwapNG(base_pool).calc_token_amount(base_inputs, is_deposit) @internal diff --git a/poetry.lock b/poetry.lock index a40132fa..56d0244a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2873,9 +2873,9 @@ forking-recommended = ["ujson"] [package.source] type = "git" -url = "https://github.com/vyperlang/titanoboa" -reference = "ce6c65ac8d4c7c208a06cb2a06f07e65d4ce9f47" -resolved_reference = "ce6c65ac8d4c7c208a06cb2a06f07e65d4ce9f47" +url = "https://github.com/vyperlang/titanoboa.git" +reference = "03949fe9e3b1c15b8d88dd169b4f5e44fb64fae0" +resolved_reference = "03949fe9e3b1c15b8d88dd169b4f5e44fb64fae0" [[package]] name = "tomli" @@ -3187,4 +3187,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "852e23bdada5987fdd4245dafa312bea5a6cfd683e2afe5aece58bdc9ff973ea" +content-hash = "b11f60d84eea1a9d63bb0c5dafd5e0193d75b0a01843d27011084e447fed6e27" diff --git a/pyproject.toml b/pyproject.toml index 4c9131f8..a0e1397f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ packages = [] [tool.poetry.dependencies] python = "^3.10" poetry = "1.5.1" -titanoboa = {git = "https://github.com/vyperlang/titanoboa", rev = "ce6c65ac8d4c7c208a06cb2a06f07e65d4ce9f47"} +titanoboa = {git = "https://github.com/vyperlang/titanoboa.git", rev = "03949fe9e3b1c15b8d88dd169b4f5e44fb64fae0"} vyper = "0.3.10" pycryptodome = "^3.18.0" pre-commit = "^3.3.3" diff --git a/tests/conftest.py b/tests/conftest.py index 531da375..13e14396 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -170,7 +170,7 @@ def meta_decimals(initial_decimals, metapool_token_type, decimals): # # @pytest.mark.only_for_token_types(2) # class TestPoolsWithOracleToken: -@pytest.fixture(autouse=True) +@pytest.fixture() def skip_by_token_type(request, pool_tokens): only_for_token_types = request.node.get_closest_marker("only_for_token_types") if only_for_token_types: @@ -179,7 +179,7 @@ def skip_by_token_type(request, pool_tokens): pytest.skip("skipped because no tokens for these types") -@pytest.fixture(autouse=True) +@pytest.fixture() def skip_rebasing(request, swap): only_for_token_types = request.node.get_closest_marker("skip_rebasing_tokens") if only_for_token_types: @@ -187,7 +187,7 @@ def skip_rebasing(request, swap): pytest.skip("skipped because test includes rebasing tokens") -@pytest.fixture(autouse=True) +@pytest.fixture() def skip_oracle(request, pool_tokens): only_for_token_types = request.node.get_closest_marker("skip_oracle_tokens") if only_for_token_types: @@ -197,7 +197,7 @@ def skip_oracle(request, pool_tokens): pytest.skip("skipped because test includes oraclised tokens") -@pytest.fixture(autouse=True) +@pytest.fixture() def only_oracle(request, pool_tokens): only_for_token_types = request.node.get_closest_marker("only_oracle_tokens") if only_for_token_types: @@ -207,7 +207,7 @@ def only_oracle(request, pool_tokens): pytest.skip("skipped because test excludes oraclised tokens") -@pytest.fixture(autouse=True) +@pytest.fixture() def only_rebasing(request, swap): marker = request.node.get_closest_marker("contains_rebasing_tokens") if marker: @@ -219,7 +219,7 @@ def only_rebasing(request, swap): # Usage # @pytest.mark.only_for_pool_type(1) # class TestMetaPool... -@pytest.fixture(autouse=True) +@pytest.fixture() def skip_by_pool_type(request, pool_type): only_for_pool_type = request.node.get_closest_marker("only_for_pool_type") if only_for_pool_type: diff --git a/tests/gauge/test_rewards.py b/tests/gauge/test_rewards.py index a9554cd7..450ee3f8 100644 --- a/tests/gauge/test_rewards.py +++ b/tests/gauge/test_rewards.py @@ -9,7 +9,7 @@ @pytest.mark.usefixtures("forked_chain") class TestGaugeRewards: class TestAddRewards: - @pytest.fixture(autouse=True) + @pytest.fixture() def initial_setup(self, owner, gauge, swap, add_initial_liquidity_owner, set_gauge_implementation): with boa.env.prank(owner): swap.approve(gauge.address, LP_AMOUNT) diff --git a/tests/pools/meta/test_meta_new_ng_base.py b/tests/pools/meta/test_meta_new_ng_base.py new file mode 100644 index 00000000..778d3ce5 --- /dev/null +++ b/tests/pools/meta/test_meta_new_ng_base.py @@ -0,0 +1,177 @@ +import itertools + +import boa +import pytest + +from tests.utils.tokens import mint_for_testing + +BASE_N_COINS = 5 + + +@pytest.fixture(scope="module") +def ng_base_pool_decimals(): + return [18] * BASE_N_COINS + + +@pytest.fixture(scope="module") +def ng_base_pool_tokens(ng_base_pool_decimals): + tokens = [] + for i in range(BASE_N_COINS): + tokens.append(boa.load("contracts/mocks/ERC20.vy", f"tkn{i}", f"tkn{i}", ng_base_pool_decimals[i])) + + return tokens + + +@pytest.fixture(scope="module") +def meta_token(): + return boa.load( + "contracts/mocks/ERC20.vy", + "OTA", + "OTA", + 18, + ) + + +@pytest.fixture(scope="module") +def ng_base_pool( + deployer, + factory, + ng_base_pool_tokens, + zero_address, + amm_interface, + set_pool_implementations, +): + pool_size = len(ng_base_pool_tokens) + offpeg_fee_multiplier = 20000000000 + method_ids = [bytes(b"")] * pool_size + oracles = [zero_address] * pool_size + A = 1000 + fee = 3000000 + + with boa.env.prank(deployer): + pool = factory.deploy_plain_pool( + "test", + "test", + [t.address for t in ng_base_pool_tokens], + A, + fee, + offpeg_fee_multiplier, + 866, + 0, + [tkn.asset_type() for tkn in ng_base_pool_tokens], + method_ids, + oracles, + ) + + return amm_interface.at(pool) + + +@pytest.fixture(scope="module") +def ng_metapool_tokens(meta_token, ng_base_pool): + return [meta_token, ng_base_pool] + + +@pytest.fixture(scope="module") +def add_ng_base_pool( + owner, + factory, + ng_base_pool, + ng_base_pool_tokens, +): + with boa.env.prank(owner): + factory.add_base_pool( + ng_base_pool.address, + ng_base_pool.address, + [0] * len(ng_base_pool_tokens), + len(ng_base_pool_tokens), + ) + + +@pytest.fixture(scope="module") +def empty_swap( + deployer, + factory, + zero_address, + meta_token, + ng_base_pool, + amm_interface_meta, + add_ng_base_pool, + set_metapool_implementations, +): + method_id = bytes(b"") + oracle = zero_address + offpeg_fee_multiplier = 20000000000 + A = 1000 + fee = 3000000 + + with boa.env.prank(deployer): + pool = factory.deploy_metapool( + ng_base_pool.address, # _base_pool: address + "test", # _name: String[32], + "test", # _symbol: String[10], + meta_token.address, # _coin: address, + A, # _A: uint256, + fee, # _fee: uint256, + offpeg_fee_multiplier, + 866, # _ma_exp_time: uint256, + 0, # _implementation_idx: uint256 + meta_token.asset_type(), # _asset_type: uint8 + method_id, # _method_id: bytes4 + oracle, # _oracle: address + ) + + return amm_interface_meta.at(pool) + + +@pytest.fixture(scope="module") +def mint_and_approve_for_bob(meta_token, ng_base_pool_tokens, bob, empty_swap, ng_base_pool): + for token in [meta_token] + ng_base_pool_tokens: + mint_for_testing(bob, 10**25, token) + token.approve(empty_swap, 2**256 - 1, sender=bob) + token.approve(ng_base_pool, 2**256 - 1, sender=bob) + + +@pytest.fixture(scope="module") +def deposit_amounts( + meta_token, + ng_base_pool, + ng_base_pool_tokens, + ng_base_pool_decimals, + empty_swap, + bob, + mint_and_approve_for_bob, +): + _deposit_amounts = [] + INITIAL_AMOUNT = 1_000_000 * BASE_N_COINS + _deposit_amounts.append(INITIAL_AMOUNT // BASE_N_COINS * 10 ** meta_token.decimals()) + + def add_base_pool_liquidity(user, base_pool, base_pool_tokens, base_pool_decimals): + amount = INITIAL_AMOUNT // BASE_N_COINS + with boa.env.prank(user): + amounts = [amount * 10**d for d in base_pool_decimals] + base_pool.add_liquidity(amounts, 0) + + add_base_pool_liquidity(bob, ng_base_pool, ng_base_pool_tokens, ng_base_pool_decimals) + _deposit_amounts.append(INITIAL_AMOUNT // BASE_N_COINS * 10 ** ng_base_pool.decimals()) + ng_base_pool.approve(empty_swap, 2**256 - 1, sender=bob) + return _deposit_amounts + + +@pytest.fixture(scope="module") +def swap(empty_swap, bob, deposit_amounts): + empty_swap.add_liquidity(deposit_amounts, 0, bob, sender=bob) + return empty_swap + + +@pytest.mark.parametrize("sending,receiving", itertools.permutations(range(4), 2)) +def test_exchange_underlying_ng_base( + swap, + bob, + sending, + receiving, +): + amount = 10**19 + expected_out = swap.get_dy_underlying(sending, receiving, amount) + actual_out = swap.exchange_underlying(sending, receiving, amount, 0, sender=bob) + + assert expected_out == actual_out diff --git a/tests/pools/meta/test_revert_meta_ng_base.py b/tests/pools/meta/test_revert_meta_ng_base.py deleted file mode 100644 index 82f2d8df..00000000 --- a/tests/pools/meta/test_revert_meta_ng_base.py +++ /dev/null @@ -1,132 +0,0 @@ -import boa -import pytest - - -@pytest.fixture(scope="module") -def borky_factory( - deployer, - fee_receiver, - owner, - gauge_implementation, - views_implementation, - math_implementation, - amm_implementation, - amm_implementation_meta, -): - with boa.env.prank(deployer): - _factory = boa.load( - "contracts/main/CurveStableSwapFactoryNG.vy", - fee_receiver, - owner, - ) - - with boa.env.prank(owner): - _factory.set_gauge_implementation(gauge_implementation.address) - _factory.set_views_implementation(views_implementation.address) - _factory.set_math_implementation(math_implementation.address) - _factory.set_pool_implementations(0, amm_implementation.address) - _factory.set_metapool_implementations(0, amm_implementation_meta.address) - - return _factory - - -# <--------------------- Functions ---------------------> - - -@pytest.fixture(scope="module") -def ng_base_pool_decimals(): - return [18, 18] - - -@pytest.fixture(scope="module") -def ng_base_pool_tokens(deployer, ng_base_pool_decimals): - tokens = [] - with boa.env.prank(deployer): - tokens.append(boa.load("contracts/mocks/ERC20.vy", "DAI", "DAI", ng_base_pool_decimals[0])) - tokens.append(boa.load("contracts/mocks/ERC20.vy", "USDC", "USDC", ng_base_pool_decimals[1])) - - return tokens - - -@pytest.fixture(scope="module") -def ng_base_pool( - deployer, - borky_factory, - ng_base_pool_tokens, - zero_address, - amm_interface, -): - asset_types = [0, 0] - pool_size = len(ng_base_pool_tokens) - offpeg_fee_multiplier = 20000000000 - method_ids = [bytes(b"")] * pool_size - oracles = [zero_address] * pool_size - A = 1000 - fee = 3000000 - - with boa.env.prank(deployer): - pool = borky_factory.deploy_plain_pool( - "test", - "test", - [t.address for t in ng_base_pool_tokens], - A, - fee, - offpeg_fee_multiplier, - 866, - 0, - asset_types, - method_ids, - oracles, - ) - - return amm_interface.at(pool) - - -@pytest.fixture(scope="module") -def add_ng_base_pool( - owner, - borky_factory, - ng_base_pool, - ng_base_pool_tokens, -): - with boa.env.prank(owner): - borky_factory.add_base_pool( - ng_base_pool.address, - ng_base_pool.address, - [0] * len(ng_base_pool_tokens), - len(ng_base_pool_tokens), - ) - - -def test_revert_metapool_deployment_with_ng_base_pool( - deployer, borky_factory, zero_address, add_ng_base_pool, ng_base_pool -): - method_id = bytes(b"") - oracle = zero_address - offpeg_fee_multiplier = 20000000000 - A = 1000 - fee = 3000000 - meta_token = boa.load( - "contracts/mocks/ERC20.vy", - "OTA", - "OTA", - 18, - ) - asset_type = meta_token.asset_type() - - with boa.reverts(): - borky_factory.deploy_metapool( - ng_base_pool.address, # _base_pool: address - "test", # _name: String[32], - "test", # _symbol: String[10], - meta_token.address, # _coin: address, - A, # _A: uint256, - fee, # _fee: uint256, - offpeg_fee_multiplier, - 866, # _ma_exp_time: uint256, - 0, # _implementation_idx: uint256 - asset_type, # _asset_type: uint8 - method_id, # _method_id: bytes4 - oracle, # _oracle: address - sender=deployer, - ) diff --git a/tests/test_get_D.py b/tests/test_get_D.py new file mode 100644 index 00000000..999c0fe0 --- /dev/null +++ b/tests/test_get_D.py @@ -0,0 +1,60 @@ +import boa +import pytest + + +@pytest.fixture(scope="module") +def new_math(): + + return boa.loads( + """ +A_PRECISION: constant(uint256) = 100 +N_COINS: constant(uint256) = 2 +MAX_COINS: constant(uint256) = 8 + +@pure +@external +def get_D(_xp: DynArray[uint256, MAX_COINS], _amp: uint256) -> uint256: + + S: uint256 = 0 + for x in _xp: + S += x + if S == 0: + return 0 + + D: uint256 = S + Ann: uint256 = _amp * N_COINS + + for i in range(255): + + D_P: uint256 = D + for x in _xp: + D_P = D_P * D / x + D_P /= pow_mod256(N_COINS, N_COINS) + Dprev: uint256 = D + + # (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + D = ( + (unsafe_div(Ann * S, A_PRECISION) + D_P * N_COINS) * D + / + ( + unsafe_div((Ann - A_PRECISION) * D, A_PRECISION) + + unsafe_add(N_COINS, 1) * D_P + ) + ) + + # Equality with the precision of 1 + if D > Dprev: + if D - Dprev <= 1: + return D + else: + if Dprev - D <= 1: + return D + # convergence typically occurs in 4 rounds or less, this should be unreachable! + # if it does happen the pool is borked and LPs can withdraw via `remove_liquidity` + raise +""" + ) + + +def test_convergence(new_math): + assert new_math.get_D([10**23, 10**18], 1000) == 9010395375710532394006