Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/test(CL Swaps): Swap fees for amount out given in #4097

Merged
merged 15 commits into from
Jan 27, 2023
21 changes: 21 additions & 0 deletions scripts/cl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Concentrated Liquidity Test Case Estimate Scripts

## Context

These scripts exist to estimate the results of the swap
concentrated liqudity test cases.

Each existing test case is estimated by calling the
respective function in `scripts/cl/main.py` main function.

The function defining a swap test case specifies which
CL go test case it estimates. See the spec for details.

## Running the scripts

To run with sage installed:
```bash
sage -python scripts/cl/main.py
```

Note, these scripts can also be run by installing `sympy` without sage.
39 changes: 39 additions & 0 deletions scripts/cl/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import sympy as sp

precision = 30

# SqrtPriceRange represents a price range between the current and the next tick
# as well as the liquidity in that price range.
# When swapping token zero for one, sqrt_price_current >= sqrt_price_next.
# When swapping token one for zero, sqrt_price_current <= sqrt_price_next.
#
# sqrt_price_next can be None. When it is None, that implies that the next sqrt
# price must be calculated. In such a case, next sqrt price depends on the
# remaining amount of token in to be swapped. This occurs for the last sqrt price
# range in the collection of sqrt price ranges that represent a swap.
#
# For example, I might have a swap of 100 ETH in from 5000 to 5001 with liquidity
# X, from 5001 to 5002 with liquidity Y,
# and from 5002 to UNKNOWN with liquidity Z. In this case, the UNKNOWN
# depends on how much ETH we have remaining after consuming liquidity X and Y.
class SqrtPriceRange:
def __init__(self, sqrt_price_current: int, sqrt_price_next: int, liquidity: sp.Float):
self.sqrt_price_start = sp.sqrt(fixed_prec_dec(sqrt_price_current))
if sqrt_price_next is not None:
self.sqrt_price_next = sp.sqrt(fixed_prec_dec(sqrt_price_next))
else:
self.sqrt_price_next = None
self.liquidity = liquidity

def fixed_prec_dec(string: str) -> sp.Float:
""" Return an equivalent of a Python Decimal with fixed precision.
"""
return sp.Float(string, precision)

def get_fee_amount_per_share(token_in: sp.Float, swap_fee: sp.Float, liquidity: sp.Float) -> sp.Float:
""" Returns the fee amount per share.
"""
fee_charge_total = token_in * swap_fee
return fee_charge_total / liquidity

zero = fixed_prec_dec("0")
267 changes: 267 additions & 0 deletions scripts/cl/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
from typing import Tuple
from common import *
import zero_for_one as zfo
import one_for_zero as ofz

def estimate_test_case(tick_ranges: list[SqrtPriceRange], token_in_initial: sp.Float, swap_fee: sp.Float, is_zero_for_one: bool) -> Tuple[sp.Float, sp.Float]:
""" Estimates a calc concentrated liquidity test case.

Given
- sqrt price range with the start sqrt price, next sqrt price and liquidity
- initial token in
- swap fee
- zero for one boolean flag
Estimates the final token out and the fee growth per share and prints it to stdout.
Also, estimates these and other values at each range and prints them to stdout.

Returns the total token out and the total fee growth per share.
"""

token_in_consumed_total, token_out_total, fee_growth_per_share_total = zero, zero, zero

for i in range(len(tick_ranges)):
tick_range = tick_ranges[i]

# Normally, for the last swap range we swap until token in runs out
# As a result, the next sqrt price for that range calculated at runtime.
is_last_range = i == len(tick_ranges) - 1
# Except for the cases where we set price limit explicitly. Then, the
# last price range may have the upper sqrt price limit configured.
is_next_price_set = tick_range.sqrt_price_next != None

is_with_next_sqrt_price = not is_last_range or is_next_price_set

if is_with_next_sqrt_price:
token_in_consumed, token_out, fee_growth_per_share = zero, zero, zero
if is_zero_for_one:
token_in_consumed, token_out, fee_growth_per_share = zfo.calc_test_case_with_next_sqrt_price(tick_range.liquidity, tick_range.sqrt_price_start, tick_range.sqrt_price_next, swap_fee)
else:
token_in_consumed, token_out, fee_growth_per_share = ofz.calc_test_case_with_next_sqrt_price(tick_range.liquidity, tick_range.sqrt_price_start, tick_range.sqrt_price_next, swap_fee)

token_in_consumed_total += token_in_consumed
token_out_total += token_out
fee_growth_per_share_total += fee_growth_per_share

else:
token_in_remaining = token_in_initial - token_in_consumed_total

if token_in_remaining < zero:
raise Exception(F"token_in_remaining {token_in_remaining} is negative with token_in_initial {token_in_initial} and token_in_consumed_total {token_in_consumed_total}")

token_out, fee_growth_per_share = zero, zero
if is_zero_for_one:
_, token_out, fee_growth_per_share = zfo.calc_test_case(tick_range.liquidity, tick_range.sqrt_price_start, token_in_remaining, swap_fee)
else:
_, token_out, fee_growth_per_share = ofz.calc_test_case(tick_range.liquidity, tick_range.sqrt_price_start, token_in_remaining, swap_fee)

token_out_total += token_out
fee_growth_per_share_total += fee_growth_per_share
print("\n")
print(F"After processing range {i}")
print(F"current token_out_total: {token_out_total}")
print(F"current current fee_growth_per_share_total: {fee_growth_per_share_total}")
print("\n\n\n")

print("\n\n")
print("Final results:")
print("token_out_total: ", token_out_total)
print("fee_growth_per_share_total: ", fee_growth_per_share_total)

return token_out_total, fee_growth_per_share_total

def validate_confirmed_results(token_out_total: sp.Float, fee_growth_per_share_total: sp.Float, expected_token_out_total: sp.Float, expected_fee_growth_per_share_total: sp.Float):
"""Validates the results of a calc concentrated liquidity test case estimates.

This validation helper exists to make sure that subsequent changes to the script do not break test cases.
"""

if sp.N(token_out_total, 18) != sp.N(expected_token_out_total, 18):
raise Exception(F"token_out_total {token_out_total} does not match expected_token_out_total {expected_token_out_total}")

if sp.N(fee_growth_per_share_total, 18) != sp.N(expected_fee_growth_per_share_total, 18):
raise Exception(F"fee_growth_per_share_total {fee_growth_per_share_total} does not match expected_fee_growth_per_share_total {expected_fee_growth_per_share_total}")

def estimate_single_position_within_one_tick_ofz():
"""Estimates and prints the results of a calc concentrated liquidity test case with a single position within one tick
when swapping token one for token zero (ofz).

go test -timeout 30s -v -run TestKeeperTestSuite/TestCalcAndSwapOutAmtGivenIn/fee_1 github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity
"""

is_zero_for_one = False
swap_fee = fixed_prec_dec("0.01")
token_in_initial = fixed_prec_dec("42000000")

tick_ranges = [
SqrtPriceRange(5000, None, fixed_prec_dec("1517882343.751510418088349649")), # last one must be computed based on remaining token in, therefore it is None
]

token_out_total, fee_growth_per_share_total = estimate_test_case(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)

expected_token_out_total = fixed_prec_dec("8312.77961614650590788243077782")
expected_fee_growth_per_share_total = fixed_prec_dec("0.000276701288297452775064000000017")

validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)

def estimate_two_positions_within_one_tick_zfo():
"""Estimates and prints the results of a calc concentrated liquidity test case with two positions within one tick
when swapping token zero for one (zfo).

go test -timeout 30s -v -run TestKeeperTestSuite/TestCalcAndSwapOutAmtGivenIn/fee_2 github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity
"""

is_zero_for_one = True
swap_fee = fixed_prec_dec("0.03")
token_in_initial = fixed_prec_dec("13370")

tick_ranges = [
SqrtPriceRange(5000, None, fixed_prec_dec("3035764687.503020836176699298")), # last one must be computed based on remaining token in, therefore it is None
]

token_out_total, fee_growth_per_share_total = estimate_test_case(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)

expected_token_out_total = fixed_prec_dec("64824917.7760329489344598324379")
expected_fee_growth_per_share_total = fixed_prec_dec("0.000000132124865162033700093060000008")

validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)

def estimate_two_consecutive_positions_zfo():
"""Estimates and prints the results of a calc concentrated liquidity test case with two consecutive positions
when swapping token zero for one (zfo).

go test -timeout 30s -v -run TestKeeperTestSuite/TestCalcAndSwapOutAmtGivenIn/fee_3 github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity
"""

is_zero_for_one = True
swap_fee = fixed_prec_dec("0.05")
token_in_initial = fixed_prec_dec("2000000")

tick_ranges = [
SqrtPriceRange(5000, 4545, fixed_prec_dec("1517882343.751510418088349649")),
SqrtPriceRange(4545, None, fixed_prec_dec("1198735489.597250295669959398")), # last one must be computed based on remaining token in, therefore it is None
]

token_out_total, fee_growth_per_share_total = estimate_test_case(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)

expected_token_out_total = fixed_prec_dec("8702563350.03654978407909736170")
expected_fee_growth_per_share_total = fixed_prec_dec("0.0000720353033851801313478676884502")

validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)

def estimate_overlapping_price_range_ofz_test():
"""Estimates and prints the results of a calc concentrated liquidity test case with overlapping price ranges
when swapping token one for token zero (ofz).

go test -timeout 30s -v -run TestKeeperTestSuite/TestCalcAndSwapOutAmtGivenIn/fee_4 github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity
"""

is_zero_for_one = False
swap_fee = fixed_prec_dec("0.1")
token_in_initial = fixed_prec_dec("10000000000")

tick_ranges = [
SqrtPriceRange(5000, 5001, fixed_prec_dec("1517882343.751510418088349649")),
SqrtPriceRange(5001, 5500, fixed_prec_dec("2188298432.35717914512760058700")),
SqrtPriceRange(5500, None, fixed_prec_dec("670416088.605668727039250938")), # last one must be computed based on remaining token in, therefore it is None
]

token_out_total, fee_growth_per_share_total = estimate_test_case(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)

expected_token_out_total = fixed_prec_dec("1708743.47809184831586199935191")
expected_fee_growth_per_share_total = fixed_prec_dec("0.598328101473707318285291820984")

validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)

def estimate_overlapping_price_range_zfo_test():
"""Estimates and prints the results of a calc concentrated liquidity test case with overlapping price ranges
when swapping token zero for one (zfo) and not consuming full liquidity of the second position.

go test -timeout 30s -v -run TestKeeperTestSuite/TestCalcAndSwapOutAmtGivenIn/fee_5 github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity
"""

is_zero_for_one = True
swap_fee = fixed_prec_dec("0.005")
token_in_initial = fixed_prec_dec("1800000")

tick_ranges = [
SqrtPriceRange(5000, 4999, fixed_prec_dec("1517882343.751510418088349649")),
SqrtPriceRange(4999, 4545, fixed_prec_dec("1517882343.751510418088349649") + fixed_prec_dec("670416215.718827443660400594")), # first and second position's liquidity.
SqrtPriceRange(4545, None, fixed_prec_dec("670416215.718827443660400594")), # last one must be computed based on remaining token in, therefore it is None
]

token_out_total, fee_growth_per_share_total = estimate_test_case(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)

expected_token_out_total = fixed_prec_dec("8440821620.46523833169832895388")
expected_fee_growth_per_share_total = fixed_prec_dec("0.00000555275275702765744105956374059")

validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)

def estimate_consecutive_positions_gap_ofz_test():
"""Estimates and prints the results of a calc concentrated liquidity test case with consecutive positions with a gap
when swapping token one for zero (ofz).

go test -timeout 30s -v -run TestKeeperTestSuite/TestCalcAndSwapOutAmtGivenIn/fee_6 github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity
"""

is_zero_for_one = False
swap_fee = fixed_prec_dec("0.03")
token_in_initial = fixed_prec_dec("10000000000")

tick_ranges = [
SqrtPriceRange(5000, 5500, fixed_prec_dec("1517882343.751510418088349649")),
SqrtPriceRange(5501, None, fixed_prec_dec("1199528406.187413669220037261")), # last one must be computed based on remaining token in, therefore it is None
]

token_out_total, fee_growth_per_share_total = estimate_test_case(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)

expected_token_out_total = fixed_prec_dec("1772029.65214390801589935811000")
expected_fee_growth_per_share_total = fixed_prec_dec("0.218688507910948644574193665912")

validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)

def estimate_slippage_protection_zfo_test():
"""Estimates and prints the results of a calc concentrated liquidity test case with slippage protection
when swapping token zero for one (zfo).

go test -timeout 30s -v -run TestKeeperTestSuite/TestCalcAndSwapOutAmtGivenIn/fee_7 github.com/osmosis-labs/osmosis/v14/x/concentrated-liquidity
"""

is_zero_for_one = True
swap_fee = fixed_prec_dec("0.01")
token_in_initial = fixed_prec_dec("13370")

tick_ranges = [
SqrtPriceRange(5000, 4994, fixed_prec_dec("1517882343.751510418088349649")),
]

token_out_total, fee_growth_per_share_total = estimate_test_case(tick_ranges, token_in_initial, swap_fee, is_zero_for_one)

expected_token_out_total = fixed_prec_dec("64417624.9871649525380486017974")
expected_fee_growth_per_share_total = fixed_prec_dec("0.0000000849292577225588233432564611676")

validate_confirmed_results(token_out_total, fee_growth_per_share_total, expected_token_out_total, expected_fee_growth_per_share_total)

def main():
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: For scripts, I suggest reviewing this file in-detail and comparing it against Go test cases. However, I don't think there is value in reviewing other internal script calculations so feel free to skip

# fee 1
estimate_single_position_within_one_tick_ofz()

# fee 2
estimate_two_positions_within_one_tick_zfo()

# fee 3
estimate_two_consecutive_positions_zfo()

# fee 4
estimate_overlapping_price_range_ofz_test()

# fee 5
estimate_overlapping_price_range_zfo_test()

# fee 6
estimate_consecutive_positions_gap_ofz_test()

# fee 7
estimate_slippage_protection_zfo_test()

if __name__ == "__main__":
main()
11 changes: 11 additions & 0 deletions scripts/cl/math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sympy as sp

def calc_liquidity_0(amount_zero: sp.Float, sqrtPriceA: sp.Float, sqrtPriceB: sp.Float) -> sp.Float:
"""Calculates and returns liquidity zero.
"""
return amount_zero * (sqrtPriceA * sqrtPriceB) / (sqrtPriceB - sqrtPriceA)

def calc_liquidity_1(amount_one: sp.Float, sqrtPriceA: sp.Float, sqrtPriceB: sp.Float) -> sp.Float:
"""Calculates and returns liquidity one.
"""
return amount_one * (sqrtPriceB - sqrtPriceA)
55 changes: 55 additions & 0 deletions scripts/cl/one_for_zero.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from typing import Tuple
import sympy as sp
from common import *

def get_next_sqrt_price(liquidity: sp.Float, sqrt_price_current: sp.Float, token_in: sp.Float, swap_fee: sp.Float) -> sp.Float:
""" Return the next sqrt price when swapping token one for zero.
"""
return sqrt_price_current + (token_in * (1 - swap_fee) / liquidity)

def get_token_out(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float) -> sp.Float:
""" Returns the token out when swapping token one for zero.
"""
return liquidity * (sqrt_price_next - sqrt_price_current) / (sqrt_price_next * sqrt_price_current)

def get_expected_token_in(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float):
""" Returns the expected token in when swapping token one for zero.
"""
return liquidity * sp.Abs((sqrt_price_current - sqrt_price_next))

def calc_test_case(liquidity: sp.Float, sqrt_price_current: sp.Float, token_in: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
""" Computes and prints all one for zero test case parameters. Next sqrt price is computed from the given parameters.

Returns the next square root price, token out and fee amount per share.
"""
sqrt_price_next = get_next_sqrt_price(liquidity, sqrt_price_current, token_in, swap_fee)
price_next = sp.Pow(sqrt_price_next, 2)
token_out = get_token_out(liquidity, sqrt_price_current, sqrt_price_next)
fee_amount_per_share = get_fee_amount_per_share(token_in, swap_fee, liquidity)

print(F"sqrt_price_next: {sqrt_price_next}")
print(F"price_next: {price_next}")
print(F"token_out: {token_out}")
print(F"fee_amount_per_share: {fee_amount_per_share}")

return sqrt_price_next, token_out, fee_amount_per_share

def calc_test_case_with_next_sqrt_price(liquidity: sp.Float, sqrt_price_current: sp.Float, sqrt_price_next: sp.Float, swap_fee: sp.Float) -> Tuple[sp.Float, sp.Float, sp.Float]:
""" Computes and prints one for zero test case parameters when next square root price is known.

Returns the expected token in, token out and fee amount per share.
"""
price_next = sp.Pow(sqrt_price_next, 2)
expected_token_in_before_fee = get_expected_token_in(liquidity, sqrt_price_current, sqrt_price_next)
expected_token_in = expected_token_in_before_fee * (1 + swap_fee)

token_out = get_token_out(liquidity, sqrt_price_current, sqrt_price_next)
fee_amount_per_share = get_fee_amount_per_share(expected_token_in_before_fee, swap_fee, liquidity)

print(F"given sqrt_price_next: {sqrt_price_next}")
print(F"price_next: {price_next}")
print(F"expected_token_in: {expected_token_in}")
print(F"token_out: {token_out}")
print(F"fee_amount_per_share: {fee_amount_per_share}")

return expected_token_in, token_out, fee_amount_per_share
Loading