Skip to content

Commit

Permalink
Merge pull request #351 from White-Whale-Defi-Platform/feat/stableswa…
Browse files Browse the repository at this point in the history
…p-case-for-lp

feat(smart-contracts): add stableswap support for provide liquidity
  • Loading branch information
nseguias authored Jun 4, 2024
2 parents ee0f33c + 2f40957 commit e6e6d2a
Show file tree
Hide file tree
Showing 18 changed files with 1,688 additions and 56 deletions.
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion contracts/liquidity_hub/incentive-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ epoch-manager.workspace = true
whale-lair.workspace = true
anyhow.workspace = true
bonding-manager.workspace = true
pool-manager.workspace = true
pool-manager.workspace = true
3 changes: 3 additions & 0 deletions contracts/liquidity_hub/pool-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ incentive-manager.workspace = true
epoch-manager.workspace = true
white-whale-testing.workspace = true
bonding-manager.workspace = true
proptest = "1.0.0"
rand = "0.8.4"
stable-swap-sim = { path = "./sim", version = "^0.1" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 55857276de2241e3d09d36aba47854e0017db66f6c5a61e306b38ad0d3b8aeeb # shrinks to amp_factor = 1, initial_user_token_a_amount = 10000000, initial_user_token_b_amount = 10000000
cc 33456e9a9f11bed69ac5171155ce7a64f73f912fcbfede19046989302d1b2da9 # shrinks to amp_factor = 10, deposit_amount_a = 0, deposit_amount_b = 0, deposit_amount_c = 0, swap_token_a_amount = 0, swap_token_b_amount = 0, swap_token_c_amount = 1, pool_token_supply = 0
cc 75c3b0922c450b034b92dc8c2ea87edff47c90bbede702d84c9fd9c672e2f31f # shrinks to amp_factor = 141, deposit_amount_a = 308442737939502983046195411808336, deposit_amount_b = 0, deposit_amount_c = 0, swap_token_a_amount = 870112623450347049437652954298478, swap_token_b_amount = 501497230776538877048085549853566, swap_token_c_amount = 24063806364666791266594852039507, pool_token_supply = 2
12 changes: 12 additions & 0 deletions contracts/liquidity_hub/pool-manager/sim/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "stable-swap-sim"
version = "0.1.0"
description = "Simulations of the StableSwap invariant compared to Curve's reference implementation."
authors = ["Paul Stelzig [email protected]>"]
edition = "2021"

[lib]
name = "sim"

[dependencies]
pyo3 = { version = "0.17.3", features = ["auto-initialize"] }
172 changes: 172 additions & 0 deletions contracts/liquidity_hub/pool-manager/sim/simulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Source from: https://github.com/curvefi/curve-contract/blob/master/tests/simulation.py

class Curve:

"""
Python model of Curve pool math.
"""

def __init__(self, A, D, n, p=None, tokens=None):
"""
A: Amplification coefficient
D: Total deposit size
n: number of currencies
p: target prices
"""
self.A = A # actually A * n ** (n - 1) because it's an invariant
self.n = n
self.fee = 10 ** 7
if p:
self.p = p
else:
self.p = [10 ** 18] * n
if isinstance(D, list):
self.x = D
else:
self.x = [D // n * 10 ** 18 // _p for _p in self.p]
self.tokens = tokens

def xp(self):
return [x * p // 10 ** 18 for x, p in zip(self.x, self.p)]

def D(self):
"""
D invariant calculation in non-overflowing integer operations
iteratively
A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i))
Converging solution:
D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1)
"""
Dprev = 0
xp = self.xp()
S = sum(xp)
D = S
Ann = self.A * self.n
while abs(D - Dprev) > 1:
D_P = D
for x in xp:
D_P = D_P * D // (self.n * x)
Dprev = D
D = (Ann * S + D_P * self.n) * D // ((Ann - 1) * D + (self.n + 1) * D_P)

return D

def y(self, i, j, x):
"""
Calculate x[j] if one makes x[i] = x
Done by solving quadratic equation iteratively.
x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A)
x_1**2 + b*x_1 = c
x_1 = (x_1**2 + c) / (2*x_1 + b)
"""
D = self.D()
xx = self.xp()
xx[i] = x # x is quantity of underlying asset brought to 1e18 precision
xx = [xx[k] for k in range(self.n) if k != j]
Ann = self.A * self.n
c = D
for y in xx:
c = c * D // (y * self.n)
c = c * D // (self.n * Ann)
b = sum(xx) + D // Ann - D
y_prev = 0
y = D
while abs(y - y_prev) > 1:
y_prev = y
y = (y ** 2 + c) // (2 * y + b)
return y # the result is in underlying units too

def y_D(self, i, _D):
"""
Calculate x[j] if one makes x[i] = x
Done by solving quadratic equation iteratively.
x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A)
x_1**2 + b*x_1 = c
x_1 = (x_1**2 + c) / (2*x_1 + b)
"""
xx = self.xp()
xx = [xx[k] for k in range(self.n) if k != i]
S = sum(xx)
Ann = self.A * self.n
c = _D
for y in xx:
c = c * _D // (y * self.n)
c = c * _D // (self.n * Ann)
b = S + _D // Ann
y_prev = 0
y = _D
while abs(y - y_prev) > 1:
y_prev = y
y = (y ** 2 + c) // (2 * y + b - _D)
return y # the result is in underlying units too

def dy(self, i, j, dx):
# dx and dy are in underlying units
xp = self.xp()
return xp[j] - self.y(i, j, xp[i] + dx)

def exchange(self, i, j, dx):
xp = self.xp()
x = xp[i] + dx
y = self.y(i, j, x)
dy = xp[j] - y
fee = dy * self.fee // 10 ** 10
assert dy > 0
self.x[i] = x * 10 ** 18 // self.p[i]
self.x[j] = (y + fee) * 10 ** 18 // self.p[j]
return dy - fee

def remove_liquidity_imbalance(self, amounts):
_fee = self.fee * self.n // (4 * (self.n - 1))

old_balances = self.x
new_balances = self.x[:]
D0 = self.D()
for i in range(self.n):
new_balances[i] -= amounts[i]
self.x = new_balances
D1 = self.D()
self.x = old_balances
fees = [0] * self.n
for i in range(self.n):
ideal_balance = D1 * old_balances[i] // D0
difference = abs(ideal_balance - new_balances[i])
fees[i] = _fee * difference // 10 ** 10
new_balances[i] -= fees[i]
self.x = new_balances
D2 = self.D()
self.x = old_balances

token_amount = (D0 - D2) * self.tokens // D0

return token_amount

def calc_withdraw_one_coin(self, token_amount, i):
xp = self.xp()
xp_reduced = list(xp)

D0 = self.D()
D1 = D0 - token_amount * D0 // self.tokens
new_y = self.y_D(i, D1)

fee = self.fee * self.n // (4 * (self.n - 1))
for j in range(self.n):
dx_expected = 0
if j == i:
dx_expected = xp[j] * D1 // D0 - new_y
else:
dx_expected = xp[j] - xp[j] * D1 // D0
xp_reduced[j] -= fee * dx_expected // 10 ** 10

self.x = [x * 10 ** 18 // p for x, p in zip(xp_reduced, self.p)]
dy = xp_reduced[i] - self.y_D(i, D1) - 1 # Withdraw less to account for rounding errors
self.x = [x * 10 ** 18 // p for x, p in zip(xp, self.p)]
dy_0 = xp[i] - new_y

return dy, dy_0 - dy
Loading

0 comments on commit e6e6d2a

Please sign in to comment.