token-swap stable curve: 256-bit math added with tests (#886)
* Stable curve math rewritten to 256-bit, reference simulation added with test integration * Formatting error fixed * Clippy warnings fixed * Fixed dependencies to remove crates not supportinf bpf * Fixed indentations * Fixed lint errors * Fixed format warnings * Fixing clippy lint errors * U256 math and simulation library refactoring, checked math used everywhere, other small fixes * Fixed conversion to optional u128
This commit is contained in:
parent
8619764700
commit
a0d0ae63ca
|
@ -1 +0,0 @@
|
|||
lib
|
|
@ -18,9 +18,11 @@ num-traits = "0.2"
|
|||
solana-program = "1.4.9"
|
||||
spl-token = { version = "3.0", path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
thiserror = "1.0"
|
||||
uint = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-sdk = "1.4.9"
|
||||
sim = { path = "./sim" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghost"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8"
|
||||
dependencies = [
|
||||
"indoc-impl",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc-impl"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63312a18f7ea8760cdd0a7c5aac1a619752a246b833545e3e36d1f81f7cd9e66"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fedd49de24d8c263613701406611410687148ae8c37cd6452650b250f753a0dd"
|
||||
dependencies = [
|
||||
"ctor",
|
||||
"ghost",
|
||||
"inventory-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory-impl"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddead8880bc50f57fcd3b5869a7f6ff92570bb4e8f6870c22e2483272f2256da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cloudabi",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880"
|
||||
dependencies = [
|
||||
"paste-impl",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste-impl"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9b90d637542bbf29b140fdd38fa308424073fd2cdf641a5680aed8020145e3c"
|
||||
dependencies = [
|
||||
"ctor",
|
||||
"indoc",
|
||||
"inventory",
|
||||
"libc",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"pyo3cls",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-derive-backend"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cee2c9fb095acb885ab7e85acc7c8e95da8c4bc7cc4b4ea64b566dfc8c91046a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3cls"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12fdd8a2f217d003c93f9819e3db1717b2e89530171edea4c0deadd90206f50"
|
||||
dependencies = [
|
||||
"pyo3-derive-backend",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "sim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pyo3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e03e57e4fcbfe7749842d53e24ccb9aa12b7252dbe5e91d2acad31834c8b8fdd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "sim"
|
||||
version = "0.1.0"
|
||||
authors = ["michaelhly <michaelhly@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
pyo3 = "0.12.3"
|
|
@ -0,0 +1,188 @@
|
|||
# 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, fee = 10 ** 7, 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 = fee
|
||||
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
|
||||
|
||||
counter = 0
|
||||
|
||||
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)
|
||||
|
||||
counter += 1
|
||||
if counter > 1000:
|
||||
break
|
||||
|
||||
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
|
||||
|
||||
counter = 0
|
||||
|
||||
while abs(y - y_prev) > 1:
|
||||
y_prev = y
|
||||
y = (y ** 2 + c) // (2 * y + b)
|
||||
|
||||
counter += 1
|
||||
if counter > 1000:
|
||||
break
|
||||
|
||||
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
|
||||
|
||||
counter = 0
|
||||
|
||||
while abs(y - y_prev) > 1:
|
||||
y_prev = y
|
||||
y = (y ** 2 + c) // (2 * y + b - _D)
|
||||
|
||||
counter += 1
|
||||
if counter > 1000:
|
||||
break
|
||||
|
||||
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
|
||||
if dy == 0:
|
||||
return 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()
|
||||
if self.fee:
|
||||
fee = self.fee - self.fee * xp[i] // sum(xp) + 5 * 10 ** 5
|
||||
else:
|
||||
fee = 0
|
||||
|
||||
D0 = self.D()
|
||||
D1 = D0 - token_amount * D0 // self.tokens
|
||||
dy = xp[i] - self.y_D(i, D1)
|
||||
|
||||
return dy - dy * fee // 10 ** 10
|
|
@ -0,0 +1,197 @@
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyTuple;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
|
||||
const FILE_NAME: &str = "simulation.py";
|
||||
const FILE_PATH: &str = "sim/simulation.py";
|
||||
const MODULE_NAME: &str = "simulation";
|
||||
|
||||
const DEFAULT_POOL_TOKENS: u128 = 0;
|
||||
const DEFAULT_TARGET_PRICE: u128 = 1000000000000000000;
|
||||
pub const MODEL_FEE_NUMERATOR: u128 = 1;
|
||||
pub const MODEL_FEE_DENOMINATOR: u128 = 1000;
|
||||
|
||||
pub struct StableSwapModel {
|
||||
py_src: String,
|
||||
pub amp_factor: u128,
|
||||
pub balances: Vec<u128>,
|
||||
pub n_coins: u8,
|
||||
pub fee: u128,
|
||||
pub target_prices: Vec<u128>,
|
||||
pub pool_tokens: u128,
|
||||
}
|
||||
|
||||
impl StableSwapModel {
|
||||
pub fn new(amp_factor: u128, balances: Vec<u128>, n_coins: u8) -> StableSwapModel {
|
||||
let mut src_file = File::open(FILE_PATH).unwrap();
|
||||
let mut src_content = String::new();
|
||||
let _ = src_file.read_to_string(&mut src_content);
|
||||
|
||||
Self {
|
||||
py_src: src_content,
|
||||
amp_factor,
|
||||
balances,
|
||||
n_coins,
|
||||
fee: 0,
|
||||
target_prices: vec![DEFAULT_TARGET_PRICE, DEFAULT_TARGET_PRICE],
|
||||
pool_tokens: DEFAULT_POOL_TOKENS,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_pool_tokens(
|
||||
amp_factor: u128,
|
||||
balances: Vec<u128>,
|
||||
n_coins: u8,
|
||||
pool_token_amount: u128,
|
||||
) -> StableSwapModel {
|
||||
let mut src_file = File::open(FILE_PATH).unwrap();
|
||||
let mut src_content = String::new();
|
||||
let _ = src_file.read_to_string(&mut src_content);
|
||||
|
||||
Self {
|
||||
py_src: src_content,
|
||||
amp_factor,
|
||||
balances,
|
||||
n_coins,
|
||||
fee: 0,
|
||||
target_prices: vec![DEFAULT_TARGET_PRICE, DEFAULT_TARGET_PRICE],
|
||||
pool_tokens: pool_token_amount,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sim_d(&self) -> u128 {
|
||||
let gil = Python::acquire_gil();
|
||||
return self
|
||||
.call0(gil.python(), "D")
|
||||
.unwrap()
|
||||
.extract(gil.python())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn sim_dy(&self, i: u128, j: u128, dx: u128) -> u128 {
|
||||
let gil = Python::acquire_gil();
|
||||
return self
|
||||
.call1(gil.python(), "dy", (i, j, dx))
|
||||
.unwrap()
|
||||
.extract(gil.python())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn sim_exchange(&self, i: u128, j: u128, dx: u128) -> u128 {
|
||||
let gil = Python::acquire_gil();
|
||||
return self
|
||||
.call1(gil.python(), "exchange", (i, j, dx))
|
||||
.unwrap()
|
||||
.extract(gil.python())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn sim_xp(&self) -> Vec<u128> {
|
||||
let gil = Python::acquire_gil();
|
||||
return self
|
||||
.call0(gil.python(), "xp")
|
||||
.unwrap()
|
||||
.extract(gil.python())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn sim_y(&self, i: u128, j: u128, x: u128) -> u128 {
|
||||
let gil = Python::acquire_gil();
|
||||
return self
|
||||
.call1(gil.python(), "y", (i, j, x))
|
||||
.unwrap()
|
||||
.extract(gil.python())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn sim_y_d(&self, i: u128, d: u128) -> u128 {
|
||||
let gil = Python::acquire_gil();
|
||||
return self
|
||||
.call1(gil.python(), "y_D", (i, d))
|
||||
.unwrap()
|
||||
.extract(gil.python())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn sim_remove_liquidity_imbalance(&self, amounts: Vec<u128>) -> u128 {
|
||||
let gil = Python::acquire_gil();
|
||||
return self
|
||||
.call1(
|
||||
gil.python(),
|
||||
"remove_liquidity_imbalance",
|
||||
PyTuple::new(gil.python(), amounts.to_vec()),
|
||||
)
|
||||
.unwrap()
|
||||
.extract(gil.python())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn sim_calc_withdraw_one_coin(&self, token_amount: u128, i: u128) -> u128 {
|
||||
let gil = Python::acquire_gil();
|
||||
return self
|
||||
.call1(gil.python(), "calc_withdraw_one_coin", (token_amount, i))
|
||||
.unwrap()
|
||||
.extract(gil.python())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn call0(&self, py: Python, method_name: &str) -> Result<PyObject, PyErr> {
|
||||
let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap();
|
||||
let model = sim
|
||||
.call1(
|
||||
"Curve",
|
||||
(
|
||||
self.amp_factor,
|
||||
self.balances.to_vec(),
|
||||
self.n_coins,
|
||||
self.fee,
|
||||
self.target_prices.to_vec(),
|
||||
self.pool_tokens,
|
||||
),
|
||||
)
|
||||
.unwrap()
|
||||
.to_object(py);
|
||||
let py_ret = model.as_ref(py).call_method0(method_name);
|
||||
self.extract_py_ret(py, py_ret)
|
||||
}
|
||||
|
||||
fn call1(
|
||||
&self,
|
||||
py: Python,
|
||||
method_name: &str,
|
||||
args: impl IntoPy<Py<PyTuple>>,
|
||||
) -> Result<PyObject, PyErr> {
|
||||
let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap();
|
||||
let model = sim
|
||||
.call1(
|
||||
"Curve",
|
||||
(
|
||||
self.amp_factor,
|
||||
self.balances.to_vec(),
|
||||
self.n_coins,
|
||||
self.fee,
|
||||
self.target_prices.to_vec(),
|
||||
self.pool_tokens,
|
||||
),
|
||||
)
|
||||
.unwrap()
|
||||
.to_object(py);
|
||||
let py_ret = model.as_ref(py).call_method1(method_name, args);
|
||||
self.extract_py_ret(py, py_ret)
|
||||
}
|
||||
|
||||
fn extract_py_ret(&self, py: Python, ret: PyResult<&PyAny>) -> Result<PyObject, PyErr> {
|
||||
match ret {
|
||||
Ok(v) => v.extract(),
|
||||
Err(e) => {
|
||||
e.print_and_set_sys_last_vars(py);
|
||||
panic!("Python execution failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_src(&self) {
|
||||
println!("{}", self.py_src);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
//! Defines useful math utils
|
||||
|
||||
#![allow(clippy::assign_op_pattern)]
|
||||
#![allow(clippy::ptr_offset_with_cast)]
|
||||
#![allow(clippy::unknown_clippy_lints)]
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
|
||||
use uint::construct_uint;
|
||||
|
||||
construct_uint! {
|
||||
pub struct U256(4);
|
||||
}
|
||||
|
||||
impl U256 {
|
||||
/// Returns selt to the power of b
|
||||
pub fn checked_u8_power(&self, b: u8) -> Option<U256> {
|
||||
let mut result = *self;
|
||||
for _ in 1..b {
|
||||
result = result.checked_mul(*self)?;
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// Returns self multiplied by b
|
||||
pub fn checked_u8_mul(&self, b: u8) -> Option<U256> {
|
||||
let mut result = *self;
|
||||
for _ in 1..b {
|
||||
result = result.checked_add(*self)?;
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// Returns true of values differ not more than by 1
|
||||
pub fn almost_equal(&self, b: &U256) -> Option<bool> {
|
||||
if self > b {
|
||||
Some(self.checked_sub(*b)? <= U256::one())
|
||||
} else {
|
||||
Some(b.checked_sub(*self)? <= U256::one())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,4 +4,5 @@ pub mod base;
|
|||
pub mod calculator;
|
||||
pub mod constant_product;
|
||||
pub mod flat;
|
||||
pub mod math;
|
||||
pub mod stable;
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
//! The curve.fi invariant calculator.
|
||||
|
||||
use crate::curve::math::U256;
|
||||
use solana_program::{
|
||||
program_error::ProgramError,
|
||||
program_pack::{IsInitialized, Pack, Sealed},
|
||||
};
|
||||
|
||||
use crate::curve::calculator::{
|
||||
calculate_fee, map_zero_to_none, CurveCalculator, DynPack, SwapResult,
|
||||
};
|
||||
use crate::curve::calculator::{calculate_fee, CurveCalculator, DynPack, SwapResult};
|
||||
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
|
||||
use num_traits::checked_pow;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
const N_COINS: u8 = 2;
|
||||
const N_COINS_SQUARED: u8 = 4;
|
||||
|
||||
/// StableCurve struct implementing CurveCalculator
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct StableCurve {
|
||||
|
@ -36,20 +37,14 @@ pub struct StableCurve {
|
|||
}
|
||||
|
||||
/// d = (leverage * sum_x + d_product * n_coins) * initial_d / ((leverage - 1) * initial_d + (n_coins + 1) * d_product)
|
||||
fn calculate_step(
|
||||
initial_d: u128,
|
||||
leverage: u128,
|
||||
sum_x: u128,
|
||||
d_product: u128,
|
||||
n_coins: u128,
|
||||
) -> Option<u128> {
|
||||
let leverage_mul = leverage.checked_mul(sum_x)?;
|
||||
let d_p_mul = d_product.checked_mul(n_coins)?;
|
||||
fn calculate_step(initial_d: &U256, leverage: u64, sum_x: u128, d_product: &U256) -> Option<U256> {
|
||||
let leverage_mul = U256::from(leverage).checked_mul(sum_x.into())?;
|
||||
let d_p_mul = d_product.checked_u8_mul(N_COINS)?;
|
||||
|
||||
let l_val = leverage_mul.checked_add(d_p_mul)?.checked_mul(initial_d)?;
|
||||
let l_val = leverage_mul.checked_add(d_p_mul)?.checked_mul(*initial_d)?;
|
||||
|
||||
let leverage_sub = leverage.checked_sub(1)?.checked_mul(initial_d)?;
|
||||
let n_coins_sum = n_coins.checked_add(1)?.checked_mul(d_product)?;
|
||||
let leverage_sub = initial_d.checked_mul((leverage.checked_sub(1)?).into())?;
|
||||
let n_coins_sum = d_product.checked_u8_mul(N_COINS.checked_add(1)?)?;
|
||||
|
||||
let r_val = leverage_sub.checked_add(n_coins_sum)?;
|
||||
|
||||
|
@ -59,20 +54,18 @@ fn calculate_step(
|
|||
/// Compute stable swap invariant (D)
|
||||
/// Equation:
|
||||
/// A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i))
|
||||
fn compute_d(amp: u128, amount_a: u128, amount_b: u128) -> Option<u128> {
|
||||
// XXX: Curve uses u256
|
||||
let n_coins: u128 = 2; // n
|
||||
let amount_a_times_coins = amount_a.checked_mul(n_coins)?;
|
||||
let amount_b_times_coins = amount_b.checked_mul(n_coins)?;
|
||||
fn compute_d(leverage: u64, amount_a: u128, amount_b: u128) -> Option<u128> {
|
||||
let amount_a_times_coins = U256::from(amount_a).checked_u8_mul(N_COINS)?;
|
||||
let amount_b_times_coins = U256::from(amount_b).checked_u8_mul(N_COINS)?;
|
||||
let sum_x = amount_a.checked_add(amount_b)?; // sum(x_i), a.k.a S
|
||||
if sum_x == 0 {
|
||||
Some(0)
|
||||
} else {
|
||||
let mut d_previous: u128;
|
||||
let mut d = sum_x;
|
||||
let leverage = amp.checked_mul(n_coins)?; // A * n
|
||||
let mut d_previous: U256;
|
||||
let mut d: U256 = sum_x.into();
|
||||
|
||||
// Newton's method to approximate D
|
||||
for _ in 0..128 {
|
||||
for _ in 0..32 {
|
||||
let mut d_product = d;
|
||||
d_product = d_product
|
||||
.checked_mul(d)?
|
||||
|
@ -82,17 +75,13 @@ fn compute_d(amp: u128, amount_a: u128, amount_b: u128) -> Option<u128> {
|
|||
.checked_div(amount_b_times_coins)?;
|
||||
d_previous = d;
|
||||
//d = (leverage * sum_x + d_p * n_coins) * d / ((leverage - 1) * d + (n_coins + 1) * d_p);
|
||||
d = calculate_step(d, leverage, sum_x, d_product, n_coins)?;
|
||||
d = calculate_step(&d, leverage, sum_x, &d_product)?;
|
||||
// Equality with the precision of 1
|
||||
if d > d_product {
|
||||
if d.checked_sub(d_previous)? <= 1 {
|
||||
break;
|
||||
}
|
||||
} else if d_previous.checked_sub(d)? <= 1 {
|
||||
if d.almost_equal(&d_previous)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(d)
|
||||
u128::try_from(d).ok()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,35 +89,41 @@ fn compute_d(amp: u128, amount_a: u128, amount_b: u128) -> Option<u128> {
|
|||
/// Solve for y:
|
||||
/// y**2 + y * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A)
|
||||
/// y**2 + b*y = c
|
||||
fn compute_new_destination_amount(amp: u128, new_source_amount: u128, d_val: u128) -> Option<u128> {
|
||||
// XXX: Curve uses u256
|
||||
let n_coins: u128 = 2;
|
||||
let leverage = amp.checked_mul(n_coins)?; // A * n
|
||||
fn compute_new_destination_amount(
|
||||
leverage: u64,
|
||||
new_source_amount: u128,
|
||||
d_val: u128,
|
||||
) -> Option<u128> {
|
||||
// Upscale to U256
|
||||
let leverage: U256 = leverage.into();
|
||||
let new_source_amount: U256 = new_source_amount.into();
|
||||
let d_val: U256 = d_val.into();
|
||||
|
||||
// sum' = prod' = x
|
||||
// c = D ** (n + 1) / (n ** (2 * n) * prod' * A)
|
||||
let c = checked_pow(d_val, n_coins.checked_add(1)? as usize)?.checked_div(
|
||||
new_source_amount.checked_mul(checked_pow(n_coins, 2)?.checked_mul(leverage)?)?,
|
||||
let c = d_val
|
||||
.checked_u8_power(N_COINS.checked_add(1)?)?
|
||||
.checked_div(
|
||||
new_source_amount
|
||||
.checked_u8_mul(N_COINS_SQUARED)?
|
||||
.checked_mul(leverage)?,
|
||||
)?;
|
||||
|
||||
// b = sum' - (A*n**n - 1) * D / (A * n**n)
|
||||
let b = new_source_amount.checked_add(d_val.checked_div(leverage)?)?; // d is subtracted on line 82
|
||||
let b = new_source_amount.checked_add(d_val.checked_div(leverage)?)?;
|
||||
|
||||
// Solve for y by approximating: y**2 + b*y = c
|
||||
let mut y_prev: u128;
|
||||
let mut y_prev: U256;
|
||||
let mut y = d_val;
|
||||
for _ in 0..128 {
|
||||
for _ in 0..32 {
|
||||
y_prev = y;
|
||||
y = (checked_pow(y, 2)?.checked_add(c)?)
|
||||
.checked_div(y.checked_mul(2)?.checked_add(b)?.checked_sub(d_val)?)?;
|
||||
if y > y_prev {
|
||||
if y.checked_sub(y_prev)? <= 1 {
|
||||
break;
|
||||
}
|
||||
} else if y_prev.checked_sub(y)? <= 1 {
|
||||
y = (y.checked_u8_power(2)?.checked_add(c)?)
|
||||
.checked_div(y.checked_u8_mul(2)?.checked_add(b)?.checked_sub(d_val)?)?;
|
||||
if y.almost_equal(&y_prev)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(y)
|
||||
u128::try_from(y).ok()
|
||||
}
|
||||
|
||||
impl CurveCalculator for StableCurve {
|
||||
|
@ -147,18 +142,17 @@ impl CurveCalculator for StableCurve {
|
|||
.checked_sub(trade_fee)?
|
||||
.checked_sub(owner_fee)?;
|
||||
|
||||
let leverage = self.amp.checked_mul(N_COINS as u64)?;
|
||||
|
||||
let new_destination_amount = compute_new_destination_amount(
|
||||
self.amp as u128,
|
||||
leverage,
|
||||
new_source_amount_less_fee,
|
||||
compute_d(
|
||||
self.amp as u128,
|
||||
swap_source_amount,
|
||||
swap_destination_amount,
|
||||
)?,
|
||||
compute_d(leverage, swap_source_amount, swap_destination_amount)?,
|
||||
)?;
|
||||
|
||||
let amount_swapped =
|
||||
map_zero_to_none(swap_destination_amount.checked_sub(new_destination_amount)?)?;
|
||||
//let amount_swapped =
|
||||
// map_zero_to_none(swap_destination_amount.checked_sub(new_destination_amount.as_u128())?)?;
|
||||
let amount_swapped = swap_destination_amount.checked_sub(new_destination_amount)?;
|
||||
let new_source_amount = swap_source_amount.checked_add(source_amount)?;
|
||||
Some(SwapResult {
|
||||
new_source_amount,
|
||||
|
@ -278,6 +272,7 @@ impl DynPack for StableCurve {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::curve::calculator::INITIAL_SWAP_POOL_AMOUNT;
|
||||
use sim::StableSwapModel;
|
||||
|
||||
#[test]
|
||||
fn initial_pool_amount() {
|
||||
|
@ -412,20 +407,84 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn constant_product_swap_no_fee() {
|
||||
let source_amount: u128 = 100;
|
||||
let swap_source_amount: u128 = 1000;
|
||||
let swap_destination_amount: u128 = 50000;
|
||||
const POOL_AMOUNTS: &[u128] = &[
|
||||
100,
|
||||
10_000,
|
||||
1_000_000,
|
||||
100_000_000,
|
||||
10_000_000_000,
|
||||
1_000_000_000_000,
|
||||
100_000_000_000_000,
|
||||
10_000_000_000_000_000,
|
||||
1_000_000_000_000_000_000,
|
||||
];
|
||||
const SWAP_AMOUNTS: &[u128] = &[
|
||||
100,
|
||||
1_000,
|
||||
10_000,
|
||||
100_000,
|
||||
1_000_000,
|
||||
10_000_000,
|
||||
100_000_000,
|
||||
1_000_000_000,
|
||||
10_000_000_000,
|
||||
100_000_000_000,
|
||||
];
|
||||
const AMP_FACTORS: &[u64] = &[1, 10, 20, 50, 75, 100, 125, 150];
|
||||
|
||||
for swap_source_amount in POOL_AMOUNTS {
|
||||
for swap_destination_amount in POOL_AMOUNTS {
|
||||
for source_amount in SWAP_AMOUNTS {
|
||||
for amp in AMP_FACTORS {
|
||||
let curve = StableCurve {
|
||||
amp: 1,
|
||||
amp: *amp,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = curve
|
||||
.swap(source_amount, swap_source_amount, swap_destination_amount)
|
||||
.unwrap();
|
||||
assert_eq!(result.new_source_amount, 1100);
|
||||
assert_eq!(result.amount_swapped, 2083);
|
||||
assert_eq!(result.new_destination_amount, 47917);
|
||||
if *source_amount >= *swap_source_amount {
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(
|
||||
"trying: source_amount={}, swap_source_amount={}, swap_destination_amount={}",
|
||||
source_amount, swap_source_amount, swap_destination_amount
|
||||
);
|
||||
|
||||
let model: StableSwapModel = StableSwapModel::new(
|
||||
curve.amp.into(),
|
||||
vec![*swap_source_amount, *swap_destination_amount],
|
||||
N_COINS,
|
||||
);
|
||||
|
||||
let result = curve.swap(
|
||||
*source_amount,
|
||||
*swap_source_amount,
|
||||
*swap_destination_amount,
|
||||
);
|
||||
|
||||
let result = result.unwrap();
|
||||
let sim_result = model.sim_exchange(0, 1, *source_amount);
|
||||
|
||||
println!(
|
||||
"result={}, sim_result={}",
|
||||
result.amount_swapped, sim_result
|
||||
);
|
||||
let diff = (sim_result as i128 - result.amount_swapped as i128).abs();
|
||||
|
||||
assert!(
|
||||
diff <= 1,
|
||||
"result={}, sim_result={}, amp={}, source_amount={}, swap_source_amount={}, swap_destination_amount={}",
|
||||
result.amount_swapped,
|
||||
sim_result,
|
||||
amp,
|
||||
source_amount,
|
||||
swap_source_amount,
|
||||
swap_destination_amount
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Reference in New Issue