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:
Yuriy Savchenko 2020-11-25 22:19:25 +02:00 committed by GitHub
parent 8619764700
commit a0d0ae63ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 859 additions and 73 deletions

View File

@ -1 +0,0 @@
lib

View File

@ -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"]

289
token-swap/program/sim/Cargo.lock generated Normal file
View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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);
}
}

View File

@ -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())
}
}
}

View File

@ -4,4 +4,5 @@ pub mod base;
pub mod calculator;
pub mod constant_product;
pub mod flat;
pub mod math;
pub mod stable;

View File

@ -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]