From a2e3dd7d493a4b98cd0a43528dbc9079bf68d7ae Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 18 Apr 2024 16:32:41 +0200 Subject: [PATCH] test: verifying `newton_y` converges + no revert --- contracts/mocks/newton_y_small_gamma.vy | 98 --------- .../math/contracts/newton_y_exposed.vy | 28 +-- tests/unitary/math/test_newton_y.py | 189 +++++++++++++++--- 3 files changed, 168 insertions(+), 147 deletions(-) delete mode 100644 contracts/mocks/newton_y_small_gamma.vy rename contracts/mocks/newton_y_large_gamma.vy => tests/unitary/math/contracts/newton_y_exposed.vy (67%) diff --git a/contracts/mocks/newton_y_small_gamma.vy b/contracts/mocks/newton_y_small_gamma.vy deleted file mode 100644 index 8fad9a5d..00000000 --- a/contracts/mocks/newton_y_small_gamma.vy +++ /dev/null @@ -1,98 +0,0 @@ -# Minimized version of the math contracts before the gamma value expansion. -# Additionally to the final value it also returns the number of iterations it took to find the value. -# For testing purposes only. -# From commit: 1c800bd7937f63a9c278a220af846d322f356dd5 - -N_COINS: constant(uint256) = 2 -A_MULTIPLIER: constant(uint256) = 10000 - -MIN_GAMMA: constant(uint256) = 10**10 -MAX_GAMMA: constant(uint256) = 2 * 10**15 - -MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 -MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000 - -@internal -@pure -def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> (uint256, uint256): - """ - Calculating x[i] given other balances x[0..N_COINS-1] and invariant D - ANN = A * N**N - This is computationally expensive. - """ - - x_j: uint256 = x[1 - i] - y: uint256 = D**2 / (x_j * N_COINS**2) - K0_i: uint256 = (10**18 * N_COINS) * x_j / D - - assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i] - - convergence_limit: uint256 = max(max(x_j / 10**14, D / 10**14), 100) - - for j in range(255): - y_prev: uint256 = y - - K0: uint256 = K0_i * y * N_COINS / D - S: uint256 = x_j + y - - _g1k0: uint256 = gamma + 10**18 - if _g1k0 > K0: - _g1k0 = _g1k0 - K0 + 1 - else: - _g1k0 = K0 - _g1k0 + 1 - - # D / (A * N**N) * _g1k0**2 / gamma**2 - mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN - - # 2*K0 / _g1k0 - mul2: uint256 = 10**18 + (2 * 10**18) * K0 / _g1k0 - - yfprime: uint256 = 10**18 * y + S * mul2 + mul1 - _dyfprime: uint256 = D * mul2 - if yfprime < _dyfprime: - y = y_prev / 2 - continue - else: - yfprime -= _dyfprime - fprime: uint256 = yfprime / y - - # y -= f / f_prime; y = (y * fprime - f) / fprime - # y = (yfprime + 10**18 * D - 10**18 * S) // fprime + mul1 // fprime * (10**18 - K0) // K0 - y_minus: uint256 = mul1 / fprime - y_plus: uint256 = (yfprime + 10**18 * D) / fprime + y_minus * 10**18 / K0 - y_minus += 10**18 * S / fprime - - if y_plus < y_minus: - y = y_prev / 2 - else: - y = y_plus - y_minus - - diff: uint256 = 0 - if y > y_prev: - diff = y - y_prev - else: - diff = y_prev - y - - if diff < max(convergence_limit, y / 10**14): - return y, j - - raise "Did not converge" - - -@external -@pure -def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> (uint256, uint256): - - # Safety checks - assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A - assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma - assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D - - y: uint256 = 0 - iterations: uint256 = 0 - - y, iterations = self._newton_y(ANN, gamma, x, D, i) - frac: uint256 = y * 10**18 / D - assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y - - return y, iterations diff --git a/contracts/mocks/newton_y_large_gamma.vy b/tests/unitary/math/contracts/newton_y_exposed.vy similarity index 67% rename from contracts/mocks/newton_y_large_gamma.vy rename to tests/unitary/math/contracts/newton_y_exposed.vy index a48c713a..1dcf36d7 100644 --- a/contracts/mocks/newton_y_large_gamma.vy +++ b/tests/unitary/math/contracts/newton_y_exposed.vy @@ -1,13 +1,11 @@ -# Minimized version of the math contracts before the gamma value expansion. -# Additionally to the final value it also returns the number of iterations it took to find the value. -# For testing purposes only. +# Minimized version of the math contracts that expose some inner details of newton_y for testing purposes: +# - Additionally to the final value it also returns the number of iterations it took to find the value. # From commit: 6dec22f6956cc04fb865d93c1e521f146e066cab N_COINS: constant(uint256) = 2 A_MULTIPLIER: constant(uint256) = 10000 MIN_GAMMA: constant(uint256) = 10**10 -MAX_GAMMA_SMALL: constant(uint256) = 2 * 10**16 MAX_GAMMA: constant(uint256) = 3 * 10**17 MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 10 @@ -78,25 +76,3 @@ def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: return y, j raise "Did not converge" - - -@external -@pure -def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> (uint256, uint256): - - # Safety checks - assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A - assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma - assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D - lim_mul: uint256 = 100 * 10**18 # 100.0 - if gamma > MAX_GAMMA_SMALL: - lim_mul = unsafe_div(unsafe_mul(lim_mul, MAX_GAMMA_SMALL), gamma) # smaller than 100.0 - - y: uint256 = 0 - iterations: uint256 = 0 - - y, iterations = self._newton_y(ANN, gamma, x, D, i, lim_mul) - frac: uint256 = y * 10**18 / D - assert (frac >= unsafe_div(10**36 / N_COINS, lim_mul)) and (frac <= unsafe_div(lim_mul, N_COINS)) # dev: unsafe value for y - - return y, iterations diff --git a/tests/unitary/math/test_newton_y.py b/tests/unitary/math/test_newton_y.py index 914f56ae..fee0c48d 100644 --- a/tests/unitary/math/test_newton_y.py +++ b/tests/unitary/math/test_newton_y.py @@ -1,11 +1,34 @@ -import boa +""" +This test suite was added as part of PR #12 to verify +the correctness of the newton_y function in the math +contract. + +Since the introduction of `get_y` this function is just used +as a fallback when the analytical method fails to find a solution +for y (roughly 3% of the time). + +Since bounds for gamma have been not only restored to the original +tricrypto levels but even pushed forward this suite aims to +test the convergence of the newton_y function in the new bounds. + +Since calls to newton_y are not that frequent anymore this test suite +tries to test it in isolation. + +While the tests are quite similar, they have been separated to obtain +more fine-grained information about the convergence of the newton_y +through hypothesis events. + +We don't test the correctness of y because newton_y should always +converge to the correct value (or not converge at all otherwise). +""" + import pytest from hypothesis import event, given, settings from hypothesis import strategies as st N_COINS = 2 -# MAX_SAMPLES = 1000000 # Increase for fuzzing -MAX_SAMPLES = 10000 +MAX_SAMPLES = 1000000 # Increase for fuzzing +# MAX_SAMPLES = 10000 N_CASES = 32 A_MUL = 10000 @@ -13,21 +36,129 @@ MAX_A = int(N_COINS**N_COINS * A_MUL * 1000) # Old bounds for gamma -# should be used only when comparing convergence with the old version -MIN_GAMMA_CMP = 10**10 -MAX_GAMMA_CMP = 2 * 10**15 +MIN_GAMMA = 10**10 +MAX_GAMMA_OLD = 2 * 10**15 + +# New bounds for gamma (min is unchanged) +MAX_GAMMA_SMALL = 2 * 10**16 # becomes stricter after MAX_GAMMA_SMALL +# TODO for now this is how far we +# we managed to push the bounds without a revert. +MAX_GAMMA = int(1.99 * 10**17) +# ideally we want: +# MAX_GAMMA = 3 * 10**17 @pytest.fixture(scope="module") -def math_large_gamma(): - return boa.load("contracts/mocks/newton_y_large_gamma.vy") +def math_exposed(): + # compile + from contracts import newton_y_exposed + # deploy + return newton_y_exposed() -@pytest.fixture(scope="module") -def math_small_gamma(): - return boa.load("contracts/mocks/newton_y_small_gamma.vy") +@pytest.mark.parametrize( + "_tmp", range(N_CASES) +) # Parallelisation hack (more details in folder's README) +@given( + A=st.integers(min_value=MIN_A, max_value=MAX_A), + D=st.integers( + min_value=10**18, max_value=10**14 * 10**18 + ), # 1 USD to 100T USD + xD=st.integers( + min_value=10**17 // 2, max_value=10**19 // 2 + ), # <- ratio 1e18 * x/D, typically 1e18 * 1 + yD=st.integers( + min_value=10**17 // 2, max_value=10**19 // 2 + ), # <- ratio 1e18 * y/D, typically 1e18 * 1 + gamma=st.integers(min_value=MIN_GAMMA, max_value=MAX_GAMMA_OLD), + j=st.integers(min_value=0, max_value=1), +) +@settings(max_examples=MAX_SAMPLES, deadline=None) +def test_equivalence( + math_exposed, math_optimized, A, D, xD, yD, gamma, j, _tmp +): + """ + Tests whether the newton_y function works the same way + for both the exposed and production versions on ranges that are + already in production. + + [MIN_GAMMA, MAX_GAMMA_OLD] = [1e10, 2e15]. + """ + # using the same prices as in get_y fuzzing + # TODO is this the correct way? + X = [D * xD // 10**18, D * yD // 10**18] + + # this value remains as before the increase of the bounds + # since we're testing the old bounds + lim_mul = int(100e18) # 100.0 + + # we can use the old version to know the number of iterations + # and the expected value + y_exposed, iterations = math_exposed.internal._newton_y( + A, gamma, X, D, j, lim_mul + ) + + # this should not revert (didn't converge or hit bounds) + y = math_optimized.newton_y(A, gamma, X, D, j) + + event(f"converges in {iterations} iterations") + assert y_exposed == y + + +@pytest.mark.parametrize( + "_tmp", range(N_CASES) +) # Parallelisation hack (more details in folder's README) +@given( + A=st.integers(min_value=MIN_A, max_value=MAX_A), + D=st.integers( + min_value=10**18, max_value=10**14 * 10**18 + ), # 1 USD to 100T USD + xD=st.integers( + min_value=10**17 // 2, max_value=10**19 // 2 + ), # <- ratio 1e18 * x/D, typically 1e18 * 1 + yD=st.integers( + min_value=10**17 // 2, max_value=10**19 // 2 + ), # <- ratio 1e18 * y/D, typically 1e18 * 1 + gamma=st.integers(min_value=MAX_GAMMA_OLD, max_value=MAX_GAMMA_SMALL), + j=st.integers(min_value=0, max_value=1), +) +@settings(max_examples=MAX_SAMPLES, deadline=None) +def test_restored(math_optimized, math_exposed, A, D, xD, yD, gamma, j, _tmp): + """ + Tests whether the bounds that have been restored to the original + tricrypto ones work as expected. + + [MAX_GAMMA_OLD, MAX_GAMMA_SMALL] = [2e15, 2e16] + """ + # using the same prices as in get_y fuzzing + # TODO is this the correct way? + X = [D * xD // 10**18, D * yD // 10**18] + + # according to vyper math contracts (get_y) since we never have + # values bigger than MAX_GAMMA_SMALL, lim_mul is always 100 + lim_mul = int(100e18) # 100.0 + + # we can use the exposed version to know the number of iterations + # and the expected value + y_exposed, iterations = math_exposed.internal._newton_y( + A, gamma, X, D, j, lim_mul + ) + + # this should not revert (didn't converge or hit bounds) + y = math_optimized.internal._newton_y(A, gamma, X, D, j, lim_mul) + + # we can use the exposed version to know the number of iterations + # since we didn't change how the computation is done + event(f"converges in {iterations} iterations") + + assert y_exposed == y + + +@pytest.mark.parametrize( + "_tmp", range(N_CASES) +) # Parallelisation hack (more details in folder's README) @given( A=st.integers(min_value=MIN_A, max_value=MAX_A), D=st.integers( @@ -39,25 +170,37 @@ def math_small_gamma(): yD=st.integers( min_value=10**17 // 2, max_value=10**19 // 2 ), # <- ratio 1e18 * y/D, typically 1e18 * 1 - gamma=st.integers(min_value=MIN_GAMMA_CMP, max_value=MAX_GAMMA_CMP), + gamma=st.integers(min_value=MAX_GAMMA_SMALL + 1, max_value=MAX_GAMMA), j=st.integers(min_value=0, max_value=1), ) @settings(max_examples=MAX_SAMPLES, deadline=None) -def test_newton_y_equivalence( - math_small_gamma, math_large_gamma, A, D, xD, yD, gamma, j +def test_new_bounds( + math_optimized, math_exposed, A, D, xD, yD, gamma, j, _tmp ): """ - Tests whether the newton_y function converges to the same - value for both the old and new versions + Tests whether the new bouds that no pool has ever reached + work as expected. + + [MAX_GAMMA_SMALL, MAX_GAMMA] = [2e16, 3e17] """ + # using the same prices as in get_y fuzzing + # TODO is this the correct way? X = [D * xD // 10**18, D * yD // 10**18] - y_small, iterations_old = math_small_gamma.newton_y(A, gamma, X, D, j) - y_large, iterations_new = math_large_gamma.newton_y(A, gamma, X, D, j) - # print(math_large_gamma.internal._newton_y) + # this comes from `get_y`` which is the only place from which _newton_y + # is called when gamma is bigger than MAX_GAMMA_SMALL lim_mul has to + # be adjusted accordingly + lim_mul = 100e18 * MAX_GAMMA_SMALL // gamma # smaller than 100.0 + + y_exposed, iterations = math_exposed.internal._newton_y( + A, gamma, X, D, j, int(lim_mul) + ) + + # this should not revert (didn't converge or hit bounds) + y = math_optimized.internal._newton_y(A, gamma, X, D, j, int(lim_mul)) - event(f"converges in {iterations_new} iterations") + # we can use the exposed version to know the number of iterations + # since we didn't change how the computation is done + event(f"converges in {iterations} iterations") - # create events depending on the differences between iterations - assert iterations_old - iterations_new == 0 - assert y_small == y_large + assert y == y_exposed