Skip to content

Commit

Permalink
test: verifying newton_y converges + no revert
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertoCentonze committed Apr 18, 2024
1 parent d5c6c5e commit a2e3dd7
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 147 deletions.
98 changes: 0 additions & 98 deletions contracts/mocks/newton_y_small_gamma.vy

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
189 changes: 166 additions & 23 deletions tests/unitary/math/test_newton_y.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,164 @@
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
MIN_A = int(N_COINS**N_COINS * A_MUL / 10)
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(
Expand All @@ -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

0 comments on commit a2e3dd7

Please sign in to comment.