From 5bb95adaabf0e3e71d4ba59b27e8f241053cf3d8 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Fri, 19 Nov 2021 16:35:48 -0300 Subject: [PATCH 01/43] bigint pool math, stable and weighted + test cases --- src/poolsMath/basicOperations.ts | 611 +++++++++++++++++++++++++++++++ src/poolsMath/stable.ts | 203 ++++++++++ src/poolsMath/weighted.ts | 48 +++ test/poolsMath.spec.ts | 173 +++++++++ tsconfig.json | 2 +- 5 files changed, 1036 insertions(+), 1 deletion(-) create mode 100644 src/poolsMath/basicOperations.ts create mode 100644 src/poolsMath/stable.ts create mode 100644 src/poolsMath/weighted.ts create mode 100644 test/poolsMath.spec.ts diff --git a/src/poolsMath/basicOperations.ts b/src/poolsMath/basicOperations.ts new file mode 100644 index 00000000..52157154 --- /dev/null +++ b/src/poolsMath/basicOperations.ts @@ -0,0 +1,611 @@ +export const BZERO = BigInt(0); +export const BONE = BigInt(1); + +const _require = (b: boolean, message: string) => { + if (!b) throw new Error(message); +}; + +export class MathSol { + /** + * @dev Returns the addition of two unsigned integers of 256 bits, reverting on overflow. + */ + // add(a: bigint, b: bigint): bigint { + // const c = a + b; + // // _require(c >= a, Errors.ADD_OVERFLOW); + // return c; + // } + + /** + * @dev Returns the addition of two signed integers, reverting on overflow. + */ + static add(a: bigint, b: bigint): bigint { + const c = a + b; + _require((b >= 0 && c >= a) || (b < 0 && c < a), 'Errors.ADD_OVERFLOW'); + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers of 256 bits, reverting on overflow. + */ + static sub(a: bigint, b: bigint): bigint { + _require(b <= a, 'Errors.SUB_OVERFLOW'); + const c = a - b; + return c; + } + + /** + * @dev Returns the subtraction of two signed integers, reverting on overflow. + */ + // sub(int256 a, int256 b) internal pure returns (int256) { + // int256 c = a - b; + // // _require((b >= 0 && c <= a) || (b < 0 && c > a), Errors.SUB_OVERFLOW); + // return c; + // } + + /** + * @dev Returns the largest of two numbers of 256 bits. + */ + static max(a: bigint, b: bigint): bigint { + return a >= b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers of 256 bits. + */ + static min(a: bigint, b: bigint): bigint { + return a < b ? a : b; + } + + static mul(a: bigint, b: bigint): bigint { + const c = a * b; + _require(a == BZERO || c / a == b, 'Errors.MUL_OVERFLOW'); + return c; + } + + static div(a: bigint, b: bigint, roundUp: boolean): bigint { + return roundUp ? this.divUp(a, b) : this.divDown(a, b); + } + + static divDown(a: bigint, b: bigint): bigint { + _require(b != BZERO, 'Errors.ZERO_DIVISION'); + return a / b; + } + + static divUp(a: bigint, b: bigint): bigint { + _require(b != BZERO, 'Errors.ZERO_DIVISION'); + + if (a == BZERO) { + return BZERO; + } else { + return BONE + (a - BONE) / b; + } + } + + // Modification: Taken from the fixed point class + static ONE = BigInt('1000000000000000000'); // 18 decimal places + static MAX_POW_RELATIVE_ERROR = BigInt(10000); + + static mulUpFixed(a: bigint, b: bigint): bigint { + const product = a * b; + _require(a == BZERO || product / a == b, 'Errors.MUL_OVERFLOW'); + + if (product == BZERO) { + return BZERO; + } else { + // The traditional divUp formula is: + // divUp(x, y) := (x + y - 1) / y + // To avoid intermediate overflow in the addition, we distribute the division and get: + // divUp(x, y) := (x - 1) / y + 1 + // Note that this requires x != 0, which we already tested for. + + return (product - BONE) / this.ONE + BONE; + } + } + + // Modification: Taken from the fixed point class + static divDownFixed(a: bigint, b: bigint): bigint { + _require(b != BZERO, 'Errors.ZERO_DIVISION'); + if (a == BZERO) { + return BZERO; + } else { + const aInflated = a * this.ONE; + // _require(aInflated / a == ONE, Errors.DIV_INTERNAL); // mul overflow + + return aInflated / b; + } + } + + // Modification: Taken from the fixed point class + static divUpFixed(a: bigint, b: bigint): bigint { + _require(b != BZERO, 'Errors.ZERO_DIVISION'); + + if (a == BZERO) { + return BZERO; + } else { + const aInflated = a * this.ONE; + _require(aInflated / a == this.ONE, 'Errors.DIV_INTERNAL'); // mul overflow + + // The traditional divUp formula is: + // divUp(x, y) := (x + y - 1) / y + // To avoid intermediate overflow in the addition, we distribute the division and get: + // divUp(x, y) := (x - 1) / y + 1 + // Note that this requires x != 0, which we already tested for. + + return (aInflated - BONE) / b + BONE; + } + } + + // Modification: Taken from the fixed point class + static powUpFixed(x: bigint, y: bigint): bigint { + const raw = LogExpMath.pow(x, y); + const maxError = this.add( + this.mulUpFixed(raw, this.MAX_POW_RELATIVE_ERROR), + BONE + ); + + return this.add(raw, maxError); + } + + // Modification: Taken from the fixed point class + static complementFixed(x: bigint): bigint { + return x < this.ONE ? this.ONE - x : BZERO; + } + + static mulDownFixed(a: bigint, b: bigint): bigint { + const product = a * b; + _require(a == BZERO || product / a == b, 'Errors.MUL_OVERFLOW'); + + return product / this.ONE; + } +} + +class LogExpMath { + // All fixed point multiplications and divisions are inlined. This means we need to divide by ONE when multiplying + // two numbers, and multiply by ONE when dividing them. + + // All arguments and return values are 18 decimal fixed point numbers. + static ONE_18: bigint = BigInt('1000000000000000000'); + + // Internally, intermediate values are computed with higher precision as 20 decimal fixed point numbers, and in the + // case of ln36, 36 decimals. + static ONE_20: bigint = BigInt('100000000000000000000'); + static ONE_36: bigint = BigInt('1000000000000000000000000000000000000'); + + // The domain of natural exponentiation is bound by the word size and number of decimals used. + // + // Because internally the result will be stored using 20 decimals, the largest possible result is + // (2^255 - 1) / 10^20, which makes the largest exponent ln((2^255 - 1) / 10^20) = 130.700829182905140221. + // The smallest possible result is 10^(-18), which makes largest negative argument + // ln(10^(-18)) = -41.446531673892822312. + // We use 130.0 and -41.0 to have some safety margin. + static MAX_NATURAL_EXPONENT: bigint = BigInt('130000000000000000000'); + static MIN_NATURAL_EXPONENT: bigint = BigInt('-41000000000000000000'); + + // Bounds for ln_36's argument. Both ln(0.9) and ln(1.1) can be represented with 36 decimal places in a fixed point + // 256 bit integer. + static LN_36_LOWER_BOUND: bigint = + BigInt(LogExpMath.ONE_18) - BigInt('100000000000000000'); + static LN_36_UPPER_BOUND: bigint = + BigInt(LogExpMath.ONE_18) + BigInt('100000000000000000'); + + static MILD_EXPONENT_BOUND: bigint = + BigInt(2) ** BigInt(254) / LogExpMath.ONE_20; + + // 18 decimal constants + static x0: bigint = BigInt('128000000000000000000'); // 2ˆ7 + static a0: bigint = BigInt( + '38877084059945950922200000000000000000000000000000000000' + ); // eˆ(x0) (no decimals) + static x1: bigint = BigInt('64000000000000000000'); // 2ˆ6 + static a1: bigint = BigInt('6235149080811616882910000000'); // eˆ(x1) (no decimals) + + // 20 decimal constants + static x2: bigint = BigInt('3200000000000000000000'); // 2ˆ5 + static a2: bigint = BigInt('7896296018268069516100000000000000'); // eˆ(x2) + static x3: bigint = BigInt('1600000000000000000000'); // 2ˆ4 + static a3: bigint = BigInt('888611052050787263676000000'); // eˆ(x3) + static x4: bigint = BigInt('800000000000000000000'); // 2ˆ3 + static a4: bigint = BigInt('298095798704172827474000'); // eˆ(x4) + static x5: bigint = BigInt('400000000000000000000'); // 2ˆ2 + static a5: bigint = BigInt('5459815003314423907810'); // eˆ(x5) + static x6: bigint = BigInt('200000000000000000000'); // 2ˆ1 + static a6: bigint = BigInt('738905609893065022723'); // eˆ(x6) + static x7: bigint = BigInt('100000000000000000000'); // 2ˆ0 + static a7: bigint = BigInt('271828182845904523536'); // eˆ(x7) + static x8: bigint = BigInt('50000000000000000000'); // 2ˆ-1 + static a8: bigint = BigInt('164872127070012814685'); // eˆ(x8) + static x9: bigint = BigInt('25000000000000000000'); // 2ˆ-2 + static a9: bigint = BigInt('128402541668774148407'); // eˆ(x9) + static x10: bigint = BigInt('12500000000000000000'); // 2ˆ-3 + static a10: bigint = BigInt('113314845306682631683'); // eˆ(x10) + static x11: bigint = BigInt('6250000000000000000'); // 2ˆ-4 + static a11: bigint = BigInt('106449445891785942956'); // eˆ(x11) + + // All arguments and return values are 18 decimal fixed point numbers. + static pow(x: bigint, y: bigint): bigint { + if (y === BZERO) { + // We solve the 0^0 indetermination by making it equal one. + return this.ONE_18; + } + + if (x == BZERO) { + return BZERO; + } + + // Instead of computing x^y directly, we instead rely on the properties of logarithms and exponentiation to + // arrive at that result. In particular, exp(ln(x)) = x, and ln(x^y) = y * ln(x). This means + // x^y = exp(y * ln(x)). + + // The ln function takes a signed value, so we need to make sure x fits in the signed 256 bit range. + _require( + x < + BigInt( + '57896044618658097711785492504343953926634992332820282019728792003956564819968' + ), + 'Errors.X_OUT_OF_BOUNDS' + ); + const x_int256 = x; + + // We will compute y * ln(x) in a single step. Depending on the value of x, we can either use ln or ln_36. In + // both cases, we leave the division by ONE_18 (due to fixed point multiplication) to the end. + + // This prevents y * ln(x) from overflowing, and at the same time guarantees y fits in the signed 256 bit range. + _require(y < this.MILD_EXPONENT_BOUND, 'Errors.Y_OUT_OF_BOUNDS'); + const y_int256 = y; + + let logx_times_y; + if ( + this.LN_36_LOWER_BOUND < x_int256 && + x_int256 < this.LN_36_UPPER_BOUND + ) { + const ln_36_x = this._ln_36(x_int256); + + // ln_36_x has 36 decimal places, so multiplying by y_int256 isn't as straightforward, since we can't just + // bring y_int256 to 36 decimal places, as it might overflow. Instead, we perform two 18 decimal + // multiplications and add the results: one with the first 18 decimals of ln_36_x, and one with the + // (downscaled) last 18 decimals. + logx_times_y = + (ln_36_x / this.ONE_18) * y_int256 + + ((ln_36_x % this.ONE_18) * y_int256) / this.ONE_18; + } else { + logx_times_y = this._ln(x_int256) * y_int256; + } + logx_times_y /= this.ONE_18; + + // Finally, we compute exp(y * ln(x)) to arrive at x^y + _require( + this.MIN_NATURAL_EXPONENT <= logx_times_y && + logx_times_y <= this.MAX_NATURAL_EXPONENT, + 'Errors.PRODUCT_OUT_OF_BOUNDS' + ); + + // return uint256(exp(logx_times_y)); + return this.exp(logx_times_y); + } + + static exp(x: bigint): bigint { + _require( + x >= this.MIN_NATURAL_EXPONENT && x <= this.MAX_NATURAL_EXPONENT, + 'Errors.INVALID_EXPONENT' + ); + + if (x < 0) { + // We only handle positive exponents: e^(-x) is computed as 1 / e^x. We can safely make x positive since it + // fits in the signed 256 bit range (as it is larger than MIN_NATURAL_EXPONENT). + // Fixed point division requires multiplying by ONE_18. + return (this.ONE_18 * this.ONE_18) / this.exp(BigInt(-1) * x); + } + + // First, we use the fact that e^(x+y) = e^x * e^y to decompose x into a sum of powers of two, which we call x_n, + // where x_n == 2^(7 - n), and e^x_n = a_n has been precomputed. We choose the first x_n, x0, to equal 2^7 + // because all larger powers are larger than MAX_NATURAL_EXPONENT, and therefore not present in the + // decomposition. + // At the end of this process we will have the product of all e^x_n = a_n that apply, and the remainder of this + // decomposition, which will be lower than the smallest x_n. + // exp(x) = k_0 * a_0 * k_1 * a_1 * ... + k_n * a_n * exp(remainder), where each k_n equals either 0 or 1. + // We mutate x by subtracting x_n, making it the remainder of the decomposition. + + // The first two a_n (e^(2^7) and e^(2^6)) are too large if stored as 18 decimal numbers, and could cause + // intermediate overflows. Instead we store them as plain integers, with 0 decimals. + // Additionally, x0 + x1 is larger than MAX_NATURAL_EXPONENT, which means they will not both be present in the + // decomposition. + + // For each x_n, we test if that term is present in the decomposition (if x is larger than it), and if so deduct + // it and compute the accumulated product. + + let firstAN; + if (x >= this.x0) { + x -= this.x0; + firstAN = this.a0; + } else if (x >= this.x1) { + x -= this.x1; + firstAN = this.a1; + } else { + firstAN = BigInt(1); // One with no decimal places + } + + // We now transform x into a 20 decimal fixed point number, to have enhanced precision when computing the + // smaller terms. + x *= BigInt(100); + + // `product` is the accumulated product of all a_n (except a0 and a1), which starts at 20 decimal fixed point + // one. Recall that fixed point multiplication requires dividing by ONE_20. + let product = this.ONE_20; + + if (x >= this.x2) { + x -= this.x2; + product = (product * this.a2) / this.ONE_20; + } + if (x >= this.x3) { + x -= this.x3; + product = (product * this.a3) / this.ONE_20; + } + if (x >= this.x4) { + x -= this.x4; + product = (product * this.a4) / this.ONE_20; + } + if (x >= this.x5) { + x -= this.x5; + product = (product * this.a5) / this.ONE_20; + } + if (x >= this.x6) { + x -= this.x6; + product = (product * this.a6) / this.ONE_20; + } + if (x >= this.x7) { + x -= this.x7; + product = (product * this.a7) / this.ONE_20; + } + if (x >= this.x8) { + x -= this.x8; + product = (product * this.a8) / this.ONE_20; + } + if (x >= this.x9) { + x -= this.x9; + product = (product * this.a9) / this.ONE_20; + } + + // x10 and x11 are unnecessary here since we have high enough precision already. + + // Now we need to compute e^x, where x is small (in particular, it is smaller than x9). We use the Taylor series + // expansion for e^x: 1 + x + (x^2 / 2!) + (x^3 / 3!) + ... + (x^n / n!). + + let seriesSum = this.ONE_20; // The initial one in the sum, with 20 decimal places. + let term; // Each term in the sum, where the nth term is (x^n / n!). + + // The first term is simply x. + term = x; + seriesSum += term; + + // Each term (x^n / n!) equals the previous one times x, divided by n. Since x is a fixed point number, + // multiplying by it requires dividing by this.ONE_20, but dividing by the non-fixed point n values does not. + + term = (term * x) / this.ONE_20 / BigInt(2); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(3); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(4); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(5); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(6); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(7); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(8); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(9); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(10); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(11); + seriesSum += term; + + term = (term * x) / this.ONE_20 / BigInt(12); + seriesSum += term; + + // 12 Taylor terms are sufficient for 18 decimal precision. + + // We now have the first a_n (with no decimals), and the product of all other a_n present, and the Taylor + // approximation of the exponentiation of the remainder (both with 20 decimals). All that remains is to multiply + // all three (one 20 decimal fixed point multiplication, dividing by this.ONE_20, and one integer multiplication), + // and then drop two digits to return an 18 decimal value. + + return (((product * seriesSum) / this.ONE_20) * firstAN) / BigInt(100); + } + + static _ln_36(x: bigint): bigint { + // Since ln(1) = 0, a value of x close to one will yield a very small result, which makes using 36 digits + // worthwhile. + + // First, we transform x to a 36 digit fixed point value. + x *= this.ONE_18; + + // We will use the following Taylor expansion, which converges very rapidly. Let z = (x - 1) / (x + 1). + // ln(x) = 2 * (z + z^3 / 3 + z^5 / 5 + z^7 / 7 + ... + z^(2 * n + 1) / (2 * n + 1)) + + // Recall that 36 digit fixed point division requires multiplying by ONE_36, and multiplication requires + // division by ONE_36. + const z = ((x - this.ONE_36) * this.ONE_36) / (x + this.ONE_36); + const z_squared = (z * z) / this.ONE_36; + + // num is the numerator of the series: the z^(2 * n + 1) term + let num = z; + + // seriesSum holds the accumulated sum of each term in the series, starting with the initial z + let seriesSum = num; + + // In each step, the numerator is multiplied by z^2 + num = (num * z_squared) / this.ONE_36; + seriesSum += num / BigInt(3); + + num = (num * z_squared) / this.ONE_36; + seriesSum += num / BigInt(5); + + num = (num * z_squared) / this.ONE_36; + seriesSum += num / BigInt(7); + + num = (num * z_squared) / this.ONE_36; + seriesSum += num / BigInt(9); + + num = (num * z_squared) / this.ONE_36; + seriesSum += num / BigInt(11); + + num = (num * z_squared) / this.ONE_36; + seriesSum += num / BigInt(13); + + num = (num * z_squared) / this.ONE_36; + seriesSum += num / BigInt(15); + + // 8 Taylor terms are sufficient for 36 decimal precision. + + // All that remains is multiplying by 2 (non fixed point). + return seriesSum * BigInt(2); + } + + /** + * @dev Internal natural logarithm (ln(a)) with signed 18 decimal fixed point argument. + */ + static _ln(a: bigint): bigint { + if (a < this.ONE_18) { + // Since ln(a^k) = k * ln(a), we can compute ln(a) as ln(a) = ln((1/a)^(-1)) = - ln((1/a)). If a is less + // than one, 1/a will be greater than one, and this if statement will not be entered in the recursive call. + // Fixed point division requires multiplying by this.ONE_18. + return BigInt(-1) * this._ln((this.ONE_18 * this.ONE_18) / a); + } + + // First, we use the fact that ln^(a * b) = ln(a) + ln(b) to decompose ln(a) into a sum of powers of two, which + // we call x_n, where x_n == 2^(7 - n), which are the natural logarithm of precomputed quantities a_n (that is, + // ln(a_n) = x_n). We choose the first x_n, x0, to equal 2^7 because the exponential of all larger powers cannot + // be represented as 18 fixed point decimal numbers in 256 bits, and are therefore larger than a. + // At the end of this process we will have the sum of all x_n = ln(a_n) that apply, and the remainder of this + // decomposition, which will be lower than the smallest a_n. + // ln(a) = k_0 * x_0 + k_1 * x_1 + ... + k_n * x_n + ln(remainder), where each k_n equals either 0 or 1. + // We mutate a by subtracting a_n, making it the remainder of the decomposition. + + // For reasons related to how `exp` works, the first two a_n (e^(2^7) and e^(2^6)) are not stored as fixed point + // numbers with 18 decimals, but instead as plain integers with 0 decimals, so we need to multiply them by + // this.ONE_18 to convert them to fixed point. + // For each a_n, we test if that term is present in the decomposition (if a is larger than it), and if so divide + // by it and compute the accumulated sum. + + let sum = BZERO; + if (a >= this.a0 * this.ONE_18) { + a /= this.a0; // Integer, not fixed point division + sum += this.x0; + } + + if (a >= this.a1 * this.ONE_18) { + a /= this.a1; // Integer, not fixed point division + sum += this.x1; + } + + // All other a_n and x_n are stored as 20 digit fixed point numbers, so we convert the sum and a to this format. + sum *= BigInt(100); + a *= BigInt(100); + + // Because further a_n are 20 digit fixed point numbers, we multiply by ONE_20 when dividing by them. + + if (a >= this.a2) { + a = (a * this.ONE_20) / this.a2; + sum += this.x2; + } + + if (a >= this.a3) { + a = (a * this.ONE_20) / this.a3; + sum += this.x3; + } + + if (a >= this.a4) { + a = (a * this.ONE_20) / this.a4; + sum += this.x4; + } + + if (a >= this.a5) { + a = (a * this.ONE_20) / this.a5; + sum += this.x5; + } + + if (a >= this.a6) { + a = (a * this.ONE_20) / this.a6; + sum += this.x6; + } + + if (a >= this.a7) { + a = (a * this.ONE_20) / this.a7; + sum += this.x7; + } + + if (a >= this.a8) { + a = (a * this.ONE_20) / this.a8; + sum += this.x8; + } + + if (a >= this.a9) { + a = (a * this.ONE_20) / this.a9; + sum += this.x9; + } + + if (a >= this.a10) { + a = (a * this.ONE_20) / this.a10; + sum += this.x10; + } + + if (a >= this.a11) { + a = (a * this.ONE_20) / this.a11; + sum += this.x11; + } + + // a is now a small number (smaller than a_11, which roughly equals 1.06). This means we can use a Taylor series + // that converges rapidly for values of `a` close to one - the same one used in ln_36. + // Let z = (a - 1) / (a + 1). + // ln(a) = 2 * (z + z^3 / 3 + z^5 / 5 + z^7 / 7 + ... + z^(2 * n + 1) / (2 * n + 1)) + + // Recall that 20 digit fixed point division requires multiplying by ONE_20, and multiplication requires + // division by ONE_20. + const z = ((a - this.ONE_20) * this.ONE_20) / (a + this.ONE_20); + const z_squared = (z * z) / this.ONE_20; + + // num is the numerator of the series: the z^(2 * n + 1) term + let num = z; + + // seriesSum holds the accumulated sum of each term in the series, starting with the initial z + let seriesSum = num; + + // In each step, the numerator is multiplied by z^2 + num = (num * z_squared) / this.ONE_20; + seriesSum += num / BigInt(3); + + num = (num * z_squared) / this.ONE_20; + seriesSum += num / BigInt(5); + + num = (num * z_squared) / this.ONE_20; + seriesSum += num / BigInt(7); + + num = (num * z_squared) / this.ONE_20; + seriesSum += num / BigInt(9); + + num = (num * z_squared) / this.ONE_20; + seriesSum += num / BigInt(11); + + // 6 Taylor terms are sufficient for 36 decimal precision. + + // Finally, we multiply by 2 (non fixed point) to compute ln(remainder) + seriesSum *= BigInt(2); + + // We now have the sum of all x_n present, and the Taylor approximation of the logarithm of the remainder (both + // with 20 decimals). All that remains is to sum these two, and then drop two digits to return a 18 decimal + // value. + + return (sum + seriesSum) / BigInt(100); + } +} diff --git a/src/poolsMath/stable.ts b/src/poolsMath/stable.ts new file mode 100644 index 00000000..a8d7f46d --- /dev/null +++ b/src/poolsMath/stable.ts @@ -0,0 +1,203 @@ +import { MathSol, BZERO } from './basicOperations'; + +const AMP_PRECISION = BigInt(1e3); + +// PairType = 'token->token' +// SwapType = 'swapExactIn' +// Would it be better to call this _calcOutGivenIn? +export function _exactTokenInForTokenOut( + amplificationParameter: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + amountIn: bigint, + fee: bigint +): bigint { + amountIn = subtractFee(amountIn, fee); + // Given that we need to have a greater final balance out, the invariant needs to be rounded up + const invariant = _calculateInvariant( + amplificationParameter, + balances, + true + ); + + const initBalance = balances[tokenIndexIn]; + balances[tokenIndexIn] = initBalance + amountIn; + const finalBalanceOut = _getTokenBalanceGivenInvariantAndAllOtherBalances( + amplificationParameter, + balances, + invariant, + tokenIndexOut + ); + + // No need to use checked arithmetic since `tokenAmountIn` was actually added to the same balance right before + // calling `_getTokenBalanceGivenInvariantAndAllOtherBalances` which doesn't alter the balances array. + // balances[tokenIndexIn] = balances[tokenIndexIn] - tokenAmountIn; + return balances[tokenIndexOut] - finalBalanceOut - BigInt(1); +} + +export function _tokenInForExactTokenOut( + amplificationParameter: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + amountOut: bigint, + fee: bigint +): bigint { + const invariant = _calculateInvariant( + amplificationParameter, + balances, + true + ); + balances[tokenIndexOut] = MathSol.sub(balances[tokenIndexOut], amountOut); + + const finalBalanceIn = _getTokenBalanceGivenInvariantAndAllOtherBalances( + amplificationParameter, + balances, + invariant, + tokenIndexIn + ); + + let amountIn = MathSol.add( + MathSol.sub(finalBalanceIn, balances[tokenIndexIn]), + BigInt(1) + ); + amountIn = addFee(amountIn, fee); + return amountIn; +} + +function _calculateInvariant( + amplificationParameter: bigint, + balances: bigint[], + roundUp: boolean +): bigint { + /********************************************************************************************** + // invariant // + // D = invariant D^(n+1) // + // A = amplification coefficient A n^n S + D = A D n^n + ----------- // + // S = sum of balances n^n P // + // P = product of balances // + // n = number of tokens // + *********x************************************************************************************/ + + // We support rounding up or down. + + let sum = BZERO; + const numTokens = balances.length; + for (let i = 0; i < numTokens; i++) { + sum = sum + balances[i]; + } + if (sum == BZERO) { + return BZERO; + } + + let prevInvariant = BZERO; + let invariant = sum; + const ampTimesTotal = amplificationParameter * BigInt(numTokens); + + for (let i = 0; i < 255; i++) { + let P_D = balances[0] * BigInt(numTokens); + for (let j = 1; j < numTokens; j++) { + P_D = MathSol.div( + MathSol.mul(MathSol.mul(P_D, balances[j]), BigInt(numTokens)), + invariant, + roundUp + ); + } + prevInvariant = invariant; + invariant = MathSol.div( + MathSol.mul(MathSol.mul(BigInt(numTokens), invariant), invariant) + + MathSol.div( + MathSol.mul(MathSol.mul(ampTimesTotal, sum), P_D), + AMP_PRECISION, + roundUp + ), + MathSol.mul(BigInt(numTokens + 1), invariant) + + // No need to use checked arithmetic for the amp precision, the amp is guaranteed to be at least 1 + MathSol.div( + MathSol.mul(ampTimesTotal - AMP_PRECISION, P_D), + AMP_PRECISION, + !roundUp + ), + roundUp + ); + + if (invariant > prevInvariant) { + if (invariant - prevInvariant <= 1) { + return invariant; + } + } else if (prevInvariant - invariant <= 1) { + return invariant; + } + } + + throw new Error('Errors.STABLE_INVARIANT_DIDNT_CONVERGE'); +} + +function _getTokenBalanceGivenInvariantAndAllOtherBalances( + amplificationParameter: bigint, + balances: bigint[], + invariant: bigint, + tokenIndex: number +): bigint { + // Rounds result up overall + + const ampTimesTotal = amplificationParameter * BigInt(balances.length); + let sum = balances[0]; + let P_D = balances[0] * BigInt(balances.length); + for (let j = 1; j < balances.length; j++) { + P_D = MathSol.divDown( + MathSol.mul(MathSol.mul(P_D, balances[j]), BigInt(balances.length)), + invariant + ); + sum = sum + balances[j]; + } + // No need to use safe math, based on the loop above `sum` is greater than or equal to `balances[tokenIndex]` + sum = sum - balances[tokenIndex]; + + const inv2 = MathSol.mul(invariant, invariant); + // We remove the balance fromm c by multiplying it + const c = MathSol.mul( + MathSol.mul( + MathSol.divUp(inv2, MathSol.mul(ampTimesTotal, P_D)), + AMP_PRECISION + ), + balances[tokenIndex] + ); + const b = + sum + + MathSol.mul(MathSol.divDown(invariant, ampTimesTotal), AMP_PRECISION); + + // We iterate to find the balance + let prevTokenBalance = BZERO; + // We multiply the first iteration outside the loop with the invariant to set the value of the + // initial approximation. + let tokenBalance = MathSol.divUp(inv2 + c, invariant + b); + + for (let i = 0; i < 255; i++) { + prevTokenBalance = tokenBalance; + + tokenBalance = MathSol.divUp( + MathSol.mul(tokenBalance, tokenBalance) + c, + MathSol.mul(tokenBalance, BigInt(2)) + b - invariant + ); + + if (tokenBalance > prevTokenBalance) { + if (tokenBalance - prevTokenBalance <= 1) { + return tokenBalance; + } + } else if (prevTokenBalance - tokenBalance <= 1) { + return tokenBalance; + } + } + throw new Error('Errors.STABLE_GET_BALANCE_DIDNT_CONVERGE'); +} + +function subtractFee(amount: bigint, fee: bigint): bigint { + const feeAmount = MathSol.mulUpFixed(amount, fee); + return amount - feeAmount; +} + +function addFee(amount: bigint, fee: bigint): bigint { + return MathSol.divUpFixed(amount, MathSol.complementFixed(fee)); +} diff --git a/src/poolsMath/weighted.ts b/src/poolsMath/weighted.ts new file mode 100644 index 00000000..5d789179 --- /dev/null +++ b/src/poolsMath/weighted.ts @@ -0,0 +1,48 @@ +import { MathSol } from './basicOperations'; + +// PairType = 'token->token' +// SwapType = 'swapExactIn' +// Would it be better to call this _calcOutGivenIn? +export function _exactTokenInForTokenOut( + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amountIn: bigint, + fee: bigint +): bigint { + // is it necessary to check ranges of variables? same for the other functions + amountIn = subtractFee(amountIn, fee); + const exponent = MathSol.divDownFixed(weightIn, weightOut); + const denominator = balanceIn + amountIn; + const base = MathSol.divUpFixed(balanceIn, denominator); + const power = MathSol.powUpFixed(base, exponent); + return MathSol.mulDownFixed(balanceOut, MathSol.complementFixed(power)); +} + +// PairType = 'token->token' +// SwapType = 'swapExactOut' +export function _tokenInForExactTokenOut( + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amountOut: bigint, + fee: bigint +): bigint { + const base = MathSol.divUpFixed(balanceOut, balanceOut - amountOut); + const exponent = MathSol.divUpFixed(weightOut, weightIn); + const power = MathSol.powUpFixed(base, exponent); + const ratio = MathSol.sub(power, MathSol.ONE); + let amountIn = MathSol.mulUpFixed(balanceIn, ratio); + return addFee(amountIn, fee); +} + +function subtractFee(amount: bigint, fee: bigint): bigint { + const feeAmount = MathSol.mulUpFixed(amount, fee); + return amount - feeAmount; +} + +function addFee(amount: bigint, fee: bigint): bigint { + return MathSol.divUpFixed(amount, MathSol.complementFixed(fee)); +} diff --git a/test/poolsMath.spec.ts b/test/poolsMath.spec.ts new file mode 100644 index 00000000..bcd44202 --- /dev/null +++ b/test/poolsMath.spec.ts @@ -0,0 +1,173 @@ +import * as weighted from '../src/poolsMath/weighted'; +import * as stable from '../src/poolsMath/stable'; +import * as SDK from '@georgeroman/balancer-v2-pools'; +import { + BigNumber as OldBigNumber, + bnum, + scale, + ZERO, +} from '../src/utils/bignumber'; +import { assert } from 'chai'; + +describe('poolsMath: numeric functions using bigint', () => { + context('weighted pools', () => { + it('_exactTokenInForTokenOut', () => { + const { result, SDKResult } = getBothValuesWeighted( + weighted._exactTokenInForTokenOut, + SDK.WeightedMath._calcOutGivenIn, + 1000, + 1, + 3000, + 2, + 30, + 0.003 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + + it('_tokenInForExactTokenOut', () => { + const { result, SDKResult } = getBothValuesWeighted( + weighted._tokenInForExactTokenOut, + SDK.WeightedMath._calcInGivenOut, + 1000, + 1, + 2000, + 2, + 30, + 0.003 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + }); + + context('stable pools', () => { + it('_exactTokenInForTokenOut', () => { + const { result, SDKResult } = getBothValuesStable( + stable._exactTokenInForTokenOut, + SDK.StableMath._calcOutGivenIn, + 1000, + [1000, 1000, 1000], + 0, + 1, + 10, + 0.04 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + it('_tokenInForExactTokenOut', () => { + const { result, SDKResult } = getBothValuesStable( + stable._tokenInForExactTokenOut, + SDK.StableMath._calcInGivenOut, + 1000, + [1000, 1000, 1000], + 0, + 1, + 10, + 0.04 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + }); +}); + +function getBothValuesWeighted( + SORFunction: ( + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amount: bigint, + fee: bigint + ) => bigint, + SDKFunction: ( + balanceIn: OldBigNumber, + weightIn: OldBigNumber, + balanceOut: OldBigNumber, + weightOut: OldBigNumber, + amount: OldBigNumber, + swapFeePercentage?: OldBigNumber + ) => OldBigNumber, + balanceIn: number, + weightIn: number, + balanceOut: number, + weightOut: number, + amount: number, + fee: number +): { result: bigint; SDKResult: OldBigNumber } { + let result = SORFunction( + BigInt(balanceIn * 10 ** 18), + BigInt(weightIn * 10 ** 18), + BigInt(balanceOut * 10 ** 18), + BigInt(weightOut * 10 ** 18), + BigInt(amount * 10 ** 18), + BigInt(fee * 10 ** 18) + ); + let SDKResult = SDKFunction( + bnum(balanceIn * 10 ** 18), + bnum(weightIn * 10 ** 18), + bnum(balanceOut * 10 ** 18), + bnum(weightOut * 10 ** 18), + bnum(amount * 10 ** 18), + bnum(fee * 10 ** 18) + ); + return { result, SDKResult }; +} + +function getBothValuesStable( + SORFunction: ( + amplificationParameter: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + amount: bigint, + fee: bigint + ) => bigint, + SDKFunction: ( + amplificationParameter: OldBigNumber, + balances: OldBigNumber[], + tokenIndexIn: number, + tokenIndexOut: number, + tokenAmount: OldBigNumber, + swapFeePercentage?: OldBigNumber + ) => OldBigNumber, + amplificationParameter: number, + balances: number[], + tokenIndexIn: number, + tokenIndexOut: number, + amount: number, + fee: number +): { result: bigint; SDKResult: OldBigNumber } { + let result = SORFunction( + BigInt(amplificationParameter), + balances.map((amount) => BigInt(amount * 10 ** 18)), + tokenIndexIn, + tokenIndexOut, + BigInt(amount * 10 ** 18), + BigInt(fee * 10 ** 18) + ); + let SDKResult = SDKFunction( + bnum(amplificationParameter), + balances.map((amount) => bnum(amount * 10 ** 18)), + tokenIndexIn, + tokenIndexOut, + bnum(amount * 10 ** 18), + bnum(fee * 10 ** 18) + ); + return { result, SDKResult }; +} diff --git a/tsconfig.json b/tsconfig.json index 58a2c07f..0fd809c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "esnext", - "target": "es6", + "target": "es2016", "declaration": true, "lib": ["es2019", "es2018.promise"], "outDir": "./dist/", From b4b320b14bea4599eb25de1573257af379dd55da Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Tue, 23 Nov 2021 21:24:28 -0300 Subject: [PATCH 02/43] spotPriceAfterSwap + tests (weighted pools) --- .../stablePool/oldButUsefulStableMath.ts | 892 ++++++++++++++++++ src/poolsMath/stable.ts | 62 +- src/poolsMath/weighted.ts | 61 +- test/poolsMath.spec.ts | 79 ++ 4 files changed, 1090 insertions(+), 4 deletions(-) create mode 100644 src/pools/stablePool/oldButUsefulStableMath.ts diff --git a/src/pools/stablePool/oldButUsefulStableMath.ts b/src/pools/stablePool/oldButUsefulStableMath.ts new file mode 100644 index 00000000..fbe69ef2 --- /dev/null +++ b/src/pools/stablePool/oldButUsefulStableMath.ts @@ -0,0 +1,892 @@ +import { INFINITESIMAL } from '../../config'; +import { BigNumber } from '../../utils/bignumber'; +import { bnum } from '../../bmath'; +import { privateEncrypt } from 'crypto'; +// All functions are adapted from the solidity ones to be found on: +// https://github.com/balancer-labs/balancer-core-v2/blob/master/contracts/pools/stable/StableMath.sol + +// TODO: implement all up and down rounding variations + +/********************************************************************************************** + // invariant // + // D = invariant to compute // + // A = amplifier n * D^2 + A * n^n * S * (n^n * P / D^(n−1)) // + // S = sum of balances ____________________________________________ // + // P = product of balances (n+1) * D + ( A * n^n − 1)* (n^n * P / D^(n−1)) // + // n = number of tokens // + **********************************************************************************************/ +export function _invariant( + amp: BigNumber, // amp + balances: BigNumber[] // balances +): BigNumber { + let sum = bnum(0); + let totalCoins = balances.length; + for (let i = 0; i < totalCoins; i++) { + sum = sum.plus(balances[i]); + } + if (sum.isZero()) { + return bnum(0); + } + let prevInv = bnum(0); + let inv = sum; + let ampTimesNpowN = amp.times(totalCoins ** totalCoins); // A*n^n + + for (let i = 0; i < 255; i++) { + let P_D = bnum(totalCoins).times(balances[0]); + for (let j = 1; j < totalCoins; j++) { + //P_D is rounded up + P_D = P_D.times(balances[j]).times(totalCoins).div(inv); + } + prevInv = inv; + //inv is rounded up + inv = bnum(totalCoins) + .times(inv) + .times(inv) + .plus(ampTimesNpowN.times(sum).times(P_D)) + .div( + bnum(totalCoins + 1) + .times(inv) + .plus(ampTimesNpowN.minus(1).times(P_D)) + ); + // Equality with the precision of 1 + if (inv.gt(prevInv)) { + if (inv.minus(prevInv).lt(bnum(10 ** -18))) { + break; + } + } else if (prevInv.minus(inv).lt(bnum(10 ** -18))) { + break; + } + } + //Result is rounded up + return inv; +} + +// // This function has to be zero if the invariant D was calculated correctly +// // It was only used for double checking that the invariant was correct +// export function _invariantValueFunction( +// amp: BigNumber, // amp +// balances: BigNumber[], // balances +// D: BigNumber +// ): BigNumber { +// let invariantValueFunction; +// let prod = bnum(1); +// let sum = bnum(0); +// for (let i = 0; i < balances.length; i++) { +// prod = prod.times(balances[i]); +// sum = sum.plus(balances[i]); +// } +// let n = bnum(balances.length); + +// // NOT! working based on Daniel's equation: https://www.notion.so/Analytical-for-2-tokens-1cd46debef6648dd81f2d75bae941fea +// // invariantValueFunction = amp.times(sum) +// // .plus((bnum(1).div(n.pow(n)).minus(amp)).times(D)) +// // .minus((bnum(1).div(n.pow(n.times(2)).times(prod))).times(D.pow(n.plus(bnum(1))))); +// invariantValueFunction = D.pow(n.plus(bnum(1))) +// .div(n.pow(n).times(prod)) +// .plus(D.times(amp.times(n.pow(n)).minus(bnum(1)))) +// .minus(amp.times(n.pow(n)).times(sum)); + +// return invariantValueFunction; +// } + +// Adapted from StableMath.sol, _outGivenIn() +// * Added swap fee at very first line +/********************************************************************************************** + // outGivenIn token x for y - polynomial equation to solve // + // ay = amount out to calculate // + // by = balance token out // + // y = by - ay // + // D = invariant D D^(n+1) // + // A = amplifier y^2 + ( S - ---------- - 1) * y - ------------- = 0 // + // n = number of tokens (A * n^n) A * n^2n * P // + // S = sum of final balances but y // + // P = product of final balances but y // + **********************************************************************************************/ +export function _exactTokenInForTokenOut(amount, poolPairData): BigNumber { + // The formula below returns some dust (due to rounding errors) but when + // we input zero the output should be zero + if (amount.isZero()) return amount; + let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = + poolPairData; + let balances = [...allBalances]; + let tokenAmountIn = amount; + tokenAmountIn = tokenAmountIn.times(bnum(1).minus(swapFee)); + + //Invariant is rounded up + let inv = _invariant(amp, balances); + let p = inv; + let sum = bnum(0); + let totalCoins = bnum(balances.length); + let n_pow_n = bnum(1); + let x = bnum(0); + for (let i = 0; i < balances.length; i++) { + n_pow_n = n_pow_n.times(totalCoins); + + if (i == tokenIndexIn) { + x = balances[i].plus(tokenAmountIn); + } else if (i != tokenIndexOut) { + x = balances[i]; + } else { + continue; + } + sum = sum.plus(x); + //Round up p + p = p.times(inv).div(x); + } + + //Calculate out balance + let y = _solveAnalyticalBalance(sum, inv, amp, n_pow_n, p); + + //Result is rounded down + // return balances[tokenIndexOut] > y ? balances[tokenIndexOut].minus(y) : 0; + return balances[tokenIndexOut].minus(y); +} + +// Adapted from StableMath.sol, _inGivenOut() +// * Added swap fee at very last line +/********************************************************************************************** + // inGivenOut token x for y - polynomial equation to solve // + // ax = amount in to calculate // + // bx = balance token in // + // x = bx + ax // + // D = invariant D D^(n+1) // + // A = amplifier x^2 + ( S - ---------- - 1) * x - ------------- = 0 // + // n = number of tokens (A * n^n) A * n^2n * P // + // S = sum of final balances but x // + // P = product of final balances but x // + **********************************************************************************************/ +export function _tokenInForExactTokenOut(amount, poolPairData): BigNumber { + // The formula below returns some dust (due to rounding errors) but when + // we input zero the output should be zero + if (amount.isZero()) return amount; + let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = + poolPairData; + let balances = [...allBalances]; + let tokenAmountOut = amount; + //Invariant is rounded up + let inv = _invariant(amp, balances); + let p = inv; + let sum = bnum(0); + let totalCoins = bnum(balances.length); + let n_pow_n = bnum(1); + let x = bnum(0); + for (let i = 0; i < balances.length; i++) { + n_pow_n = n_pow_n.times(totalCoins); + + if (i == tokenIndexOut) { + x = balances[i].minus(tokenAmountOut); + } else if (i != tokenIndexIn) { + x = balances[i]; + } else { + continue; + } + sum = sum.plus(x); + //Round up p + p = p.times(inv).div(x); + } + + //Calculate in balance + let y = _solveAnalyticalBalance(sum, inv, amp, n_pow_n, p); + + //Result is rounded up + return y.minus(balances[tokenIndexIn]).div(bnum(1).minus(swapFee)); +} + +//This function calculates the balance of a given token (tokenIndex) +// given all the other balances and the invariant +function _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp: BigNumber, + balances: BigNumber[], + inv: BigNumber, + tokenIndex: number +): BigNumber { + let p = inv; + let sum = bnum(0); + let totalCoins = balances.length; + let nPowN = bnum(1); + let x = bnum(0); + for (let i = 0; i < totalCoins; i++) { + nPowN = nPowN.times(totalCoins); + if (i != tokenIndex) { + x = balances[i]; + } else { + continue; + } + sum = sum.plus(x); + //Round up p + p = p.times(inv).div(x); + } + + // Calculate token balance + return _solveAnalyticalBalance(sum, inv, amp, nPowN, p); +} + +//This function calcuates the analytical solution to find the balance required +export function _solveAnalyticalBalance( + sum: BigNumber, + inv: BigNumber, + amp: BigNumber, + n_pow_n: BigNumber, + p: BigNumber +): BigNumber { + //Round up p + p = p.times(inv).div(amp.times(n_pow_n).times(n_pow_n)); + //Round down b + let b = sum.plus(inv.div(amp.times(n_pow_n))); + //Round up c + // let c = inv >= b + // ? inv.minus(b).plus(Math.sqrtUp(inv.minus(b).times(inv.minus(b)).plus(p.times(4)))) + // : Math.sqrtUp(b.minus(inv).times(b.minus(inv)).plus(p.times(4))).minus(b.minus(inv)); + let c; + if (inv.gte(b)) { + c = inv + .minus(b) + .plus(inv.minus(b).times(inv.minus(b)).plus(p.times(4)).sqrt()); + } else { + c = b + .minus(inv) + .times(b.minus(inv)) + .plus(p.times(4)) + .sqrt() + .minus(b.minus(inv)); + } + //Round up y + return c.div(2); +} + +/* +Adapted from StableMath.sol _exactTokensInForBPTOut() + * renamed it to _exactTokenInForBPTOut (i.e. just one token in) +*/ +export function _exactTokenInForBPTOut(amount, poolPairData): BigNumber { + // The formula below returns some dust (due to rounding errors) but when + // we input zero the output should be zero + if (amount.isZero()) return amount; + let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; + let balances = [...allBalances]; + let tokenAmountIn = amount; + // Get current invariant + let currentInvariant = _invariant(amp, balances); + + // First calculate the sum of all token balances which will be used to calculate + // the current weights of each token relative to the sum of all balances + let sumBalances = bnum(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances.plus(balances[i]); + } + + // Calculate the weighted balance ratio without considering fees + let currentWeight = balances[tokenIndexIn].div(sumBalances); + let tokenBalanceRatioWithoutFee = balances[tokenIndexIn] + .plus(tokenAmountIn) + .div(balances[tokenIndexIn]); + let weightedBalanceRatio = bnum(1).plus( + tokenBalanceRatioWithoutFee.minus(bnum(1)).times(currentWeight) + ); + + // calculate new amountIn taking into account the fee on the % excess + // Percentage of the amount supplied that will be implicitly swapped for other tokens in the pool + let tokenBalancePercentageExcess = tokenBalanceRatioWithoutFee + .minus(weightedBalanceRatio) + .div(tokenBalanceRatioWithoutFee.minus(bnum(1))); + + let amountInAfterFee = tokenAmountIn.times( + bnum(1).minus(swapFee.times(tokenBalancePercentageExcess)) + ); + balances[tokenIndexIn] = balances[tokenIndexIn].plus(amountInAfterFee); + + // get new invariant taking into account swap fees + let newInvariant = _invariant(amp, balances); + + return balanceOut.times(newInvariant.div(currentInvariant).minus(bnum(1))); +} + +/* +Flow of calculations: +amountBPTOut -> newInvariant -> (amountInProportional, amountInAfterFee) -> +amountInPercentageExcess -> amountIn +*/ +export function _tokenInForExactBPTOut(amount, poolPairData): BigNumber { + // The formula below returns some dust (due to rounding errors) but when + // we input zero the output should be zero + if (amount.isZero()) return amount; + let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; + let balances = [...allBalances]; + let bptAmountOut = amount; + + /********************************************************************************************** + // TODO description // + **********************************************************************************************/ + + // Get current invariant + let currentInvariant = _invariant(amp, balances); + // Calculate new invariant + let newInvariant = balanceOut + .plus(bptAmountOut) + .div(balanceOut) + .times(currentInvariant); + + // First calculate the sum of all token balances which will be used to calculate + // the current weight of token + let sumBalances = bnum(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances.plus(balances[i]); + } + + // get amountInAfterFee + let newBalanceTokenIndex = + _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp, + balances, + newInvariant, + tokenIndexIn + ); + let amountInAfterFee = newBalanceTokenIndex.minus(balances[tokenIndexIn]); + + // Get tokenBalancePercentageExcess + let currentWeight = balances[tokenIndexIn].div(sumBalances); + let tokenBalancePercentageExcess = bnum(1).minus(currentWeight); + + // return amountIn + return amountInAfterFee.div( + bnum(1).minus(tokenBalancePercentageExcess.times(swapFee)) + ); +} + +/* +Adapted from StableMath.sol _BPTInForExactTokensOut() to reduce it to +_BPTInForExactTokenOut (i.e. just one token out) +*/ +export function _BPTInForExactTokenOut(amount, poolPairData): BigNumber { + // The formula below returns some dust (due to rounding errors) but when + // we input zero the output should be zero + if (amount.isZero()) return amount; + let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; + let balances = [...allBalances]; + let tokenAmountOut = amount; + + // Get current invariant + let currentInvariant = _invariant(amp, balances); + + // First calculate the sum of all token balances which will be used to calculate + // the current weights of each token relative to the sum of all balances + let sumBalances = bnum(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances.plus(balances[i]); + } + + // Calculate the weighted balance ratio without considering fees + let currentWeight = balances[tokenIndexOut].div(sumBalances); + let tokenBalanceRatioWithoutFee = balances[tokenIndexOut] + .minus(tokenAmountOut) + .div(balances[tokenIndexOut]); + let weightedBalanceRatio = bnum(1).minus( + bnum(1).minus(tokenBalanceRatioWithoutFee).times(currentWeight) + ); + + // calculate new amounts in taking into account the fee on the % excess + let tokenBalancePercentageExcess = weightedBalanceRatio + .minus(tokenBalanceRatioWithoutFee) + .div(bnum(1).minus(tokenBalanceRatioWithoutFee)); + + let amountOutBeforeFee = tokenAmountOut.div( + bnum(1).minus(swapFee.times(tokenBalancePercentageExcess)) + ); + balances[tokenIndexOut] = balances[tokenIndexOut].minus(amountOutBeforeFee); + + // get new invariant taking into account swap fees + let newInvariant = _invariant(amp, balances); + + // return amountBPTIn + return balanceIn.times(bnum(1).minus(newInvariant.div(currentInvariant))); +} + +/* +Flow of calculations: +amountBPTin -> newInvariant -> (amountOutProportional, amountOutBeforeFee) -> +amountOutPercentageExcess -> amountOut +*/ +export function _exactBPTInForTokenOut(amount, poolPairData): BigNumber { + // The formula below returns some dust (due to rounding errors) but when + // we input zero the output should be zero + if (amount.isZero()) return amount; + let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; + let balances = [...allBalances]; + let bptAmountIn = amount; + /********************************************************************************************** + // TODO description // + **********************************************************************************************/ + + // Get current invariant + let currentInvariant = _invariant(amp, balances); + // Calculate new invariant + let newInvariant = balanceIn + .minus(bptAmountIn) + .div(balanceIn) + .times(currentInvariant); + + // First calculate the sum of all token balances which will be used to calculate + // the current weight of token + let sumBalances = bnum(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances.plus(balances[i]); + } + + // get amountOutBeforeFee + let newBalanceTokenIndex = + _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp, + balances, + newInvariant, + tokenIndexOut + ); + let amountOutBeforeFee = + balances[tokenIndexOut].minus(newBalanceTokenIndex); + + // Calculate tokenBalancePercentageExcess + let currentWeight = balances[tokenIndexOut].div(sumBalances); + let tokenBalancePercentageExcess = bnum(1).minus(currentWeight); + + // return amountOut + return amountOutBeforeFee.times( + bnum(1).minus(tokenBalancePercentageExcess.times(swapFee)) + ); +} + +////////////////////// +//// These functions have been added exclusively for the SORv2 +////////////////////// + +export function _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + is_first_derivative, + wrt_out +): BigNumber { + let totalCoins = balances.length; + let D = _invariant(amp, balances); + let S = bnum(0); + for (let i = 0; i < totalCoins; i++) { + if (i != tokenIndexIn && i != tokenIndexOut) { + S = S.plus(balances[i]); + } + } + let x = balances[tokenIndexIn]; + let y = balances[tokenIndexOut]; + let a = amp.times(totalCoins ** totalCoins); // = ampTimesNpowN + let b = S.minus(D).times(a).plus(D); + let twoaxy = bnum(2).times(a).times(x).times(y); + let partial_x = twoaxy.plus(a.times(y).times(y)).plus(b.times(y)); + let partial_y = twoaxy.plus(a.times(x).times(x)).plus(b.times(x)); + let ans; + if (is_first_derivative) { + ans = partial_x.div(partial_y); + } else { + let partial_xx = bnum(2).times(a).times(y); + let partial_yy = bnum(2).times(a).times(x); + let partial_xy = partial_xx.plus(partial_yy).plus(b); + let numerator; + numerator = bnum(2) + .times(partial_x) + .times(partial_y) + .times(partial_xy) + .minus(partial_xx.times(partial_y.pow(2))) + .minus(partial_yy.times(partial_x.pow(2))); + let denominator = partial_x.pow(2).times(partial_y); + ans = numerator.div(denominator); + if (wrt_out) { + ans = ans.times(partial_y).div(partial_x); + } + } + return ans; +} + +export function _poolDerivativesBPT( + amp, + balances, + bptSupply, + tokenIndexIn, + is_first_derivative, + is_BPT_out, + wrt_out +): BigNumber { + let totalCoins = balances.length; + let D = _invariant(amp, balances); + let S = bnum(0); + let D_P = D.div(totalCoins); + for (let i = 0; i < totalCoins; i++) { + if (i != tokenIndexIn) { + S = S.plus(balances[i]); + D_P = D_P.times(D).div(totalCoins * balances[i]); + } + } + let x = balances[tokenIndexIn]; + let alpha = amp.times(totalCoins ** totalCoins); // = ampTimesNpowN + let beta = alpha.times(S); + let gamma = bnum(1).minus(alpha); + let partial_x = bnum(2) + .times(alpha) + .times(x) + .plus(beta) + .plus(gamma.times(D)); + let minus_partial_D = D_P.times(totalCoins + 1).minus(gamma.times(x)); + let partial_D = bnum(0).minus(minus_partial_D); + let ans; + if (is_first_derivative) { + ans = partial_x.div(minus_partial_D).times(bptSupply).div(D); + } else { + let partial_xx = bnum(2).times(alpha); + let partial_xD = gamma; + let n_times_nplusone = totalCoins * (totalCoins + 1); + let partial_DD = bnum(0).minus(D_P.times(n_times_nplusone).div(D)); + if (is_BPT_out) { + let term1 = partial_xx.times(partial_D).div(partial_x.pow(2)); + let term2 = bnum(2).times(partial_xD).div(partial_x); + let term3 = partial_DD.div(partial_D); + ans = term1.minus(term2).plus(term3).times(D).div(bptSupply); + if (wrt_out) { + let D_prime = bnum(0).minus(partial_x.div(partial_D)); + ans = ans.div(D_prime).times(D).div(bptSupply); + } + } else { + ans = bnum(2) + .times(partial_xD) + .div(partial_D) + .minus(partial_DD.times(partial_x).div(partial_D.pow(2))) + .minus(partial_xx.div(partial_x)); + if (wrt_out) { + ans = ans + .times(partial_x) + .div(minus_partial_D) + .times(bptSupply) + .div(D); + } + } + } + return ans; +} + +///////// +/// SpotPriceAfterSwap +///////// + +// PairType = 'token->token' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapExactTokenInForTokenOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = + poolPairData; + let balances = [...allBalances]; + balances[tokenIndexIn] = balances[tokenIndexIn].plus( + amount.times(bnum(1).minus(swapFee)) + ); + balances[tokenIndexOut] = balances[tokenIndexOut].minus( + _exactTokenInForTokenOut(amount, poolPairData) + ); + let ans = _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + true, + false + ); + ans = bnum(1).div(ans.times(bnum(1).minus(swapFee))); + return ans; +} + +// PairType = 'token->token' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapTokenInForExactTokenOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = + poolPairData; + let balances = [...allBalances]; + let _in = _tokenInForExactTokenOut(amount, poolPairData).times( + bnum(1).minus(swapFee) + ); + balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in); + balances[tokenIndexOut] = balances[tokenIndexOut].minus(amount); + let ans = _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + true, + true + ); + ans = bnum(1).div(ans.times(bnum(1).minus(swapFee))); + return ans; +} + +function _feeFactor(balances, tokenIndex, swapFee): BigNumber { + let sumBalances = bnum(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances.plus(balances[i]); + } + let currentWeight = balances[tokenIndex].div(sumBalances); + let tokenBalancePercentageExcess = bnum(1).minus(currentWeight); + return bnum(1).minus(tokenBalancePercentageExcess.times(swapFee)); +} + +// PairType = 'token->BPT' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapExactTokenInForBPTOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; + let balances = [...allBalances]; + let feeFactor = _feeFactor(balances, tokenIndexIn, swapFee); + balances[tokenIndexIn] = balances[tokenIndexIn].plus( + amount.times(feeFactor) + ); + balanceOut = balanceOut.plus(_exactTokenInForBPTOut(amount, poolPairData)); + let ans = _poolDerivativesBPT( + amp, + balances, + balanceOut, + tokenIndexIn, + true, + true, + false + ); + ans = bnum(1).div(ans.times(feeFactor)); + return ans; +} + +// PairType = 'token->BPT' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapTokenInForExactBPTOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; + let balances = [...allBalances]; + let _in = _tokenInForExactBPTOut(amount, poolPairData); + let feeFactor = _feeFactor(balances, tokenIndexIn, swapFee); + balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in.times(feeFactor)); + balanceOut = balanceOut.plus(amount); + let ans = _poolDerivativesBPT( + amp, + balances, + balanceOut, + tokenIndexIn, + true, + true, + true + ); + ans = bnum(1).div(ans.times(feeFactor)); + return ans; +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapExactBPTInForTokenOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; + let balances = [...allBalances]; + let _out = _exactBPTInForTokenOut(amount, poolPairData); + let feeFactor = _feeFactor(balances, tokenIndexOut, swapFee); + balances[tokenIndexOut] = balances[tokenIndexOut].minus( + _out.div(feeFactor) + ); + balanceIn = balanceIn.minus(amount); + let ans = _poolDerivativesBPT( + amp, + balances, + balanceIn, + tokenIndexOut, + true, + false, + false + ).div(feeFactor); + return ans; +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapBPTInForExactTokenOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; + let balances = [...allBalances]; + let feeFactor = _feeFactor(balances, tokenIndexOut, swapFee); + balances[tokenIndexOut] = balances[tokenIndexOut].minus( + amount.div(feeFactor) + ); + balanceIn = balanceIn.minus(_BPTInForExactTokenOut(amount, poolPairData)); + let ans = _poolDerivativesBPT( + amp, + balances, + balanceIn, + tokenIndexOut, + true, + false, + true + ).div(feeFactor); + return ans; +} + +///////// +/// Derivatives of spotPriceAfterSwap +///////// + +// PairType = 'token->token' +// SwapType = 'swapExactIn' +export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = + poolPairData; + let balances = [...allBalances]; + balances[tokenIndexIn] = balances[tokenIndexIn].plus( + amount.times(bnum(1).minus(swapFee)) + ); + balances[tokenIndexOut] = balances[tokenIndexOut].minus( + _exactTokenInForTokenOut(amount, poolPairData) + ); + return _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + false, + false + ); +} + +// PairType = 'token->token' +// SwapType = 'swapExactOut' +export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = + poolPairData; + let balances = [...allBalances]; + let _in = _tokenInForExactTokenOut(amount, poolPairData).times( + bnum(1).minus(swapFee) + ); + balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in); + balances[tokenIndexOut] = balances[tokenIndexOut].minus(amount); + let feeFactor = bnum(1).minus(swapFee); + return _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + false, + true + ).div(feeFactor); +} + +// PairType = 'token->BPT' +// SwapType = 'swapExactIn' +export function _derivativeSpotPriceAfterSwapExactTokenInForBPTOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; + let balances = [...allBalances]; + let feeFactor = _feeFactor(balances, tokenIndexIn, swapFee); + balances[tokenIndexIn] = balances[tokenIndexIn].plus( + amount.times(feeFactor) + ); + balanceOut = balanceOut.plus(_exactTokenInForBPTOut(amount, poolPairData)); + let ans = _poolDerivativesBPT( + amp, + balances, + balanceOut, + tokenIndexIn, + false, + true, + false + ); + return ans; +} + +// PairType = 'token->BPT' +// SwapType = 'swapExactOut' +export function _derivativeSpotPriceAfterSwapTokenInForExactBPTOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; + let balances = [...allBalances]; + let _in = _tokenInForExactBPTOut(amount, poolPairData); + let feeFactor = _feeFactor(balances, tokenIndexIn, swapFee); + balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in.times(feeFactor)); + balanceOut = balanceOut.plus(amount); + return _poolDerivativesBPT( + amp, + balances, + balanceOut, + tokenIndexIn, + false, + true, + true + ).div(feeFactor); +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactIn' +export function _derivativeSpotPriceAfterSwapExactBPTInForTokenOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; + let balances = [...allBalances]; + let _out = _exactBPTInForTokenOut(amount, poolPairData); + let feeFactor = _feeFactor(balances, tokenIndexOut, swapFee); + balances[tokenIndexOut] = balances[tokenIndexOut].minus( + _out.div(feeFactor) + ); + balanceIn = balanceIn.minus(amount); + let ans = _poolDerivativesBPT( + amp, + balances, + balanceIn, + tokenIndexOut, + false, + false, + false + ); + return ans.div(feeFactor); +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactOut' +export function _derivativeSpotPriceAfterSwapBPTInForExactTokenOut( + amount, + poolPairData +): BigNumber { + let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; + let balances = [...allBalances]; + let _in = _BPTInForExactTokenOut(amount, poolPairData); + let feeFactor = _feeFactor(balances, tokenIndexOut, swapFee); + balances[tokenIndexOut] = balances[tokenIndexOut].minus( + amount.div(feeFactor) + ); + balanceIn = balanceIn.minus(_in); + let ans = _poolDerivativesBPT( + amp, + balances, + balanceIn, + tokenIndexOut, + false, + false, + true + ); + return ans.div(feeFactor.pow(2)); +} diff --git a/src/poolsMath/stable.ts b/src/poolsMath/stable.ts index a8d7f46d..fefaeba0 100644 --- a/src/poolsMath/stable.ts +++ b/src/poolsMath/stable.ts @@ -30,9 +30,6 @@ export function _exactTokenInForTokenOut( tokenIndexOut ); - // No need to use checked arithmetic since `tokenAmountIn` was actually added to the same balance right before - // calling `_getTokenBalanceGivenInvariantAndAllOtherBalances` which doesn't alter the balances array. - // balances[tokenIndexIn] = balances[tokenIndexIn] - tokenAmountIn; return balances[tokenIndexOut] - finalBalanceOut - BigInt(1); } @@ -201,3 +198,62 @@ function subtractFee(amount: bigint, fee: bigint): bigint { function addFee(amount: bigint, fee: bigint): bigint { return MathSol.divUpFixed(amount, MathSol.complementFixed(fee)); } + +/* +Flow of calculations: +amountBPTOut -> newInvariant -> (amountInProportional, amountInAfterFee) -> +amountInPercentageExcess -> amountIn +*/ +export function _tokenInForExactBPTOut( // _calcTokenInGivenExactBptOut + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + bptAmountOut: bigint, + bptTotalSupply: bigint, + fee: bigint +): bigint { + // Token in, so we round up overall. + const currentInvariant = _calculateInvariant(amp, balances, true); + const newInvariant = MathSol.mulUpFixed( + MathSol.divUp( + MathSol.add(bptTotalSupply, bptAmountOut), + bptTotalSupply + ), + currentInvariant + ); + + // Calculate amount in without fee. + const newBalanceTokenIndex = + _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp, + balances, + newInvariant, + tokenIndexIn + ); + const amountInWithoutFee = MathSol.sub( + newBalanceTokenIndex, + balances[tokenIndexIn] + ); + + // First calculate the sum of all token balances, which will be used to calculate + // the current weight of each token + let sumBalances = BigInt(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = MathSol.add(sumBalances, balances[i]); + } + + // We can now compute how much extra balance is being deposited + // and used in virtual swaps, and charge swap fees accordingly. + const currentWeight = MathSol.divDown(balances[tokenIndexIn], sumBalances); + const taxablePercentage = MathSol.complementFixed(currentWeight); + const taxableAmount = MathSol.mulUpFixed( + amountInWithoutFee, + taxablePercentage + ); + const nonTaxableAmount = MathSol.sub(amountInWithoutFee, taxableAmount); + + return MathSol.add( + nonTaxableAmount, + MathSol.divUp(taxableAmount, MathSol.sub(MathSol.ONE, fee)) + ); +} diff --git a/src/poolsMath/weighted.ts b/src/poolsMath/weighted.ts index 5d789179..adee5781 100644 --- a/src/poolsMath/weighted.ts +++ b/src/poolsMath/weighted.ts @@ -14,7 +14,7 @@ export function _exactTokenInForTokenOut( // is it necessary to check ranges of variables? same for the other functions amountIn = subtractFee(amountIn, fee); const exponent = MathSol.divDownFixed(weightIn, weightOut); - const denominator = balanceIn + amountIn; + const denominator = MathSol.add(balanceIn, amountIn); const base = MathSol.divUpFixed(balanceIn, denominator); const power = MathSol.powUpFixed(base, exponent); return MathSol.mulDownFixed(balanceOut, MathSol.complementFixed(power)); @@ -46,3 +46,62 @@ function subtractFee(amount: bigint, fee: bigint): bigint { function addFee(amount: bigint, fee: bigint): bigint { return MathSol.divUpFixed(amount, MathSol.complementFixed(fee)); } + +///////// +/// SpotPriceAfterSwap +///////// + +// PairType = 'token->token' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapExactTokenInForTokenOut( + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amountIn: bigint, + fee: bigint +): bigint { + const numerator = MathSol.mulUpFixed(balanceIn, weightOut); + let denominator = MathSol.mulUpFixed(balanceOut, weightIn); + const feeComplement = MathSol.complementFixed(fee); + denominator = MathSol.mulUpFixed(denominator, feeComplement); + const base = MathSol.divUpFixed( + balanceIn, + MathSol.add(MathSol.mulUpFixed(amountIn, feeComplement), balanceIn) + ); + const exponent = MathSol.divUpFixed(weightIn + weightOut, weightOut); + denominator = MathSol.mulUpFixed( + denominator, + MathSol.powUpFixed(base, exponent) + ); + return MathSol.divUpFixed(numerator, denominator); + // -( + // (Bi * wo) / + // (Bo * (-1 + f) * (Bi / (Ai + Bi - Ai * f)) ** ((wi + wo) / wo) * wi) + // ) +} +/* +// PairType = 'token->token' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapTokenInForExactTokenOut( + amount: OldBigNumber, + poolPairData: WeightedPoolPairData +): OldBigNumber { + const Bi = parseFloat( + formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) + ); + const Bo = parseFloat( + formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) + ); + const wi = parseFloat(formatFixed(poolPairData.weightIn, 18)); + const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); + const Ao = amount.toNumber(); + const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); + return bnum( + -( + (Bi * (Bo / (-Ao + Bo)) ** ((wi + wo) / wi) * wo) / + (Bo * (-1 + f) * wi) + ) + ); +} +*/ diff --git a/test/poolsMath.spec.ts b/test/poolsMath.spec.ts index bcd44202..ee434c42 100644 --- a/test/poolsMath.spec.ts +++ b/test/poolsMath.spec.ts @@ -1,4 +1,5 @@ import * as weighted from '../src/poolsMath/weighted'; +import * as weightedMath from '../src/pools/weightedPool/weightedMath'; import * as stable from '../src/poolsMath/stable'; import * as SDK from '@georgeroman/balancer-v2-pools'; import { @@ -8,6 +9,7 @@ import { ZERO, } from '../src/utils/bignumber'; import { assert } from 'chai'; +import { MathSol } from '../src/poolsMath/basicOperations'; describe('poolsMath: numeric functions using bigint', () => { context('weighted pools', () => { @@ -46,6 +48,22 @@ describe('poolsMath: numeric functions using bigint', () => { 'wrong result' ); }); + + it('_spotPriceAfterSwapExactTokenInForTokenOut', () => { + checkDerivative( + weighted._exactTokenInForTokenOut, + weighted._spotPriceAfterSwapExactTokenInForTokenOut, + 1000, + 1, + 7000, + 2, + 30, + 0.003, + 0.01, + 0.001, + true + ); + }); }); context('stable pools', () => { @@ -171,3 +189,64 @@ function getBothValuesStable( ); return { result, SDKResult }; } + +function checkDerivative( + fn: any, + der: any, + num_balanceIn: number, + num_weightIn: number, + num_balanceOut: number, + num_weightOut: number, + num_amount: number, + num_fee: number, + num_delta: number, + num_error: number, + inverse = false +) { + const balanceIn = s(num_balanceIn); + const weightIn = s(num_weightIn); + const balanceOut = s(num_balanceOut); + const weightOut = s(num_weightOut); + const amount = s(num_amount); + const fee = s(num_fee); + const delta = s(num_delta); + const error = s(num_error); + + let incrementalQuotient = MathSol.divUpFixed( + MathSol.sub( + fn( + balanceIn, + weightIn, + balanceOut, + weightOut, + MathSol.add(amount, delta), + fee + ), + fn(balanceIn, weightIn, balanceOut, weightOut, amount, fee) + ), + delta + ); + if (inverse) + incrementalQuotient = MathSol.divUpFixed( + MathSol.ONE, + incrementalQuotient + ); + const der_ans = der( + balanceIn, + weightIn, + balanceOut, + weightOut, + amount, + fee + ); + assert.approximately( + Number(MathSol.divUpFixed(incrementalQuotient, der_ans)), + Number(MathSol.ONE), + Number(error), + 'wrong result' + ); +} + +function s(a: number): bigint { + return BigInt(a * 10 ** 18); +} From a25292b6a6e724465b0f014c1905812450bd1385 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Wed, 24 Nov 2021 08:00:11 -0300 Subject: [PATCH 03/43] update weighted spotPriceAfterSwap --- src/poolsMath/weighted.ts | 45 ++++++++++++++++++-------------- test/poolsMath.spec.ts | 55 +++++++++++++++++++++++---------------- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/poolsMath/weighted.ts b/src/poolsMath/weighted.ts index adee5781..e4caa730 100644 --- a/src/poolsMath/weighted.ts +++ b/src/poolsMath/weighted.ts @@ -34,7 +34,7 @@ export function _tokenInForExactTokenOut( const exponent = MathSol.divUpFixed(weightOut, weightIn); const power = MathSol.powUpFixed(base, exponent); const ratio = MathSol.sub(power, MathSol.ONE); - let amountIn = MathSol.mulUpFixed(balanceIn, ratio); + const amountIn = MathSol.mulUpFixed(balanceIn, ratio); return addFee(amountIn, fee); } @@ -80,28 +80,35 @@ export function _spotPriceAfterSwapExactTokenInForTokenOut( // (Bo * (-1 + f) * (Bi / (Ai + Bi - Ai * f)) ** ((wi + wo) / wo) * wi) // ) } -/* + // PairType = 'token->token' // SwapType = 'swapExactOut' export function _spotPriceAfterSwapTokenInForExactTokenOut( - amount: OldBigNumber, - poolPairData: WeightedPoolPairData -): OldBigNumber { - const Bi = parseFloat( - formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amountOut: bigint, + fee: bigint +): bigint { + let numerator = MathSol.mulUpFixed(balanceIn, weightOut); + const feeComplement = MathSol.complementFixed(fee); + const base = MathSol.divUpFixed( + balanceOut, + MathSol.sub(balanceOut, amountOut) ); - const Bo = parseFloat( - formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) + const exponent = MathSol.divUpFixed(weightIn + weightOut, weightIn); + numerator = MathSol.mulUpFixed( + numerator, + MathSol.powUpFixed(base, exponent) ); - const wi = parseFloat(formatFixed(poolPairData.weightIn, 18)); - const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); - const Ao = amount.toNumber(); - const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); - return bnum( - -( - (Bi * (Bo / (-Ao + Bo)) ** ((wi + wo) / wi) * wo) / - (Bo * (-1 + f) * wi) - ) + const denominator = MathSol.mulUpFixed( + MathSol.mulUpFixed(balanceOut, weightIn), + feeComplement ); + return MathSol.divUpFixed(numerator, denominator); + // -( + // (Bi * (Bo / (-Ao + Bo)) ** ((wi + wo) / wi) * wo) / + // (Bo * (-1 + f) * wi) + // ) } -*/ diff --git a/test/poolsMath.spec.ts b/test/poolsMath.spec.ts index ee434c42..a24deb31 100644 --- a/test/poolsMath.spec.ts +++ b/test/poolsMath.spec.ts @@ -1,13 +1,7 @@ import * as weighted from '../src/poolsMath/weighted'; -import * as weightedMath from '../src/pools/weightedPool/weightedMath'; import * as stable from '../src/poolsMath/stable'; import * as SDK from '@georgeroman/balancer-v2-pools'; -import { - BigNumber as OldBigNumber, - bnum, - scale, - ZERO, -} from '../src/utils/bignumber'; +import { BigNumber as OldBigNumber, bnum } from '../src/utils/bignumber'; import { assert } from 'chai'; import { MathSol } from '../src/poolsMath/basicOperations'; @@ -60,10 +54,25 @@ describe('poolsMath: numeric functions using bigint', () => { 30, 0.003, 0.01, - 0.001, + 0.00001, true ); }); + it('_spotPriceAfterSwapTokenInForExactTokenOut', () => { + checkDerivative( + weighted._tokenInForExactTokenOut, + weighted._spotPriceAfterSwapTokenInForExactTokenOut, + 1000, + 1, + 7000, + 2, + 30, + 0.003, + 0.01, + 0.00001, + false + ); + }); }); context('stable pools', () => { @@ -72,7 +81,7 @@ describe('poolsMath: numeric functions using bigint', () => { stable._exactTokenInForTokenOut, SDK.StableMath._calcOutGivenIn, 1000, - [1000, 1000, 1000], + [1000, 3000, 2000], 0, 1, 10, @@ -89,7 +98,7 @@ describe('poolsMath: numeric functions using bigint', () => { stable._tokenInForExactTokenOut, SDK.StableMath._calcInGivenOut, 1000, - [1000, 1000, 1000], + [1000, 3000, 2000], 0, 1, 10, @@ -128,15 +137,15 @@ function getBothValuesWeighted( amount: number, fee: number ): { result: bigint; SDKResult: OldBigNumber } { - let result = SORFunction( - BigInt(balanceIn * 10 ** 18), - BigInt(weightIn * 10 ** 18), - BigInt(balanceOut * 10 ** 18), - BigInt(weightOut * 10 ** 18), - BigInt(amount * 10 ** 18), - BigInt(fee * 10 ** 18) + const result = SORFunction( + s(balanceIn), + s(weightIn), + s(balanceOut), + s(weightOut), + s(amount), + s(fee) ); - let SDKResult = SDKFunction( + const SDKResult = SDKFunction( bnum(balanceIn * 10 ** 18), bnum(weightIn * 10 ** 18), bnum(balanceOut * 10 ** 18), @@ -171,15 +180,15 @@ function getBothValuesStable( amount: number, fee: number ): { result: bigint; SDKResult: OldBigNumber } { - let result = SORFunction( + const result = SORFunction( BigInt(amplificationParameter), - balances.map((amount) => BigInt(amount * 10 ** 18)), + balances.map((amount) => s(amount)), tokenIndexIn, tokenIndexOut, - BigInt(amount * 10 ** 18), - BigInt(fee * 10 ** 18) + s(amount), + s(fee) ); - let SDKResult = SDKFunction( + const SDKResult = SDKFunction( bnum(amplificationParameter), balances.map((amount) => bnum(amount * 10 ** 18)), tokenIndexIn, From e1ba5bc4cd732c339e1dba08a5c949093a5a8116 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Tue, 30 Nov 2021 17:31:56 -0300 Subject: [PATCH 04/43] correctly handles spotPriceAfterSwap for stable, tokenToToken --- src/poolsMath/stable.ts | 162 ++++++++++++-- test/poolsMathStable.spec.ts | 207 ++++++++++++++++++ ...Math.spec.ts => poolsMathWeighted.spec.ts} | 87 +------- 3 files changed, 353 insertions(+), 103 deletions(-) create mode 100644 test/poolsMathStable.spec.ts rename test/{poolsMath.spec.ts => poolsMathWeighted.spec.ts} (66%) diff --git a/src/poolsMath/stable.ts b/src/poolsMath/stable.ts index fefaeba0..fa01ac5a 100644 --- a/src/poolsMath/stable.ts +++ b/src/poolsMath/stable.ts @@ -6,7 +6,7 @@ const AMP_PRECISION = BigInt(1e3); // SwapType = 'swapExactIn' // Would it be better to call this _calcOutGivenIn? export function _exactTokenInForTokenOut( - amplificationParameter: bigint, + amp: bigint, balances: bigint[], tokenIndexIn: number, tokenIndexOut: number, @@ -15,41 +15,32 @@ export function _exactTokenInForTokenOut( ): bigint { amountIn = subtractFee(amountIn, fee); // Given that we need to have a greater final balance out, the invariant needs to be rounded up - const invariant = _calculateInvariant( - amplificationParameter, - balances, - true - ); + const invariant = _calculateInvariant(amp, balances, true); const initBalance = balances[tokenIndexIn]; balances[tokenIndexIn] = initBalance + amountIn; const finalBalanceOut = _getTokenBalanceGivenInvariantAndAllOtherBalances( - amplificationParameter, + amp, balances, invariant, tokenIndexOut ); - return balances[tokenIndexOut] - finalBalanceOut - BigInt(1); } export function _tokenInForExactTokenOut( - amplificationParameter: bigint, + amp: bigint, balances: bigint[], tokenIndexIn: number, tokenIndexOut: number, amountOut: bigint, fee: bigint ): bigint { - const invariant = _calculateInvariant( - amplificationParameter, - balances, - true - ); + const invariant = _calculateInvariant(amp, balances, true); balances[tokenIndexOut] = MathSol.sub(balances[tokenIndexOut], amountOut); const finalBalanceIn = _getTokenBalanceGivenInvariantAndAllOtherBalances( - amplificationParameter, + amp, balances, invariant, tokenIndexIn @@ -64,7 +55,7 @@ export function _tokenInForExactTokenOut( } function _calculateInvariant( - amplificationParameter: bigint, + amp: bigint, balances: bigint[], roundUp: boolean ): bigint { @@ -90,7 +81,7 @@ function _calculateInvariant( let prevInvariant = BZERO; let invariant = sum; - const ampTimesTotal = amplificationParameter * BigInt(numTokens); + const ampTimesTotal = amp * BigInt(numTokens); for (let i = 0; i < 255; i++) { let P_D = balances[0] * BigInt(numTokens); @@ -132,14 +123,14 @@ function _calculateInvariant( } function _getTokenBalanceGivenInvariantAndAllOtherBalances( - amplificationParameter: bigint, + amp: bigint, balances: bigint[], invariant: bigint, tokenIndex: number ): bigint { // Rounds result up overall - const ampTimesTotal = amplificationParameter * BigInt(balances.length); + const ampTimesTotal = amp * BigInt(balances.length); let sum = balances[0]; let P_D = balances[0] * BigInt(balances.length); for (let j = 1; j < balances.length; j++) { @@ -199,6 +190,7 @@ function addFee(amount: bigint, fee: bigint): bigint { return MathSol.divUpFixed(amount, MathSol.complementFixed(fee)); } +// Untested: /* Flow of calculations: amountBPTOut -> newInvariant -> (amountInProportional, amountInAfterFee) -> @@ -257,3 +249,135 @@ export function _tokenInForExactBPTOut( // _calcTokenInGivenExactBptOut MathSol.divUp(taxableAmount, MathSol.sub(MathSol.ONE, fee)) ); } + +///////// +/// SpotPriceAfterSwap +///////// + +// PairType = 'token->token' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapExactTokenInForTokenOut( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + amountIn: bigint, + fee: bigint +): bigint { + const feeComplement = MathSol.complementFixed(fee); + const balancesCopy = [...balances]; + balances[tokenIndexIn] = MathSol.add( + balances[tokenIndexIn], + MathSol.mulUpFixed(amountIn, feeComplement) + ); + balances[tokenIndexOut] = MathSol.sub( + balances[tokenIndexOut], + _exactTokenInForTokenOut( + amp, + balancesCopy, + tokenIndexIn, + tokenIndexOut, + amountIn, + fee + ) + ); + let ans = _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + true, + false + ); + ans = MathSol.divDownFixed( + MathSol.ONE, + MathSol.mulDownFixed(ans, feeComplement) + ); + return ans; +} + +// PairType = 'token->token' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapTokenInForExactTokenOut( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + amountOut: bigint, + fee: bigint +): BigInt { + const balancesCopy = [...balances]; + let _in = _tokenInForExactTokenOut( + amp, + balancesCopy, + tokenIndexIn, + tokenIndexOut, + amountOut, + fee + ); + balances[tokenIndexIn] = balances[tokenIndexIn] + _in; + balances[tokenIndexOut] = MathSol.sub(balances[tokenIndexOut], amountOut); + let ans = _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + true, + true + ); + const feeComplement = MathSol.complementFixed(fee); + ans = MathSol.divUpFixed( + MathSol.ONE, + MathSol.mulUpFixed(ans, feeComplement) + ); + return ans; +} + +export function _poolDerivatives( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + is_first_derivative: boolean, + wrt_out: boolean +): bigint { + let totalCoins = balances.length; + let D = _calculateInvariant(amp, balances, true); + let S = BigInt(0); + for (let i = 0; i < totalCoins; i++) { + if (i != tokenIndexIn && i != tokenIndexOut) { + S += balances[i]; + } + } + let x = balances[tokenIndexIn]; + let y = balances[tokenIndexOut]; + let a = amp * BigInt(totalCoins); + let b = a * (S - D) + D * AMP_PRECISION; + let twoaxy = BigInt(2) * a * x * y; + let partial_x = twoaxy + a * y * y + b * y; + let partial_y = twoaxy + a * x * x + b * x; + let ans: bigint; + if (is_first_derivative) { + ans = MathSol.divUpFixed(partial_x, partial_y); + } else { + // Untested case: + let partial_xx = BigInt(2) * a * y; + let partial_yy = BigInt(2) * a * x; + let partial_xy = partial_xx + partial_yy + b; // AMP_PRECISION missing + let numerator: bigint; + numerator = + BigInt(2) * partial_x * partial_y * partial_xy - + partial_xx * partial_y * partial_y + + partial_yy * partial_x * partial_x; + let denominator = partial_x * partial_x * partial_y; + ans = MathSol.divUpFixed(numerator, denominator); // change the order to + if (wrt_out) { + // directly use integer operations + ans = MathSol.mulUpFixed( + MathSol.mulUpFixed(ans, partial_y), + partial_x + ); + } + } + return ans; +} diff --git a/test/poolsMathStable.spec.ts b/test/poolsMathStable.spec.ts new file mode 100644 index 00000000..157e97cd --- /dev/null +++ b/test/poolsMathStable.spec.ts @@ -0,0 +1,207 @@ +import * as stable from '../src/poolsMath/stable'; +import * as SDK from '@georgeroman/balancer-v2-pools'; +import { BigNumber as OldBigNumber, bnum } from '../src/utils/bignumber'; +import { assert } from 'chai'; +import { MathSol } from '../src/poolsMath/basicOperations'; +import { _poolDerivatives as _oldPoolDerivatives } from '../src/pools/stablePool/oldButUsefulStableMath'; +import { _poolDerivatives as _currentPoolDerivatives } from '../src/pools/stablePool/stableMath'; +import { _poolDerivatives } from '../src/poolsMath/stable'; + +describe('poolsMathStable: numeric functions using bigint', () => { + context('stable pools', () => { + it('_exactTokenInForTokenOut', () => { + const { result, SDKResult } = getBothValuesStable( + stable._exactTokenInForTokenOut, + SDK.StableMath._calcOutGivenIn, + 1000, + [1000, 3000, 2000], + 0, + 1, + 10, + 0.04 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + it('_tokenInForExactTokenOut', () => { + const { result, SDKResult } = getBothValuesStable( + stable._tokenInForExactTokenOut, + SDK.StableMath._calcInGivenOut, + 1000, + [3000, 1000, 1000], + 0, + 1, + 10, + 0.01 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + it('_spotPriceAfterSwapExactTokenInForTokenOut', () => { + const delta = 0.01; + const error = 0.00001; + checkDerivative_stable( + stable._exactTokenInForTokenOut, + stable._spotPriceAfterSwapExactTokenInForTokenOut, + 10, + [15000, 30000, 10000], + 0, + 1, + 100, + 0.01, + delta, + error, + true + ); + checkDerivative_stable( + stable._exactTokenInForTokenOut, + stable._spotPriceAfterSwapExactTokenInForTokenOut, + 10, + [15000, 30000, 10000], + 0, + 1, + 100, + 0.01, + delta, + error, + true + ); + + checkDerivative_stable( + stable._tokenInForExactTokenOut, + stable._spotPriceAfterSwapTokenInForExactTokenOut, + 10, + [10000, 10000, 10000], + 0, + 1, + 10, + 0.01, + delta, + error, + false + ); + }); + }); +}); + +function getBothValuesStable( + SORFunction: ( + amplificationParameter: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + amount: bigint, + fee: bigint + ) => bigint, + SDKFunction: ( + amplificationParameter: OldBigNumber, + balances: OldBigNumber[], + tokenIndexIn: number, + tokenIndexOut: number, + tokenAmount: OldBigNumber, + swapFeePercentage?: OldBigNumber + ) => OldBigNumber, + amplificationParameter: number, + balances: number[], + tokenIndexIn: number, + tokenIndexOut: number, + amount: number, + fee: number +): { result: bigint; SDKResult: OldBigNumber } { + const result = SORFunction( + BigInt(amplificationParameter), + balances.map((amount) => s(amount)), + tokenIndexIn, + tokenIndexOut, + s(amount), + s(fee) + ); + const SDKResult = SDKFunction( + bnum(amplificationParameter), + balances.map((amount) => bnum(amount * 10 ** 18)), + tokenIndexIn, + tokenIndexOut, + bnum(amount * 10 ** 18), + bnum(fee * 10 ** 18) + ); + return { result, SDKResult }; +} + +// To do: basic debug of components +function checkDerivative_stable( + fn: any, + der: any, + num_amp: number, + num_balances: number[], + tokenIndexIn: number, + tokenIndexOut: number, + num_amount: number, + num_fee: number, + num_delta: number, + num_error: number, + inverse = false +) { + const amp = BigInt(num_amp); + const balances1 = num_balances.map((balance, i) => { + return s(balance); + }); + const balances2 = num_balances.map((balance, i) => { + return s(balance); + }); + const balances3 = num_balances.map((balance, i) => { + return s(balance); + }); + const amount = s(num_amount); + const fee = s(num_fee); + const delta = s(num_delta); + const error = s(num_error); + + const val1 = fn( + amp, + balances1, + tokenIndexIn, + tokenIndexOut, + amount + delta, + fee + ); + const val2 = fn(amp, balances2, tokenIndexIn, tokenIndexOut, amount, fee); + let incrementalQuotient = MathSol.divUpFixed( + MathSol.sub(val1, val2), + delta + ); + if (inverse) + incrementalQuotient = MathSol.divUpFixed( + MathSol.ONE, + incrementalQuotient + ); + const der_ans = der( + amp, + balances3, + tokenIndexIn, + tokenIndexOut, + amount, + fee + ); + console.log('(inverse) incremental quotient: ', incrementalQuotient); + console.log('computed spot price: ', der_ans); + assert.approximately( + Number(MathSol.divUpFixed(incrementalQuotient, der_ans)), + Number(MathSol.ONE), + Number(error), + 'wrong result' + ); +} + +function s(a: number): bigint { + return BigInt(a * 10 ** 18); +} + +function b(a: number): OldBigNumber { + return bnum(a * 10 ** 18); +} diff --git a/test/poolsMath.spec.ts b/test/poolsMathWeighted.spec.ts similarity index 66% rename from test/poolsMath.spec.ts rename to test/poolsMathWeighted.spec.ts index a24deb31..7a27b61b 100644 --- a/test/poolsMath.spec.ts +++ b/test/poolsMathWeighted.spec.ts @@ -1,5 +1,4 @@ import * as weighted from '../src/poolsMath/weighted'; -import * as stable from '../src/poolsMath/stable'; import * as SDK from '@georgeroman/balancer-v2-pools'; import { BigNumber as OldBigNumber, bnum } from '../src/utils/bignumber'; import { assert } from 'chai'; @@ -44,7 +43,7 @@ describe('poolsMath: numeric functions using bigint', () => { }); it('_spotPriceAfterSwapExactTokenInForTokenOut', () => { - checkDerivative( + checkDerivative_weighted( weighted._exactTokenInForTokenOut, weighted._spotPriceAfterSwapExactTokenInForTokenOut, 1000, @@ -59,7 +58,7 @@ describe('poolsMath: numeric functions using bigint', () => { ); }); it('_spotPriceAfterSwapTokenInForExactTokenOut', () => { - checkDerivative( + checkDerivative_weighted( weighted._tokenInForExactTokenOut, weighted._spotPriceAfterSwapTokenInForExactTokenOut, 1000, @@ -74,43 +73,6 @@ describe('poolsMath: numeric functions using bigint', () => { ); }); }); - - context('stable pools', () => { - it('_exactTokenInForTokenOut', () => { - const { result, SDKResult } = getBothValuesStable( - stable._exactTokenInForTokenOut, - SDK.StableMath._calcOutGivenIn, - 1000, - [1000, 3000, 2000], - 0, - 1, - 10, - 0.04 - ); - assert.equal( - result.toString(), - SDKResult.toString(), - 'wrong result' - ); - }); - it('_tokenInForExactTokenOut', () => { - const { result, SDKResult } = getBothValuesStable( - stable._tokenInForExactTokenOut, - SDK.StableMath._calcInGivenOut, - 1000, - [1000, 3000, 2000], - 0, - 1, - 10, - 0.04 - ); - assert.equal( - result.toString(), - SDKResult.toString(), - 'wrong result' - ); - }); - }); }); function getBothValuesWeighted( @@ -156,50 +118,7 @@ function getBothValuesWeighted( return { result, SDKResult }; } -function getBothValuesStable( - SORFunction: ( - amplificationParameter: bigint, - balances: bigint[], - tokenIndexIn: number, - tokenIndexOut: number, - amount: bigint, - fee: bigint - ) => bigint, - SDKFunction: ( - amplificationParameter: OldBigNumber, - balances: OldBigNumber[], - tokenIndexIn: number, - tokenIndexOut: number, - tokenAmount: OldBigNumber, - swapFeePercentage?: OldBigNumber - ) => OldBigNumber, - amplificationParameter: number, - balances: number[], - tokenIndexIn: number, - tokenIndexOut: number, - amount: number, - fee: number -): { result: bigint; SDKResult: OldBigNumber } { - const result = SORFunction( - BigInt(amplificationParameter), - balances.map((amount) => s(amount)), - tokenIndexIn, - tokenIndexOut, - s(amount), - s(fee) - ); - const SDKResult = SDKFunction( - bnum(amplificationParameter), - balances.map((amount) => bnum(amount * 10 ** 18)), - tokenIndexIn, - tokenIndexOut, - bnum(amount * 10 ** 18), - bnum(fee * 10 ** 18) - ); - return { result, SDKResult }; -} - -function checkDerivative( +function checkDerivative_weighted( fn: any, der: any, num_balanceIn: number, From 4b6c580a88c61ab17e5cab95da0e5f19d2fdc42a Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Wed, 1 Dec 2021 21:25:01 -0300 Subject: [PATCH 05/43] introduce linear.ts with functions translated from solidity --- src/poolsMath/linear.ts | 360 ++++++++++++++++++++++++++++++++++++++++ src/poolsMath/stable.ts | 3 +- 2 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 src/poolsMath/linear.ts diff --git a/src/poolsMath/linear.ts b/src/poolsMath/linear.ts new file mode 100644 index 00000000..77dc504d --- /dev/null +++ b/src/poolsMath/linear.ts @@ -0,0 +1,360 @@ +// functions translated from +// https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/pool-linear/contracts/LinearMath.sol +import { MathSol, BZERO } from './basicOperations'; + +type Params = { + fee: bigint; + rate: bigint; + lowerTarget: bigint; + upperTarget: bigint; +}; + +export function _calcBptOutPerMainIn( + mainIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + + if (bptSupply == BigInt(0)) { + return _toNominal(mainIn, params); + } + + const previousNominalMain = _toNominal(mainBalance, params); + const afterNominalMain = _toNominal(mainBalance + mainIn, params); + const deltaNominalMain = afterNominalMain - previousNominalMain; + const invariant = _calcInvariantUp( + previousNominalMain, + wrappedBalance, + params + ); + return MathSol.divDown( + MathSol.mulDownFixed(bptSupply, deltaNominalMain), + invariant + ); +} + +export function _calcBptInPerMainOut( + mainOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + const previousNominalMain = _toNominal(mainBalance, params); + const afterNominalMain = _toNominal(mainBalance - mainOut, params); + const deltaNominalMain = previousNominalMain - afterNominalMain; + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params + ); + return MathSol.divUp( + MathSol.mulUpFixed(bptSupply, deltaNominalMain), + invariant + ); +} + +export function _calcWrappedOutPerMainIn( + mainIn: bigint, + mainBalance: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + const previousNominalMain = _toNominal(mainBalance, params); + const afterNominalMain = _toNominal(mainBalance + mainIn, params); + const deltaNominalMain = afterNominalMain - previousNominalMain; + return MathSol.divDown(deltaNominalMain, params.rate); +} + +export function _calcWrappedInPerMainOut( + mainOut: bigint, + mainBalance: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + const previousNominalMain = _toNominal(mainBalance, params); + const afterNominalMain = _toNominal(mainBalance - mainOut, params); + const deltaNominalMain = previousNominalMain - afterNominalMain; + return MathSol.divUp(deltaNominalMain, params.rate); +} + +export function _calcMainInPerBptOut( + bptOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + if (bptSupply == BigInt(0)) { + return _fromNominal(bptOut, params); + } + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantUp( + previousNominalMain, + wrappedBalance, + params + ); + const deltaNominalMain = MathSol.divUp( + MathSol.mulUpFixed(invariant, bptOut), + bptSupply + ); + const afterNominalMain = previousNominalMain + deltaNominalMain; + const newMainBalance = _fromNominal(afterNominalMain, params); + return newMainBalance - mainBalance; +} + +export function _calcMainOutPerBptIn( + bptIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params + ); + const deltaNominalMain = MathSol.divDown( + MathSol.mulDownFixed(invariant, bptIn), + bptSupply + ); + const afterNominalMain = previousNominalMain - deltaNominalMain; + const newMainBalance = _fromNominal(afterNominalMain, params); + return mainBalance - newMainBalance; +} + +export function _calcMainOutPerWrappedIn( + wrappedIn: bigint, + mainBalance: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + const previousNominalMain = _toNominal(mainBalance, params); + const deltaNominalMain = MathSol.mulDownFixed(wrappedIn, params.rate); + const afterNominalMain = previousNominalMain - deltaNominalMain; + const newMainBalance = _fromNominal(afterNominalMain, params); + return mainBalance - newMainBalance; +} + +export function _calcMainInPerWrappedOut( + wrappedOut: bigint, + mainBalance: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + const previousNominalMain = _toNominal(mainBalance, params); + const deltaNominalMain = MathSol.mulUpFixed(wrappedOut, params.rate); + const afterNominalMain = previousNominalMain + deltaNominalMain; + const newMainBalance = _fromNominal(afterNominalMain, params); + return newMainBalance - mainBalance; +} + +function _calcBptOutPerWrappedIn( + wrappedIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + if (bptSupply == BigInt(0)) { + // Return nominal DAI + return MathSol.mulDownFixed(wrappedIn, params.rate); + } + + const nominalMain = _toNominal(mainBalance, params); + const previousInvariant = _calcInvariantUp( + nominalMain, + wrappedBalance, + params + ); + const newWrappedBalance = wrappedBalance + wrappedIn; + const newInvariant = _calcInvariantDown( + nominalMain, + newWrappedBalance, + params + ); + const newBptBalance = MathSol.divDown( + MathSol.mulDownFixed(bptSupply, newInvariant), + previousInvariant + ); + return newBptBalance - bptSupply; +} + +function _calcBptInPerWrappedOut( + wrappedOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + const nominalMain = _toNominal(mainBalance, params); + const previousInvariant = _calcInvariantUp( + nominalMain, + wrappedBalance, + params + ); + const newWrappedBalance = wrappedBalance - wrappedOut; + const newInvariant = _calcInvariantDown( + nominalMain, + newWrappedBalance, + params + ); + const newBptBalance = MathSol.divDown( + MathSol.mulDownFixed(bptSupply, newInvariant), + previousInvariant + ); + return bptSupply - newBptBalance; +} + +export function _calcWrappedInPerBptOut( + bptOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + if (bptSupply == BigInt(0)) { + // Return nominal DAI + return MathSol.divUpFixed(bptOut, params.rate); + } + + const nominalMain = _toNominal(mainBalance, params); + const previousInvariant = _calcInvariantUp( + nominalMain, + wrappedBalance, + params + ); + const newBptBalance = bptSupply + bptOut; + const newWrappedBalance = MathSol.divUpFixed( + MathSol.mulUpFixed( + MathSol.divUpFixed(newBptBalance, bptSupply), + previousInvariant + ) - nominalMain, + params.rate + ); + return newWrappedBalance - wrappedBalance; +} + +export function _calcWrappedOutPerBptIn( + bptIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + const nominalMain = _toNominal(mainBalance, params); + const previousInvariant = _calcInvariantUp( + nominalMain, + wrappedBalance, + params + ); + const newBptBalance = bptSupply - bptIn; + const newWrappedBalance = MathSol.divUpFixed( + MathSol.mulUpFixed( + MathSol.divUpFixed(newBptBalance, bptSupply), + previousInvariant + ) - nominalMain, + params.rate + ); + return wrappedBalance - newWrappedBalance; +} + +function _calcInvariantUp( + nominalMainBalance: bigint, + wrappedBalance: bigint, + params: Params +): bigint { + return nominalMainBalance + MathSol.mulUpFixed(wrappedBalance, params.rate); +} + +function _calcInvariantDown( + nominalMainBalance: bigint, + wrappedBalance: bigint, + params: Params +): bigint { + return ( + nominalMainBalance + MathSol.mulDownFixed(wrappedBalance, params.rate) + ); +} + +export function _toNominal(amount: bigint, params: Params): bigint { + if ( + amount < + MathSol.mulUpFixed(MathSol.ONE - params.fee, params.lowerTarget) + ) { + return MathSol.divUpFixed(amount, MathSol.ONE - params.fee); + } else if ( + amount < + params.upperTarget - MathSol.mulUpFixed(params.fee, params.lowerTarget) + ) { + return amount + MathSol.mulUpFixed(params.fee, params.lowerTarget); + } else { + return ( + amount + + MathSol.divUpFixed( + MathSol.mulUpFixed( + params.lowerTarget + params.upperTarget, + params.fee + ), + MathSol.ONE + params.fee + ) + ); + } +} + +export function _fromNominal(nominal: bigint, params: Params): bigint { + if (nominal < params.lowerTarget) { + return MathSol.mulUpFixed(nominal, MathSol.ONE - params.fee); + } else if (nominal < params.upperTarget) { + return nominal - MathSol.mulUpFixed(params.fee, params.lowerTarget); + } else { + return ( + MathSol.mulUpFixed(nominal, MathSol.ONE + params.fee) - + MathSol.mulUpFixed( + params.fee, + params.lowerTarget + params.upperTarget + ) + ); + } +} + +export function _calcTokensOutGivenExactBptIn( + balances: bigint[], + bptAmountIn: bigint, + bptTotalSupply: bigint, + bptIndex: number +): bigint[] { + /********************************************************************************************** + // exactBPTInForTokensOut // + // (per token) // + // aO = tokenAmountOut / bptIn \ // + // b = tokenBalance a0 = b * | --------------------- | // + // bptIn = bptAmountIn \ bptTotalSupply / // + // bpt = bptTotalSupply // + **********************************************************************************************/ + + // Since we're computing an amount out, we round down overall. This means rounding down on both the + // multiplication and division. + + const bptRatio = MathSol.divDownFixed(bptAmountIn, bptTotalSupply); + let amountsOut: bigint[] = new Array(balances.length); + for (let i = 0; i < balances.length; i++) { + // BPT is skipped as those tokens are not the LPs, but rather the preminted and undistributed amount. + if (i != bptIndex) { + amountsOut[i] = MathSol.mulDownFixed(balances[i], bptRatio); + } + } + return amountsOut; +} diff --git a/src/poolsMath/stable.ts b/src/poolsMath/stable.ts index fa01ac5a..d1fb659e 100644 --- a/src/poolsMath/stable.ts +++ b/src/poolsMath/stable.ts @@ -370,9 +370,8 @@ export function _poolDerivatives( partial_xx * partial_y * partial_y + partial_yy * partial_x * partial_x; let denominator = partial_x * partial_x * partial_y; - ans = MathSol.divUpFixed(numerator, denominator); // change the order to + ans = MathSol.divUpFixed(numerator, denominator); // change the order to directly use integer operations if (wrt_out) { - // directly use integer operations ans = MathSol.mulUpFixed( MathSol.mulUpFixed(ans, partial_y), partial_x From 9757000ab3240dd7cc353187c64dc95049df44a1 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Mon, 6 Dec 2021 14:40:09 -0300 Subject: [PATCH 06/43] linear: test cases and correction --- src/poolsMath/linear.ts | 20 +-- test/poolsMathLinear.spec.ts | 259 +++++++++++++++++++++++++++++++++++ test/poolsMathStable.spec.ts | 2 - 3 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 test/poolsMathLinear.spec.ts diff --git a/src/poolsMath/linear.ts b/src/poolsMath/linear.ts index 77dc504d..5bd88234 100644 --- a/src/poolsMath/linear.ts +++ b/src/poolsMath/linear.ts @@ -30,7 +30,7 @@ export function _calcBptOutPerMainIn( wrappedBalance, params ); - return MathSol.divDown( + return MathSol.divDownFixed( MathSol.mulDownFixed(bptSupply, deltaNominalMain), invariant ); @@ -52,7 +52,7 @@ export function _calcBptInPerMainOut( wrappedBalance, params ); - return MathSol.divUp( + return MathSol.divUpFixed( MathSol.mulUpFixed(bptSupply, deltaNominalMain), invariant ); @@ -67,7 +67,7 @@ export function _calcWrappedOutPerMainIn( const previousNominalMain = _toNominal(mainBalance, params); const afterNominalMain = _toNominal(mainBalance + mainIn, params); const deltaNominalMain = afterNominalMain - previousNominalMain; - return MathSol.divDown(deltaNominalMain, params.rate); + return MathSol.divDownFixed(deltaNominalMain, params.rate); } export function _calcWrappedInPerMainOut( @@ -79,7 +79,7 @@ export function _calcWrappedInPerMainOut( const previousNominalMain = _toNominal(mainBalance, params); const afterNominalMain = _toNominal(mainBalance - mainOut, params); const deltaNominalMain = previousNominalMain - afterNominalMain; - return MathSol.divUp(deltaNominalMain, params.rate); + return MathSol.divUpFixed(deltaNominalMain, params.rate); } export function _calcMainInPerBptOut( @@ -99,7 +99,7 @@ export function _calcMainInPerBptOut( wrappedBalance, params ); - const deltaNominalMain = MathSol.divUp( + const deltaNominalMain = MathSol.divUpFixed( MathSol.mulUpFixed(invariant, bptOut), bptSupply ); @@ -122,7 +122,7 @@ export function _calcMainOutPerBptIn( wrappedBalance, params ); - const deltaNominalMain = MathSol.divDown( + const deltaNominalMain = MathSol.divDownFixed( MathSol.mulDownFixed(invariant, bptIn), bptSupply ); @@ -157,7 +157,7 @@ export function _calcMainInPerWrappedOut( return newMainBalance - mainBalance; } -function _calcBptOutPerWrappedIn( +export function _calcBptOutPerWrappedIn( wrappedIn: bigint, mainBalance: bigint, wrappedBalance: bigint, @@ -182,14 +182,14 @@ function _calcBptOutPerWrappedIn( newWrappedBalance, params ); - const newBptBalance = MathSol.divDown( + const newBptBalance = MathSol.divDownFixed( MathSol.mulDownFixed(bptSupply, newInvariant), previousInvariant ); return newBptBalance - bptSupply; } -function _calcBptInPerWrappedOut( +export function _calcBptInPerWrappedOut( wrappedOut: bigint, mainBalance: bigint, wrappedBalance: bigint, @@ -209,7 +209,7 @@ function _calcBptInPerWrappedOut( newWrappedBalance, params ); - const newBptBalance = MathSol.divDown( + const newBptBalance = MathSol.divDownFixed( MathSol.mulDownFixed(bptSupply, newInvariant), previousInvariant ); diff --git a/test/poolsMathLinear.spec.ts b/test/poolsMathLinear.spec.ts new file mode 100644 index 00000000..b4e56f17 --- /dev/null +++ b/test/poolsMathLinear.spec.ts @@ -0,0 +1,259 @@ +import * as linear from '../src/poolsMath/linear'; +import { MathSol } from '../src/poolsMath/basicOperations'; +import { assert } from 'chai'; + +describe('poolsMathLinear', function () { + const ERROR = 1e-14; // 1e-14 + + const params = { + fee: s(0.01), + rate: s(1), + lowerTarget: s(1000), + upperTarget: s(2000), + }; + + describe('init', () => { + it('given main in', async () => { + params.rate = s(1); + const mainIn = s(1); + const mainBalance = s(0); + const wrappedBalance = s(0); + const bptSupply = s(0); + + const bptIn = linear._calcBptOutPerMainIn( + mainIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(bptIn, s(1.010101010101010101), ERROR); + }); + + it('given BPT out', async () => { + params.rate = s(1); + const bptOut = s(1.010101010101010102); + const mainBalance = s(0); + const wrappedBalance = s(0); + const bptSupply = s(0); + + const mainIn = linear._calcMainInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(mainBalance + mainIn, s(1), ERROR); + }); + }); + + describe('swap bpt & main', () => { + it('given main in', async () => { + params.rate = s(1); + const mainIn = s(100); + const mainBalance = s(1); + const wrappedBalance = s(0); + const bptSupply = s(1.010101010101010101); + + const bptOut = linear._calcBptOutPerMainIn( + mainIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(bptSupply + bptOut, s(102.020202020202020202), ERROR); + }); + + it('given BPT out', async () => { + params.rate = s(1.3); + const bptOut = s(100); + const mainBalance = s(455.990803937038319103); + const wrappedBalance = s(138.463846384639); + const bptSupply = s(704.587755444953); + + const mainIn = linear._calcMainInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(mainBalance + mainIn, s(546), ERROR); + }); + + it('given BPT in', async () => { + params.rate = s(1.3); + const bptIn = s(100); + const mainBalance = s(546); + const wrappedBalance = s(138.463846384639); + const bptSupply = s(804.587755444953); + + const mainOut = linear._calcMainOutPerBptIn( + bptIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(mainBalance - mainOut, s(455.990803937038319103), ERROR); + }); + + it('given main out', async () => { + params.rate = s(1); + const mainOut = s(50); + const mainBalance = s(101); + const wrappedBalance = s(0); + const bptSupply = s(102.020202020202020202); + + const bptIn = linear._calcBptInPerMainOut( + mainOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(bptSupply - bptIn, s(51.515151515151515151), ERROR); + }); + }); + + describe('swap main & wrapped', () => { + it('given main out', async () => { + params.rate = s(1); + const mainOut = s(10); + const mainBalance = s(51); + const wrappedBalance = s(0); + + const wrappedIn = linear._calcWrappedInPerMainOut( + mainOut, + mainBalance, + params + ); + verify(wrappedBalance + wrappedIn, s(10.10101010101010101), ERROR); + }); + + it('given main in', async () => { + params.rate = s(1); + const mainIn = s(5); + const mainBalance = s(41); + const wrappedBalance = s(10.10101010101010101); + + const wrappedOut = linear._calcWrappedOutPerMainIn( + mainIn, + mainBalance, + params + ); + verify(wrappedBalance - wrappedOut, s(5.050505050505050505), ERROR); + }); + + it('given wrapped out', async () => { + params.rate = s(1.3); + const wrappedOut = s(900); + const mainBalance = s(931.695980314809); + + const mainIn = linear._calcMainInPerWrappedOut( + wrappedOut, + mainBalance, + params + ); + verify(mainBalance + mainIn, s(2102.21812133126978788), ERROR); + }); + + it('given wrapped in', async () => { + params.rate = s(1.3); + const wrappedIn = s(50); + const mainBalance = s(996.10705082304); + + const mainOut = linear._calcMainOutPerWrappedIn( + wrappedIn, + mainBalance, + params + ); + verify(mainBalance - mainOut, s(931.6959803148096), ERROR); + }); + }); + + describe('swap bpt & wrapped', () => { + it('given wrapped in', async () => { + params.rate = s(1); + const wrappedIn = s(50); + const wrappedBalance = s(0); + const mainBalance = s(101); + const bptSupply = s(102.020202020202); + + const bptOut = linear._calcBptOutPerWrappedIn( + wrappedIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(bptSupply + bptOut, s(152.020202020202), ERROR); + }); + + it('given BPT out', async () => { + params.rate = s(1.2); + const bptOut = s(10); + const mainBalance = s(101); + const wrappedBalance = s(131); + const bptSupply = s(242.692607692922); + + const wrappedIn = linear._calcWrappedInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(wrappedBalance + wrappedIn, s(139.900841153356), ERROR); + }); + + it('given BPT in', async () => { + params.rate = s(1); + const bptIn = s(10); + const mainBalance = s(101); + const wrappedBalance = s(131); + const bptSupply = s(242.692607692922); + + const wrappedOut = linear._calcWrappedOutPerBptIn( + bptIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(wrappedBalance - wrappedOut, s(121.398545541402), ERROR); + }); + + it('given wrapped out', async () => { + params.rate = s(1.3); + const wrappedOut = s(10); + const wrappedBalance = s(70); + const mainBalance = s(101); + const bptSupply = s(172.020202020202020202); + + const bptIn = linear._calcBptInPerWrappedOut( + wrappedOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(bptSupply - bptIn, s(160.434561745986), ERROR); + }); + }); +}); + +function s(a: number): bigint { + return BigInt(a * 10 ** 18); +} + +function verify(result: bigint, expected: bigint, error: number): void { + assert.approximately( + Number(MathSol.divUpFixed(result, expected)) / 10 ** 18, + 1, + error, + 'wrong result' + ); +} diff --git a/test/poolsMathStable.spec.ts b/test/poolsMathStable.spec.ts index 157e97cd..601b268c 100644 --- a/test/poolsMathStable.spec.ts +++ b/test/poolsMathStable.spec.ts @@ -188,8 +188,6 @@ function checkDerivative_stable( amount, fee ); - console.log('(inverse) incremental quotient: ', incrementalQuotient); - console.log('computed spot price: ', der_ans); assert.approximately( Number(MathSol.divUpFixed(incrementalQuotient, der_ans)), Number(MathSol.ONE), From 8d1c7a9fab2fb998d7d2a501523c98c2574760be Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Tue, 7 Dec 2021 19:31:21 -0300 Subject: [PATCH 07/43] stable: add bpt functions + some test cases for these --- src/poolsMath/stable.ts | 453 ++++++++++++++++++++++++++--------- test/poolsMathStable.spec.ts | 133 +++++++++- 2 files changed, 464 insertions(+), 122 deletions(-) diff --git a/src/poolsMath/stable.ts b/src/poolsMath/stable.ts index d1fb659e..b70ce638 100644 --- a/src/poolsMath/stable.ts +++ b/src/poolsMath/stable.ts @@ -2,58 +2,6 @@ import { MathSol, BZERO } from './basicOperations'; const AMP_PRECISION = BigInt(1e3); -// PairType = 'token->token' -// SwapType = 'swapExactIn' -// Would it be better to call this _calcOutGivenIn? -export function _exactTokenInForTokenOut( - amp: bigint, - balances: bigint[], - tokenIndexIn: number, - tokenIndexOut: number, - amountIn: bigint, - fee: bigint -): bigint { - amountIn = subtractFee(amountIn, fee); - // Given that we need to have a greater final balance out, the invariant needs to be rounded up - const invariant = _calculateInvariant(amp, balances, true); - - const initBalance = balances[tokenIndexIn]; - balances[tokenIndexIn] = initBalance + amountIn; - const finalBalanceOut = _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp, - balances, - invariant, - tokenIndexOut - ); - return balances[tokenIndexOut] - finalBalanceOut - BigInt(1); -} - -export function _tokenInForExactTokenOut( - amp: bigint, - balances: bigint[], - tokenIndexIn: number, - tokenIndexOut: number, - amountOut: bigint, - fee: bigint -): bigint { - const invariant = _calculateInvariant(amp, balances, true); - balances[tokenIndexOut] = MathSol.sub(balances[tokenIndexOut], amountOut); - - const finalBalanceIn = _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp, - balances, - invariant, - tokenIndexIn - ); - - let amountIn = MathSol.add( - MathSol.sub(finalBalanceIn, balances[tokenIndexIn]), - BigInt(1) - ); - amountIn = addFee(amountIn, fee); - return amountIn; -} - function _calculateInvariant( amp: bigint, balances: bigint[], @@ -122,6 +70,343 @@ function _calculateInvariant( throw new Error('Errors.STABLE_INVARIANT_DIDNT_CONVERGE'); } +// PairType = 'token->token' +// SwapType = 'swapExactIn' +export function _calcOutGivenIn( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + amountIn: bigint, + fee: bigint +): bigint { + amountIn = subtractFee(amountIn, fee); + // Given that we need to have a greater final balance out, the invariant needs to be rounded up + const invariant = _calculateInvariant(amp, balances, true); + + const initBalance = balances[tokenIndexIn]; + balances[tokenIndexIn] = initBalance + amountIn; + const finalBalanceOut = _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp, + balances, + invariant, + tokenIndexOut + ); + return balances[tokenIndexOut] - finalBalanceOut - BigInt(1); +} + +export function _calcInGivenOut( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + tokenIndexOut: number, + amountOut: bigint, + fee: bigint +): bigint { + const invariant = _calculateInvariant(amp, balances, true); + balances[tokenIndexOut] = MathSol.sub(balances[tokenIndexOut], amountOut); + + const finalBalanceIn = _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp, + balances, + invariant, + tokenIndexIn + ); + + let amountIn = MathSol.add( + MathSol.sub(finalBalanceIn, balances[tokenIndexIn]), + BigInt(1) + ); + amountIn = addFee(amountIn, fee); + return amountIn; +} + +export function _calcBptOutGivenExactTokensIn( + amp: bigint, + balances: bigint[], + amountsIn: bigint[], + bptTotalSupply: bigint, + swapFeePercentage: bigint +): bigint { + // BPT out, so we round down overall. + + // First loop calculates the sum of all token balances, which will be used to calculate + // the current weights of each token, relative to this sum + let sumBalances = BigInt(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances + balances[i]; + } + + // Calculate the weighted balance ratio without considering fees + let balanceRatiosWithFee: bigint[] = new Array(amountsIn.length); + // The weighted sum of token balance ratios with fee + let invariantRatioWithFees = BigInt(0); + for (let i = 0; i < balances.length; i++) { + let currentWeight = MathSol.divDownFixed(balances[i], sumBalances); + balanceRatiosWithFee[i] = MathSol.divDownFixed( + balances[i] + amountsIn[i], + balances[i] + ); + invariantRatioWithFees = + invariantRatioWithFees + + MathSol.mulDownFixed(balanceRatiosWithFee[i], currentWeight); + } + + // Second loop calculates new amounts in, taking into account the fee on the percentage excess + let newBalances: bigint[] = new Array(balances.length); + for (let i = 0; i < balances.length; i++) { + let amountInWithoutFee: bigint; + + // Check if the balance ratio is greater than the ideal ratio to charge fees or not + if (balanceRatiosWithFee[i] > invariantRatioWithFees) { + const nonTaxableAmount = MathSol.mulDownFixed( + balances[i], + invariantRatioWithFees - MathSol.ONE + ); + const taxableAmount = amountsIn[i] - nonTaxableAmount; + // No need to use checked arithmetic for the swap fee, it is guaranteed to be lower than 50% + amountInWithoutFee = + nonTaxableAmount + + MathSol.mulDownFixed( + taxableAmount, + MathSol.ONE - swapFeePercentage + ); + } else { + amountInWithoutFee = amountsIn[i]; + } + newBalances[i] = balances[i] + amountInWithoutFee; + } + + // Get current and new invariants, taking swap fees into account + const currentInvariant = _calculateInvariant(amp, balances, true); + const newInvariant = _calculateInvariant(amp, newBalances, false); + const invariantRatio = MathSol.divDownFixed(newInvariant, currentInvariant); + + // If the invariant didn't increase for any reason, we simply don't mint BPT + if (invariantRatio > MathSol.ONE) { + return MathSol.mulDownFixed( + bptTotalSupply, + invariantRatio - MathSol.ONE + ); + } else { + return BigInt(0); + } +} + +export function _calcTokenInGivenExactBptOut( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + bptAmountOut: bigint, + bptTotalSupply: bigint, + fee: bigint +): bigint { + // Token in, so we round up overall. + const currentInvariant = _calculateInvariant(amp, balances, true); + const newInvariant = MathSol.mulUpFixed( + MathSol.divUpFixed( + MathSol.add(bptTotalSupply, bptAmountOut), + bptTotalSupply + ), + currentInvariant + ); + + // Calculate amount in without fee. + const newBalanceTokenIndex = + _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp, + balances, + newInvariant, + tokenIndexIn + ); + const amountInWithoutFee = MathSol.sub( + newBalanceTokenIndex, + balances[tokenIndexIn] + ); + + // First calculate the sum of all token balances, which will be used to calculate + // the current weight of each token + let sumBalances = BigInt(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = MathSol.add(sumBalances, balances[i]); + } + + // We can now compute how much extra balance is being deposited + // and used in virtual swaps, and charge swap fees accordingly. + const currentWeight = MathSol.divDownFixed( + balances[tokenIndexIn], + sumBalances + ); + const taxablePercentage = MathSol.complementFixed(currentWeight); + const taxableAmount = MathSol.mulUpFixed( + amountInWithoutFee, + taxablePercentage + ); + const nonTaxableAmount = MathSol.sub(amountInWithoutFee, taxableAmount); + + return MathSol.add( + nonTaxableAmount, + MathSol.divUpFixed(taxableAmount, MathSol.sub(MathSol.ONE, fee)) + ); +} + +/* +Flow of calculations: +amountsTokenOut -> amountsOutProportional -> +amountOutPercentageExcess -> amountOutBeforeFee -> newInvariant -> amountBPTIn +*/ +export function _calcBptInGivenExactTokensOut( + amp: bigint, + balances: bigint[], + amountsOut: bigint[], + bptTotalSupply: bigint, + swapFeePercentage: bigint +): bigint { + // BPT in, so we round up overall. + + // First loop calculates the sum of all token balances, which will be used to calculate + // the current weights of each token relative to this sum + let sumBalances = BigInt(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances + balances[i]; + } + + // Calculate the weighted balance ratio without considering fees + let balanceRatiosWithoutFee: bigint[] = new Array(amountsOut.length); + let invariantRatioWithoutFees = BigInt(0); + for (let i = 0; i < balances.length; i++) { + const currentWeight = MathSol.divUpFixed(balances[i], sumBalances); + balanceRatiosWithoutFee[i] = MathSol.divUpFixed( + balances[i] - amountsOut[i], + balances[i] + ); + invariantRatioWithoutFees = + invariantRatioWithoutFees + + MathSol.mulUpFixed(balanceRatiosWithoutFee[i], currentWeight); + } + + // Second loop calculates new amounts in, taking into account the fee on the percentage excess + let newBalances: bigint[] = new Array(balances.length); + for (let i = 0; i < balances.length; i++) { + // Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it to + // 'token out'. This results in slightly larger price impact. + + let amountOutWithFee: bigint; + if (invariantRatioWithoutFees > balanceRatiosWithoutFee[i]) { + const nonTaxableAmount = MathSol.mulDownFixed( + balances[i], + MathSol.complementFixed(invariantRatioWithoutFees) + ); + const taxableAmount = amountsOut[i] - nonTaxableAmount; + // No need to use checked arithmetic for the swap fee, it is guaranteed to be lower than 50% + amountOutWithFee = + nonTaxableAmount + + MathSol.divUpFixed( + taxableAmount, + MathSol.ONE - swapFeePercentage + ); + } else { + amountOutWithFee = amountsOut[i]; + } + newBalances[i] = balances[i] - amountOutWithFee; + } + + // Get current and new invariants, taking into account swap fees + const currentInvariant = _calculateInvariant(amp, balances, true); + const newInvariant = _calculateInvariant(amp, newBalances, false); + const invariantRatio = MathSol.divUpFixed(newInvariant, currentInvariant); + + // return amountBPTIn + return MathSol.mulUpFixed( + bptTotalSupply, + MathSol.complementFixed(invariantRatio) + ); +} + +export function _calcTokenOutGivenExactBptIn( + amp: bigint, + balances: bigint[], + tokenIndex: number, + bptAmountIn: bigint, + bptTotalSupply: bigint, + swapFeePercentage: bigint +): bigint { + // Token out, so we round down overall. + + // Get the current and new invariants. Since we need a bigger new invariant, we round the current one up. + const currentInvariant = _calculateInvariant(amp, balances, true); + const newInvariant = MathSol.mulUpFixed( + MathSol.divUpFixed(bptTotalSupply - bptAmountIn, bptTotalSupply), + currentInvariant + ); + + // Calculate amount out without fee + const newBalanceTokenIndex = + _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp, + balances, + newInvariant, + tokenIndex + ); + const amountOutWithoutFee = balances[tokenIndex] - newBalanceTokenIndex; + + // First calculate the sum of all token balances, which will be used to calculate + // the current weight of each token + let sumBalances = BigInt(0); + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances + balances[i]; + } + + // We can now compute how much excess balance is being withdrawn as a result of the virtual swaps, which result + // in swap fees. + const currentWeight = MathSol.divDownFixed( + balances[tokenIndex], + sumBalances + ); + const taxablePercentage = MathSol.complementFixed(currentWeight); + + // Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it + // to 'token out'. This results in slightly larger price impact. Fees are rounded up. + const taxableAmount = MathSol.mulUpFixed( + amountOutWithoutFee, + taxablePercentage + ); + const nonTaxableAmount = amountOutWithoutFee - taxableAmount; + + // No need to use checked arithmetic for the swap fee, it is guaranteed to be lower than 50% + return ( + nonTaxableAmount + + MathSol.mulDownFixed(taxableAmount, MathSol.ONE - swapFeePercentage) + ); +} + +export function _calcTokensOutGivenExactBptIn( + balances: bigint[], + bptAmountIn: bigint, + bptTotalSupply: bigint +): bigint[] { + /********************************************************************************************** + // exactBPTInForTokensOut // + // (per token) // + // aO = tokenAmountOut / bptIn \ // + // b = tokenBalance a0 = b * | --------------------- | // + // bptIn = bptAmountIn \ bptTotalSupply / // + // bpt = bptTotalSupply // + **********************************************************************************************/ + + // Since we're computing an amount out, we round down overall. This means rounding down on both the + // multiplication and division. + + const bptRatio = MathSol.divDownFixed(bptAmountIn, bptTotalSupply); + + let amountsOut: bigint[] = new Array(balances.length); + for (let i = 0; i < balances.length; i++) { + amountsOut[i] = MathSol.mulDownFixed(balances[i], bptRatio); + } + + return amountsOut; +} + function _getTokenBalanceGivenInvariantAndAllOtherBalances( amp: bigint, balances: bigint[], @@ -190,66 +475,6 @@ function addFee(amount: bigint, fee: bigint): bigint { return MathSol.divUpFixed(amount, MathSol.complementFixed(fee)); } -// Untested: -/* -Flow of calculations: -amountBPTOut -> newInvariant -> (amountInProportional, amountInAfterFee) -> -amountInPercentageExcess -> amountIn -*/ -export function _tokenInForExactBPTOut( // _calcTokenInGivenExactBptOut - amp: bigint, - balances: bigint[], - tokenIndexIn: number, - bptAmountOut: bigint, - bptTotalSupply: bigint, - fee: bigint -): bigint { - // Token in, so we round up overall. - const currentInvariant = _calculateInvariant(amp, balances, true); - const newInvariant = MathSol.mulUpFixed( - MathSol.divUp( - MathSol.add(bptTotalSupply, bptAmountOut), - bptTotalSupply - ), - currentInvariant - ); - - // Calculate amount in without fee. - const newBalanceTokenIndex = - _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp, - balances, - newInvariant, - tokenIndexIn - ); - const amountInWithoutFee = MathSol.sub( - newBalanceTokenIndex, - balances[tokenIndexIn] - ); - - // First calculate the sum of all token balances, which will be used to calculate - // the current weight of each token - let sumBalances = BigInt(0); - for (let i = 0; i < balances.length; i++) { - sumBalances = MathSol.add(sumBalances, balances[i]); - } - - // We can now compute how much extra balance is being deposited - // and used in virtual swaps, and charge swap fees accordingly. - const currentWeight = MathSol.divDown(balances[tokenIndexIn], sumBalances); - const taxablePercentage = MathSol.complementFixed(currentWeight); - const taxableAmount = MathSol.mulUpFixed( - amountInWithoutFee, - taxablePercentage - ); - const nonTaxableAmount = MathSol.sub(amountInWithoutFee, taxableAmount); - - return MathSol.add( - nonTaxableAmount, - MathSol.divUp(taxableAmount, MathSol.sub(MathSol.ONE, fee)) - ); -} - ///////// /// SpotPriceAfterSwap ///////// @@ -272,7 +497,7 @@ export function _spotPriceAfterSwapExactTokenInForTokenOut( ); balances[tokenIndexOut] = MathSol.sub( balances[tokenIndexOut], - _exactTokenInForTokenOut( + _calcOutGivenIn( amp, balancesCopy, tokenIndexIn, @@ -307,7 +532,7 @@ export function _spotPriceAfterSwapTokenInForExactTokenOut( fee: bigint ): BigInt { const balancesCopy = [...balances]; - let _in = _tokenInForExactTokenOut( + let _in = _calcInGivenOut( amp, balancesCopy, tokenIndexIn, diff --git a/test/poolsMathStable.spec.ts b/test/poolsMathStable.spec.ts index 601b268c..04d1b8dc 100644 --- a/test/poolsMathStable.spec.ts +++ b/test/poolsMathStable.spec.ts @@ -9,9 +9,9 @@ import { _poolDerivatives } from '../src/poolsMath/stable'; describe('poolsMathStable: numeric functions using bigint', () => { context('stable pools', () => { - it('_exactTokenInForTokenOut', () => { + it('out given in', () => { const { result, SDKResult } = getBothValuesStable( - stable._exactTokenInForTokenOut, + stable._calcOutGivenIn, SDK.StableMath._calcOutGivenIn, 1000, [1000, 3000, 2000], @@ -26,9 +26,9 @@ describe('poolsMathStable: numeric functions using bigint', () => { 'wrong result' ); }); - it('_tokenInForExactTokenOut', () => { + it('in given out', () => { const { result, SDKResult } = getBothValuesStable( - stable._tokenInForExactTokenOut, + stable._calcInGivenOut, SDK.StableMath._calcInGivenOut, 1000, [3000, 1000, 1000], @@ -43,11 +43,48 @@ describe('poolsMathStable: numeric functions using bigint', () => { 'wrong result' ); }); + it('bpt out given in', () => { + const { result, SDKResult } = getBothValuesStableBPTOutGivenIn( + stable._calcBptOutGivenExactTokensIn, + SDK.StableMath._calcBptOutGivenExactTokensIn, + 1000, + [3000, 1000, 1000], + [10, 20, 5], + 1600, + 0.01 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + it('in given bpt out', () => { + const { result, SDKResult } = getBothValuesStableInGivenBPTOut( + stable._calcTokenInGivenExactBptOut, + SDK.StableMath._calcTokenInGivenExactBptOut, + 1000, + [3000, 1000, 1000], + 0, + 60, + 1600, + 0.01 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + // TO DO: + // _calcBptInGivenExactTokensOut + // _calcTokenOutGivenExactBptIn + // _calcTokensOutGivenExactBptIn it('_spotPriceAfterSwapExactTokenInForTokenOut', () => { const delta = 0.01; const error = 0.00001; checkDerivative_stable( - stable._exactTokenInForTokenOut, + stable._calcOutGivenIn, stable._spotPriceAfterSwapExactTokenInForTokenOut, 10, [15000, 30000, 10000], @@ -60,7 +97,7 @@ describe('poolsMathStable: numeric functions using bigint', () => { true ); checkDerivative_stable( - stable._exactTokenInForTokenOut, + stable._calcOutGivenIn, stable._spotPriceAfterSwapExactTokenInForTokenOut, 10, [15000, 30000, 10000], @@ -74,7 +111,7 @@ describe('poolsMathStable: numeric functions using bigint', () => { ); checkDerivative_stable( - stable._tokenInForExactTokenOut, + stable._calcInGivenOut, stable._spotPriceAfterSwapTokenInForExactTokenOut, 10, [10000, 10000, 10000], @@ -133,7 +170,87 @@ function getBothValuesStable( return { result, SDKResult }; } -// To do: basic debug of components +function getBothValuesStableBPTOutGivenIn( + SORFunction: ( + amp: bigint, + balances: bigint[], + amountsIn: bigint[], + bptTotalSupply: bigint, + swapFeePercentage: bigint + ) => bigint, + SDKFunction: ( + amp: OldBigNumber, + balances: OldBigNumber[], + amountsIn: OldBigNumber[], + bptTotalSupply: OldBigNumber, + swapFeePercentage: OldBigNumber + ) => OldBigNumber, + amp: number, + balances: number[], + amountsIn: number[], + bptTotalSupply: number, + fee: number +): { result: any; SDKResult: any } { + const result = SORFunction( + BigInt(amp), + balances.map((amount) => s(amount)), + amountsIn.map((amount) => s(amount)), + s(bptTotalSupply), + s(fee) + ); + const SDKResult = SDKFunction( + bnum(amp), + balances.map((amount) => b(amount)), + amountsIn.map((amount) => b(amount)), + b(bptTotalSupply), + b(fee) + ); + return { result, SDKResult }; +} + +function getBothValuesStableInGivenBPTOut( + SORFunction: ( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + bptAmountOut: bigint, + bptTotalSupply: bigint, + fee: bigint + ) => bigint, + SDKFunction: ( + amp: OldBigNumber, + balances: OldBigNumber[], + tokenIndexIn: number, + bptAmountOut: OldBigNumber, + bptTotalSupply: OldBigNumber, + fee: OldBigNumber + ) => OldBigNumber, + amp: number, + balances: number[], + tokenIndexIn: number, + bptAmountOut: number, + bptTotalSupply: number, + fee: number +): { result: any; SDKResult: any } { + const result = SORFunction( + BigInt(amp), + balances.map((amount) => s(amount)), + tokenIndexIn, + s(bptAmountOut), + s(bptTotalSupply), + s(fee) + ); + const SDKResult = SDKFunction( + bnum(amp), + balances.map((amount) => b(amount)), + tokenIndexIn, + b(bptAmountOut), + b(bptTotalSupply), + b(fee) + ); + return { result, SDKResult }; +} + function checkDerivative_stable( fn: any, der: any, From 9588e25d6f956cefac2911f10861ac36e42419ed Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Wed, 8 Dec 2021 17:14:33 -0300 Subject: [PATCH 08/43] stable: corrections and tests --- src/poolsMath/stable.ts | 45 +++++++------ test/poolsMathStable.spec.ts | 119 +++++++++++++++++++++++++++-------- 2 files changed, 116 insertions(+), 48 deletions(-) diff --git a/src/poolsMath/stable.ts b/src/poolsMath/stable.ts index b70ce638..46a25289 100644 --- a/src/poolsMath/stable.ts +++ b/src/poolsMath/stable.ts @@ -138,11 +138,11 @@ export function _calcBptOutGivenExactTokensIn( } // Calculate the weighted balance ratio without considering fees - let balanceRatiosWithFee: bigint[] = new Array(amountsIn.length); + const balanceRatiosWithFee: bigint[] = new Array(amountsIn.length); // The weighted sum of token balance ratios with fee let invariantRatioWithFees = BigInt(0); for (let i = 0; i < balances.length; i++) { - let currentWeight = MathSol.divDownFixed(balances[i], sumBalances); + const currentWeight = MathSol.divDownFixed(balances[i], sumBalances); balanceRatiosWithFee[i] = MathSol.divDownFixed( balances[i] + amountsIn[i], balances[i] @@ -153,7 +153,7 @@ export function _calcBptOutGivenExactTokensIn( } // Second loop calculates new amounts in, taking into account the fee on the percentage excess - let newBalances: bigint[] = new Array(balances.length); + const newBalances: bigint[] = new Array(balances.length); for (let i = 0; i < balances.length; i++) { let amountInWithoutFee: bigint; @@ -272,7 +272,7 @@ export function _calcBptInGivenExactTokensOut( } // Calculate the weighted balance ratio without considering fees - let balanceRatiosWithoutFee: bigint[] = new Array(amountsOut.length); + const balanceRatiosWithoutFee: bigint[] = new Array(amountsOut.length); let invariantRatioWithoutFees = BigInt(0); for (let i = 0; i < balances.length; i++) { const currentWeight = MathSol.divUpFixed(balances[i], sumBalances); @@ -286,7 +286,7 @@ export function _calcBptInGivenExactTokensOut( } // Second loop calculates new amounts in, taking into account the fee on the percentage excess - let newBalances: bigint[] = new Array(balances.length); + const newBalances: bigint[] = new Array(balances.length); for (let i = 0; i < balances.length; i++) { // Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it to // 'token out'. This results in slightly larger price impact. @@ -314,7 +314,7 @@ export function _calcBptInGivenExactTokensOut( // Get current and new invariants, taking into account swap fees const currentInvariant = _calculateInvariant(amp, balances, true); const newInvariant = _calculateInvariant(amp, newBalances, false); - const invariantRatio = MathSol.divUpFixed(newInvariant, currentInvariant); + const invariantRatio = MathSol.divDownFixed(newInvariant, currentInvariant); // return amountBPTIn return MathSol.mulUpFixed( @@ -399,7 +399,7 @@ export function _calcTokensOutGivenExactBptIn( const bptRatio = MathSol.divDownFixed(bptAmountIn, bptTotalSupply); - let amountsOut: bigint[] = new Array(balances.length); + const amountsOut: bigint[] = new Array(balances.length); for (let i = 0; i < balances.length; i++) { amountsOut[i] = MathSol.mulDownFixed(balances[i], bptRatio); } @@ -532,7 +532,7 @@ export function _spotPriceAfterSwapTokenInForExactTokenOut( fee: bigint ): BigInt { const balancesCopy = [...balances]; - let _in = _calcInGivenOut( + const _in = _calcInGivenOut( amp, balancesCopy, tokenIndexIn, @@ -566,35 +566,34 @@ export function _poolDerivatives( is_first_derivative: boolean, wrt_out: boolean ): bigint { - let totalCoins = balances.length; - let D = _calculateInvariant(amp, balances, true); + const totalCoins = balances.length; + const D = _calculateInvariant(amp, balances, true); let S = BigInt(0); for (let i = 0; i < totalCoins; i++) { if (i != tokenIndexIn && i != tokenIndexOut) { S += balances[i]; } } - let x = balances[tokenIndexIn]; - let y = balances[tokenIndexOut]; - let a = amp * BigInt(totalCoins); - let b = a * (S - D) + D * AMP_PRECISION; - let twoaxy = BigInt(2) * a * x * y; - let partial_x = twoaxy + a * y * y + b * y; - let partial_y = twoaxy + a * x * x + b * x; + const x = balances[tokenIndexIn]; + const y = balances[tokenIndexOut]; + const a = amp * BigInt(totalCoins); + const b = a * (S - D) + D * AMP_PRECISION; + const twoaxy = BigInt(2) * a * x * y; + const partial_x = twoaxy + a * y * y + b * y; + const partial_y = twoaxy + a * x * x + b * x; let ans: bigint; if (is_first_derivative) { ans = MathSol.divUpFixed(partial_x, partial_y); } else { // Untested case: - let partial_xx = BigInt(2) * a * y; - let partial_yy = BigInt(2) * a * x; - let partial_xy = partial_xx + partial_yy + b; // AMP_PRECISION missing - let numerator: bigint; - numerator = + const partial_xx = BigInt(2) * a * y; + const partial_yy = BigInt(2) * a * x; + const partial_xy = partial_xx + partial_yy + b; // AMP_PRECISION missing + const numerator = BigInt(2) * partial_x * partial_y * partial_xy - partial_xx * partial_y * partial_y + partial_yy * partial_x * partial_x; - let denominator = partial_x * partial_x * partial_y; + const denominator = partial_x * partial_x * partial_y; ans = MathSol.divUpFixed(numerator, denominator); // change the order to directly use integer operations if (wrt_out) { ans = MathSol.mulUpFixed( diff --git a/test/poolsMathStable.spec.ts b/test/poolsMathStable.spec.ts index 04d1b8dc..5a95782a 100644 --- a/test/poolsMathStable.spec.ts +++ b/test/poolsMathStable.spec.ts @@ -3,9 +3,6 @@ import * as SDK from '@georgeroman/balancer-v2-pools'; import { BigNumber as OldBigNumber, bnum } from '../src/utils/bignumber'; import { assert } from 'chai'; import { MathSol } from '../src/poolsMath/basicOperations'; -import { _poolDerivatives as _oldPoolDerivatives } from '../src/pools/stablePool/oldButUsefulStableMath'; -import { _poolDerivatives as _currentPoolDerivatives } from '../src/pools/stablePool/stableMath'; -import { _poolDerivatives } from '../src/poolsMath/stable'; describe('poolsMathStable: numeric functions using bigint', () => { context('stable pools', () => { @@ -43,8 +40,8 @@ describe('poolsMathStable: numeric functions using bigint', () => { 'wrong result' ); }); - it('bpt out given in', () => { - const { result, SDKResult } = getBothValuesStableBPTOutGivenIn( + it('bpt out given tokens in', () => { + const { result, SDKResult } = getBothValuesBPTGivenExactTokens( stable._calcBptOutGivenExactTokensIn, SDK.StableMath._calcBptOutGivenExactTokensIn, 1000, @@ -60,7 +57,7 @@ describe('poolsMathStable: numeric functions using bigint', () => { ); }); it('in given bpt out', () => { - const { result, SDKResult } = getBothValuesStableInGivenBPTOut( + const { result, SDKResult } = getBothValuesTokenGivenBPT( stable._calcTokenInGivenExactBptOut, SDK.StableMath._calcTokenInGivenExactBptOut, 1000, @@ -76,10 +73,54 @@ describe('poolsMathStable: numeric functions using bigint', () => { 'wrong result' ); }); - // TO DO: - // _calcBptInGivenExactTokensOut - // _calcTokenOutGivenExactBptIn - // _calcTokensOutGivenExactBptIn + it('bpt in given tokens out', () => { + const { result, SDKResult } = getBothValuesBPTGivenExactTokens( + stable._calcBptInGivenExactTokensOut, + SDK.StableMath._calcBptInGivenExactTokensOut, + 1000, + [3000, 1000, 1000], + [10, 20, 5], + 1600, + 0.01 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + it('out given bpt in', () => { + const { result, SDKResult } = getBothValuesTokenGivenBPT( + stable._calcTokenOutGivenExactBptIn, + SDK.StableMath._calcTokenOutGivenExactBptIn, + 1000, + [3000, 1000, 1000], + 0, + 60, + 1600, + 0.01 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + it('tokens out given bpt in', () => { + const { result, SDKResult } = getBothValuesTokensOutGivenBPTIn( + stable._calcTokensOutGivenExactBptIn, + SDK.StableMath._calcTokensOutGivenExactBptIn, + [3000, 1000, 1000], + 60, + 1600 + ); + assert.equal( + result.toString(), + SDKResult.toString(), + 'wrong result' + ); + }); + it('_spotPriceAfterSwapExactTokenInForTokenOut', () => { const delta = 0.01; const error = 0.00001; @@ -170,50 +211,50 @@ function getBothValuesStable( return { result, SDKResult }; } -function getBothValuesStableBPTOutGivenIn( +function getBothValuesBPTGivenExactTokens( SORFunction: ( amp: bigint, balances: bigint[], - amountsIn: bigint[], + amounts: bigint[], bptTotalSupply: bigint, swapFeePercentage: bigint ) => bigint, SDKFunction: ( amp: OldBigNumber, balances: OldBigNumber[], - amountsIn: OldBigNumber[], + amounts: OldBigNumber[], bptTotalSupply: OldBigNumber, swapFeePercentage: OldBigNumber ) => OldBigNumber, amp: number, balances: number[], - amountsIn: number[], + amounts: number[], bptTotalSupply: number, fee: number ): { result: any; SDKResult: any } { const result = SORFunction( BigInt(amp), balances.map((amount) => s(amount)), - amountsIn.map((amount) => s(amount)), + amounts.map((amount) => s(amount)), s(bptTotalSupply), s(fee) ); const SDKResult = SDKFunction( bnum(amp), balances.map((amount) => b(amount)), - amountsIn.map((amount) => b(amount)), + amounts.map((amount) => b(amount)), b(bptTotalSupply), b(fee) ); return { result, SDKResult }; } -function getBothValuesStableInGivenBPTOut( +function getBothValuesTokenGivenBPT( SORFunction: ( amp: bigint, balances: bigint[], tokenIndexIn: number, - bptAmountOut: bigint, + bptAmount: bigint, bptTotalSupply: bigint, fee: bigint ) => bigint, @@ -221,14 +262,14 @@ function getBothValuesStableInGivenBPTOut( amp: OldBigNumber, balances: OldBigNumber[], tokenIndexIn: number, - bptAmountOut: OldBigNumber, + bptAmount: OldBigNumber, bptTotalSupply: OldBigNumber, fee: OldBigNumber ) => OldBigNumber, amp: number, balances: number[], tokenIndexIn: number, - bptAmountOut: number, + bptAmount: number, bptTotalSupply: number, fee: number ): { result: any; SDKResult: any } { @@ -236,7 +277,7 @@ function getBothValuesStableInGivenBPTOut( BigInt(amp), balances.map((amount) => s(amount)), tokenIndexIn, - s(bptAmountOut), + s(bptAmount), s(bptTotalSupply), s(fee) ); @@ -244,13 +285,41 @@ function getBothValuesStableInGivenBPTOut( bnum(amp), balances.map((amount) => b(amount)), tokenIndexIn, - b(bptAmountOut), + b(bptAmount), b(bptTotalSupply), b(fee) ); return { result, SDKResult }; } +function getBothValuesTokensOutGivenBPTIn( + SORFunction: ( + balances: bigint[], + bptAmountIn: bigint, + bptTotalSupply: bigint + ) => bigint[], + SDKFunction: ( + balances: OldBigNumber[], + bptAmountIn: OldBigNumber, + bptTotalSupply: OldBigNumber + ) => OldBigNumber[], + balances: number[], + bptAmountIn: number, + bptTotalSupply: number +): { result: any; SDKResult: any } { + const result = SORFunction( + balances.map((amount) => s(amount)), + s(bptAmountIn), + s(bptTotalSupply) + ); + const SDKResult = SDKFunction( + balances.map((amount) => b(amount)), + b(bptAmountIn), + b(bptTotalSupply) + ); + return { result, SDKResult }; +} + function checkDerivative_stable( fn: any, der: any, @@ -265,13 +334,13 @@ function checkDerivative_stable( inverse = false ) { const amp = BigInt(num_amp); - const balances1 = num_balances.map((balance, i) => { + const balances1 = num_balances.map((balance) => { return s(balance); }); - const balances2 = num_balances.map((balance, i) => { + const balances2 = num_balances.map((balance) => { return s(balance); }); - const balances3 = num_balances.map((balance, i) => { + const balances3 = num_balances.map((balance) => { return s(balance); }); const amount = s(num_amount); From 4262e0497711c9983a89a41dfc74b625be3d1851 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Fri, 17 Dec 2021 17:13:15 -0300 Subject: [PATCH 09/43] stable: spot price after swap, token-BPT --- src/poolsMath/stable.ts | 198 +++++++++++++++++++++++++++++++++++ test/poolsMathStable.spec.ts | 171 +++++++++++++++++++++++++++++- 2 files changed, 364 insertions(+), 5 deletions(-) diff --git a/src/poolsMath/stable.ts b/src/poolsMath/stable.ts index 46a25289..a0758eb1 100644 --- a/src/poolsMath/stable.ts +++ b/src/poolsMath/stable.ts @@ -558,6 +558,143 @@ export function _spotPriceAfterSwapTokenInForExactTokenOut( return ans; } +// PairType = 'token->BPT' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapExactTokenInForBPTOut( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + bptTotalSupply: bigint, + amountIn: bigint + // assuming zero fee +): bigint { + balances[tokenIndexIn] = balances[tokenIndexIn] + amountIn; + // working + const amountsIn = balances.map((_value, index) => + index == tokenIndexIn ? amountIn : BigInt(0) + ); + const finalBPTSupply = + bptTotalSupply + + _calcBptOutGivenExactTokensIn( + amp, + balances, + amountsIn, + bptTotalSupply, + BigInt(0) + ); + let ans = _poolDerivativesBPT( + amp, + balances, + finalBPTSupply, + tokenIndexIn, + true, + true, + false + ); + ans = MathSol.divUpFixed(MathSol.ONE, ans); + return ans; +} + +// PairType = 'token->BPT' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapTokenInForExactBPTOut( + amp: bigint, + balances: bigint[], + tokenIndexIn: number, + bptTotalSupply: bigint, + amountOut: bigint + // assuming zero fee +): bigint { + const balancesCopy = [...balances]; + const _in = _calcTokenInGivenExactBptOut( + amp, + balancesCopy, + tokenIndexIn, + amountOut, + bptTotalSupply, + BigInt(0) + ); + balances[tokenIndexIn] = balances[tokenIndexIn] + _in; + let ans = _poolDerivativesBPT( + amp, + balances, + bptTotalSupply + amountOut, + tokenIndexIn, + true, + true, + true + ); + ans = MathSol.divUpFixed(MathSol.ONE, ans); // ONE.div(ans.times(feeFactor)); + return ans; +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapExactBPTInForTokenOut( + amp: bigint, + balances: bigint[], + tokenIndexOut: number, + bptTotalSupply: bigint, + amountIn: bigint + // assuming zero fee +): bigint { + // balances copy not necessary? + const _out = _calcTokenOutGivenExactBptIn( + amp, + balances, + tokenIndexOut, + amountIn, + bptTotalSupply, + BigInt(0) + ); + balances[tokenIndexOut] = balances[tokenIndexOut] - _out; + const bptTotalSupplyAfter = MathSol.sub(bptTotalSupply, amountIn); + const ans = _poolDerivativesBPT( + amp, + balances, + bptTotalSupplyAfter, + tokenIndexOut, + true, + false, + false + ); + return ans; +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapBPTInForExactTokenOut( + amp: bigint, + balances: bigint[], + tokenIndexOut: number, + bptTotalSupply: bigint, + amountOut: bigint +): bigint { + balances[tokenIndexOut] = MathSol.sub(balances[tokenIndexOut], amountOut); + const amountsOut = balances.map((_value, index) => + index == tokenIndexOut ? amountOut : BigInt(0) + ); + const bptTotalSupplyAfter = + bptTotalSupply - + _calcBptInGivenExactTokensOut( + amp, + balances, + amountsOut, + bptTotalSupply, + BigInt(0) + ); + const ans = _poolDerivativesBPT( + amp, + balances, + bptTotalSupplyAfter, + tokenIndexOut, + true, + false, + true + ); + return ans; +} + export function _poolDerivatives( amp: bigint, balances: bigint[], @@ -604,3 +741,64 @@ export function _poolDerivatives( } return ans; } + +export function _poolDerivativesBPT( + amp: bigint, + balances: bigint[], + bptSupply: bigint, + tokenIndexIn: number, + is_first_derivative: boolean, + is_BPT_out: boolean, + wrt_out: boolean +): bigint { + const totalCoins = balances.length; + const D = _calculateInvariant(amp, balances, true); + let S = BigInt(0); + let D_P = D / BigInt(totalCoins); + for (let i = 0; i < totalCoins; i++) { + if (i != tokenIndexIn) { + S = S + balances[i]; + D_P = (D_P * D) / (BigInt(totalCoins) * balances[i]); + } + } + const x = balances[tokenIndexIn]; + const alpha = amp * BigInt(totalCoins); + const beta = alpha * S; // units = 10 ** 21 + const gamma = BigInt(AMP_PRECISION) - alpha; + const partial_x = BigInt(2) * alpha * x + beta + gamma * D; + const minus_partial_D = + D_P * BigInt(totalCoins + 1) * AMP_PRECISION - gamma * x; + const partial_D = -minus_partial_D; + const ans = MathSol.divUpFixed( + (partial_x * bptSupply) / minus_partial_D, + D + ); + /* + if (is_first_derivative) { + ans = MathSol.divUpFixed((partial_x * bptSupply) / minus_partial_D, D); + } else { + let partial_xx = bnum(2).times(alpha); + let partial_xD = gamma; + let n_times_nplusone = totalCoins * (totalCoins + 1); + let partial_DD = bnum(0).minus( D_P.times(n_times_nplusone).div(D) ); + if (is_BPT_out) { + let term1 = partial_xx.times(partial_D).div( partial_x.pow(2) ); + let term2 = bnum(2).times(partial_xD).div(partial_x); + let term3 = partial_DD.div(partial_D); + ans = (term1.minus(term2).plus(term3)).times(D).div(bptSupply) + if (wrt_out) { + let D_prime = bnum(0).minus( partial_x.div(partial_D) ); + ans = ans.div( D_prime ).times(D).div(bptSupply); + } + } else { + ans = bnum(2).times(partial_xD).div(partial_D).minus( + partial_DD.times(partial_x).div(partial_D.pow(2)) ).minus( + partial_xx.div(partial_x) ); + if (wrt_out) { + ans = ans.times(partial_x).div(minus_partial_D).times(bptSupply).div(D); + } + } + } +*/ + return ans; +} diff --git a/test/poolsMathStable.spec.ts b/test/poolsMathStable.spec.ts index 5a95782a..3bdfa6f5 100644 --- a/test/poolsMathStable.spec.ts +++ b/test/poolsMathStable.spec.ts @@ -121,10 +121,10 @@ describe('poolsMathStable: numeric functions using bigint', () => { ); }); - it('_spotPriceAfterSwapExactTokenInForTokenOut', () => { + it('spotPriceTokenToken', () => { const delta = 0.01; const error = 0.00001; - checkDerivative_stable( + checkDerivative_TokToTok( stable._calcOutGivenIn, stable._spotPriceAfterSwapExactTokenInForTokenOut, 10, @@ -137,7 +137,7 @@ describe('poolsMathStable: numeric functions using bigint', () => { error, true ); - checkDerivative_stable( + checkDerivative_TokToTok( stable._calcOutGivenIn, stable._spotPriceAfterSwapExactTokenInForTokenOut, 10, @@ -151,7 +151,7 @@ describe('poolsMathStable: numeric functions using bigint', () => { true ); - checkDerivative_stable( + checkDerivative_TokToTok( stable._calcInGivenOut, stable._spotPriceAfterSwapTokenInForExactTokenOut, 10, @@ -165,6 +165,59 @@ describe('poolsMathStable: numeric functions using bigint', () => { false ); }); + + it('spotPriceTokenBPT', () => { + const delta = 0.01; + const error = 0.00001; + checkDerivative_ExactTokenBPT( + stable._calcBptOutGivenExactTokensIn, + stable._spotPriceAfterSwapExactTokenInForBPTOut, + 10, + [15000, 30000, 10000], + 10000, + 1, + 100, + delta, + error, + true + ); + checkDerivative_ExactTokenBPT( + stable._calcBptInGivenExactTokensOut, + stable._spotPriceAfterSwapBPTInForExactTokenOut, + 10, + [15000, 30000, 10000], + 10000, + 1, + 100, + delta, + error, + false + ); + checkDerivative_exactBPTToken( + stable._calcTokenInGivenExactBptOut, + stable._spotPriceAfterSwapTokenInForExactBPTOut, + 10, + [2000, 1000, 1000], + 0, + 1, + 10000, + 0.0001, + 0.0001, + false + ); + checkDerivative_exactBPTToken( + stable._calcTokenOutGivenExactBptIn, + stable._spotPriceAfterSwapExactBPTInForTokenOut, + 10, + [2000, 1000, 1000], + 0, + 1, + 10000, + 0.0001, + 0.0001, + true + ); + }); }); }); @@ -320,7 +373,7 @@ function getBothValuesTokensOutGivenBPTIn( return { result, SDKResult }; } -function checkDerivative_stable( +function checkDerivative_TokToTok( fn: any, der: any, num_amp: number, @@ -382,6 +435,114 @@ function checkDerivative_stable( ); } +function checkDerivative_ExactTokenBPT( + fn: any, + der: any, + num_amp: number, + num_balances: number[], + num_bptSupply: number, + tokenIndex: number, + num_amount: number, + num_delta: number, + num_error: number, + inverse = false +) { + const amp = BigInt(num_amp); + const balances1 = num_balances.map((balance) => { + return s(balance); + }); + const balances2 = num_balances.map((balance) => { + return s(balance); + }); + const balances3 = num_balances.map((balance) => { + return s(balance); + }); + const bptSupply = s(num_bptSupply); + const amount = s(num_amount); + const delta = s(num_delta); + const error = s(num_error); + + const amounts = balances1.map((_value, index) => + index == tokenIndex ? amount : BigInt(0) + ); + const amountsPlusDelta = balances1.map((_value, index) => + index == tokenIndex ? amount + delta : BigInt(0) + ); + + const val1 = fn(amp, balances1, amountsPlusDelta, bptSupply, BigInt(0)); + const val2 = fn(amp, balances2, amounts, bptSupply, BigInt(0)); + let incrementalQuotient = MathSol.divUpFixed( + MathSol.sub(val1, val2), + delta + ); + if (inverse) + incrementalQuotient = MathSol.divUpFixed( + MathSol.ONE, + incrementalQuotient + ); + const der_ans = der(amp, balances3, tokenIndex, bptSupply, amount); + assert.approximately( + Number(MathSol.divUpFixed(incrementalQuotient, der_ans)), + Number(MathSol.ONE), + Number(error), + 'wrong result' + ); +} + +function checkDerivative_exactBPTToken( + fn: any, + der: any, + num_amp: number, + num_balances: number[], + tokenIndex: number, + num_amount: number, + num_bptSupply: number, + num_delta: number, + num_error: number, + inverse = false +) { + const amp = BigInt(num_amp); + const balances1 = num_balances.map((balance) => { + return s(balance); + }); + const balances2 = num_balances.map((balance) => { + return s(balance); + }); + const balances3 = num_balances.map((balance) => { + return s(balance); + }); + const bptSupply = s(num_bptSupply); + const amount = s(num_amount); + const delta = s(num_delta); + const error = s(num_error); + + const val1 = fn( + amp, + balances1, + tokenIndex, + amount + delta, + bptSupply, + BigInt(0) + ); + const val2 = fn(amp, balances2, tokenIndex, amount, bptSupply, BigInt(0)); + let incrementalQuotient = MathSol.divUpFixed( + MathSol.sub(val1, val2), + delta + ); + if (inverse) + incrementalQuotient = MathSol.divUpFixed( + MathSol.ONE, + incrementalQuotient + ); + const der_ans = der(amp, balances3, tokenIndex, bptSupply, amount); + assert.approximately( + Number(MathSol.divUpFixed(incrementalQuotient, der_ans)), + Number(MathSol.ONE), + Number(error), + 'wrong result' + ); +} + function s(a: number): bigint { return BigInt(a * 10 ** 18); } From 8f0aef557ad0bf3a152b9e5bbf4af23f791f78c9 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Mon, 20 Dec 2021 15:35:15 -0300 Subject: [PATCH 10/43] update linear: _toNominal, _fromNominal and tests --- src/poolsMath/linear.ts | 56 ++- test/poolsMathLinear.spec.ts | 676 ++++++++++++++++++++++++----------- 2 files changed, 500 insertions(+), 232 deletions(-) diff --git a/src/poolsMath/linear.ts b/src/poolsMath/linear.ts index 5bd88234..8f51e384 100644 --- a/src/poolsMath/linear.ts +++ b/src/poolsMath/linear.ts @@ -289,43 +289,39 @@ function _calcInvariantDown( ); } -export function _toNominal(amount: bigint, params: Params): bigint { - if ( - amount < - MathSol.mulUpFixed(MathSol.ONE - params.fee, params.lowerTarget) - ) { - return MathSol.divUpFixed(amount, MathSol.ONE - params.fee); - } else if ( - amount < - params.upperTarget - MathSol.mulUpFixed(params.fee, params.lowerTarget) - ) { - return amount + MathSol.mulUpFixed(params.fee, params.lowerTarget); +function _toNominal(real: bigint, params: Params): bigint { + // Fees are always rounded down: either direction would work but we need to be consistent, and rounding down + // uses less gas. + if (real < params.lowerTarget) { + const fees = MathSol.mulDownFixed( + params.lowerTarget - real, + params.fee + ); + return MathSol.sub(real, fees); + } else if (real <= params.upperTarget) { + return real; } else { - return ( - amount + - MathSol.divUpFixed( - MathSol.mulUpFixed( - params.lowerTarget + params.upperTarget, - params.fee - ), - MathSol.ONE + params.fee - ) + const fees = MathSol.mulDownFixed( + real - params.upperTarget, + params.fee ); + return MathSol.sub(real, fees); } } -export function _fromNominal(nominal: bigint, params: Params): bigint { +function _fromNominal(nominal: bigint, params: Params): bigint { + // Since real = nominal + fees, rounding down fees is equivalent to rounding down real. if (nominal < params.lowerTarget) { - return MathSol.mulUpFixed(nominal, MathSol.ONE - params.fee); - } else if (nominal < params.upperTarget) { - return nominal - MathSol.mulUpFixed(params.fee, params.lowerTarget); + return MathSol.divDownFixed( + nominal + MathSol.mulDownFixed(params.fee, params.lowerTarget), + MathSol.ONE + params.fee + ); + } else if (nominal <= params.upperTarget) { + return nominal; } else { - return ( - MathSol.mulUpFixed(nominal, MathSol.ONE + params.fee) - - MathSol.mulUpFixed( - params.fee, - params.lowerTarget + params.upperTarget - ) + return MathSol.divDownFixed( + nominal - MathSol.mulDownFixed(params.fee, params.upperTarget), + MathSol.ONE - params.fee ); } } diff --git a/test/poolsMathLinear.spec.ts b/test/poolsMathLinear.spec.ts index b4e56f17..8832dfd3 100644 --- a/test/poolsMathLinear.spec.ts +++ b/test/poolsMathLinear.spec.ts @@ -3,244 +3,516 @@ import { MathSol } from '../src/poolsMath/basicOperations'; import { assert } from 'chai'; describe('poolsMathLinear', function () { - const ERROR = 1e-14; // 1e-14 - - const params = { - fee: s(0.01), - rate: s(1), - lowerTarget: s(1000), - upperTarget: s(2000), - }; - describe('init', () => { - it('given main in', async () => { - params.rate = s(1); - const mainIn = s(1); - const mainBalance = s(0); - const wrappedBalance = s(0); - const bptSupply = s(0); - - const bptIn = linear._calcBptOutPerMainIn( + const params = { + fee: s(0.01), + rate: s(1), + lowerTarget: s(0), + upperTarget: s(200), + }; + + const mainBalance = s(0); + const wrappedBalance = s(0); + const bptSupply = s(0); + + it('debug given main in within lower and upper', async () => { + const mainIn = s(5); + const result = linear._calcBptOutPerMainIn( mainIn, mainBalance, wrappedBalance, bptSupply, params ); - verify(bptIn, s(1.010101010101010101), ERROR); + verify(result, s(5), 0); }); - it('given BPT out', async () => { - params.rate = s(1); - const bptOut = s(1.010101010101010102); - const mainBalance = s(0); - const wrappedBalance = s(0); - const bptSupply = s(0); - - const mainIn = linear._calcMainInPerBptOut( - bptOut, + it('debug given main in over upper', async () => { + const mainIn = s(400); + const result = linear._calcBptOutPerMainIn( + mainIn, mainBalance, wrappedBalance, bptSupply, params ); - verify(mainBalance + mainIn, s(1), ERROR); + verify(result, s(398), 0); }); }); - describe('swap bpt & main', () => { - it('given main in', async () => { - params.rate = s(1); - const mainIn = s(100); - const mainBalance = s(1); - const wrappedBalance = s(0); - const bptSupply = s(1.010101010101010101); + const params = { + fee: s(0.01), + rate: s(1), + lowerTarget: s(100), + upperTarget: s(200), + }; - const bptOut = linear._calcBptOutPerMainIn( - mainIn, - mainBalance, - wrappedBalance, - bptSupply, - params - ); - verify(bptSupply + bptOut, s(102.020202020202020202), ERROR); + context('with main below lower', () => { + const mainBalance = s(35); + const wrappedBalance = s(15.15); + const bptSupply = s(49.5); + + context('swap bpt & main', () => { + context('main in', () => { + it('given main in', async () => { + const mainIn = s(100); + const result = linear._calcBptOutPerMainIn( + mainIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(100.65), 0); + }); + + it('given BPT out', async () => { + const bptOut = s(100.65); + const result = linear._calcMainInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(100), 0); + }); + }); + + context('main out', () => { + it('given BPT in', async () => { + const bptIn = s(10.1); + const result = linear._calcMainOutPerBptIn( + bptIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(10), 0); + }); + + it('given main out', async () => { + const mainOut = s(10); + const result = linear._calcBptInPerMainOut( + mainOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(10.1), 0); + }); + }); }); - it('given BPT out', async () => { - params.rate = s(1.3); - const bptOut = s(100); - const mainBalance = s(455.990803937038319103); - const wrappedBalance = s(138.463846384639); - const bptSupply = s(704.587755444953); - - const mainIn = linear._calcMainInPerBptOut( - bptOut, - mainBalance, - wrappedBalance, - bptSupply, - params - ); - verify(mainBalance + mainIn, s(546), ERROR); + describe('swap main & wrapped', () => { + context('main in', () => { + it('given main in', async () => { + const mainIn = s(10); + + const result = linear._calcWrappedOutPerMainIn( + mainIn, + mainBalance, + params + ); + verify(result, s(10.1), 0); + }); + + it('given wrapped out', async () => { + const wrappedOut = s(10.1); + const result = linear._calcMainInPerWrappedOut( + wrappedOut, + mainBalance, + params + ); + verify(result, s(10), 0); + }); + }); + + context('main out', () => { + it('given main out', async () => { + const mainOut = s(10); + const result = linear._calcWrappedInPerMainOut( + mainOut, + mainBalance, + params + ); + verify(result, s(10.1), 0); + }); + + it('given wrapped in', async () => { + const wrappedIn = s(10.1); + const result = linear._calcMainOutPerWrappedIn( + wrappedIn, + mainBalance, + params + ); + verify(result, s(10), 0); + }); + }); }); - it('given BPT in', async () => { - params.rate = s(1.3); - const bptIn = s(100); - const mainBalance = s(546); - const wrappedBalance = s(138.463846384639); - const bptSupply = s(804.587755444953); - - const mainOut = linear._calcMainOutPerBptIn( - bptIn, - mainBalance, - wrappedBalance, - bptSupply, - params - ); - verify(mainBalance - mainOut, s(455.990803937038319103), ERROR); - }); - - it('given main out', async () => { - params.rate = s(1); - const mainOut = s(50); - const mainBalance = s(101); - const wrappedBalance = s(0); - const bptSupply = s(102.020202020202020202); - - const bptIn = linear._calcBptInPerMainOut( - mainOut, - mainBalance, - wrappedBalance, - bptSupply, - params - ); - verify(bptSupply - bptIn, s(51.515151515151515151), ERROR); + describe('swap bpt & wrapped', () => { + it('given wrapped in', async () => { + const wrappedIn = s(5); + const result = linear._calcBptOutPerWrappedIn( + wrappedIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given BPT out', async () => { + const bptOut = s(5); + const result = linear._calcWrappedInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given BPT in', async () => { + const bptIn = s(5); + const result = linear._calcWrappedOutPerBptIn( + bptIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given wrapped out', async () => { + const wrappedOut = s(5); + const result = linear._calcBptInPerWrappedOut( + wrappedOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); }); }); - describe('swap main & wrapped', () => { - it('given main out', async () => { - params.rate = s(1); - const mainOut = s(10); - const mainBalance = s(51); - const wrappedBalance = s(0); - - const wrappedIn = linear._calcWrappedInPerMainOut( - mainOut, - mainBalance, - params - ); - verify(wrappedBalance + wrappedIn, s(10.10101010101010101), ERROR); + context('with main within lower and upper', () => { + const mainBalance = s(130); + const wrappedBalance = s(20); + const bptSupply = s(150); + + context('swap bpt & main', () => { + context('main in', () => { + it('given main in', async () => { + const mainIn = s(100); + const result = linear._calcBptOutPerMainIn( + mainIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(99.7), 0); + }); + + it('given BPT out', async () => { + const bptOut = s(99.7); + const result = linear._calcMainInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(100), 0); + }); + }); + + context('main out', () => { + it('given BPT in', async () => { + const bptIn = s(100.7); + const result = linear._calcMainOutPerBptIn( + bptIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(100), 0); + }); + + it('given main out', async () => { + const mainOut = s(100); + const result = linear._calcBptInPerMainOut( + mainOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(100.7), 0); + }); + }); }); - it('given main in', async () => { - params.rate = s(1); - const mainIn = s(5); - const mainBalance = s(41); - const wrappedBalance = s(10.10101010101010101); - - const wrappedOut = linear._calcWrappedOutPerMainIn( - mainIn, - mainBalance, - params - ); - verify(wrappedBalance - wrappedOut, s(5.050505050505050505), ERROR); + describe('swap main & wrapped', () => { + context('main in', () => { + it('given main in', async () => { + const mainIn = s(20); + const result = linear._calcWrappedOutPerMainIn( + mainIn, + mainBalance, + params + ); + verify(result, s(20), 0); + }); + + it('given wrapped out', async () => { + const wrappedOut = s(20); + const result = linear._calcMainInPerWrappedOut( + wrappedOut, + mainBalance, + params + ); + verify(result, s(20), 0); + }); + }); + + context('main out', () => { + it('given main out', async () => { + const mainOut = s(20); + const result = linear._calcWrappedInPerMainOut( + mainOut, + mainBalance, + params + ); + verify(result, s(20), 0); + }); + + it('given wrapped in', async () => { + const wrappedIn = s(20); + const result = linear._calcMainOutPerWrappedIn( + wrappedIn, + mainBalance, + params + ); + verify(result, s(20), 0); + }); + }); }); - it('given wrapped out', async () => { - params.rate = s(1.3); - const wrappedOut = s(900); - const mainBalance = s(931.695980314809); - - const mainIn = linear._calcMainInPerWrappedOut( - wrappedOut, - mainBalance, - params - ); - verify(mainBalance + mainIn, s(2102.21812133126978788), ERROR); - }); - - it('given wrapped in', async () => { - params.rate = s(1.3); - const wrappedIn = s(50); - const mainBalance = s(996.10705082304); - - const mainOut = linear._calcMainOutPerWrappedIn( - wrappedIn, - mainBalance, - params - ); - verify(mainBalance - mainOut, s(931.6959803148096), ERROR); + describe('swap bpt & wrapped', () => { + it('given wrapped in', async () => { + const wrappedIn = s(5); + const result = linear._calcBptOutPerWrappedIn( + wrappedIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given BPT out', async () => { + const bptOut = s(5); + const result = linear._calcWrappedInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given BPT in', async () => { + const bptIn = s(5); + const result = linear._calcWrappedOutPerBptIn( + bptIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given wrapped out', async () => { + const wrappedOut = s(5); + const result = linear._calcBptInPerWrappedOut( + wrappedOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); }); }); - describe('swap bpt & wrapped', () => { - it('given wrapped in', async () => { - params.rate = s(1); - const wrappedIn = s(50); - const wrappedBalance = s(0); - const mainBalance = s(101); - const bptSupply = s(102.020202020202); - - const bptOut = linear._calcBptOutPerWrappedIn( - wrappedIn, - mainBalance, - wrappedBalance, - bptSupply, - params - ); - verify(bptSupply + bptOut, s(152.020202020202), ERROR); - }); - - it('given BPT out', async () => { - params.rate = s(1.2); - const bptOut = s(10); - const mainBalance = s(101); - const wrappedBalance = s(131); - const bptSupply = s(242.692607692922); - - const wrappedIn = linear._calcWrappedInPerBptOut( - bptOut, - mainBalance, - wrappedBalance, - bptSupply, - params - ); - verify(wrappedBalance + wrappedIn, s(139.900841153356), ERROR); + context('with main above upper', () => { + const mainBalance = s(240); + const wrappedBalance = s(59.4); + const bptSupply = s(299); + + context('swap bpt & main', () => { + context('main in', () => { + it('given main in', async () => { + const mainIn = s(100); + const result = linear._calcBptOutPerMainIn( + mainIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(99), 0); + }); + + it('given BPT out', async () => { + const bptOut = s(99); + const result = linear._calcMainInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(100), 0); + }); + }); + + context('main out', () => { + it('given BPT in', async () => { + const bptIn = s(99.6); + const result = linear._calcMainOutPerBptIn( + bptIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(100), 0); + }); + + it('given main out', async () => { + const mainOut = s(100); + const result = linear._calcBptInPerMainOut( + mainOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(99.6), 0); + }); + }); }); - it('given BPT in', async () => { - params.rate = s(1); - const bptIn = s(10); - const mainBalance = s(101); - const wrappedBalance = s(131); - const bptSupply = s(242.692607692922); - - const wrappedOut = linear._calcWrappedOutPerBptIn( - bptIn, - mainBalance, - wrappedBalance, - bptSupply, - params - ); - verify(wrappedBalance - wrappedOut, s(121.398545541402), ERROR); + describe('swap main & wrapped', () => { + context('main in', () => { + it('given main in', async () => { + const mainIn = s(50); + const result = linear._calcWrappedOutPerMainIn( + mainIn, + mainBalance, + params + ); + verify(result, s(49.5), 0); + }); + + it('given wrapped out', async () => { + const wrappedOut = s(49.5); + const result = linear._calcMainInPerWrappedOut( + wrappedOut, + mainBalance, + params + ); + verify(result, s(50), 0); + }); + }); + + context('main out', () => { + it('given main out', async () => { + const mainOut = s(55); + const result = linear._calcWrappedInPerMainOut( + mainOut, + mainBalance, + params + ); + verify(result, s(54.6), 0); + }); + + it('given wrapped in', async () => { + const wrappedIn = s(54.6); + const result = linear._calcMainOutPerWrappedIn( + wrappedIn, + mainBalance, + params + ); + verify(result, s(55), 0); + }); + }); }); - it('given wrapped out', async () => { - params.rate = s(1.3); - const wrappedOut = s(10); - const wrappedBalance = s(70); - const mainBalance = s(101); - const bptSupply = s(172.020202020202020202); - - const bptIn = linear._calcBptInPerWrappedOut( - wrappedOut, - mainBalance, - wrappedBalance, - bptSupply, - params - ); - verify(bptSupply - bptIn, s(160.434561745986), ERROR); + describe('swap bpt & wrapped', () => { + it('given wrapped in', async () => { + const wrappedIn = s(5); + const result = linear._calcBptOutPerWrappedIn( + wrappedIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given BPT out', async () => { + const bptOut = s(5); + const result = linear._calcWrappedInPerBptOut( + bptOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given BPT in', async () => { + const bptIn = s(5); + const result = linear._calcWrappedOutPerBptIn( + bptIn, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); + + it('given wrapped out', async () => { + const wrappedOut = s(5); + const result = linear._calcBptInPerWrappedOut( + wrappedOut, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + verify(result, s(5), 0); + }); }); }); }); From 5d6034ab82dfac02e91061e876cfd47f0deeb86e Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Tue, 21 Dec 2021 16:01:11 -0300 Subject: [PATCH 11/43] linear: add spot price after swap with tests; add poolsMath readme --- src/poolsMath/README.md | 10 ++ src/poolsMath/linear.ts | 205 ++++++++++++++++++++++++++++++----- test/poolsMathLinear.spec.ts | 168 +++++++++++++++++++++++++++- 3 files changed, 356 insertions(+), 27 deletions(-) create mode 100644 src/poolsMath/README.md diff --git a/src/poolsMath/README.md b/src/poolsMath/README.md new file mode 100644 index 00000000..1b72e6ff --- /dev/null +++ b/src/poolsMath/README.md @@ -0,0 +1,10 @@ +## Pools formulas using bigint type + +Swap outcome and "spot price after swap" formulas for weighted, stable and linear pools. +Amounts are represented using bigint type. Swap outcomes formulas should +match exactly those from smart contracts. + +Test cases are found in poolsMathWeighted.spec.ts, poolsMathStable.spec.ts poolsMathLinear.spec.ts. + +It is necessary to review whether to use MathSol operations or native +,-,\*,/ case by case. MathSol operations are able to reproduce overflows while native operations produce a much more readable code. For instance, for "spot price after swap" native operations +are preferred since in this case there are not smart contract analogs, amount limits are assumed to have been checked elsewhere, and some formulas get complicated, specially for stable pools. diff --git a/src/poolsMath/linear.ts b/src/poolsMath/linear.ts index 8f51e384..5eea3781 100644 --- a/src/poolsMath/linear.ts +++ b/src/poolsMath/linear.ts @@ -289,6 +289,142 @@ function _calcInvariantDown( ); } +export function _calcTokensOutGivenExactBptIn( + balances: bigint[], + bptAmountIn: bigint, + bptTotalSupply: bigint, + bptIndex: number +): bigint[] { + /********************************************************************************************** + // exactBPTInForTokensOut // + // (per token) // + // aO = tokenAmountOut / bptIn \ // + // b = tokenBalance a0 = b * | --------------------- | // + // bptIn = bptAmountIn \ bptTotalSupply / // + // bpt = bptTotalSupply // + **********************************************************************************************/ + + // Since we're computing an amount out, we round down overall. This means rounding down on both the + // multiplication and division. + + const bptRatio = MathSol.divDownFixed(bptAmountIn, bptTotalSupply); + let amountsOut: bigint[] = new Array(balances.length); + for (let i = 0; i < balances.length; i++) { + // BPT is skipped as those tokens are not the LPs, but rather the preminted and undistributed amount. + if (i != bptIndex) { + amountsOut[i] = MathSol.mulDownFixed(balances[i], bptRatio); + } + } + return amountsOut; +} + +///////// +/// SpotPriceAfterSwap +///////// + +// PairType = 'token->BPT' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapBptOutPerMainIn( + mainIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + const finalMainBalance = mainIn + mainBalance; + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params + ); + let poolFactor = MathSol.ONE; + if (bptSupply != BigInt(0)) { + poolFactor = MathSol.divUpFixed(invariant, bptSupply); + } + return MathSol.divUpFixed( + poolFactor, + rightDerivativeToNominal(finalMainBalance, params) + ); +} + +// PairType = 'token->BPT' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapMainInPerBptOut( + bptOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params + ); + let poolFactor = MathSol.ONE; + if (bptSupply != BigInt(0)) { + poolFactor = MathSol.divUpFixed(invariant, bptSupply); + } + const deltaNominalMain = MathSol.mulUpFixed(bptOut, poolFactor); + const afterNominalMain = previousNominalMain + deltaNominalMain; + return MathSol.mulUpFixed( + poolFactor, + rightDerivativeFromNominal(afterNominalMain, params) + ); +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapMainOutPerBptIn( + bptIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params + ); + const poolFactor = MathSol.divDownFixed(invariant, bptSupply); + const deltaNominalMain = MathSol.mulDownFixed(bptIn, poolFactor); + const afterNominalMain = MathSol.sub(previousNominalMain, deltaNominalMain); + return MathSol.divUpFixed( + MathSol.ONE, + MathSol.mulUpFixed( + poolFactor, + leftDerivativeFromNominal(afterNominalMain, params) + ) + ); +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapBptInPerMainOut( + mainOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + const finalMainBalance = MathSol.sub(mainBalance, mainOut); + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params + ); + const poolFactor = MathSol.divUpFixed(invariant, bptSupply); + return MathSol.divUpFixed( + leftDerivativeToNominal(finalMainBalance, params), + poolFactor + ); +} + function _toNominal(real: bigint, params: Params): bigint { // Fees are always rounded down: either direction would work but we need to be consistent, and rounding down // uses less gas. @@ -309,6 +445,30 @@ function _toNominal(real: bigint, params: Params): bigint { } } +function leftDerivativeToNominal(amount: bigint, params: Params): bigint { + const oneMinusFee = MathSol.complementFixed(params.fee); + const onePlusFee = MathSol.ONE + params.fee; + if (amount <= params.lowerTarget) { + return onePlusFee; + } else if (amount <= params.upperTarget) { + return MathSol.ONE; + } else { + return oneMinusFee; + } +} + +function rightDerivativeToNominal(amount: bigint, params: Params): bigint { + const oneMinusFee = MathSol.complementFixed(params.fee); + const onePlusFee = MathSol.ONE + params.fee; + if (amount < params.lowerTarget) { + return onePlusFee; + } else if (amount < params.upperTarget) { + return MathSol.ONE; + } else { + return oneMinusFee; + } +} + function _fromNominal(nominal: bigint, params: Params): bigint { // Since real = nominal + fees, rounding down fees is equivalent to rounding down real. if (nominal < params.lowerTarget) { @@ -326,31 +486,26 @@ function _fromNominal(nominal: bigint, params: Params): bigint { } } -export function _calcTokensOutGivenExactBptIn( - balances: bigint[], - bptAmountIn: bigint, - bptTotalSupply: bigint, - bptIndex: number -): bigint[] { - /********************************************************************************************** - // exactBPTInForTokensOut // - // (per token) // - // aO = tokenAmountOut / bptIn \ // - // b = tokenBalance a0 = b * | --------------------- | // - // bptIn = bptAmountIn \ bptTotalSupply / // - // bpt = bptTotalSupply // - **********************************************************************************************/ - - // Since we're computing an amount out, we round down overall. This means rounding down on both the - // multiplication and division. +function leftDerivativeFromNominal(amount: bigint, params: Params): bigint { + const oneMinusFee = MathSol.complementFixed(params.fee); + const onePlusFee = MathSol.ONE + params.fee; + if (amount <= params.lowerTarget) { + return MathSol.divUpFixed(MathSol.ONE, onePlusFee); + } else if (amount <= params.upperTarget) { + return MathSol.ONE; + } else { + return MathSol.divUpFixed(MathSol.ONE, oneMinusFee); + } +} - const bptRatio = MathSol.divDownFixed(bptAmountIn, bptTotalSupply); - let amountsOut: bigint[] = new Array(balances.length); - for (let i = 0; i < balances.length; i++) { - // BPT is skipped as those tokens are not the LPs, but rather the preminted and undistributed amount. - if (i != bptIndex) { - amountsOut[i] = MathSol.mulDownFixed(balances[i], bptRatio); - } +function rightDerivativeFromNominal(amount: bigint, params: Params): bigint { + const oneMinusFee = MathSol.complementFixed(params.fee); + const onePlusFee = MathSol.ONE + params.fee; + if (amount < params.lowerTarget) { + return MathSol.divUpFixed(MathSol.ONE, onePlusFee); + } else if (amount < params.upperTarget) { + return MathSol.ONE; + } else { + return MathSol.divUpFixed(MathSol.ONE, oneMinusFee); } - return amountsOut; } diff --git a/test/poolsMathLinear.spec.ts b/test/poolsMathLinear.spec.ts index 8832dfd3..cdb3290d 100644 --- a/test/poolsMathLinear.spec.ts +++ b/test/poolsMathLinear.spec.ts @@ -3,6 +3,9 @@ import { MathSol } from '../src/poolsMath/basicOperations'; import { assert } from 'chai'; describe('poolsMathLinear', function () { + // For swap outcome functions: + // Test cases copied from smart contract tests, therefore rate is always 1. + // But we should also test different rates. describe('init', () => { const params = { fee: s(0.01), @@ -15,7 +18,7 @@ describe('poolsMathLinear', function () { const wrappedBalance = s(0); const bptSupply = s(0); - it('debug given main in within lower and upper', async () => { + it('given main in within lower and upper', async () => { const mainIn = s(5); const result = linear._calcBptOutPerMainIn( mainIn, @@ -27,7 +30,7 @@ describe('poolsMathLinear', function () { verify(result, s(5), 0); }); - it('debug given main in over upper', async () => { + it('given main in over upper', async () => { const mainIn = s(400); const result = linear._calcBptOutPerMainIn( mainIn, @@ -515,6 +518,90 @@ describe('poolsMathLinear', function () { }); }); }); + + describe('spot prices', () => { + const delta = 0.01; + const error = 0.00001; + const params = { + fee: s(0.04), + rate: s(1.2), + lowerTarget: s(1000), + upperTarget: s(2000), + }; + const mainBalanceTest = [500, 1400, 8000]; + it('BptOutPerMainIn', () => { + for (let mainBalance of mainBalanceTest) { + for (let amount of [70, 700]) { + checkDerivative( + linear._calcBptOutPerMainIn, + linear._spotPriceAfterSwapBptOutPerMainIn, + amount, + mainBalance, + 8000, // wrappedBalance + 3500, // bptSupply + params, + delta, + error, + true + ); + } + } + }); + it('MainInPerBptOut', () => { + for (let mainBalance of mainBalanceTest) { + for (let amount of [70, 7000]) { + checkDerivative( + linear._calcMainInPerBptOut, + linear._spotPriceAfterSwapMainInPerBptOut, + amount, + mainBalance, + 8000, // wrappedBalance + 3500, // bptSupply + params, + delta, + error, + false + ); + } + } + }); + it('MainOutPerBptIn', () => { + for (let mainBalance of mainBalanceTest) { + for (let amount of [70, 150]) { + checkDerivative( + linear._calcMainOutPerBptIn, + linear._spotPriceAfterSwapMainOutPerBptIn, + amount, + mainBalance, + 8000, // wrappedBalance + 3500, // bptSupply + params, + delta, + error, + true + ); + } + } + }); + it('BptInPerMainOut', () => { + for (let mainBalance of mainBalanceTest) { + for (let amount of [70, 420]) { + checkDerivative( + linear._calcBptInPerMainOut, + linear._spotPriceAfterSwapBptInPerMainOut, + amount, + mainBalance, + 8000, // wrappedBalance + 3500, // bptSupply + params, + delta, + error, + false + ); + } + } + }); + }); }); function s(a: number): bigint { @@ -529,3 +616,80 @@ function verify(result: bigint, expected: bigint, error: number): void { 'wrong result' ); } + +function checkDerivative( + fn: ( + mainIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: { + fee: bigint; + rate: bigint; + lowerTarget: bigint; + upperTarget: bigint; + } + ) => bigint, + derivative: ( + mainIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: { + fee: bigint; + rate: bigint; + lowerTarget: bigint; + upperTarget: bigint; + } + ) => bigint, + num_amount: number, + num_mainBalance: number, + num_wrappedBalance: number, + num_bptSupply: number, + params: { + fee: bigint; + rate: bigint; + lowerTarget: bigint; + upperTarget: bigint; + }, + num_delta: number, + num_error: number, + inverse: boolean +) { + const amount = s(num_amount); + const mainBalance = s(num_mainBalance); + const wrappedBalance = s(num_wrappedBalance); + const bptSupply = s(num_bptSupply); + const delta = s(num_delta); + const error = s(num_error); + const val1 = fn( + amount + delta, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + const val2 = fn(amount, mainBalance, wrappedBalance, bptSupply, params); + let incrementalQuotient = MathSol.divUpFixed( + MathSol.sub(val1, val2), + delta + ); + if (inverse) + incrementalQuotient = MathSol.divUpFixed( + MathSol.ONE, + incrementalQuotient + ); + const der_ans = derivative( + amount, + mainBalance, + wrappedBalance, + bptSupply, + params + ); + assert.approximately( + Number(MathSol.divUpFixed(incrementalQuotient, der_ans)), + Number(MathSol.ONE), + Number(error), + 'wrong result' + ); +} From 591ee6f29ae608993a0376d802b094d24de3d10e Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 11 Jan 2022 11:48:26 +0000 Subject: [PATCH 12/43] Weigthed pool - updated to BigInt maths. --- .../stablePool/oldButUsefulStableMath.ts | 892 ------------------ src/pools/weightedPool/weightedMath.ts | 191 ++-- src/pools/weightedPool/weightedPool.ts | 84 +- src/poolsMath/weighted.ts | 114 --- test/filtersAndPaths.spec.ts | 22 +- test/fullSwaps.spec.ts | 10 +- test/poolsMathWeighted.spec.ts | 18 +- 7 files changed, 188 insertions(+), 1143 deletions(-) delete mode 100644 src/pools/stablePool/oldButUsefulStableMath.ts delete mode 100644 src/poolsMath/weighted.ts diff --git a/src/pools/stablePool/oldButUsefulStableMath.ts b/src/pools/stablePool/oldButUsefulStableMath.ts deleted file mode 100644 index fbe69ef2..00000000 --- a/src/pools/stablePool/oldButUsefulStableMath.ts +++ /dev/null @@ -1,892 +0,0 @@ -import { INFINITESIMAL } from '../../config'; -import { BigNumber } from '../../utils/bignumber'; -import { bnum } from '../../bmath'; -import { privateEncrypt } from 'crypto'; -// All functions are adapted from the solidity ones to be found on: -// https://github.com/balancer-labs/balancer-core-v2/blob/master/contracts/pools/stable/StableMath.sol - -// TODO: implement all up and down rounding variations - -/********************************************************************************************** - // invariant // - // D = invariant to compute // - // A = amplifier n * D^2 + A * n^n * S * (n^n * P / D^(n−1)) // - // S = sum of balances ____________________________________________ // - // P = product of balances (n+1) * D + ( A * n^n − 1)* (n^n * P / D^(n−1)) // - // n = number of tokens // - **********************************************************************************************/ -export function _invariant( - amp: BigNumber, // amp - balances: BigNumber[] // balances -): BigNumber { - let sum = bnum(0); - let totalCoins = balances.length; - for (let i = 0; i < totalCoins; i++) { - sum = sum.plus(balances[i]); - } - if (sum.isZero()) { - return bnum(0); - } - let prevInv = bnum(0); - let inv = sum; - let ampTimesNpowN = amp.times(totalCoins ** totalCoins); // A*n^n - - for (let i = 0; i < 255; i++) { - let P_D = bnum(totalCoins).times(balances[0]); - for (let j = 1; j < totalCoins; j++) { - //P_D is rounded up - P_D = P_D.times(balances[j]).times(totalCoins).div(inv); - } - prevInv = inv; - //inv is rounded up - inv = bnum(totalCoins) - .times(inv) - .times(inv) - .plus(ampTimesNpowN.times(sum).times(P_D)) - .div( - bnum(totalCoins + 1) - .times(inv) - .plus(ampTimesNpowN.minus(1).times(P_D)) - ); - // Equality with the precision of 1 - if (inv.gt(prevInv)) { - if (inv.minus(prevInv).lt(bnum(10 ** -18))) { - break; - } - } else if (prevInv.minus(inv).lt(bnum(10 ** -18))) { - break; - } - } - //Result is rounded up - return inv; -} - -// // This function has to be zero if the invariant D was calculated correctly -// // It was only used for double checking that the invariant was correct -// export function _invariantValueFunction( -// amp: BigNumber, // amp -// balances: BigNumber[], // balances -// D: BigNumber -// ): BigNumber { -// let invariantValueFunction; -// let prod = bnum(1); -// let sum = bnum(0); -// for (let i = 0; i < balances.length; i++) { -// prod = prod.times(balances[i]); -// sum = sum.plus(balances[i]); -// } -// let n = bnum(balances.length); - -// // NOT! working based on Daniel's equation: https://www.notion.so/Analytical-for-2-tokens-1cd46debef6648dd81f2d75bae941fea -// // invariantValueFunction = amp.times(sum) -// // .plus((bnum(1).div(n.pow(n)).minus(amp)).times(D)) -// // .minus((bnum(1).div(n.pow(n.times(2)).times(prod))).times(D.pow(n.plus(bnum(1))))); -// invariantValueFunction = D.pow(n.plus(bnum(1))) -// .div(n.pow(n).times(prod)) -// .plus(D.times(amp.times(n.pow(n)).minus(bnum(1)))) -// .minus(amp.times(n.pow(n)).times(sum)); - -// return invariantValueFunction; -// } - -// Adapted from StableMath.sol, _outGivenIn() -// * Added swap fee at very first line -/********************************************************************************************** - // outGivenIn token x for y - polynomial equation to solve // - // ay = amount out to calculate // - // by = balance token out // - // y = by - ay // - // D = invariant D D^(n+1) // - // A = amplifier y^2 + ( S - ---------- - 1) * y - ------------- = 0 // - // n = number of tokens (A * n^n) A * n^2n * P // - // S = sum of final balances but y // - // P = product of final balances but y // - **********************************************************************************************/ -export function _exactTokenInForTokenOut(amount, poolPairData): BigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - let balances = [...allBalances]; - let tokenAmountIn = amount; - tokenAmountIn = tokenAmountIn.times(bnum(1).minus(swapFee)); - - //Invariant is rounded up - let inv = _invariant(amp, balances); - let p = inv; - let sum = bnum(0); - let totalCoins = bnum(balances.length); - let n_pow_n = bnum(1); - let x = bnum(0); - for (let i = 0; i < balances.length; i++) { - n_pow_n = n_pow_n.times(totalCoins); - - if (i == tokenIndexIn) { - x = balances[i].plus(tokenAmountIn); - } else if (i != tokenIndexOut) { - x = balances[i]; - } else { - continue; - } - sum = sum.plus(x); - //Round up p - p = p.times(inv).div(x); - } - - //Calculate out balance - let y = _solveAnalyticalBalance(sum, inv, amp, n_pow_n, p); - - //Result is rounded down - // return balances[tokenIndexOut] > y ? balances[tokenIndexOut].minus(y) : 0; - return balances[tokenIndexOut].minus(y); -} - -// Adapted from StableMath.sol, _inGivenOut() -// * Added swap fee at very last line -/********************************************************************************************** - // inGivenOut token x for y - polynomial equation to solve // - // ax = amount in to calculate // - // bx = balance token in // - // x = bx + ax // - // D = invariant D D^(n+1) // - // A = amplifier x^2 + ( S - ---------- - 1) * x - ------------- = 0 // - // n = number of tokens (A * n^n) A * n^2n * P // - // S = sum of final balances but x // - // P = product of final balances but x // - **********************************************************************************************/ -export function _tokenInForExactTokenOut(amount, poolPairData): BigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - let balances = [...allBalances]; - let tokenAmountOut = amount; - //Invariant is rounded up - let inv = _invariant(amp, balances); - let p = inv; - let sum = bnum(0); - let totalCoins = bnum(balances.length); - let n_pow_n = bnum(1); - let x = bnum(0); - for (let i = 0; i < balances.length; i++) { - n_pow_n = n_pow_n.times(totalCoins); - - if (i == tokenIndexOut) { - x = balances[i].minus(tokenAmountOut); - } else if (i != tokenIndexIn) { - x = balances[i]; - } else { - continue; - } - sum = sum.plus(x); - //Round up p - p = p.times(inv).div(x); - } - - //Calculate in balance - let y = _solveAnalyticalBalance(sum, inv, amp, n_pow_n, p); - - //Result is rounded up - return y.minus(balances[tokenIndexIn]).div(bnum(1).minus(swapFee)); -} - -//This function calculates the balance of a given token (tokenIndex) -// given all the other balances and the invariant -function _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp: BigNumber, - balances: BigNumber[], - inv: BigNumber, - tokenIndex: number -): BigNumber { - let p = inv; - let sum = bnum(0); - let totalCoins = balances.length; - let nPowN = bnum(1); - let x = bnum(0); - for (let i = 0; i < totalCoins; i++) { - nPowN = nPowN.times(totalCoins); - if (i != tokenIndex) { - x = balances[i]; - } else { - continue; - } - sum = sum.plus(x); - //Round up p - p = p.times(inv).div(x); - } - - // Calculate token balance - return _solveAnalyticalBalance(sum, inv, amp, nPowN, p); -} - -//This function calcuates the analytical solution to find the balance required -export function _solveAnalyticalBalance( - sum: BigNumber, - inv: BigNumber, - amp: BigNumber, - n_pow_n: BigNumber, - p: BigNumber -): BigNumber { - //Round up p - p = p.times(inv).div(amp.times(n_pow_n).times(n_pow_n)); - //Round down b - let b = sum.plus(inv.div(amp.times(n_pow_n))); - //Round up c - // let c = inv >= b - // ? inv.minus(b).plus(Math.sqrtUp(inv.minus(b).times(inv.minus(b)).plus(p.times(4)))) - // : Math.sqrtUp(b.minus(inv).times(b.minus(inv)).plus(p.times(4))).minus(b.minus(inv)); - let c; - if (inv.gte(b)) { - c = inv - .minus(b) - .plus(inv.minus(b).times(inv.minus(b)).plus(p.times(4)).sqrt()); - } else { - c = b - .minus(inv) - .times(b.minus(inv)) - .plus(p.times(4)) - .sqrt() - .minus(b.minus(inv)); - } - //Round up y - return c.div(2); -} - -/* -Adapted from StableMath.sol _exactTokensInForBPTOut() - * renamed it to _exactTokenInForBPTOut (i.e. just one token in) -*/ -export function _exactTokenInForBPTOut(amount, poolPairData): BigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; - let balances = [...allBalances]; - let tokenAmountIn = amount; - // Get current invariant - let currentInvariant = _invariant(amp, balances); - - // First calculate the sum of all token balances which will be used to calculate - // the current weights of each token relative to the sum of all balances - let sumBalances = bnum(0); - for (let i = 0; i < balances.length; i++) { - sumBalances = sumBalances.plus(balances[i]); - } - - // Calculate the weighted balance ratio without considering fees - let currentWeight = balances[tokenIndexIn].div(sumBalances); - let tokenBalanceRatioWithoutFee = balances[tokenIndexIn] - .plus(tokenAmountIn) - .div(balances[tokenIndexIn]); - let weightedBalanceRatio = bnum(1).plus( - tokenBalanceRatioWithoutFee.minus(bnum(1)).times(currentWeight) - ); - - // calculate new amountIn taking into account the fee on the % excess - // Percentage of the amount supplied that will be implicitly swapped for other tokens in the pool - let tokenBalancePercentageExcess = tokenBalanceRatioWithoutFee - .minus(weightedBalanceRatio) - .div(tokenBalanceRatioWithoutFee.minus(bnum(1))); - - let amountInAfterFee = tokenAmountIn.times( - bnum(1).minus(swapFee.times(tokenBalancePercentageExcess)) - ); - balances[tokenIndexIn] = balances[tokenIndexIn].plus(amountInAfterFee); - - // get new invariant taking into account swap fees - let newInvariant = _invariant(amp, balances); - - return balanceOut.times(newInvariant.div(currentInvariant).minus(bnum(1))); -} - -/* -Flow of calculations: -amountBPTOut -> newInvariant -> (amountInProportional, amountInAfterFee) -> -amountInPercentageExcess -> amountIn -*/ -export function _tokenInForExactBPTOut(amount, poolPairData): BigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; - let balances = [...allBalances]; - let bptAmountOut = amount; - - /********************************************************************************************** - // TODO description // - **********************************************************************************************/ - - // Get current invariant - let currentInvariant = _invariant(amp, balances); - // Calculate new invariant - let newInvariant = balanceOut - .plus(bptAmountOut) - .div(balanceOut) - .times(currentInvariant); - - // First calculate the sum of all token balances which will be used to calculate - // the current weight of token - let sumBalances = bnum(0); - for (let i = 0; i < balances.length; i++) { - sumBalances = sumBalances.plus(balances[i]); - } - - // get amountInAfterFee - let newBalanceTokenIndex = - _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp, - balances, - newInvariant, - tokenIndexIn - ); - let amountInAfterFee = newBalanceTokenIndex.minus(balances[tokenIndexIn]); - - // Get tokenBalancePercentageExcess - let currentWeight = balances[tokenIndexIn].div(sumBalances); - let tokenBalancePercentageExcess = bnum(1).minus(currentWeight); - - // return amountIn - return amountInAfterFee.div( - bnum(1).minus(tokenBalancePercentageExcess.times(swapFee)) - ); -} - -/* -Adapted from StableMath.sol _BPTInForExactTokensOut() to reduce it to -_BPTInForExactTokenOut (i.e. just one token out) -*/ -export function _BPTInForExactTokenOut(amount, poolPairData): BigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; - let balances = [...allBalances]; - let tokenAmountOut = amount; - - // Get current invariant - let currentInvariant = _invariant(amp, balances); - - // First calculate the sum of all token balances which will be used to calculate - // the current weights of each token relative to the sum of all balances - let sumBalances = bnum(0); - for (let i = 0; i < balances.length; i++) { - sumBalances = sumBalances.plus(balances[i]); - } - - // Calculate the weighted balance ratio without considering fees - let currentWeight = balances[tokenIndexOut].div(sumBalances); - let tokenBalanceRatioWithoutFee = balances[tokenIndexOut] - .minus(tokenAmountOut) - .div(balances[tokenIndexOut]); - let weightedBalanceRatio = bnum(1).minus( - bnum(1).minus(tokenBalanceRatioWithoutFee).times(currentWeight) - ); - - // calculate new amounts in taking into account the fee on the % excess - let tokenBalancePercentageExcess = weightedBalanceRatio - .minus(tokenBalanceRatioWithoutFee) - .div(bnum(1).minus(tokenBalanceRatioWithoutFee)); - - let amountOutBeforeFee = tokenAmountOut.div( - bnum(1).minus(swapFee.times(tokenBalancePercentageExcess)) - ); - balances[tokenIndexOut] = balances[tokenIndexOut].minus(amountOutBeforeFee); - - // get new invariant taking into account swap fees - let newInvariant = _invariant(amp, balances); - - // return amountBPTIn - return balanceIn.times(bnum(1).minus(newInvariant.div(currentInvariant))); -} - -/* -Flow of calculations: -amountBPTin -> newInvariant -> (amountOutProportional, amountOutBeforeFee) -> -amountOutPercentageExcess -> amountOut -*/ -export function _exactBPTInForTokenOut(amount, poolPairData): BigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; - let balances = [...allBalances]; - let bptAmountIn = amount; - /********************************************************************************************** - // TODO description // - **********************************************************************************************/ - - // Get current invariant - let currentInvariant = _invariant(amp, balances); - // Calculate new invariant - let newInvariant = balanceIn - .minus(bptAmountIn) - .div(balanceIn) - .times(currentInvariant); - - // First calculate the sum of all token balances which will be used to calculate - // the current weight of token - let sumBalances = bnum(0); - for (let i = 0; i < balances.length; i++) { - sumBalances = sumBalances.plus(balances[i]); - } - - // get amountOutBeforeFee - let newBalanceTokenIndex = - _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp, - balances, - newInvariant, - tokenIndexOut - ); - let amountOutBeforeFee = - balances[tokenIndexOut].minus(newBalanceTokenIndex); - - // Calculate tokenBalancePercentageExcess - let currentWeight = balances[tokenIndexOut].div(sumBalances); - let tokenBalancePercentageExcess = bnum(1).minus(currentWeight); - - // return amountOut - return amountOutBeforeFee.times( - bnum(1).minus(tokenBalancePercentageExcess.times(swapFee)) - ); -} - -////////////////////// -//// These functions have been added exclusively for the SORv2 -////////////////////// - -export function _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - is_first_derivative, - wrt_out -): BigNumber { - let totalCoins = balances.length; - let D = _invariant(amp, balances); - let S = bnum(0); - for (let i = 0; i < totalCoins; i++) { - if (i != tokenIndexIn && i != tokenIndexOut) { - S = S.plus(balances[i]); - } - } - let x = balances[tokenIndexIn]; - let y = balances[tokenIndexOut]; - let a = amp.times(totalCoins ** totalCoins); // = ampTimesNpowN - let b = S.minus(D).times(a).plus(D); - let twoaxy = bnum(2).times(a).times(x).times(y); - let partial_x = twoaxy.plus(a.times(y).times(y)).plus(b.times(y)); - let partial_y = twoaxy.plus(a.times(x).times(x)).plus(b.times(x)); - let ans; - if (is_first_derivative) { - ans = partial_x.div(partial_y); - } else { - let partial_xx = bnum(2).times(a).times(y); - let partial_yy = bnum(2).times(a).times(x); - let partial_xy = partial_xx.plus(partial_yy).plus(b); - let numerator; - numerator = bnum(2) - .times(partial_x) - .times(partial_y) - .times(partial_xy) - .minus(partial_xx.times(partial_y.pow(2))) - .minus(partial_yy.times(partial_x.pow(2))); - let denominator = partial_x.pow(2).times(partial_y); - ans = numerator.div(denominator); - if (wrt_out) { - ans = ans.times(partial_y).div(partial_x); - } - } - return ans; -} - -export function _poolDerivativesBPT( - amp, - balances, - bptSupply, - tokenIndexIn, - is_first_derivative, - is_BPT_out, - wrt_out -): BigNumber { - let totalCoins = balances.length; - let D = _invariant(amp, balances); - let S = bnum(0); - let D_P = D.div(totalCoins); - for (let i = 0; i < totalCoins; i++) { - if (i != tokenIndexIn) { - S = S.plus(balances[i]); - D_P = D_P.times(D).div(totalCoins * balances[i]); - } - } - let x = balances[tokenIndexIn]; - let alpha = amp.times(totalCoins ** totalCoins); // = ampTimesNpowN - let beta = alpha.times(S); - let gamma = bnum(1).minus(alpha); - let partial_x = bnum(2) - .times(alpha) - .times(x) - .plus(beta) - .plus(gamma.times(D)); - let minus_partial_D = D_P.times(totalCoins + 1).minus(gamma.times(x)); - let partial_D = bnum(0).minus(minus_partial_D); - let ans; - if (is_first_derivative) { - ans = partial_x.div(minus_partial_D).times(bptSupply).div(D); - } else { - let partial_xx = bnum(2).times(alpha); - let partial_xD = gamma; - let n_times_nplusone = totalCoins * (totalCoins + 1); - let partial_DD = bnum(0).minus(D_P.times(n_times_nplusone).div(D)); - if (is_BPT_out) { - let term1 = partial_xx.times(partial_D).div(partial_x.pow(2)); - let term2 = bnum(2).times(partial_xD).div(partial_x); - let term3 = partial_DD.div(partial_D); - ans = term1.minus(term2).plus(term3).times(D).div(bptSupply); - if (wrt_out) { - let D_prime = bnum(0).minus(partial_x.div(partial_D)); - ans = ans.div(D_prime).times(D).div(bptSupply); - } - } else { - ans = bnum(2) - .times(partial_xD) - .div(partial_D) - .minus(partial_DD.times(partial_x).div(partial_D.pow(2))) - .minus(partial_xx.div(partial_x)); - if (wrt_out) { - ans = ans - .times(partial_x) - .div(minus_partial_D) - .times(bptSupply) - .div(D); - } - } - } - return ans; -} - -///////// -/// SpotPriceAfterSwap -///////// - -// PairType = 'token->token' -// SwapType = 'swapExactIn' -export function _spotPriceAfterSwapExactTokenInForTokenOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - let balances = [...allBalances]; - balances[tokenIndexIn] = balances[tokenIndexIn].plus( - amount.times(bnum(1).minus(swapFee)) - ); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - _exactTokenInForTokenOut(amount, poolPairData) - ); - let ans = _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - true, - false - ); - ans = bnum(1).div(ans.times(bnum(1).minus(swapFee))); - return ans; -} - -// PairType = 'token->token' -// SwapType = 'swapExactOut' -export function _spotPriceAfterSwapTokenInForExactTokenOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - let balances = [...allBalances]; - let _in = _tokenInForExactTokenOut(amount, poolPairData).times( - bnum(1).minus(swapFee) - ); - balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in); - balances[tokenIndexOut] = balances[tokenIndexOut].minus(amount); - let ans = _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - true, - true - ); - ans = bnum(1).div(ans.times(bnum(1).minus(swapFee))); - return ans; -} - -function _feeFactor(balances, tokenIndex, swapFee): BigNumber { - let sumBalances = bnum(0); - for (let i = 0; i < balances.length; i++) { - sumBalances = sumBalances.plus(balances[i]); - } - let currentWeight = balances[tokenIndex].div(sumBalances); - let tokenBalancePercentageExcess = bnum(1).minus(currentWeight); - return bnum(1).minus(tokenBalancePercentageExcess.times(swapFee)); -} - -// PairType = 'token->BPT' -// SwapType = 'swapExactIn' -export function _spotPriceAfterSwapExactTokenInForBPTOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; - let balances = [...allBalances]; - let feeFactor = _feeFactor(balances, tokenIndexIn, swapFee); - balances[tokenIndexIn] = balances[tokenIndexIn].plus( - amount.times(feeFactor) - ); - balanceOut = balanceOut.plus(_exactTokenInForBPTOut(amount, poolPairData)); - let ans = _poolDerivativesBPT( - amp, - balances, - balanceOut, - tokenIndexIn, - true, - true, - false - ); - ans = bnum(1).div(ans.times(feeFactor)); - return ans; -} - -// PairType = 'token->BPT' -// SwapType = 'swapExactOut' -export function _spotPriceAfterSwapTokenInForExactBPTOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; - let balances = [...allBalances]; - let _in = _tokenInForExactBPTOut(amount, poolPairData); - let feeFactor = _feeFactor(balances, tokenIndexIn, swapFee); - balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in.times(feeFactor)); - balanceOut = balanceOut.plus(amount); - let ans = _poolDerivativesBPT( - amp, - balances, - balanceOut, - tokenIndexIn, - true, - true, - true - ); - ans = bnum(1).div(ans.times(feeFactor)); - return ans; -} - -// PairType = 'BPT->token' -// SwapType = 'swapExactIn' -export function _spotPriceAfterSwapExactBPTInForTokenOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; - let balances = [...allBalances]; - let _out = _exactBPTInForTokenOut(amount, poolPairData); - let feeFactor = _feeFactor(balances, tokenIndexOut, swapFee); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - _out.div(feeFactor) - ); - balanceIn = balanceIn.minus(amount); - let ans = _poolDerivativesBPT( - amp, - balances, - balanceIn, - tokenIndexOut, - true, - false, - false - ).div(feeFactor); - return ans; -} - -// PairType = 'BPT->token' -// SwapType = 'swapExactOut' -export function _spotPriceAfterSwapBPTInForExactTokenOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; - let balances = [...allBalances]; - let feeFactor = _feeFactor(balances, tokenIndexOut, swapFee); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - amount.div(feeFactor) - ); - balanceIn = balanceIn.minus(_BPTInForExactTokenOut(amount, poolPairData)); - let ans = _poolDerivativesBPT( - amp, - balances, - balanceIn, - tokenIndexOut, - true, - false, - true - ).div(feeFactor); - return ans; -} - -///////// -/// Derivatives of spotPriceAfterSwap -///////// - -// PairType = 'token->token' -// SwapType = 'swapExactIn' -export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - let balances = [...allBalances]; - balances[tokenIndexIn] = balances[tokenIndexIn].plus( - amount.times(bnum(1).minus(swapFee)) - ); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - _exactTokenInForTokenOut(amount, poolPairData) - ); - return _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - false, - false - ); -} - -// PairType = 'token->token' -// SwapType = 'swapExactOut' -export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - let balances = [...allBalances]; - let _in = _tokenInForExactTokenOut(amount, poolPairData).times( - bnum(1).minus(swapFee) - ); - balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in); - balances[tokenIndexOut] = balances[tokenIndexOut].minus(amount); - let feeFactor = bnum(1).minus(swapFee); - return _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - false, - true - ).div(feeFactor); -} - -// PairType = 'token->BPT' -// SwapType = 'swapExactIn' -export function _derivativeSpotPriceAfterSwapExactTokenInForBPTOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; - let balances = [...allBalances]; - let feeFactor = _feeFactor(balances, tokenIndexIn, swapFee); - balances[tokenIndexIn] = balances[tokenIndexIn].plus( - amount.times(feeFactor) - ); - balanceOut = balanceOut.plus(_exactTokenInForBPTOut(amount, poolPairData)); - let ans = _poolDerivativesBPT( - amp, - balances, - balanceOut, - tokenIndexIn, - false, - true, - false - ); - return ans; -} - -// PairType = 'token->BPT' -// SwapType = 'swapExactOut' -export function _derivativeSpotPriceAfterSwapTokenInForExactBPTOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, balanceOut, tokenIndexIn, swapFee } = poolPairData; - let balances = [...allBalances]; - let _in = _tokenInForExactBPTOut(amount, poolPairData); - let feeFactor = _feeFactor(balances, tokenIndexIn, swapFee); - balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in.times(feeFactor)); - balanceOut = balanceOut.plus(amount); - return _poolDerivativesBPT( - amp, - balances, - balanceOut, - tokenIndexIn, - false, - true, - true - ).div(feeFactor); -} - -// PairType = 'BPT->token' -// SwapType = 'swapExactIn' -export function _derivativeSpotPriceAfterSwapExactBPTInForTokenOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; - let balances = [...allBalances]; - let _out = _exactBPTInForTokenOut(amount, poolPairData); - let feeFactor = _feeFactor(balances, tokenIndexOut, swapFee); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - _out.div(feeFactor) - ); - balanceIn = balanceIn.minus(amount); - let ans = _poolDerivativesBPT( - amp, - balances, - balanceIn, - tokenIndexOut, - false, - false, - false - ); - return ans.div(feeFactor); -} - -// PairType = 'BPT->token' -// SwapType = 'swapExactOut' -export function _derivativeSpotPriceAfterSwapBPTInForExactTokenOut( - amount, - poolPairData -): BigNumber { - let { amp, allBalances, balanceIn, tokenIndexOut, swapFee } = poolPairData; - let balances = [...allBalances]; - let _in = _BPTInForExactTokenOut(amount, poolPairData); - let feeFactor = _feeFactor(balances, tokenIndexOut, swapFee); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - amount.div(feeFactor) - ); - balanceIn = balanceIn.minus(_in); - let ans = _poolDerivativesBPT( - amp, - balances, - balanceIn, - tokenIndexOut, - false, - false, - true - ); - return ans.div(feeFactor.pow(2)); -} diff --git a/src/pools/weightedPool/weightedMath.ts b/src/pools/weightedPool/weightedMath.ts index 5ef563dd..0f44352f 100644 --- a/src/pools/weightedPool/weightedMath.ts +++ b/src/pools/weightedPool/weightedMath.ts @@ -1,65 +1,122 @@ import { formatFixed } from '@ethersproject/bignumber'; import { BigNumber as OldBigNumber, bnum } from '../../utils/bignumber'; import { WeightedPoolPairData } from './weightedPool'; -// All functions came from https://www.wolframcloud.com/obj/fernando.martinel/Published/SOR_equations_published.nb +import { MathSol } from '../../poolsMath/basicOperations'; -///////// -/// Swap functions -///////// +// The following function are BigInt versions implemented by Sergio. +// BigInt was requested from integrators as it is more efficient. +// Swap outcomes formulas should match exactly those from smart contracts. +// PairType = 'token->token' +// SwapType = 'swapExactIn' +export function _calcOutGivenIn( + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amountIn: bigint, + fee: bigint +): bigint { + // is it necessary to check ranges of variables? same for the other functions + amountIn = subtractFee(amountIn, fee); + const exponent = MathSol.divDownFixed(weightIn, weightOut); + const denominator = MathSol.add(balanceIn, amountIn); + const base = MathSol.divUpFixed(balanceIn, denominator); + const power = MathSol.powUpFixed(base, exponent); + return MathSol.mulDownFixed(balanceOut, MathSol.complementFixed(power)); +} +// PairType = 'token->token' +// SwapType = 'swapExactOut' +export function _calcInGivenOut( + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amountOut: bigint, + fee: bigint +): bigint { + const base = MathSol.divUpFixed(balanceOut, balanceOut - amountOut); + const exponent = MathSol.divUpFixed(weightOut, weightIn); + const power = MathSol.powUpFixed(base, exponent); + const ratio = MathSol.sub(power, MathSol.ONE); + const amountIn = MathSol.mulUpFixed(balanceIn, ratio); + return addFee(amountIn, fee); +} + +function subtractFee(amount: bigint, fee: bigint): bigint { + const feeAmount = MathSol.mulUpFixed(amount, fee); + return amount - feeAmount; +} + +function addFee(amount: bigint, fee: bigint): bigint { + return MathSol.divUpFixed(amount, MathSol.complementFixed(fee)); +} + +// TO DO - Swap old versions of these in Pool for the BigInt version // PairType = 'token->token' // SwapType = 'swapExactIn' -export function _exactTokenInForTokenOut( - amount: OldBigNumber, - poolPairData: WeightedPoolPairData -): OldBigNumber { - const Bi = parseFloat( - formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) - ); - const Bo = parseFloat( - formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) - ); - const wi = parseFloat(formatFixed(poolPairData.weightIn, 18)); - const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); - const Ai = amount.toNumber(); - const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); - return bnum(Bo * (1 - (Bi / (Bi + Ai * (1 - f))) ** (wi / wo))); - // return Bo.times( - // bnum(1).minus( - // bnum( - // Bi.div( - // Bi.plus(Ai.times(bnum(1).minus(f))) - // ).toNumber() ** wi.div(wo).toNumber() - // ) - // ) - // ) +export function _spotPriceAfterSwapExactTokenInForTokenOutBigInt( + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amountIn: bigint, + fee: bigint +): bigint { + const numerator = MathSol.mulUpFixed(balanceIn, weightOut); + let denominator = MathSol.mulUpFixed(balanceOut, weightIn); + const feeComplement = MathSol.complementFixed(fee); + denominator = MathSol.mulUpFixed(denominator, feeComplement); + const base = MathSol.divUpFixed( + balanceIn, + MathSol.add(MathSol.mulUpFixed(amountIn, feeComplement), balanceIn) + ); + const exponent = MathSol.divUpFixed(weightIn + weightOut, weightOut); + denominator = MathSol.mulUpFixed( + denominator, + MathSol.powUpFixed(base, exponent) + ); + return MathSol.divUpFixed(numerator, denominator); + // -( + // (Bi * wo) / + // (Bo * (-1 + f) * (Bi / (Ai + Bi - Ai * f)) ** ((wi + wo) / wo) * wi) + // ) } // PairType = 'token->token' // SwapType = 'swapExactOut' -export function _tokenInForExactTokenOut( - amount: OldBigNumber, - poolPairData: WeightedPoolPairData -): OldBigNumber { - const Bi = parseFloat( - formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) - ); - const Bo = parseFloat( - formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) - ); - const wi = parseFloat(formatFixed(poolPairData.weightIn, 18)); - const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); - const Ao = amount.toNumber(); - const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); - return bnum((Bi * (-1 + (Bo / (-Ao + Bo)) ** (wo / wi))) / (1 - f)); - // return Bi.times( - // bnum(-1).plus( - // Bo.div(Bo.minus(Ao)).toNumber() ** - // wo.div(wi).toNumber() - // ) - // ).div(bnum(1).minus(f)); +export function _spotPriceAfterSwapTokenInForExactTokenOutBigInt( + balanceIn: bigint, + weightIn: bigint, + balanceOut: bigint, + weightOut: bigint, + amountOut: bigint, + fee: bigint +): bigint { + let numerator = MathSol.mulUpFixed(balanceIn, weightOut); + const feeComplement = MathSol.complementFixed(fee); + const base = MathSol.divUpFixed( + balanceOut, + MathSol.sub(balanceOut, amountOut) + ); + const exponent = MathSol.divUpFixed(weightIn + weightOut, weightIn); + numerator = MathSol.mulUpFixed( + numerator, + MathSol.powUpFixed(base, exponent) + ); + const denominator = MathSol.mulUpFixed( + MathSol.mulUpFixed(balanceOut, weightIn), + feeComplement + ); + return MathSol.divUpFixed(numerator, denominator); + // -( + // (Bi * (Bo / (-Ao + Bo)) ** ((wi + wo) / wi) * wo) / + // (Bo * (-1 + f) * wi) + // ) } +// The following functions are TS versions originally implemented by Fernando +// All functions came from https://www.wolframcloud.com/obj/fernando.martinel/Published/SOR_equations_published.nb // PairType = 'token->BPT' // SwapType = 'swapExactOut' export function _spotPriceAfterSwapTokenInForExactBPTOut( @@ -80,12 +137,12 @@ export function _spotPriceAfterSwapTokenInForExactBPTOut( } ///////// -/// SpotPriceAfterSwap +/// Derivatives of spotPriceAfterSwap ///////// // PairType = 'token->token' // SwapType = 'swapExactIn' -export function _spotPriceAfterSwapExactTokenInForTokenOut( +export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( amount: OldBigNumber, poolPairData: WeightedPoolPairData ): OldBigNumber { @@ -99,17 +156,12 @@ export function _spotPriceAfterSwapExactTokenInForTokenOut( const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); const Ai = amount.toNumber(); const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); - return bnum( - -( - (Bi * wo) / - (Bo * (-1 + f) * (Bi / (Ai + Bi - Ai * f)) ** ((wi + wo) / wo) * wi) - ) - ); + return bnum((wi + wo) / (Bo * (Bi / (Ai + Bi - Ai * f)) ** (wi / wo) * wi)); } // PairType = 'token->token' // SwapType = 'swapExactOut' -export function _spotPriceAfterSwapTokenInForExactTokenOut( +export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( amount: OldBigNumber, poolPairData: WeightedPoolPairData ): OldBigNumber { @@ -125,19 +177,15 @@ export function _spotPriceAfterSwapTokenInForExactTokenOut( const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); return bnum( -( - (Bi * (Bo / (-Ao + Bo)) ** ((wi + wo) / wi) * wo) / - (Bo * (-1 + f) * wi) + (Bi * (Bo / (-Ao + Bo)) ** (wo / wi) * wo * (wi + wo)) / + ((Ao - Bo) ** 2 * (-1 + f) * wi ** 2) ) ); } -///////// -/// Derivatives of spotPriceAfterSwap -///////// - // PairType = 'token->token' // SwapType = 'swapExactIn' -export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( +export function _spotPriceAfterSwapExactTokenInForTokenOut( amount: OldBigNumber, poolPairData: WeightedPoolPairData ): OldBigNumber { @@ -151,12 +199,17 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); const Ai = amount.toNumber(); const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); - return bnum((wi + wo) / (Bo * (Bi / (Ai + Bi - Ai * f)) ** (wi / wo) * wi)); + return bnum( + -( + (Bi * wo) / + (Bo * (-1 + f) * (Bi / (Ai + Bi - Ai * f)) ** ((wi + wo) / wo) * wi) + ) + ); } // PairType = 'token->token' // SwapType = 'swapExactOut' -export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( +export function _spotPriceAfterSwapTokenInForExactTokenOut( amount: OldBigNumber, poolPairData: WeightedPoolPairData ): OldBigNumber { @@ -172,8 +225,8 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); return bnum( -( - (Bi * (Bo / (-Ao + Bo)) ** (wo / wi) * wo * (wi + wo)) / - ((Ao - Bo) ** 2 * (-1 + f) * wi ** 2) + (Bi * (Bo / (-Ao + Bo)) ** ((wi + wo) / wi) * wo) / + (Bo * (-1 + f) * wi) ) ); } diff --git a/src/pools/weightedPool/weightedPool.ts b/src/pools/weightedPool/weightedPool.ts index b9fdc524..fd5dc08a 100644 --- a/src/pools/weightedPool/weightedPool.ts +++ b/src/pools/weightedPool/weightedPool.ts @@ -6,7 +6,6 @@ import { ZERO, } from '../../utils/bignumber'; import { isSameAddress } from '../../utils'; -import * as SDK from '@georgeroman/balancer-v2-pools'; import { PoolBase, PoolTypes, @@ -18,8 +17,8 @@ import { NoNullableField, } from '../../types'; import { - _exactTokenInForTokenOut, - _tokenInForExactTokenOut, + _calcOutGivenIn, + _calcInGivenOut, _spotPriceAfterSwapExactTokenInForTokenOut, _spotPriceAfterSwapTokenInForExactTokenOut, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, @@ -180,64 +179,63 @@ export class WeightedPool implements PoolBase { // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) + // calcOutGivenIn _exactTokenInForTokenOut( poolPairData: WeightedPoolPairData, amount: OldBigNumber, exact: boolean ): OldBigNumber { - if (exact) { - try { - // poolPair balances are normalised so must be scaled before use - const amt = SDK.WeightedMath._calcOutGivenIn( - bnum(poolPairData.balanceIn.toString()), - bnum(poolPairData.weightIn.toString()), - bnum(poolPairData.balanceOut.toString()), - bnum(poolPairData.weightOut.toString()), - scale(amount, poolPairData.decimalsIn), - bnum(poolPairData.swapFee.toString()) - ); - // return normalised amount - return scale(amt, -poolPairData.decimalsOut); - } catch (err) { - return ZERO; - } + if (amount.isNaN()) return amount; + + try { + const amt = _calcOutGivenIn( + poolPairData.balanceIn.toBigInt(), + poolPairData.weightIn.toBigInt(), + poolPairData.balanceOut.toBigInt(), + poolPairData.weightOut.toBigInt(), + parseFixed( + amount.dp(poolPairData.decimalsIn, 1).toString(), + poolPairData.decimalsIn + ).toBigInt(), + poolPairData.swapFee.toBigInt() + ); + // return human scaled + const amtOldBn = bnum(amt.toString()); + return scale(amtOldBn, -poolPairData.decimalsOut); + } catch (err) { + return ZERO; } - return _exactTokenInForTokenOut(amount, poolPairData).dp( - poolPairData.decimalsOut, - 1 - ); } // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_UP mode (0) + // calcInGivenOut _tokenInForExactTokenOut( poolPairData: WeightedPoolPairData, amount: OldBigNumber, exact: boolean ): OldBigNumber { - if (exact) { - try { - // poolPair balances are normalised so must be scaled before use - const amt = SDK.WeightedMath._calcInGivenOut( - bnum(poolPairData.balanceIn.toString()), - bnum(poolPairData.weightIn.toString()), - bnum(poolPairData.balanceOut.toString()), - bnum(poolPairData.weightOut.toString()), - scale(amount, poolPairData.decimalsOut), - bnum(poolPairData.swapFee.toString()) - ); + if (amount.isNaN()) return amount; - // return normalised amount - return scale(amt, -poolPairData.decimalsIn); - } catch (err) { - return ZERO; - } + try { + const amt = _calcInGivenOut( + poolPairData.balanceIn.toBigInt(), + poolPairData.weightIn.toBigInt(), + poolPairData.balanceOut.toBigInt(), + poolPairData.weightOut.toBigInt(), + parseFixed( + amount.dp(poolPairData.decimalsOut, 1).toString(), + poolPairData.decimalsOut + ).toBigInt(), + poolPairData.swapFee.toBigInt() + ); + // return human scaled + const amtOldBn = bnum(amt.toString()); + return scale(amtOldBn, -poolPairData.decimalsIn); + } catch (err) { + return ZERO; } - return _tokenInForExactTokenOut(amount, poolPairData).dp( - poolPairData.decimalsIn, - 0 - ); } _spotPriceAfterSwapExactTokenInForTokenOut( diff --git a/src/poolsMath/weighted.ts b/src/poolsMath/weighted.ts deleted file mode 100644 index e4caa730..00000000 --- a/src/poolsMath/weighted.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { MathSol } from './basicOperations'; - -// PairType = 'token->token' -// SwapType = 'swapExactIn' -// Would it be better to call this _calcOutGivenIn? -export function _exactTokenInForTokenOut( - balanceIn: bigint, - weightIn: bigint, - balanceOut: bigint, - weightOut: bigint, - amountIn: bigint, - fee: bigint -): bigint { - // is it necessary to check ranges of variables? same for the other functions - amountIn = subtractFee(amountIn, fee); - const exponent = MathSol.divDownFixed(weightIn, weightOut); - const denominator = MathSol.add(balanceIn, amountIn); - const base = MathSol.divUpFixed(balanceIn, denominator); - const power = MathSol.powUpFixed(base, exponent); - return MathSol.mulDownFixed(balanceOut, MathSol.complementFixed(power)); -} - -// PairType = 'token->token' -// SwapType = 'swapExactOut' -export function _tokenInForExactTokenOut( - balanceIn: bigint, - weightIn: bigint, - balanceOut: bigint, - weightOut: bigint, - amountOut: bigint, - fee: bigint -): bigint { - const base = MathSol.divUpFixed(balanceOut, balanceOut - amountOut); - const exponent = MathSol.divUpFixed(weightOut, weightIn); - const power = MathSol.powUpFixed(base, exponent); - const ratio = MathSol.sub(power, MathSol.ONE); - const amountIn = MathSol.mulUpFixed(balanceIn, ratio); - return addFee(amountIn, fee); -} - -function subtractFee(amount: bigint, fee: bigint): bigint { - const feeAmount = MathSol.mulUpFixed(amount, fee); - return amount - feeAmount; -} - -function addFee(amount: bigint, fee: bigint): bigint { - return MathSol.divUpFixed(amount, MathSol.complementFixed(fee)); -} - -///////// -/// SpotPriceAfterSwap -///////// - -// PairType = 'token->token' -// SwapType = 'swapExactIn' -export function _spotPriceAfterSwapExactTokenInForTokenOut( - balanceIn: bigint, - weightIn: bigint, - balanceOut: bigint, - weightOut: bigint, - amountIn: bigint, - fee: bigint -): bigint { - const numerator = MathSol.mulUpFixed(balanceIn, weightOut); - let denominator = MathSol.mulUpFixed(balanceOut, weightIn); - const feeComplement = MathSol.complementFixed(fee); - denominator = MathSol.mulUpFixed(denominator, feeComplement); - const base = MathSol.divUpFixed( - balanceIn, - MathSol.add(MathSol.mulUpFixed(amountIn, feeComplement), balanceIn) - ); - const exponent = MathSol.divUpFixed(weightIn + weightOut, weightOut); - denominator = MathSol.mulUpFixed( - denominator, - MathSol.powUpFixed(base, exponent) - ); - return MathSol.divUpFixed(numerator, denominator); - // -( - // (Bi * wo) / - // (Bo * (-1 + f) * (Bi / (Ai + Bi - Ai * f)) ** ((wi + wo) / wo) * wi) - // ) -} - -// PairType = 'token->token' -// SwapType = 'swapExactOut' -export function _spotPriceAfterSwapTokenInForExactTokenOut( - balanceIn: bigint, - weightIn: bigint, - balanceOut: bigint, - weightOut: bigint, - amountOut: bigint, - fee: bigint -): bigint { - let numerator = MathSol.mulUpFixed(balanceIn, weightOut); - const feeComplement = MathSol.complementFixed(fee); - const base = MathSol.divUpFixed( - balanceOut, - MathSol.sub(balanceOut, amountOut) - ); - const exponent = MathSol.divUpFixed(weightIn + weightOut, weightIn); - numerator = MathSol.mulUpFixed( - numerator, - MathSol.powUpFixed(base, exponent) - ); - const denominator = MathSol.mulUpFixed( - MathSol.mulUpFixed(balanceOut, weightIn), - feeComplement - ); - return MathSol.divUpFixed(numerator, denominator); - // -( - // (Bi * (Bo / (-Ao + Bo)) ** ((wi + wo) / wi) * wo) / - // (Bo * (-1 + f) * wi) - // ) -} diff --git a/test/filtersAndPaths.spec.ts b/test/filtersAndPaths.spec.ts index 79dac19a..f390320a 100644 --- a/test/filtersAndPaths.spec.ts +++ b/test/filtersAndPaths.spec.ts @@ -363,7 +363,7 @@ describe('Tests pools filtering and path processing', () => { ); // Known results taken from previous version - assert.equal(maxAmt.toString(), '1620713758415909242296'); + assert.equal(maxAmt.toString(), '1620713758415916763619'); checkPath( ['0x75286e183d923a5f52f52be205e358c5c9101b09'], poolsAll, @@ -410,7 +410,7 @@ describe('Tests pools filtering and path processing', () => { assert.equal( pathsSorted[3].limitAmount.toString(), - '33610758014622430' + '33610758022143753' ); }); @@ -585,13 +585,13 @@ describe('Tests pools filtering and path processing', () => { Zero ); - assert.equal(total.toString(), '0.979134514480937'); + assert.equal(total.toString(), '0.979134514480936'); assert.equal(swaps.length, 2); assert.equal( swaps[0][0].pool, '0x0481d726c3d25250a8963221945ed93b8a5315a9' ); - assert.equal(swaps[0][0].swapAmount, '0.500000000000022424'); + assert.equal(swaps[0][0].swapAmount, '0.500000000000016951'); assert.equal(swaps[0][0].tokenIn, tokenIn); assert.equal( swaps[0][0].tokenOut, @@ -601,7 +601,7 @@ describe('Tests pools filtering and path processing', () => { swaps[0][1].pool, '0x07d13ed39ee291c1506675ff42f9b2b6b50e2d3e' ); - assert.equal(swaps[0][1].swapAmount, '0.494754097206656'); + assert.equal(swaps[0][1].swapAmount, '0.49475509621737'); assert.equal( swaps[0][1].tokenIn, '0x0000000000085d4780b73119b644ae5ecd22b376' @@ -611,7 +611,7 @@ describe('Tests pools filtering and path processing', () => { swaps[1][0].pool, '0x0481d726c3d25250a8963221945ed93b8a5315a9' ); - assert.equal(swaps[1][0].swapAmount, '0.499999999999977576'); + assert.equal(swaps[1][0].swapAmount, '0.499999999999983049'); assert.equal(swaps[1][0].tokenIn, tokenIn); assert.equal( swaps[1][0].tokenOut, @@ -621,7 +621,7 @@ describe('Tests pools filtering and path processing', () => { swaps[1][1].pool, '0x07d13ed39ee291c1506675ff42f9b2b6b50e2d3e' ); - assert.equal(swaps[1][1].swapAmount, '0.494755096217348'); + assert.equal(swaps[1][1].swapAmount, '0.494754097206633'); assert.equal( swaps[1][1].tokenIn, '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' @@ -708,7 +708,7 @@ describe('Tests pools filtering and path processing', () => { swaps[0][0].pool, '0x0481d726c3d25250a8963221945ed93b8a5315a9' ); - assert.equal(swaps[0][0].swapAmount, '0.505303156638908081'); + assert.equal(swaps[0][0].swapAmount, '0.505303156638987879'); assert.equal(swaps[0][0].tokenIn, tokenIn); assert.equal( swaps[0][0].tokenOut, @@ -718,7 +718,7 @@ describe('Tests pools filtering and path processing', () => { swaps[0][1].pool, '0x07d13ed39ee291c1506675ff42f9b2b6b50e2d3e' ); - assert.equal(swaps[0][1].swapAmount, '0.499999999999981612'); + assert.equal(swaps[0][1].swapAmount, '0.500000000000060474'); assert.equal( swaps[0][1].tokenIn, '0x0000000000085d4780b73119b644ae5ecd22b376' @@ -728,7 +728,7 @@ describe('Tests pools filtering and path processing', () => { swaps[1][0].pool, '0x0481d726c3d25250a8963221945ed93b8a5315a9' ); - assert.equal(swaps[1][0].swapAmount, '0.505303156638945455'); + assert.equal(swaps[1][0].swapAmount, '0.505303156638865657'); assert.equal(swaps[1][0].tokenIn, tokenIn); assert.equal( swaps[1][0].tokenOut, @@ -738,7 +738,7 @@ describe('Tests pools filtering and path processing', () => { swaps[1][1].pool, '0x07d13ed39ee291c1506675ff42f9b2b6b50e2d3e' ); - assert.equal(swaps[1][1].swapAmount, '0.500000000000018388'); + assert.equal(swaps[1][1].swapAmount, '0.499999999999939526'); assert.equal( swaps[1][1].tokenIn, '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' diff --git a/test/fullSwaps.spec.ts b/test/fullSwaps.spec.ts index 29debd37..3960b469 100644 --- a/test/fullSwaps.spec.ts +++ b/test/fullSwaps.spec.ts @@ -138,7 +138,7 @@ describe('Tests full swaps against known values', () => { // These test should highlight any changes in maths that may unexpectedly change result assert.equal( swapInfo.returnAmount.toString(), - '2932407280899120', + '2932404354186254', 'V2 sanity check.' ); }).timeout(10000); @@ -233,7 +233,7 @@ describe('Tests full swaps against known values', () => { }); // The expected test results are from previous version - assert.equal(swapInfo.returnAmount.toString(), '99251606996029317'); + assert.equal(swapInfo.returnAmount.toString(), '99250614458957445'); assert.equal(swapInfo.swaps.length, 2); assert.equal( swapInfo.swaps[0].poolId, @@ -247,7 +247,7 @@ describe('Tests full swaps against known values', () => { swapInfo.tokenAddresses[swapInfo.swaps[0].assetOutIndex], USDC.address ); - assert.equal(swapInfo.swaps[0].amount, '89884'); + assert.equal(swapInfo.swaps[0].amount, '89880'); assert.equal( swapInfo.swaps[1].poolId, '0x57755f7dec33320bca83159c26e93751bfd30fbe' @@ -260,7 +260,7 @@ describe('Tests full swaps against known values', () => { swapInfo.tokenAddresses[swapInfo.swaps[1].assetOutIndex], USDC.address ); - assert.equal(swapInfo.swaps[1].amount, '10116'); + assert.equal(swapInfo.swaps[1].amount, '10120'); // assert.equal(marketSp.toString(), '0.9924950453298881'); // TODO Different method to V1 so find diff result 0.9925374301712606 }).timeout(10000); @@ -475,7 +475,7 @@ describe('Tests full swaps against known values', () => { }, }); - assert.equal(swapInfo.returnAmount.toString(), '100601647114105022960'); + assert.equal(swapInfo.returnAmount.toString(), '100601645390427390974'); assert.equal(swapInfo.swaps.length, 3); assert.equal( swapInfo.swaps[0].poolId, diff --git a/test/poolsMathWeighted.spec.ts b/test/poolsMathWeighted.spec.ts index 7a27b61b..d7a6f164 100644 --- a/test/poolsMathWeighted.spec.ts +++ b/test/poolsMathWeighted.spec.ts @@ -1,4 +1,4 @@ -import * as weighted from '../src/poolsMath/weighted'; +import * as weighted from '../src/pools/weightedPool/weightedMath'; import * as SDK from '@georgeroman/balancer-v2-pools'; import { BigNumber as OldBigNumber, bnum } from '../src/utils/bignumber'; import { assert } from 'chai'; @@ -6,9 +6,9 @@ import { MathSol } from '../src/poolsMath/basicOperations'; describe('poolsMath: numeric functions using bigint', () => { context('weighted pools', () => { - it('_exactTokenInForTokenOut', () => { + it('_calcOutGivenIn', () => { const { result, SDKResult } = getBothValuesWeighted( - weighted._exactTokenInForTokenOut, + weighted._calcOutGivenIn, SDK.WeightedMath._calcOutGivenIn, 1000, 1, @@ -24,9 +24,9 @@ describe('poolsMath: numeric functions using bigint', () => { ); }); - it('_tokenInForExactTokenOut', () => { + it('_calcInGivenOut', () => { const { result, SDKResult } = getBothValuesWeighted( - weighted._tokenInForExactTokenOut, + weighted._calcInGivenOut, SDK.WeightedMath._calcInGivenOut, 1000, 1, @@ -44,8 +44,8 @@ describe('poolsMath: numeric functions using bigint', () => { it('_spotPriceAfterSwapExactTokenInForTokenOut', () => { checkDerivative_weighted( - weighted._exactTokenInForTokenOut, - weighted._spotPriceAfterSwapExactTokenInForTokenOut, + weighted._calcOutGivenIn, + weighted._spotPriceAfterSwapExactTokenInForTokenOutBigInt, 1000, 1, 7000, @@ -59,8 +59,8 @@ describe('poolsMath: numeric functions using bigint', () => { }); it('_spotPriceAfterSwapTokenInForExactTokenOut', () => { checkDerivative_weighted( - weighted._tokenInForExactTokenOut, - weighted._spotPriceAfterSwapTokenInForExactTokenOut, + weighted._calcInGivenOut, + weighted._spotPriceAfterSwapTokenInForExactTokenOutBigInt, 1000, 1, 7000, From 126ee897e5da4574fd8fa584e634b9baafff39a9 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Mon, 17 Jan 2022 15:46:46 +0000 Subject: [PATCH 13/43] Linear pool - updated to BigInt maths. --- src/pools/linearPool/exactMaths.ts | 308 ------------- src/pools/linearPool/linearMath.ts | 687 +++++++++++++++++++++-------- src/pools/linearPool/linearPool.ts | 493 +++++++++++---------- src/poolsMath/linear.ts | 511 --------------------- test/linear.spec.ts | 7 +- test/linearMath.spec.ts | 451 ------------------- test/poolsMathLinear.spec.ts | 2 +- 7 files changed, 769 insertions(+), 1690 deletions(-) delete mode 100644 src/pools/linearPool/exactMaths.ts delete mode 100644 src/poolsMath/linear.ts delete mode 100644 test/linearMath.spec.ts diff --git a/src/pools/linearPool/exactMaths.ts b/src/pools/linearPool/exactMaths.ts deleted file mode 100644 index 82e7a27f..00000000 --- a/src/pools/linearPool/exactMaths.ts +++ /dev/null @@ -1,308 +0,0 @@ -// Ported from Solidity: -// https://github.com/balancer-labs/balancer-v2-monorepo/blob/88a14eb623f6a22ef3f1afc5a8c49ebfa7eeceed/pkg/pool-linear/contracts/LinearMath.sol - -import BigNumber from './utils/big-number'; -import * as fp from './utils/math/fixed-point'; -import * as math from './utils/math/math'; - -type Params = { - fee: BigNumber; - lowerTarget: BigNumber; - upperTarget: BigNumber; -}; - -export const _calcBptOutPerMainIn = ( - mainIn: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - bptSupply: BigNumber, - params: Params -): BigNumber => { - // Amount out, so we round down overall. - - if (bptSupply.isZero()) { - return _toNominal(mainIn, params); - } - - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = _toNominal(fp.add(mainBalance, mainIn), params); - const deltaNominalMain = fp.sub(afterNominalMain, previousNominalMain); - const invariant = _calcInvariant(previousNominalMain, wrappedBalance); - return math.divDown(math.mul(bptSupply, deltaNominalMain), invariant); -}; - -export const _calcBptInPerMainOut = ( - mainOut: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - bptSupply: BigNumber, - params: Params -): BigNumber => { - // Amount in, so we round up overall. - - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = _toNominal(fp.sub(mainBalance, mainOut), params); - const deltaNominalMain = fp.sub(previousNominalMain, afterNominalMain); - const invariant = _calcInvariant(previousNominalMain, wrappedBalance); - return math.divUp(math.mul(bptSupply, deltaNominalMain), invariant); -}; - -export const _calcWrappedOutPerMainIn = ( - mainIn: BigNumber, - mainBalance: BigNumber, - params: Params -): BigNumber => { - // Amount out, so we round down overall. - - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = _toNominal(fp.add(mainBalance, mainIn), params); - return fp.sub(afterNominalMain, previousNominalMain); -}; - -export const _calcWrappedInPerMainOut = ( - mainOut: BigNumber, - mainBalance: BigNumber, - params: Params -): BigNumber => { - // Amount in, so we round up overall. - - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = _toNominal(fp.sub(mainBalance, mainOut), params); - return fp.sub(previousNominalMain, afterNominalMain); -}; - -export const _calcMainInPerBptOut = ( - bptOut: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - bptSupply: BigNumber, - params: Params -): BigNumber => { - // Amount in, so we round up overall. - - if (bptSupply.isZero()) { - return _fromNominal(bptOut, params); - } - - const previousNominalMain = _toNominal(mainBalance, params); - const invariant = _calcInvariant(previousNominalMain, wrappedBalance); - const deltaNominalMain = math.divUp(math.mul(invariant, bptOut), bptSupply); - const afterNominalMain = fp.add(previousNominalMain, deltaNominalMain); - const newMainBalance = _fromNominal(afterNominalMain, params); - return fp.sub(newMainBalance, mainBalance); -}; - -export const _calcMainOutPerBptIn = ( - bptIn: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - bptSupply: BigNumber, - params: Params -): BigNumber => { - // Amount out, so we round down overall. - - const previousNominalMain = _toNominal(mainBalance, params); - const invariant = _calcInvariant(previousNominalMain, wrappedBalance); - const deltaNominalMain = math.divDown( - math.mul(invariant, bptIn), - bptSupply - ); - const afterNominalMain = fp.sub(previousNominalMain, deltaNominalMain); - const newMainBalance = _fromNominal(afterNominalMain, params); - return fp.sub(mainBalance, newMainBalance); -}; - -export const _calcMainOutPerWrappedIn = ( - wrappedIn: BigNumber, - mainBalance: BigNumber, - params: Params -): BigNumber => { - // Amount out, so we round down overall. - - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = fp.sub(previousNominalMain, wrappedIn); - const newMainBalance = _fromNominal(afterNominalMain, params); - return fp.sub(mainBalance, newMainBalance); -}; - -export const _calcMainInPerWrappedOut = ( - wrappedOut: BigNumber, - mainBalance: BigNumber, - params: Params -): BigNumber => { - // Amount in, so we round up overall. - - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = fp.add(previousNominalMain, wrappedOut); - const newMainBalance = _fromNominal(afterNominalMain, params); - return fp.sub(newMainBalance, mainBalance); -}; - -export const _calcBptOutPerWrappedIn = ( - wrappedIn: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - bptSupply: BigNumber, - params: Params -): BigNumber => { - // Amount out, so we round down overall. - - if (bptSupply.isZero()) { - return wrappedIn; - } - - const nominalMain = _toNominal(mainBalance, params); - const previousInvariant = _calcInvariant(nominalMain, wrappedBalance); - - const newWrappedBalance = fp.add(wrappedBalance, wrappedIn); - const newInvariant = _calcInvariant(nominalMain, newWrappedBalance); - - const newBptBalance = math.divDown( - math.mul(bptSupply, newInvariant), - previousInvariant - ); - - return fp.sub(newBptBalance, bptSupply); -}; - -export const _calcBptInPerWrappedOut = ( - wrappedOut: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - bptSupply: BigNumber, - params: Params -): BigNumber => { - // Amount in, so we round up overall. - - const nominalMain = _toNominal(mainBalance, params); - const previousInvariant = _calcInvariant(nominalMain, wrappedBalance); - - const newWrappedBalance = fp.sub(wrappedBalance, wrappedOut); - const newInvariant = _calcInvariant(nominalMain, newWrappedBalance); - - const newBptBalance = math.divDown( - math.mul(bptSupply, newInvariant), - previousInvariant - ); - - return fp.sub(bptSupply, newBptBalance); -}; - -export const _calcWrappedInPerBptOut = ( - bptOut: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - bptSupply: BigNumber, - params: Params -): BigNumber => { - // Amount in, so we round up overall. - - if (bptSupply.isZero()) { - return bptOut; - } - - const nominalMain = _toNominal(mainBalance, params); - const previousInvariant = _calcInvariant(nominalMain, wrappedBalance); - - const newBptBalance = fp.add(bptSupply, bptOut); - const newWrappedBalance = fp.sub( - math.divUp(math.mul(newBptBalance, previousInvariant), bptSupply), - nominalMain - ); - - return fp.sub(newWrappedBalance, wrappedBalance); -}; - -export const _calcWrappedOutPerBptIn = ( - bptIn: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - bptSupply: BigNumber, - params: Params -): BigNumber => { - // Amount out, so we round down overall. - - const nominalMain = _toNominal(mainBalance, params); - const previousInvariant = _calcInvariant(nominalMain, wrappedBalance); - - const newBptBalance = fp.sub(bptSupply, bptIn); - const newWrappedBalance = fp.sub( - math.divUp(math.mul(newBptBalance, previousInvariant), bptSupply), - nominalMain - ); - - return fp.sub(wrappedBalance, newWrappedBalance); -}; - -const _calcInvariant = ( - nominalMainBalance: BigNumber, - wrappedBalance: BigNumber -): BigNumber => { - return fp.add(nominalMainBalance, wrappedBalance); -}; - -const _toNominal = (real: BigNumber, params: Params): BigNumber => { - // Fees are always rounded down: either direction would work but we need to be consistent, and rounding down - // uses less gas. - - if (real.lt(params.lowerTarget)) { - const fees = fp.mulDown(math.sub(params.lowerTarget, real), params.fee); - return fp.sub(real, fees); - } else if (real.lte(params.upperTarget)) { - return real; - } else { - const fees = fp.mulDown(math.sub(real, params.upperTarget), params.fee); - return fp.sub(real, fees); - } -}; - -const _fromNominal = (nominal: BigNumber, params: Params): BigNumber => { - // Since real = nominal + fees, rounding down fees is equivalent to rounding down real. - - if (nominal.lt(params.lowerTarget)) { - return fp.divDown( - fp.add(nominal, fp.mulDown(params.fee, params.lowerTarget)), - fp.add(fp.ONE, params.fee) - ); - } else if (nominal.lte(params.upperTarget)) { - return nominal; - } else { - return fp.divDown( - fp.sub(nominal, fp.mulDown(params.fee, params.upperTarget)), - fp.sub(fp.ONE, params.fee) - ); - } -}; - -export const _calcTokensOutGivenExactBptIn = ( - balances: BigNumber[], - bptAmountIn: BigNumber, - bptTotalSupply: BigNumber, - bptIndex: number -): BigNumber[] => { - /********************************************************************************************** - // exactBPTInForTokensOut // - // (per token) // - // aO = tokenAmountOut / bptIn \ // - // b = tokenBalance a0 = b * | --------------------- | // - // bptIn = bptAmountIn \ bptTotalSupply / // - // bpt = bptTotalSupply // - **********************************************************************************************/ - - // Since we're computing an amount out, we round down overall. This means rounding down on both the - // multiplication and division. - - const bptRatio = fp.divDown(bptAmountIn, bptTotalSupply); - - const amountsOut: BigNumber[] = []; - for (let i = 0; i < balances.length; i++) { - // BPT is skipped as those tokens are not the LPs, but rather the preminted and undistributed amount. - if (i !== bptIndex) { - amountsOut.push(fp.mulDown(balances[i], bptRatio)); - } else { - amountsOut.push(fp.ZERO); - } - } - - return amountsOut; -}; diff --git a/src/pools/linearPool/linearMath.ts b/src/pools/linearPool/linearMath.ts index 3cd5652e..3a646eaa 100644 --- a/src/pools/linearPool/linearMath.ts +++ b/src/pools/linearPool/linearMath.ts @@ -1,196 +1,529 @@ -import { BigNumber } from '../../utils/bignumber'; +import { BigNumber as linear } from '../../utils/bignumber'; import { bnum } from '../../utils/bignumber'; import { formatFixed } from '@ethersproject/bignumber'; +import { MathSol } from '../../poolsMath/basicOperations'; import { LinearPoolPairData } from './linearPool'; -///////// -/// Swap functions -///////// +type Params = { + fee: bigint; + rate: bigint; + lowerTarget: bigint; + upperTarget: bigint; +}; + +export function _calcBptOutPerMainIn( + mainIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + + if (bptSupply == BigInt(0)) { + return _toNominal(mainIn, params); + } -// PairType = 'token->token' -// SwapType = 'swapExactIn' -export function _exactTokenInForTokenOut( - amount: BigNumber, - poolPairData: LinearPoolPairData -): BigNumber { - // This is not expected to be used by SOR - // but could still be implemented - throw new Error('Function not implemented.'); + const previousNominalMain = _toNominal(mainBalance, params); + const afterNominalMain = _toNominal(mainBalance + mainIn, params); + const deltaNominalMain = afterNominalMain - previousNominalMain; + const invariant = _calcInvariantUp( + previousNominalMain, + wrappedBalance, + params + ); + return MathSol.divDownFixed( + MathSol.mulDownFixed(bptSupply, deltaNominalMain), + invariant + ); } -// PairType = 'token->token' -// SwapType = 'swapExactOut' -export function _tokenInForExactTokenOut( - amount: BigNumber, - poolPairData: LinearPoolPairData -): BigNumber { - // This is not expected to be used by SOR - // but could still be implemented - throw new Error('Function not implemented.'); +export function _calcBptInPerMainOut( + mainOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + const previousNominalMain = _toNominal(mainBalance, params); + const afterNominalMain = _toNominal(mainBalance - mainOut, params); + const deltaNominalMain = previousNominalMain - afterNominalMain; + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params + ); + return MathSol.divUpFixed( + MathSol.mulUpFixed(bptSupply, deltaNominalMain), + invariant + ); } -// PairType = 'token->BPT' -// SwapType = 'swapExactIn' -export function _exactMainTokenInForBPTOut( - amount: BigNumber, - poolPairData: LinearPoolPairData -): BigNumber { - const mainIn = bnum(amount.toString()); - const mainBalance = bnum( - formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) +export function _calcBptInPerWrappedOut( + wrappedOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + const nominalMain = _toNominal(mainBalance, params); + const previousInvariant = _calcInvariantUp( + nominalMain, + wrappedBalance, + params ); - const wrappedBalance = bnum( - formatFixed( - poolPairData.wrappedBalance.toString(), - poolPairData.wrappedDecimals - ) + const newWrappedBalance = wrappedBalance - wrappedOut; + const newInvariant = _calcInvariantDown( + nominalMain, + newWrappedBalance, + params ); - const virtualBptSupply = bnum( - formatFixed(poolPairData.virtualBptSupply, 18) + const newBptBalance = MathSol.divDownFixed( + MathSol.mulDownFixed(bptSupply, newInvariant), + previousInvariant ); - const params: BigNumber[] = [ - bnum(formatFixed(poolPairData.swapFee, 18)), - bnum(formatFixed(poolPairData.rate.toString(), 18)), - bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), - bnum(formatFixed(poolPairData.upperTarget.toString(), 18)), - ]; + return bptSupply - newBptBalance; +} + +export function _calcWrappedOutPerMainIn( + mainIn: bigint, + mainBalance: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + const previousNominalMain = _toNominal(mainBalance, params); + const afterNominalMain = _toNominal(mainBalance + mainIn, params); + const deltaNominalMain = afterNominalMain - previousNominalMain; + return MathSol.divDownFixed(deltaNominalMain, params.rate); +} + +export function _calcWrappedInPerMainOut( + mainOut: bigint, + mainBalance: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + const previousNominalMain = _toNominal(mainBalance, params); + const afterNominalMain = _toNominal(mainBalance - mainOut, params); + const deltaNominalMain = previousNominalMain - afterNominalMain; + return MathSol.divUpFixed(deltaNominalMain, params.rate); +} - if (virtualBptSupply.eq(0)) { - return toNominal(mainIn, params); +export function _calcMainInPerBptOut( + bptOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + if (bptSupply == BigInt(0)) { + return _fromNominal(bptOut, params); } + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantUp( + previousNominalMain, + wrappedBalance, + params + ); + const deltaNominalMain = MathSol.divUpFixed( + MathSol.mulUpFixed(invariant, bptOut), + bptSupply + ); + const afterNominalMain = previousNominalMain + deltaNominalMain; + const newMainBalance = _fromNominal(afterNominalMain, params); + return newMainBalance - mainBalance; +} - const previousNominalMain = toNominal(mainBalance, params); - const afterNominalMain = toNominal(mainBalance.plus(mainIn), params); - const deltaNominalMain = afterNominalMain.minus(previousNominalMain); - const invariant = calcInvariant( +export function _calcMainOutPerBptIn( + bptIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( previousNominalMain, wrappedBalance, params ); - const bptOut = virtualBptSupply.times(deltaNominalMain).div(invariant); - return bptOut; + const deltaNominalMain = MathSol.divDownFixed( + MathSol.mulDownFixed(invariant, bptIn), + bptSupply + ); + const afterNominalMain = previousNominalMain - deltaNominalMain; + const newMainBalance = _fromNominal(afterNominalMain, params); + return mainBalance - newMainBalance; } -// PairType = 'token->BPT' -// SwapType = 'swapExactOut' -export function _mainTokenInForExactBPTOut( - amount: BigNumber, - poolPairData: LinearPoolPairData -): BigNumber { - const bptOut = bnum(amount.toString()); - const virtualBptSupply = bnum( - formatFixed(poolPairData.virtualBptSupply, 18) +export function _calcMainOutPerWrappedIn( + wrappedIn: bigint, + mainBalance: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + const previousNominalMain = _toNominal(mainBalance, params); + const deltaNominalMain = MathSol.mulDownFixed(wrappedIn, params.rate); + const afterNominalMain = previousNominalMain - deltaNominalMain; + const newMainBalance = _fromNominal(afterNominalMain, params); + return mainBalance - newMainBalance; +} + +export function _calcMainInPerWrappedOut( + wrappedOut: bigint, + mainBalance: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + const previousNominalMain = _toNominal(mainBalance, params); + const deltaNominalMain = MathSol.mulUpFixed(wrappedOut, params.rate); + const afterNominalMain = previousNominalMain + deltaNominalMain; + const newMainBalance = _fromNominal(afterNominalMain, params); + return newMainBalance - mainBalance; +} + +export function _calcBptOutPerWrappedIn( + wrappedIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + if (bptSupply == BigInt(0)) { + // Return nominal DAI + return MathSol.mulDownFixed(wrappedIn, params.rate); + } + + const nominalMain = _toNominal(mainBalance, params); + const previousInvariant = _calcInvariantUp( + nominalMain, + wrappedBalance, + params ); - const mainBalance = bnum( - formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) + const newWrappedBalance = wrappedBalance + wrappedIn; + const newInvariant = _calcInvariantDown( + nominalMain, + newWrappedBalance, + params ); - const wrappedBalance = bnum( - formatFixed( - poolPairData.wrappedBalance.toString(), - poolPairData.wrappedDecimals - ) + const newBptBalance = MathSol.divDownFixed( + MathSol.mulDownFixed(bptSupply, newInvariant), + previousInvariant ); - const params: BigNumber[] = [ - bnum(formatFixed(poolPairData.swapFee, 18)), - bnum(formatFixed(poolPairData.rate.toString(), 18)), - bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), - bnum(formatFixed(poolPairData.upperTarget.toString(), 18)), - ]; + return newBptBalance - bptSupply; +} - if (virtualBptSupply.eq(0)) { - return fromNominal(bptOut, params); +export function _calcWrappedInPerBptOut( + bptOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount in, so we round up overall. + if (bptSupply == BigInt(0)) { + // Return nominal DAI + return MathSol.divUpFixed(bptOut, params.rate); } - const previousNominalMain = toNominal(mainBalance, params); - const invariant = calcInvariant( - previousNominalMain, + + const nominalMain = _toNominal(mainBalance, params); + const previousInvariant = _calcInvariantUp( + nominalMain, wrappedBalance, params ); - const deltaNominalMain = bptOut.times(invariant).div(virtualBptSupply); - const afterNominalMain = previousNominalMain.plus(deltaNominalMain); - const newMainBalance = fromNominal(afterNominalMain, params); - const mainIn = newMainBalance.minus(mainBalance); - return mainIn; + const newBptBalance = bptSupply + bptOut; + const newWrappedBalance = MathSol.divUpFixed( + MathSol.mulUpFixed( + MathSol.divUpFixed(newBptBalance, bptSupply), + previousInvariant + ) - nominalMain, + params.rate + ); + return newWrappedBalance - wrappedBalance; } -// PairType = 'BPT->token' -// SwapType = 'swapExactIn' -export function _BPTInForExactMainTokenOut( - amount: BigNumber, - poolPairData: LinearPoolPairData -): BigNumber { - const mainOut = bnum(amount.toString()); - const mainBalance = bnum( - formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) +export function _calcWrappedOutPerBptIn( + bptIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + // Amount out, so we round down overall. + const nominalMain = _toNominal(mainBalance, params); + const previousInvariant = _calcInvariantUp( + nominalMain, + wrappedBalance, + params ); - const wrappedBalance = bnum( - formatFixed( - poolPairData.wrappedBalance.toString(), - poolPairData.wrappedDecimals - ) + const newBptBalance = bptSupply - bptIn; + const newWrappedBalance = MathSol.divUpFixed( + MathSol.mulUpFixed( + MathSol.divUpFixed(newBptBalance, bptSupply), + previousInvariant + ) - nominalMain, + params.rate ); - const virtualBptSupply = bnum( - formatFixed(poolPairData.virtualBptSupply, 18) + return wrappedBalance - newWrappedBalance; +} + +function _calcInvariantUp( + nominalMainBalance: bigint, + wrappedBalance: bigint, + params: Params +): bigint { + return nominalMainBalance + MathSol.mulUpFixed(wrappedBalance, params.rate); +} + +function _calcInvariantDown( + nominalMainBalance: bigint, + wrappedBalance: bigint, + params: Params +): bigint { + return ( + nominalMainBalance + MathSol.mulDownFixed(wrappedBalance, params.rate) ); - const params: BigNumber[] = [ - bnum(formatFixed(poolPairData.swapFee, 18)), - bnum(formatFixed(poolPairData.rate.toString(), 18)), - bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), - bnum(formatFixed(poolPairData.upperTarget.toString(), 18)), - ]; +} - const previousNominalMain = toNominal(mainBalance, params); - const afterNominalMain = toNominal(mainBalance.minus(mainOut), params); - const deltaNominalMain = previousNominalMain.minus(afterNominalMain); - const invariant = calcInvariant( +function _toNominal(real: bigint, params: Params): bigint { + // Fees are always rounded down: either direction would work but we need to be consistent, and rounding down + // uses less gas. + if (real < params.lowerTarget) { + const fees = MathSol.mulDownFixed( + params.lowerTarget - real, + params.fee + ); + return MathSol.sub(real, fees); + } else if (real <= params.upperTarget) { + return real; + } else { + const fees = MathSol.mulDownFixed( + real - params.upperTarget, + params.fee + ); + return MathSol.sub(real, fees); + } +} + +function _fromNominal(nominal: bigint, params: Params): bigint { + // Since real = nominal + fees, rounding down fees is equivalent to rounding down real. + if (nominal < params.lowerTarget) { + return MathSol.divDownFixed( + nominal + MathSol.mulDownFixed(params.fee, params.lowerTarget), + MathSol.ONE + params.fee + ); + } else if (nominal <= params.upperTarget) { + return nominal; + } else { + return MathSol.divDownFixed( + nominal - MathSol.mulDownFixed(params.fee, params.upperTarget), + MathSol.ONE - params.fee + ); + } +} + +function leftDerivativeToNominalBigInt(amount: bigint, params: Params): bigint { + const oneMinusFee = MathSol.complementFixed(params.fee); + const onePlusFee = MathSol.ONE + params.fee; + if (amount <= params.lowerTarget) { + return onePlusFee; + } else if (amount <= params.upperTarget) { + return MathSol.ONE; + } else { + return oneMinusFee; + } +} + +function rightDerivativeToNominalBigInt( + amount: bigint, + params: Params +): bigint { + const oneMinusFee = MathSol.complementFixed(params.fee); + const onePlusFee = MathSol.ONE + params.fee; + if (amount < params.lowerTarget) { + return onePlusFee; + } else if (amount < params.upperTarget) { + return MathSol.ONE; + } else { + return oneMinusFee; + } +} + +function leftDerivativeFromNominalBigInt( + amount: bigint, + params: Params +): bigint { + const oneMinusFee = MathSol.complementFixed(params.fee); + const onePlusFee = MathSol.ONE + params.fee; + if (amount <= params.lowerTarget) { + return MathSol.divUpFixed(MathSol.ONE, onePlusFee); + } else if (amount <= params.upperTarget) { + return MathSol.ONE; + } else { + return MathSol.divUpFixed(MathSol.ONE, oneMinusFee); + } +} + +function rightDerivativeFromNominalBigInt( + amount: bigint, + params: Params +): bigint { + const oneMinusFee = MathSol.complementFixed(params.fee); + const onePlusFee = MathSol.ONE + params.fee; + if (amount < params.lowerTarget) { + return MathSol.divUpFixed(MathSol.ONE, onePlusFee); + } else if (amount < params.upperTarget) { + return MathSol.ONE; + } else { + return MathSol.divUpFixed(MathSol.ONE, oneMinusFee); + } +} + +export function _calcTokensOutGivenExactBptIn( + balances: bigint[], + bptAmountIn: bigint, + bptTotalSupply: bigint, + bptIndex: number +): bigint[] { + /********************************************************************************************** + // exactBPTInForTokensOut // + // (per token) // + // aO = tokenAmountOut / bptIn \ // + // b = tokenBalance a0 = b * | --------------------- | // + // bptIn = bptAmountIn \ bptTotalSupply / // + // bpt = bptTotalSupply // + **********************************************************************************************/ + + // Since we're computing an amount out, we round down overall. This means rounding down on both the + // multiplication and division. + + const bptRatio = MathSol.divDownFixed(bptAmountIn, bptTotalSupply); + const amountsOut: bigint[] = new Array(balances.length); + for (let i = 0; i < balances.length; i++) { + // BPT is skipped as those tokens are not the LPs, but rather the preminted and undistributed amount. + if (i != bptIndex) { + amountsOut[i] = MathSol.mulDownFixed(balances[i], bptRatio); + } + } + return amountsOut; +} + +///////// +/// SpotPriceAfterSwap +///////// + +// PairType = 'token->BPT' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapBptOutPerMainIn( + mainIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + const finalMainBalance = mainIn + mainBalance; + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( previousNominalMain, wrappedBalance, params ); - const bptIn = virtualBptSupply.times(deltaNominalMain.div(invariant)); - return bptIn; + let poolFactor = MathSol.ONE; + if (bptSupply != BigInt(0)) { + poolFactor = MathSol.divUpFixed(invariant, bptSupply); + } + return MathSol.divUpFixed( + poolFactor, + rightDerivativeToNominalBigInt(finalMainBalance, params) + ); } -// PairType = 'BPT->token' +// PairType = 'token->BPT' // SwapType = 'swapExactOut' -export function _exactBPTInForMainTokenOut( - amount: BigNumber, - poolPairData: LinearPoolPairData -): BigNumber { - const bptIn = bnum(amount.toString()); - const mainBalance = bnum( - formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) +export function _spotPriceAfterSwapMainInPerBptOut( + bptOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params ); - const wrappedBalance = bnum( - formatFixed( - poolPairData.wrappedBalance.toString(), - poolPairData.wrappedDecimals - ) + let poolFactor = MathSol.ONE; + if (bptSupply != BigInt(0)) { + poolFactor = MathSol.divUpFixed(invariant, bptSupply); + } + const deltaNominalMain = MathSol.mulUpFixed(bptOut, poolFactor); + const afterNominalMain = previousNominalMain + deltaNominalMain; + return MathSol.mulUpFixed( + poolFactor, + rightDerivativeFromNominalBigInt(afterNominalMain, params) ); - const virtualBptSupply = bnum( - formatFixed(poolPairData.virtualBptSupply, 18) +} + +// PairType = 'BPT->token' +// SwapType = 'swapExactIn' +export function _spotPriceAfterSwapMainOutPerBptIn( + bptIn: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( + previousNominalMain, + wrappedBalance, + params ); - const params: BigNumber[] = [ - bnum(formatFixed(poolPairData.swapFee, 18)), - bnum(formatFixed(poolPairData.rate.toString(), 18)), - bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), - bnum(formatFixed(poolPairData.upperTarget.toString(), 18)), - ]; + const poolFactor = MathSol.divDownFixed(invariant, bptSupply); + const deltaNominalMain = MathSol.mulDownFixed(bptIn, poolFactor); + const afterNominalMain = MathSol.sub(previousNominalMain, deltaNominalMain); + return MathSol.divUpFixed( + MathSol.ONE, + MathSol.mulUpFixed( + poolFactor, + leftDerivativeFromNominalBigInt(afterNominalMain, params) + ) + ); +} - const previousNominalMain = toNominal(mainBalance, params); - const invariant = calcInvariant( +// PairType = 'BPT->token' +// SwapType = 'swapExactOut' +export function _spotPriceAfterSwapBptInPerMainOut( + mainOut: bigint, + mainBalance: bigint, + wrappedBalance: bigint, + bptSupply: bigint, + params: Params +): bigint { + const finalMainBalance = MathSol.sub(mainBalance, mainOut); + const previousNominalMain = _toNominal(mainBalance, params); + const invariant = _calcInvariantDown( previousNominalMain, wrappedBalance, params ); - const deltaNominalMain = invariant.times(bptIn).div(virtualBptSupply); - const afterNominalMain = previousNominalMain.minus(deltaNominalMain); - const newMainBalance = fromNominal(afterNominalMain, params); - const mainOut = mainBalance.minus(newMainBalance); - return mainOut; + const poolFactor = MathSol.divUpFixed(invariant, bptSupply); + return MathSol.divUpFixed( + leftDerivativeToNominalBigInt(finalMainBalance, params), + poolFactor + ); } +////////////////// + ///////// /// SpotPriceAfterSwap ///////// @@ -200,7 +533,7 @@ export function _exactBPTInForMainTokenOut( export function _spotPriceAfterSwapExactTokenInForTokenOut( amount, poolPairData -): BigNumber { +): linear { // This is not expected to be used by SOR // but could still be implemented throw new Error('Function not implemented.'); @@ -211,7 +544,7 @@ export function _spotPriceAfterSwapExactTokenInForTokenOut( export function _spotPriceAfterSwapTokenInForExactTokenOut( amount, poolPairData -): BigNumber { +): linear { // This is not expected to be used by SOR // but could still be implemented throw new Error('Function not implemented.'); @@ -220,9 +553,9 @@ export function _spotPriceAfterSwapTokenInForExactTokenOut( // PairType = 'token->BPT' // SwapType = 'swapExactIn' export function _spotPriceAfterSwapExactTokenInForBPTOut( - amount: BigNumber, + amount: linear, poolPairData: LinearPoolPairData -): BigNumber { +): linear { const mainIn = bnum(amount.toString()); const mainBalance = bnum( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -237,7 +570,7 @@ export function _spotPriceAfterSwapExactTokenInForBPTOut( const virtualBptSupply = bnum( formatFixed(poolPairData.virtualBptSupply, 18) ); - const params: BigNumber[] = [ + const params: linear[] = [ bnum(formatFixed(poolPairData.swapFee, 18)), bnum(formatFixed(poolPairData.rate.toString(), 18)), bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), @@ -260,9 +593,9 @@ export function _spotPriceAfterSwapExactTokenInForBPTOut( // PairType = 'token->BPT' // SwapType = 'swapExactOut' export function _spotPriceAfterSwapTokenInForExactBPTOut( - amount: BigNumber, + amount: linear, poolPairData: LinearPoolPairData -): BigNumber { +): linear { const bptOut = bnum(amount.toString()); const virtualBptSupply = bnum( formatFixed(poolPairData.virtualBptSupply, 18) @@ -276,7 +609,7 @@ export function _spotPriceAfterSwapTokenInForExactBPTOut( poolPairData.wrappedDecimals ) ); - const params: BigNumber[] = [ + const params: linear[] = [ bnum(formatFixed(poolPairData.swapFee, 18)), bnum(formatFixed(poolPairData.rate.toString(), 18)), bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), @@ -303,9 +636,9 @@ export function _spotPriceAfterSwapTokenInForExactBPTOut( // PairType = 'BPT->token' // SwapType = 'swapExactIn' export function _spotPriceAfterSwapExactBPTInForTokenOut( - amount: BigNumber, + amount: linear, poolPairData: LinearPoolPairData -): BigNumber { +): linear { const bptIn = bnum(amount.toString()); const mainBalance = bnum( formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) @@ -319,7 +652,7 @@ export function _spotPriceAfterSwapExactBPTInForTokenOut( const virtualBptSupply = bnum( formatFixed(poolPairData.virtualBptSupply, 18) ); - const params: BigNumber[] = [ + const params: linear[] = [ bnum(formatFixed(poolPairData.swapFee, 18)), bnum(formatFixed(poolPairData.rate.toString(), 18)), bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), @@ -343,9 +676,9 @@ export function _spotPriceAfterSwapExactBPTInForTokenOut( // PairType = 'BPT->token' // SwapType = 'swapExactOut' export function _spotPriceAfterSwapBPTInForExactTokenOut( - amount: BigNumber, + amount: linear, poolPairData: LinearPoolPairData -): BigNumber { +): linear { const mainOut = bnum(amount.toString()); const mainBalance = bnum( formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) @@ -360,7 +693,7 @@ export function _spotPriceAfterSwapBPTInForExactTokenOut( formatFixed(poolPairData.virtualBptSupply, 18) ); const finalMainBalance = mainBalance.minus(mainOut); - const params: BigNumber[] = [ + const params: linear[] = [ bnum(formatFixed(poolPairData.swapFee, 18)), bnum(formatFixed(poolPairData.rate.toString(), 18)), bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), @@ -390,7 +723,7 @@ export function _spotPriceAfterSwapBPTInForExactTokenOut( export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( amount, poolPairData -): BigNumber { +): linear { // This is not expected to be used by SOR // but could still be implemented throw new Error('Function not implemented.'); @@ -401,7 +734,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( amount, poolPairData -): BigNumber { +): linear { // This is not expected to be used by SOR // but could still be implemented throw new Error('Function not implemented.'); @@ -412,7 +745,7 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( export function _derivativeSpotPriceAfterSwapExactTokenInForBPTOut( amount, poolPairData -): BigNumber { +): linear { return bnum(0); } @@ -421,7 +754,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForBPTOut( export function _derivativeSpotPriceAfterSwapTokenInForExactBPTOut( amount, poolPairData -): BigNumber { +): linear { return bnum(0); } @@ -430,7 +763,7 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactBPTOut( export function _derivativeSpotPriceAfterSwapExactBPTInForTokenOut( amount, poolPairData -): BigNumber { +): linear { return bnum(0); } @@ -439,20 +772,20 @@ export function _derivativeSpotPriceAfterSwapExactBPTInForTokenOut( export function _derivativeSpotPriceAfterSwapBPTInForExactTokenOut( amount, poolPairData -): BigNumber { +): linear { return bnum(0); } function calcInvariant( - nominalMainBalance: BigNumber, - wrappedBalance: BigNumber, - params: BigNumber[] -): BigNumber { + nominalMainBalance: linear, + wrappedBalance: linear, + params: linear[] +): linear { const rate = params[1]; return nominalMainBalance.plus(wrappedBalance.times(rate)); } -function toNominal(amount: BigNumber, params: BigNumber[]): BigNumber { +function toNominal(amount: linear, params: linear[]): linear { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -471,10 +804,8 @@ function toNominal(amount: BigNumber, params: BigNumber[]): BigNumber { return amount.minus(fees); } } -function leftDerivativeToNominal( - amount: BigNumber, - params: BigNumber[] -): BigNumber { + +function leftDerivativeToNominal(amount: linear, params: linear[]): linear { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -489,10 +820,7 @@ function leftDerivativeToNominal( } } -function rightDerivativeToNominal( - amount: BigNumber, - params: BigNumber[] -): BigNumber { +function rightDerivativeToNominal(amount: linear, params: linear[]): linear { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -506,7 +834,8 @@ function rightDerivativeToNominal( return oneMinusFee; } } -function fromNominal(nominal: BigNumber, params: BigNumber[]): BigNumber { + +function fromNominal(nominal: linear, params: linear[]): linear { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -520,10 +849,7 @@ function fromNominal(nominal: BigNumber, params: BigNumber[]): BigNumber { return nominal.minus(upperTarget.times(fee)).div(oneMinusFee); } } -function leftDerivativeFromNominal( - amount: BigNumber, - params: BigNumber[] -): BigNumber { +function leftDerivativeFromNominal(amount: linear, params: linear[]): linear { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -538,10 +864,7 @@ function leftDerivativeFromNominal( } } -function rightDerivativeFromNominal( - amount: BigNumber, - params: BigNumber[] -): BigNumber { +function rightDerivativeFromNominal(amount: linear, params: linear[]): linear { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; diff --git a/src/pools/linearPool/linearPool.ts b/src/pools/linearPool/linearPool.ts index 27c90baf..f74d74c9 100644 --- a/src/pools/linearPool/linearPool.ts +++ b/src/pools/linearPool/linearPool.ts @@ -3,20 +3,6 @@ import { bnum, scale, ZERO } from '../../utils/bignumber'; import { BigNumber as OldBigNumber } from '../../utils/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; import { isSameAddress } from '../../utils'; -import { - _calcBptInPerWrappedOut, - _calcMainOutPerWrappedIn, - _calcWrappedOutPerMainIn, - _calcBptOutPerMainIn, - _calcMainOutPerBptIn, - _calcBptOutPerWrappedIn, - _calcWrappedOutPerBptIn, - _calcWrappedInPerMainOut, - _calcMainInPerWrappedOut, - _calcMainInPerBptOut, - _calcBptInPerMainOut, - _calcWrappedInPerBptOut, -} from './exactMaths'; import { PoolBase, PoolTypes, @@ -27,10 +13,18 @@ import { SubgraphToken, } from '../../types'; import { - _exactMainTokenInForBPTOut, - _exactBPTInForMainTokenOut, - _mainTokenInForExactBPTOut, - _BPTInForExactMainTokenOut, + _calcBptOutPerMainIn, + _calcBptInPerWrappedOut, + _calcBptInPerMainOut, + _calcWrappedOutPerMainIn, + _calcWrappedInPerMainOut, + _calcMainInPerBptOut, + _calcMainOutPerBptIn, + _calcMainOutPerWrappedIn, + _calcMainInPerWrappedOut, + _calcBptOutPerWrappedIn, + _calcWrappedInPerBptOut, + _calcWrappedOutPerBptIn, _spotPriceAfterSwapExactTokenInForTokenOut, _spotPriceAfterSwapExactTokenInForBPTOut, _spotPriceAfterSwapExactBPTInForTokenOut, @@ -62,12 +56,12 @@ type LinearPoolToken = Pick< export type LinearPoolPairData = PoolPairBase & { pairType: PairTypes; wrappedBalance: OldBigNumber; // If main token is USDC then wrapped token is aUSDC (or a wrapped version of it) + wrappedBalanceScaled: BigNumber; // If main token is USDC then wrapped token is aUSDC (or a wrapped version of it) wrappedDecimals: number; - rate: OldBigNumber; // PriceRate of wrapped token + rate: BigNumber; // PriceRate of wrapped token lowerTarget: BigNumber; // Target determine the range where there are positive, zero or negative fees upperTarget: BigNumber; // when the "main token" has a balance below lowerTarget, there are negative fees when adding main token mainBalanceScaled: BigNumber; // Scaled are used for EVM/SDK maths - wrappedBalanceScaled: BigNumber; bptBalanceScaled: BigNumber; virtualBptSupply: BigNumber; }; @@ -178,15 +172,10 @@ export class LinearPool implements PoolBase { const allBalancesScaled = this.tokens.map(({ balance }) => parseFixed(balance, 18) ); - const priceRate = this.tokens[this.wrappedIndex].priceRate; - const mainBalanceScaled = allBalancesScaled[this.mainIndex]; - const wrappedBalanceScaled = allBalancesScaled[this.wrappedIndex] - .mul(parseFixed(priceRate, 18)) - .div(ONE); - const bptBalanceScaled = allBalancesScaled[this.bptIndex]; // https://github.com/balancer-labs/balancer-v2-monorepo/blob/88a14eb623f6a22ef3f1afc5a8c49ebfa7eeceed/pkg/pool-linear/contracts/LinearPool.sol#L247 // VirtualBPTSupply must be used for the maths // TO DO - SG should be updated to so that totalShares should return VirtualSupply + const bptBalanceScaled = allBalancesScaled[this.bptIndex]; const virtualBptSupply = this.MAX_TOKEN_BALANCE.sub(bptBalanceScaled); const poolPairData: LinearPoolPairData = { @@ -205,12 +194,12 @@ export class LinearPool implements PoolBase { bnum(this.tokens[this.wrappedIndex].balance), this.wrappedDecimals ), + wrappedBalanceScaled: allBalancesScaled[this.wrappedIndex], // Note this is not multiplied by rate wrappedDecimals: this.wrappedDecimals, - rate: scale(bnum(priceRate), 18), + rate: parseFixed(this.tokens[this.wrappedIndex].priceRate, 18), lowerTarget: this.lowerTarget, upperTarget: this.upperTarget, - mainBalanceScaled, - wrappedBalanceScaled, + mainBalanceScaled: allBalancesScaled[this.mainIndex], bptBalanceScaled, virtualBptSupply, }; @@ -223,7 +212,7 @@ export class LinearPool implements PoolBase { } getLimitAmountSwap( - poolPairData: PoolPairBase, + poolPairData: LinearPoolPairData, swapType: SwapTypes ): OldBigNumber { // Needs to return human scaled numbers @@ -235,12 +224,13 @@ export class LinearPool implements PoolBase { if (swapType === SwapTypes.SwapExactIn) { if (linearPoolPairData.pairType === PairTypes.MainTokenToBpt) { - return _mainTokenInForExactBPTOut( - balanceOutHuman, - linearPoolPairData - ) - .times(bnum(this.ALMOST_ONE.toString())) - .div(bnum(ONE.toString())); + return this._mainTokenInForExactBPTOut( + poolPairData, + balanceOutHuman + .times(this.ALMOST_ONE.toString()) + .div(ONE.toString()), + true + ); } else if ( linearPoolPairData.pairType === PairTypes.WrappedTokenToBpt ) { @@ -251,37 +241,23 @@ export class LinearPool implements PoolBase { ) { // Limit is amount of BPT in for pool balance of tokenOut // Amount must be in human scale - const limit = _BPTInForExactMainTokenOut( - balanceOutHuman, - linearPoolPairData - ) - .times(bnum(this.ALMOST_ONE.toString())) - .div(bnum(ONE.toString())); - - return limit; + return this._BPTInForExactMainTokenOut( + linearPoolPairData, + balanceOutHuman + .times(this.ALMOST_ONE.toString()) + .div(ONE.toString()), + true + ); } else if ( linearPoolPairData.pairType === PairTypes.BptToWrappedToken ) { - // Limit is amount of BPT in for pool balance of tokenOut - const limit = _calcBptInPerWrappedOut( - bnum(poolPairData.balanceOut.toString()), - bnum(linearPoolPairData.mainBalanceScaled.toString()), - bnum(linearPoolPairData.wrappedBalanceScaled.toString()), - bnum(linearPoolPairData.bptBalanceScaled.toString()), - { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum( - linearPoolPairData.lowerTarget.toString() - ), - upperTarget: bnum( - linearPoolPairData.upperTarget.toString() - ), - } - ) - .times(bnum(this.ALMOST_ONE.toString())) - .div(bnum(ONE.toString())) - .div(bnum(ONE.toString())); - + const limit = this._BPTInForExactWrappedTokenOut( + poolPairData, + balanceOutHuman + .times(this.ALMOST_ONE.toString()) + .div(ONE.toString()), + true + ); // Returning Human scale return limit; } else if ( @@ -384,22 +360,26 @@ export class LinearPool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtScaled = parseFixed(amount.toString(), 18); const amt = _calcMainOutPerWrappedIn( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), } ); // return human readable number // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { return ZERO; } @@ -413,22 +393,26 @@ export class LinearPool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtScaled = parseFixed(amount.toString(), 18); const amt = _calcWrappedOutPerMainIn( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), } ); // return human readable number // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { return ZERO; } @@ -439,33 +423,33 @@ export class LinearPool implements PoolBase { amount: OldBigNumber, exact: boolean ): OldBigNumber { - if (exact) { - try { - // All values should use 1e18 fixed point - // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); - - const amt = _calcBptOutPerMainIn( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), - bnum(poolPairData.wrappedBalanceScaled.toString()), - bnum(poolPairData.virtualBptSupply.toString()), - { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), - } - ); - // return human readable number - // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, - // i.e. when using token with 2decimals 0.002 should be returned as 0 - // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); - } catch (err) { - return ZERO; - } - } else { - return _exactMainTokenInForBPTOut(amount, poolPairData); + try { + // All values should use 1e18 fixed point + // i.e. 1USDC => 1e18 not 1e6 + const amtScaled = parseFixed(amount.toString(), 18); + + const amt = _calcBptOutPerMainIn( + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), + poolPairData.wrappedBalanceScaled.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), + { + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), + } + ); + // return human readable number + // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, + // i.e. when using token with 2decimals 0.002 should be returned as 0 + // Uses ROUND_DOWN mode (1) + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); + } catch (err) { + return ZERO; } } @@ -474,32 +458,34 @@ export class LinearPool implements PoolBase { amount: OldBigNumber, exact: boolean ): OldBigNumber { - if (exact) { - try { - // All values should use 1e18 fixed point - // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); - - const amt = _calcMainOutPerBptIn( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), - bnum(poolPairData.wrappedBalanceScaled.toString()), - bnum(poolPairData.virtualBptSupply.toString()), - { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), - } - ); - // return human readable number - // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, - // i.e. when using token with 2decimals 0.002 should be returned as 0 - // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); - } catch (err) { - return ZERO; - } - } else return _exactBPTInForMainTokenOut(amount, poolPairData); + try { + // All values should use 1e18 fixed point + // i.e. 1USDC => 1e18 not 1e6 + const amtScaled = parseFixed(amount.toString(), 18); + + const amt = _calcMainOutPerBptIn( + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), + poolPairData.wrappedBalanceScaled.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), + { + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), + } + ); + // return human readable number + // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, + // i.e. when using token with 2decimals 0.002 should be returned as 0 + // Uses ROUND_DOWN mode (1) + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); + } catch (err) { + return ZERO; + } } _exactWrappedTokenInForBPTOut( @@ -510,24 +496,30 @@ export class LinearPool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtNoRate = parseFixed(amount.toString(), 18) + .mul(ONE) + .div(poolPairData.rate); const amt = _calcBptOutPerWrappedIn( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), - bnum(poolPairData.wrappedBalanceScaled.toString()), - bnum(poolPairData.virtualBptSupply.toString()), + amtNoRate.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), + poolPairData.wrappedBalanceScaled.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), } ); // return human readable number // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { return ZERO; } @@ -541,24 +533,32 @@ export class LinearPool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtScaled = parseFixed(amount.toString(), 18); const amt = _calcWrappedOutPerBptIn( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), - bnum(poolPairData.wrappedBalanceScaled.toString()), - bnum(poolPairData.virtualBptSupply.toString()), + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), + poolPairData.wrappedBalanceScaled.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), } ); + + const amtWithRate = BigNumber.from(amt) + .mul(poolPairData.rate) + .div(ONE); // return human readable number // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amtWithRate.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { return ZERO; } @@ -612,22 +612,26 @@ export class LinearPool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtScaled = parseFixed(amount.toString(), 18); const amt = _calcWrappedInPerMainOut( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), } ); // return human readable number // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { return ZERO; } @@ -641,22 +645,26 @@ export class LinearPool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtScaled = parseFixed(amount.toString(), 18); const amt = _calcMainInPerWrappedOut( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), } ); // return human readable number // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { return ZERO; } @@ -667,34 +675,35 @@ export class LinearPool implements PoolBase { amount: OldBigNumber, exact: boolean ): OldBigNumber { - if (exact) { - try { - // All values should use 1e18 fixed point - // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); - // in = main - // out = BPT - const amt = _calcMainInPerBptOut( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), - bnum(poolPairData.wrappedBalanceScaled.toString()), - bnum(poolPairData.virtualBptSupply.toString()), - { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), - } - ); - // return human readable number - // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, - // i.e. when using token with 2decimals 0.002 should be returned as 0 - // Uses ROUND_UP mode (0) - return scale(amt, -18).dp(poolPairData.decimalsIn, 0); - } catch (err) { - return ZERO; - } + try { + // All values should use 1e18 fixed point + // i.e. 1USDC => 1e18 not 1e6 + const amtScaled = parseFixed(amount.toString(), 18); + // in = main + // out = BPT + const amt = _calcMainInPerBptOut( + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), + poolPairData.wrappedBalanceScaled.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), + { + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), + } + ); + // return human readable number + // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, + // i.e. when using token with 2decimals 0.002 should be returned as 0 + // Uses ROUND_UP mode (0) + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsIn, + 0 + ); + } catch (err) { + return ZERO; } - return _mainTokenInForExactBPTOut(amount, poolPairData); } _BPTInForExactMainTokenOut( @@ -702,33 +711,35 @@ export class LinearPool implements PoolBase { amount: OldBigNumber, exact: boolean ): OldBigNumber { - if (exact) { - try { - // All values should use 1e18 fixed point - // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); - - const amt = _calcBptInPerMainOut( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), - bnum(poolPairData.wrappedBalanceScaled.toString()), - bnum(poolPairData.virtualBptSupply.toString()), - { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), - } - ); - // return human readable number - // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, - // i.e. when using token with 2decimals 0.002 should be returned as 0 - // Uses ROUND_UP mode (0) - return scale(amt, -18).dp(poolPairData.decimalsIn, 0); - } catch (err) { - return ZERO; - } + try { + // All values should use 1e18 fixed point + // i.e. 1USDC => 1e18 not 1e6 + const amtScaled = parseFixed(amount.toString(), 18); + + const amt = _calcBptInPerMainOut( + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), + poolPairData.wrappedBalanceScaled.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), + { + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), + } + ); + // return human readable number + // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, + // i.e. when using token with 2decimals 0.002 should be returned as 0 + // Uses ROUND_UP mode (0) + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsIn, + 0 + ); + } catch (err) { + console.log('OUCH?', err); + return ZERO; } - return _BPTInForExactMainTokenOut(amount, poolPairData); } _wrappedTokenInForExactBPTOut( @@ -739,24 +750,32 @@ export class LinearPool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtScaled = parseFixed(amount.toString(), 18); const amt = _calcWrappedInPerBptOut( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), - bnum(poolPairData.wrappedBalanceScaled.toString()), - bnum(poolPairData.virtualBptSupply.toString()), + amtScaled.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), + poolPairData.wrappedBalanceScaled.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), } ); + + const amtWithRate = BigNumber.from(amt) + .mul(poolPairData.rate) + .div(ONE); // return human readable number // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amtWithRate.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { return ZERO; } @@ -770,24 +789,30 @@ export class LinearPool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtNoRate = parseFixed(amount.toString(), 18) + .mul(ONE) + .div(poolPairData.rate); const amt = _calcBptInPerWrappedOut( - amtScaled, - bnum(poolPairData.mainBalanceScaled.toString()), - bnum(poolPairData.wrappedBalanceScaled.toString()), - bnum(poolPairData.virtualBptSupply.toString()), + amtNoRate.toBigInt(), + poolPairData.mainBalanceScaled.toBigInt(), + poolPairData.wrappedBalanceScaled.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), { - fee: bnum(poolPairData.swapFee.toString()), - lowerTarget: bnum(poolPairData.lowerTarget.toString()), - upperTarget: bnum(poolPairData.upperTarget.toString()), + fee: poolPairData.swapFee.toBigInt(), + lowerTarget: poolPairData.lowerTarget.toBigInt(), + upperTarget: poolPairData.upperTarget.toBigInt(), + rate: poolPairData.rate.toBigInt(), } ); // return human readable number // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { return ZERO; } diff --git a/src/poolsMath/linear.ts b/src/poolsMath/linear.ts deleted file mode 100644 index 5eea3781..00000000 --- a/src/poolsMath/linear.ts +++ /dev/null @@ -1,511 +0,0 @@ -// functions translated from -// https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/pool-linear/contracts/LinearMath.sol -import { MathSol, BZERO } from './basicOperations'; - -type Params = { - fee: bigint; - rate: bigint; - lowerTarget: bigint; - upperTarget: bigint; -}; - -export function _calcBptOutPerMainIn( - mainIn: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - // Amount out, so we round down overall. - - if (bptSupply == BigInt(0)) { - return _toNominal(mainIn, params); - } - - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = _toNominal(mainBalance + mainIn, params); - const deltaNominalMain = afterNominalMain - previousNominalMain; - const invariant = _calcInvariantUp( - previousNominalMain, - wrappedBalance, - params - ); - return MathSol.divDownFixed( - MathSol.mulDownFixed(bptSupply, deltaNominalMain), - invariant - ); -} - -export function _calcBptInPerMainOut( - mainOut: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - // Amount in, so we round up overall. - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = _toNominal(mainBalance - mainOut, params); - const deltaNominalMain = previousNominalMain - afterNominalMain; - const invariant = _calcInvariantDown( - previousNominalMain, - wrappedBalance, - params - ); - return MathSol.divUpFixed( - MathSol.mulUpFixed(bptSupply, deltaNominalMain), - invariant - ); -} - -export function _calcWrappedOutPerMainIn( - mainIn: bigint, - mainBalance: bigint, - params: Params -): bigint { - // Amount out, so we round down overall. - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = _toNominal(mainBalance + mainIn, params); - const deltaNominalMain = afterNominalMain - previousNominalMain; - return MathSol.divDownFixed(deltaNominalMain, params.rate); -} - -export function _calcWrappedInPerMainOut( - mainOut: bigint, - mainBalance: bigint, - params: Params -): bigint { - // Amount in, so we round up overall. - const previousNominalMain = _toNominal(mainBalance, params); - const afterNominalMain = _toNominal(mainBalance - mainOut, params); - const deltaNominalMain = previousNominalMain - afterNominalMain; - return MathSol.divUpFixed(deltaNominalMain, params.rate); -} - -export function _calcMainInPerBptOut( - bptOut: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - // Amount in, so we round up overall. - if (bptSupply == BigInt(0)) { - return _fromNominal(bptOut, params); - } - const previousNominalMain = _toNominal(mainBalance, params); - const invariant = _calcInvariantUp( - previousNominalMain, - wrappedBalance, - params - ); - const deltaNominalMain = MathSol.divUpFixed( - MathSol.mulUpFixed(invariant, bptOut), - bptSupply - ); - const afterNominalMain = previousNominalMain + deltaNominalMain; - const newMainBalance = _fromNominal(afterNominalMain, params); - return newMainBalance - mainBalance; -} - -export function _calcMainOutPerBptIn( - bptIn: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - // Amount out, so we round down overall. - const previousNominalMain = _toNominal(mainBalance, params); - const invariant = _calcInvariantDown( - previousNominalMain, - wrappedBalance, - params - ); - const deltaNominalMain = MathSol.divDownFixed( - MathSol.mulDownFixed(invariant, bptIn), - bptSupply - ); - const afterNominalMain = previousNominalMain - deltaNominalMain; - const newMainBalance = _fromNominal(afterNominalMain, params); - return mainBalance - newMainBalance; -} - -export function _calcMainOutPerWrappedIn( - wrappedIn: bigint, - mainBalance: bigint, - params: Params -): bigint { - // Amount out, so we round down overall. - const previousNominalMain = _toNominal(mainBalance, params); - const deltaNominalMain = MathSol.mulDownFixed(wrappedIn, params.rate); - const afterNominalMain = previousNominalMain - deltaNominalMain; - const newMainBalance = _fromNominal(afterNominalMain, params); - return mainBalance - newMainBalance; -} - -export function _calcMainInPerWrappedOut( - wrappedOut: bigint, - mainBalance: bigint, - params: Params -): bigint { - // Amount in, so we round up overall. - const previousNominalMain = _toNominal(mainBalance, params); - const deltaNominalMain = MathSol.mulUpFixed(wrappedOut, params.rate); - const afterNominalMain = previousNominalMain + deltaNominalMain; - const newMainBalance = _fromNominal(afterNominalMain, params); - return newMainBalance - mainBalance; -} - -export function _calcBptOutPerWrappedIn( - wrappedIn: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - // Amount out, so we round down overall. - if (bptSupply == BigInt(0)) { - // Return nominal DAI - return MathSol.mulDownFixed(wrappedIn, params.rate); - } - - const nominalMain = _toNominal(mainBalance, params); - const previousInvariant = _calcInvariantUp( - nominalMain, - wrappedBalance, - params - ); - const newWrappedBalance = wrappedBalance + wrappedIn; - const newInvariant = _calcInvariantDown( - nominalMain, - newWrappedBalance, - params - ); - const newBptBalance = MathSol.divDownFixed( - MathSol.mulDownFixed(bptSupply, newInvariant), - previousInvariant - ); - return newBptBalance - bptSupply; -} - -export function _calcBptInPerWrappedOut( - wrappedOut: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - // Amount in, so we round up overall. - const nominalMain = _toNominal(mainBalance, params); - const previousInvariant = _calcInvariantUp( - nominalMain, - wrappedBalance, - params - ); - const newWrappedBalance = wrappedBalance - wrappedOut; - const newInvariant = _calcInvariantDown( - nominalMain, - newWrappedBalance, - params - ); - const newBptBalance = MathSol.divDownFixed( - MathSol.mulDownFixed(bptSupply, newInvariant), - previousInvariant - ); - return bptSupply - newBptBalance; -} - -export function _calcWrappedInPerBptOut( - bptOut: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - // Amount in, so we round up overall. - if (bptSupply == BigInt(0)) { - // Return nominal DAI - return MathSol.divUpFixed(bptOut, params.rate); - } - - const nominalMain = _toNominal(mainBalance, params); - const previousInvariant = _calcInvariantUp( - nominalMain, - wrappedBalance, - params - ); - const newBptBalance = bptSupply + bptOut; - const newWrappedBalance = MathSol.divUpFixed( - MathSol.mulUpFixed( - MathSol.divUpFixed(newBptBalance, bptSupply), - previousInvariant - ) - nominalMain, - params.rate - ); - return newWrappedBalance - wrappedBalance; -} - -export function _calcWrappedOutPerBptIn( - bptIn: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - // Amount out, so we round down overall. - const nominalMain = _toNominal(mainBalance, params); - const previousInvariant = _calcInvariantUp( - nominalMain, - wrappedBalance, - params - ); - const newBptBalance = bptSupply - bptIn; - const newWrappedBalance = MathSol.divUpFixed( - MathSol.mulUpFixed( - MathSol.divUpFixed(newBptBalance, bptSupply), - previousInvariant - ) - nominalMain, - params.rate - ); - return wrappedBalance - newWrappedBalance; -} - -function _calcInvariantUp( - nominalMainBalance: bigint, - wrappedBalance: bigint, - params: Params -): bigint { - return nominalMainBalance + MathSol.mulUpFixed(wrappedBalance, params.rate); -} - -function _calcInvariantDown( - nominalMainBalance: bigint, - wrappedBalance: bigint, - params: Params -): bigint { - return ( - nominalMainBalance + MathSol.mulDownFixed(wrappedBalance, params.rate) - ); -} - -export function _calcTokensOutGivenExactBptIn( - balances: bigint[], - bptAmountIn: bigint, - bptTotalSupply: bigint, - bptIndex: number -): bigint[] { - /********************************************************************************************** - // exactBPTInForTokensOut // - // (per token) // - // aO = tokenAmountOut / bptIn \ // - // b = tokenBalance a0 = b * | --------------------- | // - // bptIn = bptAmountIn \ bptTotalSupply / // - // bpt = bptTotalSupply // - **********************************************************************************************/ - - // Since we're computing an amount out, we round down overall. This means rounding down on both the - // multiplication and division. - - const bptRatio = MathSol.divDownFixed(bptAmountIn, bptTotalSupply); - let amountsOut: bigint[] = new Array(balances.length); - for (let i = 0; i < balances.length; i++) { - // BPT is skipped as those tokens are not the LPs, but rather the preminted and undistributed amount. - if (i != bptIndex) { - amountsOut[i] = MathSol.mulDownFixed(balances[i], bptRatio); - } - } - return amountsOut; -} - -///////// -/// SpotPriceAfterSwap -///////// - -// PairType = 'token->BPT' -// SwapType = 'swapExactIn' -export function _spotPriceAfterSwapBptOutPerMainIn( - mainIn: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - const finalMainBalance = mainIn + mainBalance; - const previousNominalMain = _toNominal(mainBalance, params); - const invariant = _calcInvariantDown( - previousNominalMain, - wrappedBalance, - params - ); - let poolFactor = MathSol.ONE; - if (bptSupply != BigInt(0)) { - poolFactor = MathSol.divUpFixed(invariant, bptSupply); - } - return MathSol.divUpFixed( - poolFactor, - rightDerivativeToNominal(finalMainBalance, params) - ); -} - -// PairType = 'token->BPT' -// SwapType = 'swapExactOut' -export function _spotPriceAfterSwapMainInPerBptOut( - bptOut: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - const previousNominalMain = _toNominal(mainBalance, params); - const invariant = _calcInvariantDown( - previousNominalMain, - wrappedBalance, - params - ); - let poolFactor = MathSol.ONE; - if (bptSupply != BigInt(0)) { - poolFactor = MathSol.divUpFixed(invariant, bptSupply); - } - const deltaNominalMain = MathSol.mulUpFixed(bptOut, poolFactor); - const afterNominalMain = previousNominalMain + deltaNominalMain; - return MathSol.mulUpFixed( - poolFactor, - rightDerivativeFromNominal(afterNominalMain, params) - ); -} - -// PairType = 'BPT->token' -// SwapType = 'swapExactIn' -export function _spotPriceAfterSwapMainOutPerBptIn( - bptIn: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - const previousNominalMain = _toNominal(mainBalance, params); - const invariant = _calcInvariantDown( - previousNominalMain, - wrappedBalance, - params - ); - const poolFactor = MathSol.divDownFixed(invariant, bptSupply); - const deltaNominalMain = MathSol.mulDownFixed(bptIn, poolFactor); - const afterNominalMain = MathSol.sub(previousNominalMain, deltaNominalMain); - return MathSol.divUpFixed( - MathSol.ONE, - MathSol.mulUpFixed( - poolFactor, - leftDerivativeFromNominal(afterNominalMain, params) - ) - ); -} - -// PairType = 'BPT->token' -// SwapType = 'swapExactOut' -export function _spotPriceAfterSwapBptInPerMainOut( - mainOut: bigint, - mainBalance: bigint, - wrappedBalance: bigint, - bptSupply: bigint, - params: Params -): bigint { - const finalMainBalance = MathSol.sub(mainBalance, mainOut); - const previousNominalMain = _toNominal(mainBalance, params); - const invariant = _calcInvariantDown( - previousNominalMain, - wrappedBalance, - params - ); - const poolFactor = MathSol.divUpFixed(invariant, bptSupply); - return MathSol.divUpFixed( - leftDerivativeToNominal(finalMainBalance, params), - poolFactor - ); -} - -function _toNominal(real: bigint, params: Params): bigint { - // Fees are always rounded down: either direction would work but we need to be consistent, and rounding down - // uses less gas. - if (real < params.lowerTarget) { - const fees = MathSol.mulDownFixed( - params.lowerTarget - real, - params.fee - ); - return MathSol.sub(real, fees); - } else if (real <= params.upperTarget) { - return real; - } else { - const fees = MathSol.mulDownFixed( - real - params.upperTarget, - params.fee - ); - return MathSol.sub(real, fees); - } -} - -function leftDerivativeToNominal(amount: bigint, params: Params): bigint { - const oneMinusFee = MathSol.complementFixed(params.fee); - const onePlusFee = MathSol.ONE + params.fee; - if (amount <= params.lowerTarget) { - return onePlusFee; - } else if (amount <= params.upperTarget) { - return MathSol.ONE; - } else { - return oneMinusFee; - } -} - -function rightDerivativeToNominal(amount: bigint, params: Params): bigint { - const oneMinusFee = MathSol.complementFixed(params.fee); - const onePlusFee = MathSol.ONE + params.fee; - if (amount < params.lowerTarget) { - return onePlusFee; - } else if (amount < params.upperTarget) { - return MathSol.ONE; - } else { - return oneMinusFee; - } -} - -function _fromNominal(nominal: bigint, params: Params): bigint { - // Since real = nominal + fees, rounding down fees is equivalent to rounding down real. - if (nominal < params.lowerTarget) { - return MathSol.divDownFixed( - nominal + MathSol.mulDownFixed(params.fee, params.lowerTarget), - MathSol.ONE + params.fee - ); - } else if (nominal <= params.upperTarget) { - return nominal; - } else { - return MathSol.divDownFixed( - nominal - MathSol.mulDownFixed(params.fee, params.upperTarget), - MathSol.ONE - params.fee - ); - } -} - -function leftDerivativeFromNominal(amount: bigint, params: Params): bigint { - const oneMinusFee = MathSol.complementFixed(params.fee); - const onePlusFee = MathSol.ONE + params.fee; - if (amount <= params.lowerTarget) { - return MathSol.divUpFixed(MathSol.ONE, onePlusFee); - } else if (amount <= params.upperTarget) { - return MathSol.ONE; - } else { - return MathSol.divUpFixed(MathSol.ONE, oneMinusFee); - } -} - -function rightDerivativeFromNominal(amount: bigint, params: Params): bigint { - const oneMinusFee = MathSol.complementFixed(params.fee); - const onePlusFee = MathSol.ONE + params.fee; - if (amount < params.lowerTarget) { - return MathSol.divUpFixed(MathSol.ONE, onePlusFee); - } else if (amount < params.upperTarget) { - return MathSol.ONE; - } else { - return MathSol.divUpFixed(MathSol.ONE, oneMinusFee); - } -} diff --git a/test/linear.spec.ts b/test/linear.spec.ts index e2ac8bd9..a9da9451 100644 --- a/test/linear.spec.ts +++ b/test/linear.spec.ts @@ -141,7 +141,7 @@ describe('linear pool tests', () => { swapType, pools, poolIndex, - bnum('8138925365362304138472.897007980246347837') + bnum('8138925365362304138472.897010550433213647') ); }); @@ -152,7 +152,7 @@ describe('linear pool tests', () => { SwapTypes.SwapExactIn, singleLinear.pools, 0, - bnum('937.8947355124653801') + bnum('937.89473235457065896') ); }); @@ -612,7 +612,8 @@ describe('linear pool tests', () => { parseFixed('25.001542', aUSDT.decimals), kovanPools.pools ); - expect(returnAmount).to.eq('25002051893909811321'); + expect(returnAmount).to.eq('25002051893909811319'); + // Note - before BigInt this was passing with 25002051893909811321 }); it('WrappedToken>BPT, SwapExactOut', async () => { diff --git a/test/linearMath.spec.ts b/test/linearMath.spec.ts deleted file mode 100644 index 9dc578d7..00000000 --- a/test/linearMath.spec.ts +++ /dev/null @@ -1,451 +0,0 @@ -// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/linearMath.spec.ts -import { assert } from 'chai'; -import { PoolTypes } from '../src/types'; -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; -import { BigNumber as OldBigNumber, scale } from '../src/utils/bignumber'; -import { bnum } from '../src/utils/bignumber'; -import * as linearMath from '../src/pools/linearPool/linearMath'; -import { LinearPoolPairData } from '../src/pools/linearPool/linearPool'; -import { - _calcBptOutPerMainIn, - _calcMainOutPerBptIn, - _calcMainInPerBptOut, - _calcBptInPerMainOut, -} from '../src/pools/linearPool/exactMaths'; - -describe('linearMath', () => { - const params = { - fee: b(0.02), - lowerTarget: b(1000), - upperTarget: b(2000), - }; - - let poolPairData; // = makeLinearPoolPairData(0, 0); - context('swap outcomes', () => { - it('_exactTokenInForBPTOut', () => { - poolPairData = makeLinearPoolPairData( - parseFixed('10000', 18), // balanceIn - parseFixed('10000', 18), // balanceOut - parseFixed('10000', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkOutcome( - linearMath._exactMainTokenInForBPTOut, - poolPairData, - 100, - _calcBptOutPerMainIn( - b(100), - b(10000), - b(110), // this includes the rate - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - poolPairData = makeLinearPoolPairData( - parseFixed('900', 18), // balanceIn - parseFixed('10000', 18), // balanceOut - parseFixed('900', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkOutcome( - linearMath._exactMainTokenInForBPTOut, - poolPairData, - 1300, - _calcBptOutPerMainIn( - b(1300), - b(900), - b(110), - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - }); - - it('_tokenInForExactBPTOut', () => { - poolPairData = makeLinearPoolPairData( - parseFixed('900', 18), // balanceIn - parseFixed('10000', 18), // balanceOut - parseFixed('900', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkOutcome( - linearMath._mainTokenInForExactBPTOut, - poolPairData, - 100, - _calcMainInPerBptOut( - b(100), - b(900), - b(110), - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - checkOutcome( - linearMath._mainTokenInForExactBPTOut, - poolPairData, - 5000, - _calcMainInPerBptOut( - b(5000), - b(900), - b(110), - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - }); - - it('_BPTInForExactTokenOut', () => { - poolPairData = makeLinearPoolPairData( - parseFixed('10000', 18), // balanceIn - parseFixed('900', 18), // balanceOut - parseFixed('900', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkOutcome( - linearMath._BPTInForExactMainTokenOut, - poolPairData, - 200, - _calcBptInPerMainOut( - b(200), - b(900), - b(110), - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - poolPairData = makeLinearPoolPairData( - parseFixed('10000', 18), // balanceIn - parseFixed('2500', 18), // balanceOut - parseFixed('2500', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkOutcome( - linearMath._BPTInForExactMainTokenOut, - poolPairData, - 1600, - _calcBptInPerMainOut( - b(1600), - b(2500), - b(110), - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - checkOutcome( - linearMath._BPTInForExactMainTokenOut, - poolPairData, - 800, - _calcBptInPerMainOut( - b(800), - b(2500), - b(110), - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - }); - - it('_exactBPTInForTokenOut', () => { - poolPairData = makeLinearPoolPairData( - parseFixed('10000', 18), // balanceIn - parseFixed('2500', 18), // balanceOut - parseFixed('2500', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkOutcome( - linearMath._exactBPTInForMainTokenOut, - poolPairData, - 200, - _calcMainOutPerBptIn( - b(200), - b(2500), - b(110), - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - checkOutcome( - linearMath._exactBPTInForMainTokenOut, - poolPairData, - 5000, - _calcMainOutPerBptIn( - b(5000), - b(2500), - b(110), - b(10000), - params - ).toNumber() / - 10 ** 18, - 0 - ); - }); - }); - - context('spot price after swap', () => { - it('_spotPriceAfterSwapExactTokenInForBPTOut', () => { - poolPairData = makeLinearPoolPairData( - parseFixed('500', 18), // balanceIn - parseFixed('10000', 18), // balanceOut - parseFixed('500', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkDerivative( - linearMath._exactMainTokenInForBPTOut, - linearMath._spotPriceAfterSwapExactTokenInForBPTOut, - poolPairData, - 200, - 0.001, - 0.00000001, - true - ); - checkDerivative( - linearMath._exactMainTokenInForBPTOut, - linearMath._spotPriceAfterSwapExactTokenInForBPTOut, - poolPairData, - 1500, - 0.001, - 0.00000001, - true - ); - }); - - it('_spotPriceAfterSwapTokenInForExactBPTOut', () => { - checkDerivative( - linearMath._mainTokenInForExactBPTOut, - linearMath._spotPriceAfterSwapTokenInForExactBPTOut, - poolPairData, - 500, - 0.001, - 0.00000001, - false - ); - checkDerivative( - linearMath._mainTokenInForExactBPTOut, - linearMath._spotPriceAfterSwapTokenInForExactBPTOut, - poolPairData, - 40000, - 0.001, - 0.00000001, - false - ); - }); - - it('_spotPriceAfterSwapBPTInForExactTokenOut', () => { - poolPairData = makeLinearPoolPairData( - parseFixed('1000', 18), // balanceIn - parseFixed('3500', 18), // balanceOut - parseFixed('3500', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkDerivative( - linearMath._BPTInForExactMainTokenOut, - linearMath._spotPriceAfterSwapBPTInForExactTokenOut, - poolPairData, - 200, - 0.001, - 0.00000001, - false - ); - checkDerivative( - linearMath._BPTInForExactMainTokenOut, - linearMath._spotPriceAfterSwapBPTInForExactTokenOut, - poolPairData, - 1600, - 0.001, - 0.00000001, - false - ); - checkDerivative( - linearMath._BPTInForExactMainTokenOut, - linearMath._spotPriceAfterSwapBPTInForExactTokenOut, - poolPairData, - 3000, - 0.001, - 0.00000001, - false - ); - }); - - it('_spotPriceAfterSwapExactBPTInForTokenOut', () => { - poolPairData = makeLinearPoolPairData( - parseFixed('10000', 18), // balanceIn - parseFixed('3500', 18), // balanceOut - parseFixed('3500', 18), // mainBalance - parseFixed('100', 18), // wrappedBalance - parseFixed('10000', 0) // virtualBptSupply - ); - checkDerivative( - linearMath._exactBPTInForMainTokenOut, - linearMath._spotPriceAfterSwapExactBPTInForTokenOut, - poolPairData, - 200, - 0.001, - 0.00000001, - true - ); - checkDerivative( - linearMath._exactBPTInForMainTokenOut, - linearMath._spotPriceAfterSwapExactBPTInForTokenOut, - poolPairData, - 5000, - 0.001, - 0.00000001, - true - ); - checkDerivative( - linearMath._exactBPTInForMainTokenOut, - linearMath._spotPriceAfterSwapExactBPTInForTokenOut, - poolPairData, - 8000, - 0.001, - 0.00000001, - true - ); - }); - }); - - context('derivatives of spot price after swap', () => { - it('derivatives of spot price are zero', () => { - checkOutcome( - linearMath._derivativeSpotPriceAfterSwapExactTokenInForBPTOut, - poolPairData, - 100, - 0, - 0 - ); - checkOutcome( - linearMath._derivativeSpotPriceAfterSwapTokenInForExactBPTOut, - poolPairData, - 100, - 0, - 0 - ); - checkOutcome( - linearMath._derivativeSpotPriceAfterSwapExactBPTInForTokenOut, - poolPairData, - 100, - 0, - 0 - ); - checkOutcome( - linearMath._derivativeSpotPriceAfterSwapBPTInForExactTokenOut, - poolPairData, - 100, - 0, - 0 - ); - }); - }); -}); - -function makeLinearPoolPairData( - balanceIn: BigNumber, - balanceOut: BigNumber, - mainBalance: BigNumber, - wrappedBalance: BigNumber, - virtualBptSupply: BigNumber, - swapFee: BigNumber = parseFixed('0.02', 18), - rate: OldBigNumber = scale(bnum('1.1'), 18), - lowerTarget: BigNumber = parseFixed('1000', 18), - upperTarget: BigNumber = parseFixed('2000', 18), - pairType = 0 -): LinearPoolPairData { - return { - pairType: pairType, - balanceIn, - balanceOut, - wrappedBalance: bnum(wrappedBalance.toString()), - wrappedDecimals: 18, - rate: rate, - lowerTarget: lowerTarget, - upperTarget: upperTarget, - swapFee, - id: 'ignored', - address: 'ignored', - poolType: PoolTypes.Linear, - tokenIn: 'ignored', - tokenOut: 'ignored', - decimalsIn: 18, - decimalsOut: 18, - mainBalanceScaled: parseFixed(mainBalance.toString(), 18), - wrappedBalanceScaled: parseFixed(wrappedBalance.toString(), 18), - bptBalanceScaled: parseFixed('0', 18), - virtualBptSupply: parseFixed(virtualBptSupply.toString(), 18), - }; -} - -function checkOutcome( - fn: ( - amount: OldBigNumber, - poolPairData: LinearPoolPairData - ) => OldBigNumber, - poolPairData: LinearPoolPairData, - amount: number, - expected: number, - error: number -) { - // const amt = scale(bnum(amount), 18); - assert.approximately( - fn(bnum(amount), poolPairData).toNumber(), - expected, - error, - 'wrong result' - ); -} - -function checkDerivative( - fn: ( - amount: OldBigNumber, - poolPairData: LinearPoolPairData - ) => OldBigNumber, - der: ( - amount: OldBigNumber, - poolPairData: LinearPoolPairData - ) => OldBigNumber, - poolPairData: LinearPoolPairData, - amount: number, - delta: number, - error: number, - inverse = false -) { - const x = bnum(amount); - let incrementalQuotient = fn(x.plus(delta), poolPairData) - .minus(fn(x, poolPairData)) - .div(delta); - if (inverse) incrementalQuotient = bnum(1).div(incrementalQuotient); - const der_ans = der(x, poolPairData); - assert.approximately( - incrementalQuotient.div(der_ans).toNumber(), - 1, - error, - 'wrong result' - ); -} - -function b(arg: number): OldBigNumber { - return bnum(arg * 10 ** 18); -} diff --git a/test/poolsMathLinear.spec.ts b/test/poolsMathLinear.spec.ts index cdb3290d..8a7019b3 100644 --- a/test/poolsMathLinear.spec.ts +++ b/test/poolsMathLinear.spec.ts @@ -1,4 +1,4 @@ -import * as linear from '../src/poolsMath/linear'; +import * as linear from '../src/pools/linearPool/linearMath'; import { MathSol } from '../src/poolsMath/basicOperations'; import { assert } from 'chai'; From eb68ac8716ed59c4abefbf1558ccf284999f7018 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 18 Jan 2022 10:24:53 +0000 Subject: [PATCH 14/43] Consolidate stable/meta ts maths to single implementation. --- src/pools/metaStablePool/metaStableMath.ts | 450 --------------------- src/pools/metaStablePool/metaStablePool.ts | 2 +- src/pools/stablePool/stableMath.ts | 426 +++++++++---------- 3 files changed, 201 insertions(+), 677 deletions(-) delete mode 100644 src/pools/metaStablePool/metaStableMath.ts diff --git a/src/pools/metaStablePool/metaStableMath.ts b/src/pools/metaStablePool/metaStableMath.ts deleted file mode 100644 index fccfe1d6..00000000 --- a/src/pools/metaStablePool/metaStableMath.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { BigNumber, formatFixed } from '@ethersproject/bignumber'; -import { WeiPerEther as EONE } from '@ethersproject/constants'; -import { - BigNumber as OldBigNumber, - bnum, - ZERO, - ONE, -} from '../../utils/bignumber'; -import { MetaStablePoolPairData } from './metaStablePool'; -// All functions are adapted from the solidity ones to be found on: -// https://github.com/balancer-labs/balancer-core-v2/blob/master/contracts/pools/stable/StableMath.sol - -// TODO: implement all up and down rounding variations - -/********************************************************************************************** - // invariant // - // D = invariant to compute // - // A = amplifier n * D^2 + A * n^n * S * (n^n * P / D^(n−1)) // - // S = sum of balances ____________________________________________ // - // P = product of balances (n+1) * D + ( A * n^n − 1)* (n^n * P / D^(n−1)) // - // n = number of tokens // - **********************************************************************************************/ -export function _invariant( - amp: BigNumber, // amp - balances: OldBigNumber[] // balances -): OldBigNumber { - let sum = ZERO; - const totalCoins = balances.length; - for (let i = 0; i < totalCoins; i++) { - sum = sum.plus(balances[i]); - } - if (sum.isZero()) { - return ZERO; - } - let prevInv = ZERO; - let inv = sum; - - // amp is passed as an ethers bignumber while maths uses bignumber.js - const ampAdjusted = bnum(formatFixed(amp, 3)); - const ampTimesNpowN = ampAdjusted.times(totalCoins ** totalCoins); // A*n^n - - for (let i = 0; i < 255; i++) { - let P_D = bnum(totalCoins).times(balances[0]); - for (let j = 1; j < totalCoins; j++) { - //P_D is rounded up - P_D = P_D.times(balances[j]).times(totalCoins).div(inv); - } - prevInv = inv; - //inv is rounded up - inv = bnum(totalCoins) - .times(inv) - .times(inv) - .plus(ampTimesNpowN.times(sum).times(P_D)) - .div( - bnum(totalCoins + 1) - .times(inv) - .plus(ampTimesNpowN.minus(1).times(P_D)) - ); - // Equality with the precision of 1 - if (inv.gt(prevInv)) { - if (inv.minus(prevInv).lt(bnum(10 ** -18))) { - break; - } - } else if (prevInv.minus(inv).lt(bnum(10 ** -18))) { - break; - } - } - //Result is rounded up - return inv; -} - -// // This function has to be zero if the invariant D was calculated correctly -// // It was only used for double checking that the invariant was correct -// export function _invariantValueFunction( -// amp: OldBigNumber, // amp -// balances: OldBigNumber[], // balances -// D: OldBigNumber -// ): OldBigNumber { -// let invariantValueFunction; -// let prod = ONE; -// let sum = ZERO; -// for (let i = 0; i < balances.length; i++) { -// prod = prod.times(balances[i]); -// sum = sum.plus(balances[i]); -// } -// let n = bnum(balances.length); - -// // NOT! working based on Daniel's equation: https://www.notion.so/Analytical-for-2-tokens-1cd46debef6648dd81f2d75bae941fea -// // invariantValueFunction = amp.times(sum) -// // .plus((ONE.div(n.pow(n)).minus(amp)).times(D)) -// // .minus((ONE.div(n.pow(n.times(2)).times(prod))).times(D.pow(n.plus(ONE)))); -// invariantValueFunction = D.pow(n.plus(ONE)) -// .div(n.pow(n).times(prod)) -// .plus(D.times(amp.times(n.pow(n)).minus(ONE))) -// .minus(amp.times(n.pow(n)).times(sum)); - -// return invariantValueFunction; -// } - -// Adapted from StableMath.sol, _outGivenIn() -// * Added swap fee at very first line -/********************************************************************************************** - // outGivenIn token x for y - polynomial equation to solve // - // ay = amount out to calculate // - // by = balance token out // - // y = by - ay // - // D = invariant D D^(n+1) // - // A = amplifier y^2 + ( S - ---------- - 1) * y - ------------- = 0 // - // n = number of tokens (A * n^n) A * n^2n * P // - // S = sum of final balances but y // - // P = product of final balances but y // - **********************************************************************************************/ -export function _exactTokenInForTokenOut( - amount: OldBigNumber, - poolPairData: MetaStablePoolPairData -): OldBigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - const balances = [...allBalances]; - let tokenAmountIn = amount; - tokenAmountIn = tokenAmountIn - .times(EONE.sub(swapFee).toString()) - .div(EONE.toString()); - - //Invariant is rounded up - const inv = _invariant(amp, balances); - let p = inv; - let sum = ZERO; - const totalCoins = bnum(balances.length); - let n_pow_n = ONE; - let x = ZERO; - for (let i = 0; i < balances.length; i++) { - n_pow_n = n_pow_n.times(totalCoins); - - if (i == tokenIndexIn) { - x = balances[i].plus(tokenAmountIn); - } else if (i != tokenIndexOut) { - x = balances[i]; - } else { - continue; - } - sum = sum.plus(x); - //Round up p - p = p.times(inv).div(x); - } - - //Calculate out balance - const y = _solveAnalyticalBalance(sum, inv, amp, n_pow_n, p); - - //Result is rounded down - // return balances[tokenIndexOut] > y ? balances[tokenIndexOut].minus(y) : 0; - return balances[tokenIndexOut].minus(y); -} - -// Adapted from StableMath.sol, _inGivenOut() -// * Added swap fee at very last line -/********************************************************************************************** - // inGivenOut token x for y - polynomial equation to solve // - // ax = amount in to calculate // - // bx = balance token in // - // x = bx + ax // - // D = invariant D D^(n+1) // - // A = amplifier x^2 + ( S - ---------- - 1) * x - ------------- = 0 // - // n = number of tokens (A * n^n) A * n^2n * P // - // S = sum of final balances but x // - // P = product of final balances but x // - **********************************************************************************************/ -export function _tokenInForExactTokenOut( - amount: OldBigNumber, - poolPairData: MetaStablePoolPairData -): OldBigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - const balances = [...allBalances]; - const tokenAmountOut = amount; - //Invariant is rounded up - const inv = _invariant(amp, balances); - let p = inv; - let sum = ZERO; - const totalCoins = bnum(balances.length); - let n_pow_n = ONE; - let x = ZERO; - for (let i = 0; i < balances.length; i++) { - n_pow_n = n_pow_n.times(totalCoins); - - if (i == tokenIndexOut) { - x = balances[i].minus(tokenAmountOut); - } else if (i != tokenIndexIn) { - x = balances[i]; - } else { - continue; - } - sum = sum.plus(x); - //Round up p - p = p.times(inv).div(x); - } - - //Calculate in balance - const y = _solveAnalyticalBalance(sum, inv, amp, n_pow_n, p); - - //Result is rounded up - return y - .minus(balances[tokenIndexIn]) - .multipliedBy(EONE.toString()) - .div(EONE.sub(swapFee).toString()); -} - -//This function calculates the balance of a given token (tokenIndex) -// given all the other balances and the invariant -function _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp: BigNumber, - balances: OldBigNumber[], - inv: OldBigNumber, - tokenIndex: number -): OldBigNumber { - let p = inv; - let sum = ZERO; - const totalCoins = balances.length; - let nPowN = ONE; - let x = ZERO; - for (let i = 0; i < totalCoins; i++) { - nPowN = nPowN.times(totalCoins); - if (i != tokenIndex) { - x = balances[i]; - } else { - continue; - } - sum = sum.plus(x); - //Round up p - p = p.times(inv).div(x); - } - - // Calculate token balance - return _solveAnalyticalBalance(sum, inv, amp, nPowN, p); -} - -//This function calcuates the analytical solution to find the balance required -export function _solveAnalyticalBalance( - sum: OldBigNumber, - inv: OldBigNumber, - amp: BigNumber, - n_pow_n: OldBigNumber, - p: OldBigNumber -): OldBigNumber { - // amp is passed as an ethers bignumber while maths uses bignumber.js - const oldBN_amp = bnum(formatFixed(amp, 3)); - - //Round up p - p = p.times(inv).div(oldBN_amp.times(n_pow_n).times(n_pow_n)); - //Round down b - const b = sum.plus(inv.div(oldBN_amp.times(n_pow_n))); - //Round up c - // let c = inv >= b - // ? inv.minus(b).plus(Math.sqrtUp(inv.minus(b).times(inv.minus(b)).plus(p.times(4)))) - // : Math.sqrtUp(b.minus(inv).times(b.minus(inv)).plus(p.times(4))).minus(b.minus(inv)); - let c; - if (inv.gte(b)) { - c = inv - .minus(b) - .plus(inv.minus(b).times(inv.minus(b)).plus(p.times(4)).sqrt()); - } else { - c = b - .minus(inv) - .times(b.minus(inv)) - .plus(p.times(4)) - .sqrt() - .minus(b.minus(inv)); - } - //Round up y - return c.div(2); -} - -////////////////////// -//// These functions have been added exclusively for the SORv2 -////////////////////// - -export function _poolDerivatives( - amp: BigNumber, - balances: OldBigNumber[], - tokenIndexIn: number, - tokenIndexOut: number, - is_first_derivative: boolean, - wrt_out: boolean -): OldBigNumber { - const totalCoins = balances.length; - const D = _invariant(amp, balances); - let S = ZERO; - for (let i = 0; i < totalCoins; i++) { - if (i != tokenIndexIn && i != tokenIndexOut) { - S = S.plus(balances[i]); - } - } - const x = balances[tokenIndexIn]; - const y = balances[tokenIndexOut]; - // amp is passed as an ethers bignumber while maths uses bignumber.js - const ampAdjusted = bnum(formatFixed(amp, 3)); - const a = ampAdjusted.times(totalCoins ** totalCoins); // = ampTimesNpowN - const b = S.minus(D).times(a).plus(D); - const twoaxy = bnum(2).times(a).times(x).times(y); - const partial_x = twoaxy.plus(a.times(y).times(y)).plus(b.times(y)); - const partial_y = twoaxy.plus(a.times(x).times(x)).plus(b.times(x)); - let ans; - if (is_first_derivative) { - ans = partial_x.div(partial_y); - } else { - const partial_xx = bnum(2).times(a).times(y); - const partial_yy = bnum(2).times(a).times(x); - const partial_xy = partial_xx.plus(partial_yy).plus(b); - const numerator = bnum(2) - .times(partial_x) - .times(partial_y) - .times(partial_xy) - .minus(partial_xx.times(partial_y.pow(2))) - .minus(partial_yy.times(partial_x.pow(2))); - const denominator = partial_x.pow(2).times(partial_y); - ans = numerator.div(denominator); - if (wrt_out) { - ans = ans.times(partial_y).div(partial_x); - } - } - return ans; -} - -///////// -/// SpotPriceAfterSwap -///////// - -// PairType = 'token->token' -// SwapType = 'swapExactIn' -export function _spotPriceAfterSwapExactTokenInForTokenOut( - amount: OldBigNumber, - poolPairData: MetaStablePoolPairData -): OldBigNumber { - const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - const balances = [...allBalances]; - balances[tokenIndexIn] = balances[tokenIndexIn].plus( - amount.times(EONE.sub(swapFee).toString()).div(EONE.toString()) - ); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - _exactTokenInForTokenOut(amount, poolPairData) - ); - let ans = _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - true, - false - ); - ans = ONE.div(ans.times(EONE.sub(swapFee).toString()).div(EONE.toString())); - return ans; -} - -// PairType = 'token->token' -// SwapType = 'swapExactOut' -export function _spotPriceAfterSwapTokenInForExactTokenOut( - amount: OldBigNumber, - poolPairData: MetaStablePoolPairData -): OldBigNumber { - const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - const balances = [...allBalances]; - const _in = _tokenInForExactTokenOut(amount, poolPairData) - .times(EONE.sub(swapFee).toString()) - .div(EONE.toString()); - balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in); - balances[tokenIndexOut] = balances[tokenIndexOut].minus(amount); - let ans = _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - true, - true - ); - ans = ONE.div(ans.times(EONE.sub(swapFee).toString()).div(EONE.toString())); - return ans; -} - -function _feeFactor( - balances: OldBigNumber[], - tokenIndex: number, - swapFee: OldBigNumber -): OldBigNumber { - const sumBalances = balances.reduce((a, b) => a.plus(b)); - const currentWeight = balances[tokenIndex].div(sumBalances); - const tokenBalancePercentageExcess = ONE.minus(currentWeight); - return ONE.minus(tokenBalancePercentageExcess.times(swapFee)); -} - -///////// -/// Derivatives of spotPriceAfterSwap -///////// - -// PairType = 'token->token' -// SwapType = 'swapExactIn' -export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( - amount: OldBigNumber, - poolPairData: MetaStablePoolPairData -): OldBigNumber { - const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - const balances = [...allBalances]; - balances[tokenIndexIn] = balances[tokenIndexIn].plus( - amount.times(EONE.sub(swapFee).toString()).div(EONE.toString()) - ); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - _exactTokenInForTokenOut(amount, poolPairData) - ); - return _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - false, - false - ); -} - -// PairType = 'token->token' -// SwapType = 'swapExactOut' -export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( - amount: OldBigNumber, - poolPairData: MetaStablePoolPairData -): OldBigNumber { - const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - const balances = [...allBalances]; - const _in = _tokenInForExactTokenOut(amount, poolPairData) - .times(EONE.sub(swapFee).toString()) - .div(EONE.toString()); - balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in); - balances[tokenIndexOut] = balances[tokenIndexOut].minus(amount); - const feeFactor = EONE.div(swapFee).toString(); - return _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - false, - true - ).div(feeFactor); -} diff --git a/src/pools/metaStablePool/metaStablePool.ts b/src/pools/metaStablePool/metaStablePool.ts index a8e54344..ffe3e52c 100644 --- a/src/pools/metaStablePool/metaStablePool.ts +++ b/src/pools/metaStablePool/metaStablePool.ts @@ -23,7 +23,7 @@ import { _spotPriceAfterSwapTokenInForExactTokenOut, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, -} from './metaStableMath'; +} from '../stablePool/stableMath'; import { StablePoolPairData } from 'pools/stablePool/stablePool'; type MetaStablePoolToken = Pick< diff --git a/src/pools/stablePool/stableMath.ts b/src/pools/stablePool/stableMath.ts index d310bb4e..0c172c1a 100644 --- a/src/pools/stablePool/stableMath.ts +++ b/src/pools/stablePool/stableMath.ts @@ -69,34 +69,6 @@ export function _invariant( return inv; } -// // This function has to be zero if the invariant D was calculated correctly -// // It was only used for double checking that the invariant was correct -// export function _invariantValueFunction( -// amp: BigNumber, // amp -// balances: BigNumber[], // balances -// D: BigNumber -// ): BigNumber { -// let invariantValueFunction; -// let prod = ONE; -// let sum = ZERO; -// for (let i = 0; i < balances.length; i++) { -// prod = prod.times(balances[i]); -// sum = sum.plus(balances[i]); -// } -// let n = bnum(balances.length); - -// // NOT! working based on Daniel's equation: https://www.notion.so/Analytical-for-2-tokens-1cd46debef6648dd81f2d75bae941fea -// // invariantValueFunction = amp.times(sum) -// // .plus((ONE.div(n.pow(n)).minus(amp)).times(D)) -// // .minus((ONE.div(n.pow(n.times(2)).times(prod))).times(D.pow(n.plus(ONE)))); -// invariantValueFunction = D.pow(n.plus(ONE)) -// .div(n.pow(n).times(prod)) -// .plus(D.times(amp.times(n.pow(n)).minus(ONE))) -// .minus(amp.times(n.pow(n)).times(sum)); - -// return invariantValueFunction; -// } - // Adapted from StableMath.sol, _outGivenIn() // * Added swap fee at very first line /********************************************************************************************** @@ -211,95 +183,6 @@ export function _tokenInForExactTokenOut( .div(EONE.sub(swapFee).toString()); } -/* -Flow of calculations: -amountBPTOut -> newInvariant -> (amountInProportional, amountInAfterFee) -> -amountInPercentageExcess -> amountIn -*/ -export function _tokenInForExactBPTOut( - amount: OldBigNumber, - poolPairData: StablePoolPairData -): OldBigNumber { - // The formula below returns some dust (due to rounding errors) but when - // we input zero the output should be zero - if (amount.isZero()) return amount; - const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - const balances = [...allBalances]; - const bptAmountOut = amount; - - /********************************************************************************************** - // TODO description // - **********************************************************************************************/ - - // Get current invariant - const currentInvariant = _invariant(amp, balances); - // Calculate new invariant - const newInvariant = allBalances[tokenIndexOut] - .plus(bptAmountOut) - .div(allBalances[tokenIndexOut]) - .times(currentInvariant); - - // First calculate the sum of all token balances which will be used to calculate - // the current weight of token - let sumBalances = ZERO; - for (let i = 0; i < balances.length; i++) { - sumBalances = sumBalances.plus(balances[i]); - } - - // get amountInAfterFee - const newBalanceTokenIndex = - _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp, - balances, - newInvariant, - tokenIndexIn - ); - const amountInAfterFee = newBalanceTokenIndex.minus(balances[tokenIndexIn]); - - // Get tokenBalancePercentageExcess - const currentWeight = balances[tokenIndexIn].div(sumBalances); - const tokenBalancePercentageExcess = ONE.minus(currentWeight); - - // return amountIn - return amountInAfterFee.div( - ONE.minus( - tokenBalancePercentageExcess - .times(swapFee.toString()) - .div(EONE.toString()) - ) - ); -} - -//This function calculates the balance of a given token (tokenIndex) -// given all the other balances and the invariant -function _getTokenBalanceGivenInvariantAndAllOtherBalances( - amp: BigNumber, - balances: OldBigNumber[], - inv: OldBigNumber, - tokenIndex: number -): OldBigNumber { - let p = inv; - let sum = ZERO; - const totalCoins = balances.length; - let nPowN = ONE; - let x = ZERO; - for (let i = 0; i < totalCoins; i++) { - nPowN = nPowN.times(totalCoins); - if (i != tokenIndex) { - x = balances[i]; - } else { - continue; - } - sum = sum.plus(x); - //Round up p - p = p.times(inv).div(x); - } - - // Calculate token balance - return _solveAnalyticalBalance(sum, inv, amp, nPowN, p); -} - //This function calcuates the analytical solution to find the balance required export function _solveAnalyticalBalance( sum: OldBigNumber, @@ -390,73 +273,6 @@ export function _poolDerivatives( /// SpotPriceAfterSwap ///////// -export function _poolDerivativesBPT( - amp: BigNumber, - balances: OldBigNumber[], - bptSupply: OldBigNumber, - tokenIndexIn: number, - is_first_derivative: boolean, - is_BPT_out: boolean, - wrt_out: boolean -): OldBigNumber { - const totalCoins = balances.length; - const D = _invariant(amp, balances); - let S = ZERO; - let D_P = D.div(totalCoins); - for (let i = 0; i < totalCoins; i++) { - if (i != tokenIndexIn) { - S = S.plus(balances[i]); - D_P = D_P.times(D).div(balances[i].times(totalCoins)); - } - } - const x = balances[tokenIndexIn]; - // amp is passed as an ethers bignumber while maths uses bignumber.js - const ampAdjusted = bnum(formatFixed(amp, 3)); - const alpha = ampAdjusted.times(totalCoins ** totalCoins); // = ampTimesNpowN - const beta = alpha.times(S); - const gamma = ONE.minus(alpha); - const partial_x = bnum(2) - .times(alpha) - .times(x) - .plus(beta) - .plus(gamma.times(D)); - const minus_partial_D = D_P.times(totalCoins + 1).minus(gamma.times(x)); - const partial_D = ZERO.minus(minus_partial_D); - let ans; - if (is_first_derivative) { - ans = partial_x.div(minus_partial_D).times(bptSupply).div(D); - } else { - const partial_xx = bnum(2).times(alpha); - const partial_xD = gamma; - const n_times_nplusone = totalCoins * (totalCoins + 1); - const partial_DD = ZERO.minus(D_P.times(n_times_nplusone).div(D)); - if (is_BPT_out) { - const term1 = partial_xx.times(partial_D).div(partial_x.pow(2)); - const term2 = bnum(2).times(partial_xD).div(partial_x); - const term3 = partial_DD.div(partial_D); - ans = term1.minus(term2).plus(term3).times(D).div(bptSupply); - if (wrt_out) { - const D_prime = ZERO.minus(partial_x.div(partial_D)); - ans = ans.div(D_prime).times(D).div(bptSupply); - } - } else { - ans = bnum(2) - .times(partial_xD) - .div(partial_D) - .minus(partial_DD.times(partial_x).div(partial_D.pow(2))) - .minus(partial_xx.div(partial_x)); - if (wrt_out) { - ans = ans - .times(partial_x) - .div(minus_partial_D) - .times(bptSupply) - .div(D); - } - } - } - return ans; -} - // PairType = 'token->token' // SwapType = 'swapExactIn' export function _spotPriceAfterSwapExactTokenInForTokenOut( @@ -510,6 +326,62 @@ export function _spotPriceAfterSwapTokenInForExactTokenOut( return ans; } +///////// +/// Derivatives of spotPriceAfterSwap +///////// + +// PairType = 'token->token' +// SwapType = 'swapExactIn' +export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + amount: OldBigNumber, + poolPairData: StablePoolPairData +): OldBigNumber { + const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = + poolPairData; + const balances = [...allBalances]; + balances[tokenIndexIn] = balances[tokenIndexIn].plus( + amount.times(EONE.sub(swapFee).toString()).div(EONE.toString()) + ); + balances[tokenIndexOut] = balances[tokenIndexOut].minus( + _exactTokenInForTokenOut(amount, poolPairData) + ); + return _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + false, + false + ); +} + +// PairType = 'token->token' +// SwapType = 'swapExactOut' +export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + amount: OldBigNumber, + poolPairData: StablePoolPairData +): OldBigNumber { + const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = + poolPairData; + const balances = [...allBalances]; + const _in = _tokenInForExactTokenOut(amount, poolPairData) + .times(EONE.sub(swapFee).toString()) + .div(EONE.toString()); + balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in); + balances[tokenIndexOut] = balances[tokenIndexOut].minus(amount); + const feeFactor = EONE.div(swapFee).toString(); + return _poolDerivatives( + amp, + balances, + tokenIndexIn, + tokenIndexOut, + false, + true + ).div(feeFactor); +} + +// The following are used in front-end helper functions + function _feeFactor( balances: OldBigNumber[], tokenIndex: number, @@ -553,56 +425,158 @@ export function _spotPriceAfterSwapTokenInForExactBPTOut( return ans; } -///////// -/// Derivatives of spotPriceAfterSwap -///////// - -// PairType = 'token->token' -// SwapType = 'swapExactIn' -export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( +/* +Flow of calculations: +amountBPTOut -> newInvariant -> (amountInProportional, amountInAfterFee) -> +amountInPercentageExcess -> amountIn +*/ +export function _tokenInForExactBPTOut( amount: OldBigNumber, poolPairData: StablePoolPairData ): OldBigNumber { + // The formula below returns some dust (due to rounding errors) but when + // we input zero the output should be zero + if (amount.isZero()) return amount; const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = poolPairData; const balances = [...allBalances]; - balances[tokenIndexIn] = balances[tokenIndexIn].plus( - amount.times(EONE.sub(swapFee).toString()).div(EONE.toString()) - ); - balances[tokenIndexOut] = balances[tokenIndexOut].minus( - _exactTokenInForTokenOut(amount, poolPairData) - ); - return _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - false, - false + const bptAmountOut = amount; + + /********************************************************************************************** + // TODO description // + **********************************************************************************************/ + + // Get current invariant + const currentInvariant = _invariant(amp, balances); + // Calculate new invariant + const newInvariant = allBalances[tokenIndexOut] + .plus(bptAmountOut) + .div(allBalances[tokenIndexOut]) + .times(currentInvariant); + + // First calculate the sum of all token balances which will be used to calculate + // the current weight of token + let sumBalances = ZERO; + for (let i = 0; i < balances.length; i++) { + sumBalances = sumBalances.plus(balances[i]); + } + + // get amountInAfterFee + const newBalanceTokenIndex = + _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp, + balances, + newInvariant, + tokenIndexIn + ); + const amountInAfterFee = newBalanceTokenIndex.minus(balances[tokenIndexIn]); + + // Get tokenBalancePercentageExcess + const currentWeight = balances[tokenIndexIn].div(sumBalances); + const tokenBalancePercentageExcess = ONE.minus(currentWeight); + + // return amountIn + return amountInAfterFee.div( + ONE.minus( + tokenBalancePercentageExcess + .times(swapFee.toString()) + .div(EONE.toString()) + ) ); } -// PairType = 'token->token' -// SwapType = 'swapExactOut' -export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( - amount: OldBigNumber, - poolPairData: StablePoolPairData +//This function calculates the balance of a given token (tokenIndex) +// given all the other balances and the invariant +function _getTokenBalanceGivenInvariantAndAllOtherBalances( + amp: BigNumber, + balances: OldBigNumber[], + inv: OldBigNumber, + tokenIndex: number ): OldBigNumber { - const { amp, allBalances, tokenIndexIn, tokenIndexOut, swapFee } = - poolPairData; - const balances = [...allBalances]; - const _in = _tokenInForExactTokenOut(amount, poolPairData) - .times(EONE.sub(swapFee).toString()) - .div(EONE.toString()); - balances[tokenIndexIn] = balances[tokenIndexIn].plus(_in); - balances[tokenIndexOut] = balances[tokenIndexOut].minus(amount); - const feeFactor = EONE.div(swapFee).toString(); - return _poolDerivatives( - amp, - balances, - tokenIndexIn, - tokenIndexOut, - false, - true - ).div(feeFactor); + let p = inv; + let sum = ZERO; + const totalCoins = balances.length; + let nPowN = ONE; + let x = ZERO; + for (let i = 0; i < totalCoins; i++) { + nPowN = nPowN.times(totalCoins); + if (i != tokenIndex) { + x = balances[i]; + } else { + continue; + } + sum = sum.plus(x); + //Round up p + p = p.times(inv).div(x); + } + + // Calculate token balance + return _solveAnalyticalBalance(sum, inv, amp, nPowN, p); +} + +export function _poolDerivativesBPT( + amp: BigNumber, + balances: OldBigNumber[], + bptSupply: OldBigNumber, + tokenIndexIn: number, + is_first_derivative: boolean, + is_BPT_out: boolean, + wrt_out: boolean +): OldBigNumber { + const totalCoins = balances.length; + const D = _invariant(amp, balances); + let S = ZERO; + let D_P = D.div(totalCoins); + for (let i = 0; i < totalCoins; i++) { + if (i != tokenIndexIn) { + S = S.plus(balances[i]); + D_P = D_P.times(D).div(balances[i].times(totalCoins)); + } + } + const x = balances[tokenIndexIn]; + // amp is passed as an ethers bignumber while maths uses bignumber.js + const ampAdjusted = bnum(formatFixed(amp, 3)); + const alpha = ampAdjusted.times(totalCoins ** totalCoins); // = ampTimesNpowN + const beta = alpha.times(S); + const gamma = ONE.minus(alpha); + const partial_x = bnum(2) + .times(alpha) + .times(x) + .plus(beta) + .plus(gamma.times(D)); + const minus_partial_D = D_P.times(totalCoins + 1).minus(gamma.times(x)); + const partial_D = ZERO.minus(minus_partial_D); + let ans; + if (is_first_derivative) { + ans = partial_x.div(minus_partial_D).times(bptSupply).div(D); + } else { + const partial_xx = bnum(2).times(alpha); + const partial_xD = gamma; + const n_times_nplusone = totalCoins * (totalCoins + 1); + const partial_DD = ZERO.minus(D_P.times(n_times_nplusone).div(D)); + if (is_BPT_out) { + const term1 = partial_xx.times(partial_D).div(partial_x.pow(2)); + const term2 = bnum(2).times(partial_xD).div(partial_x); + const term3 = partial_DD.div(partial_D); + ans = term1.minus(term2).plus(term3).times(D).div(bptSupply); + if (wrt_out) { + const D_prime = ZERO.minus(partial_x.div(partial_D)); + ans = ans.div(D_prime).times(D).div(bptSupply); + } + } else { + ans = bnum(2) + .times(partial_xD) + .div(partial_D) + .minus(partial_DD.times(partial_x).div(partial_D.pow(2))) + .minus(partial_xx.div(partial_x)); + if (wrt_out) { + ans = ans + .times(partial_x) + .div(minus_partial_D) + .times(bptSupply) + .div(D); + } + } + } + return ans; } From ad0edb35e36ac403c77bb4c4c6822a543d455f4c Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 18 Jan 2022 10:41:13 +0000 Subject: [PATCH 15/43] Reorg new maths. --- src/pools/linearPool/linearMath.ts | 2 +- src/{poolsMath => pools/stablePool}/stable.ts | 12 +++++++++++- src/pools/stablePool/stableMath.ts | 8 ++++---- src/pools/weightedPool/weightedMath.ts | 2 +- src/poolsMath/README.md | 10 ---------- src/{poolsMath => utils}/basicOperations.ts | 0 test/poolsMathLinear.spec.ts | 2 +- test/poolsMathStable.spec.ts | 4 ++-- test/poolsMathWeighted.spec.ts | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) rename src/{poolsMath => pools/stablePool}/stable.ts (96%) delete mode 100644 src/poolsMath/README.md rename src/{poolsMath => utils}/basicOperations.ts (100%) diff --git a/src/pools/linearPool/linearMath.ts b/src/pools/linearPool/linearMath.ts index 3a646eaa..cc1c9296 100644 --- a/src/pools/linearPool/linearMath.ts +++ b/src/pools/linearPool/linearMath.ts @@ -1,7 +1,7 @@ import { BigNumber as linear } from '../../utils/bignumber'; import { bnum } from '../../utils/bignumber'; import { formatFixed } from '@ethersproject/bignumber'; -import { MathSol } from '../../poolsMath/basicOperations'; +import { MathSol } from '../../utils/basicOperations'; import { LinearPoolPairData } from './linearPool'; diff --git a/src/poolsMath/stable.ts b/src/pools/stablePool/stable.ts similarity index 96% rename from src/poolsMath/stable.ts rename to src/pools/stablePool/stable.ts index a0758eb1..16d1ceac 100644 --- a/src/poolsMath/stable.ts +++ b/src/pools/stablePool/stable.ts @@ -1,4 +1,14 @@ -import { MathSol, BZERO } from './basicOperations'; +/* +Swap outcome and "spot price after swap" formulas for weighted, stable and linear pools. +Amounts are represented using bigint type. Swap outcomes formulas should +match exactly those from smart contracts. + +Test cases are found in poolsMathWeighted.spec.ts, poolsMathStable.spec.ts poolsMathLinear.spec.ts. + +It is necessary to review whether to use MathSol operations or native +,-,\*,/ case by case. MathSol operations are able to reproduce overflows while native operations produce a much more readable code. For instance, for "spot price after swap" native operations +are preferred since in this case there are not smart contract analogs, amount limits are assumed to have been checked elsewhere, and some formulas get complicated, specially for stable pools. +*/ +import { MathSol, BZERO } from '../../utils/basicOperations'; const AMP_PRECISION = BigInt(1e3); diff --git a/src/pools/stablePool/stableMath.ts b/src/pools/stablePool/stableMath.ts index 0c172c1a..2fc94320 100644 --- a/src/pools/stablePool/stableMath.ts +++ b/src/pools/stablePool/stableMath.ts @@ -184,7 +184,7 @@ export function _tokenInForExactTokenOut( } //This function calcuates the analytical solution to find the balance required -export function _solveAnalyticalBalance( +function _solveAnalyticalBalance( sum: OldBigNumber, inv: OldBigNumber, amp: BigNumber, @@ -222,7 +222,7 @@ export function _solveAnalyticalBalance( //// These functions have been added exclusively for the SORv2 ////////////////////// -export function _poolDerivatives( +function _poolDerivatives( amp: BigNumber, balances: OldBigNumber[], tokenIndexIn: number, @@ -430,7 +430,7 @@ Flow of calculations: amountBPTOut -> newInvariant -> (amountInProportional, amountInAfterFee) -> amountInPercentageExcess -> amountIn */ -export function _tokenInForExactBPTOut( +function _tokenInForExactBPTOut( amount: OldBigNumber, poolPairData: StablePoolPairData ): OldBigNumber { @@ -514,7 +514,7 @@ function _getTokenBalanceGivenInvariantAndAllOtherBalances( return _solveAnalyticalBalance(sum, inv, amp, nPowN, p); } -export function _poolDerivativesBPT( +function _poolDerivativesBPT( amp: BigNumber, balances: OldBigNumber[], bptSupply: OldBigNumber, diff --git a/src/pools/weightedPool/weightedMath.ts b/src/pools/weightedPool/weightedMath.ts index 0f44352f..efde47a5 100644 --- a/src/pools/weightedPool/weightedMath.ts +++ b/src/pools/weightedPool/weightedMath.ts @@ -1,7 +1,7 @@ import { formatFixed } from '@ethersproject/bignumber'; import { BigNumber as OldBigNumber, bnum } from '../../utils/bignumber'; import { WeightedPoolPairData } from './weightedPool'; -import { MathSol } from '../../poolsMath/basicOperations'; +import { MathSol } from '../../utils/basicOperations'; // The following function are BigInt versions implemented by Sergio. // BigInt was requested from integrators as it is more efficient. diff --git a/src/poolsMath/README.md b/src/poolsMath/README.md deleted file mode 100644 index 1b72e6ff..00000000 --- a/src/poolsMath/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## Pools formulas using bigint type - -Swap outcome and "spot price after swap" formulas for weighted, stable and linear pools. -Amounts are represented using bigint type. Swap outcomes formulas should -match exactly those from smart contracts. - -Test cases are found in poolsMathWeighted.spec.ts, poolsMathStable.spec.ts poolsMathLinear.spec.ts. - -It is necessary to review whether to use MathSol operations or native +,-,\*,/ case by case. MathSol operations are able to reproduce overflows while native operations produce a much more readable code. For instance, for "spot price after swap" native operations -are preferred since in this case there are not smart contract analogs, amount limits are assumed to have been checked elsewhere, and some formulas get complicated, specially for stable pools. diff --git a/src/poolsMath/basicOperations.ts b/src/utils/basicOperations.ts similarity index 100% rename from src/poolsMath/basicOperations.ts rename to src/utils/basicOperations.ts diff --git a/test/poolsMathLinear.spec.ts b/test/poolsMathLinear.spec.ts index 8a7019b3..43ae2065 100644 --- a/test/poolsMathLinear.spec.ts +++ b/test/poolsMathLinear.spec.ts @@ -1,5 +1,5 @@ import * as linear from '../src/pools/linearPool/linearMath'; -import { MathSol } from '../src/poolsMath/basicOperations'; +import { MathSol } from '../src/utils/basicOperations'; import { assert } from 'chai'; describe('poolsMathLinear', function () { diff --git a/test/poolsMathStable.spec.ts b/test/poolsMathStable.spec.ts index 3bdfa6f5..d947c315 100644 --- a/test/poolsMathStable.spec.ts +++ b/test/poolsMathStable.spec.ts @@ -1,8 +1,8 @@ -import * as stable from '../src/poolsMath/stable'; +import * as stable from '../src/pools/stablePool/stable'; import * as SDK from '@georgeroman/balancer-v2-pools'; import { BigNumber as OldBigNumber, bnum } from '../src/utils/bignumber'; import { assert } from 'chai'; -import { MathSol } from '../src/poolsMath/basicOperations'; +import { MathSol } from '../src/utils/basicOperations'; describe('poolsMathStable: numeric functions using bigint', () => { context('stable pools', () => { diff --git a/test/poolsMathWeighted.spec.ts b/test/poolsMathWeighted.spec.ts index d7a6f164..a92e8762 100644 --- a/test/poolsMathWeighted.spec.ts +++ b/test/poolsMathWeighted.spec.ts @@ -2,7 +2,7 @@ import * as weighted from '../src/pools/weightedPool/weightedMath'; import * as SDK from '@georgeroman/balancer-v2-pools'; import { BigNumber as OldBigNumber, bnum } from '../src/utils/bignumber'; import { assert } from 'chai'; -import { MathSol } from '../src/poolsMath/basicOperations'; +import { MathSol } from '../src/utils/basicOperations'; describe('poolsMath: numeric functions using bigint', () => { context('weighted pools', () => { From f886bfdce097eab25bd9b4be0575294af813c208 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 18 Jan 2022 11:42:59 +0000 Subject: [PATCH 16/43] Add BigInt maths to StablePool. --- .../{stable.ts => stableMathBigInt.ts} | 0 src/pools/stablePool/stablePool.ts | 38 ++++++++++--------- test/poolsMathStable.spec.ts | 2 +- 3 files changed, 22 insertions(+), 18 deletions(-) rename src/pools/stablePool/{stable.ts => stableMathBigInt.ts} (100%) diff --git a/src/pools/stablePool/stable.ts b/src/pools/stablePool/stableMathBigInt.ts similarity index 100% rename from src/pools/stablePool/stable.ts rename to src/pools/stablePool/stableMathBigInt.ts diff --git a/src/pools/stablePool/stablePool.ts b/src/pools/stablePool/stablePool.ts index 46c7ed24..5bf3888b 100644 --- a/src/pools/stablePool/stablePool.ts +++ b/src/pools/stablePool/stablePool.ts @@ -17,7 +17,6 @@ import { SubgraphPoolBase, SubgraphToken, } from '../../types'; -import * as SDK from '@georgeroman/balancer-v2-pools'; import { _invariant, _spotPriceAfterSwapExactTokenInForTokenOut, @@ -25,6 +24,7 @@ import { _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, } from './stableMath'; +import { _calcOutGivenIn, _calcInGivenOut } from './stableMathBigInt'; type StablePoolToken = Pick; @@ -189,24 +189,26 @@ export class StablePool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtScaled = parseFixed(amount.dp(18).toString(), 18); - const amt = SDK.StableMath._calcOutGivenIn( - bnum(this.amp.toString()), + const amt = _calcOutGivenIn( + this.amp.toBigInt(), poolPairData.allBalancesScaled.map((balance) => - bnum(balance.toString()) + balance.toBigInt() ), poolPairData.tokenIndexIn, poolPairData.tokenIndexOut, - amtScaled, - bnum(poolPairData.swapFee.toString()) + amtScaled.toBigInt(), + poolPairData.swapFee.toBigInt() ); - // return normalised amount // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_DOWN mode (1) - return scale(amt, -18).dp(poolPairData.decimalsOut, 1); + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsOut, + 1 + ); } catch (err) { console.error(`_evmoutGivenIn: ${err.message}`); return ZERO; @@ -221,24 +223,26 @@ export class StablePool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); + const amtScaledBn = parseFixed(amount.dp(18).toString(), 18); - const amt = SDK.StableMath._calcInGivenOut( - bnum(this.amp.toString()), + const amt = _calcInGivenOut( + this.amp.toBigInt(), poolPairData.allBalancesScaled.map((balance) => - bnum(balance.toString()) + balance.toBigInt() ), poolPairData.tokenIndexIn, poolPairData.tokenIndexOut, - amtScaled, - bnum(poolPairData.swapFee.toString()) + amtScaledBn.toBigInt(), + poolPairData.swapFee.toBigInt() ); - // return normalised amount // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, // i.e. when using token with 2decimals 0.002 should be returned as 0 // Uses ROUND_UP mode (0) - return scale(amt, -18).dp(poolPairData.decimalsIn, 0); + return scale(bnum(amt.toString()), -18).dp( + poolPairData.decimalsIn, + 0 + ); } catch (err) { console.error(`_evminGivenOut: ${err.message}`); return ZERO; diff --git a/test/poolsMathStable.spec.ts b/test/poolsMathStable.spec.ts index d947c315..f2586fc8 100644 --- a/test/poolsMathStable.spec.ts +++ b/test/poolsMathStable.spec.ts @@ -1,4 +1,4 @@ -import * as stable from '../src/pools/stablePool/stable'; +import * as stable from '../src/pools/stablePool/stableMathBigInt'; import * as SDK from '@georgeroman/balancer-v2-pools'; import { BigNumber as OldBigNumber, bnum } from '../src/utils/bignumber'; import { assert } from 'chai'; From 0c6af26118862bf161899a0185db85a75a3c6047 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 19 Jan 2022 09:52:06 +0000 Subject: [PATCH 17/43] Fix broken metastable tests. --- test/metaStablePools.spec.ts | 68 ++++++++-------- test/testData/metaStablePools/multihop.json | 78 +++++++++++++------ test/testData/metaStablePools/singlePool.json | 34 ++++---- test/testData/weightedPools/singlePool.json | 1 + test/weightedPools.spec.ts | 9 +-- 5 files changed, 109 insertions(+), 81 deletions(-) diff --git a/test/metaStablePools.spec.ts b/test/metaStablePools.spec.ts index 55301e87..9655e1ce 100644 --- a/test/metaStablePools.spec.ts +++ b/test/metaStablePools.spec.ts @@ -4,14 +4,19 @@ import cloneDeep from 'lodash.clonedeep'; import { BigNumber, parseFixed } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; import { JsonRpcProvider } from '@ethersproject/providers'; -import { SOR } from '../src'; -import { SwapInfo, SwapTypes, PoolTypes, SubgraphPoolBase } from '../src/types'; +import { SOR, SwapInfo, SwapTypes, PoolTypes, SubgraphPoolBase } from '../src'; import { bnum } from '../src/utils/bignumber'; import { MetaStablePool, MetaStablePoolPairData, } from '../src/pools/metaStablePool/metaStablePool'; import { BAL, USDC, WETH } from './lib/constants'; +import poolsFromFile from './testData/metaStablePools/singlePool.json'; +import poolsFromFileMultihop from './testData/metaStablePools/multihop.json'; + +const randomETH = '0x42d6622dece394b54999fbd73d108123806f6a18'; +const baseToken = '0x0000000000000000000000000000000000011111'; +const metaToken = '0x0000000000000000000000000000000000022222'; const gasPrice = parseFixed('30', 9); const maxPools = 4; @@ -20,11 +25,6 @@ const provider = new JsonRpcProvider( `https://mainnet.infura.io/v3/${process.env.INFURA}` ); -// const BPT = '0xebfed10e11dc08fcda1af1fda146945e8710f22e'; -const stETH = '0xae7ab96520de3a18e5e111b5eaab095312d7fe84'; -const randomETH = '0x42d6622dece394b54999fbd73d108123806f6a18'; -// const PTSP = '0x5f304f6cf88dc76b414f301e05adfb5a429e8b67'; - async function getStableComparrison( stablePools: SubgraphPoolBase[], tokenIn: string, @@ -53,7 +53,6 @@ async function getStableComparrison( describe(`Tests for MetaStable Pools.`, () => { context('limit amounts', () => { it(`tests getLimitAmountSwap SwapExactIn`, async () => { - const poolsFromFile = require('./testData/metaStablePools/singlePool.json'); const pool = cloneDeep(poolsFromFile.metaStablePool[0]); const swapType = SwapTypes.SwapExactIn; @@ -96,7 +95,6 @@ describe(`Tests for MetaStable Pools.`, () => { parseFixed(pool.tokens[0].balance, 18), parseFixed(pool.tokens[1].balance, 18), ], - invariant: bnum(0), tokenIndexIn: 0, tokenIndexOut: 1, tokenInPriceRate: parseFixed(pool.tokens[0].priceRate, 18), @@ -110,7 +108,6 @@ describe(`Tests for MetaStable Pools.`, () => { }); it(`tests getLimitAmountSwap SwapExactOut`, async () => { - const poolsFromFile = require('./testData/metaStablePools/singlePool.json'); const pool = cloneDeep(poolsFromFile.metaStablePool[0]); const swapType = SwapTypes.SwapExactOut; @@ -147,7 +144,6 @@ describe(`Tests for MetaStable Pools.`, () => { amp: BigNumber.from(pool.amp), allBalances: [], allBalancesScaled: [], - invariant: bnum(0), tokenIndexIn: 0, tokenIndexOut: 1, tokenInPriceRate: parseFixed(pool.tokens[0].priceRate, 18), @@ -166,7 +162,6 @@ describe(`Tests for MetaStable Pools.`, () => { context('direct pool', () => { it(`Full Swap - swapExactIn No Route`, async () => { - const poolsFromFile = require('./testData/metaStablePools/singlePool.json'); const pools: SubgraphPoolBase[] = cloneDeep( poolsFromFile.metaStablePool ); @@ -193,7 +188,6 @@ describe(`Tests for MetaStable Pools.`, () => { }); it(`Full Swap - swapExactOut No Route`, async () => { - const poolsFromFile = require('./testData/metaStablePools/singlePool.json'); const pools: SubgraphPoolBase[] = cloneDeep( poolsFromFile.metaStablePool ); @@ -219,14 +213,13 @@ describe(`Tests for MetaStable Pools.`, () => { expect(swapInfo.swaps.length).eq(0); }); - it(`Full Swap - swapExactIn, Token ETH >Token Meta`, async () => { - const poolsFromFile = require('./testData/metaStablePools/singlePool.json'); + it(`Full Swap - swapExactIn, Base>Meta`, async () => { const pools: SubgraphPoolBase[] = cloneDeep( poolsFromFile.metaStablePool ); - const tokenIn = WETH.address; + const tokenIn = baseToken; const tokenInPriceRate = ONE; - const tokenOut = stETH; + const tokenOut = metaToken; const tokenOutPriceRate = ONE.div(2); const swapType = SwapTypes.SwapExactIn; const swapAmt = parseFixed('1', 18); // Would expect ~ 2 back @@ -243,9 +236,7 @@ describe(`Tests for MetaStable Pools.`, () => { swapAmt, { gasPrice, maxPools } ); - const stablePools: SubgraphPoolBase[] = poolsFromFile.stablePool; - const swapInfoStable = await getStableComparrison( stablePools, tokenIn, @@ -254,6 +245,7 @@ describe(`Tests for MetaStable Pools.`, () => { swapAmt.mul(tokenInPriceRate).div(ONE) ); + expect(swapInfo.returnAmount.gt(0)).to.be.true; expect(swapInfoStable.tokenAddresses).to.deep.eq( swapInfo.tokenAddresses ); @@ -288,14 +280,13 @@ describe(`Tests for MetaStable Pools.`, () => { }); }).timeout(10000); - it(`Full Swap - swapExactIn, Token Meta > Token ETH`, async () => { - const poolsFromFile = require('./testData/metaStablePools/singlePool.json'); + it(`Full Swap - swapExactIn, Meta>Base`, async () => { const pools: SubgraphPoolBase[] = cloneDeep( poolsFromFile.metaStablePool ); - const tokenIn = stETH; + const tokenIn = metaToken; const tokenInPriceRate = ONE.div(2); - const tokenOut = WETH.address; + const tokenOut = baseToken; const swapType = SwapTypes.SwapExactIn; const swapAmt = parseFixed('1', 18); // Would expect ~ 1 back @@ -323,6 +314,7 @@ describe(`Tests for MetaStable Pools.`, () => { swapAmt.mul(tokenInPriceRate).div(ONE) ); + expect(swapInfo.returnAmount.gt(0)).to.be.true; expect(swapInfoStable.tokenAddresses).to.deep.eq( swapInfo.tokenAddresses ); @@ -353,13 +345,12 @@ describe(`Tests for MetaStable Pools.`, () => { }); }).timeout(10000); - it(`Full Swap - swapExactOut, Token ETH >Token Meta`, async () => { - const poolsFromFile = require('./testData/metaStablePools/singlePool.json'); + it(`Full Swap - swapExactOut, Base>Meta`, async () => { const pools: SubgraphPoolBase[] = cloneDeep( poolsFromFile.metaStablePool ); - const tokenIn = WETH.address; - const tokenOut = stETH; + const tokenIn = baseToken; + const tokenOut = metaToken; const tokenOutPriceRate = ONE.div(2); const swapType = SwapTypes.SwapExactOut; const swapAmt = parseFixed('2', 18); // Would expect ~ 1 as input @@ -387,6 +378,7 @@ describe(`Tests for MetaStable Pools.`, () => { swapAmt.mul(tokenOutPriceRate).div(ONE) ); + expect(swapInfo.returnAmount.gt(0)).to.be.true; expect(swapInfoStable.tokenAddresses).to.deep.eq( swapInfo.tokenAddresses ); @@ -417,14 +409,13 @@ describe(`Tests for MetaStable Pools.`, () => { }); }).timeout(10000); - it(`Full Swap - swapExactOut, Token Meta > Token ETH`, async () => { - const poolsFromFile = require('./testData/metaStablePools/singlePool.json'); + it(`Full Swap - swapExactOut, Meta>Base`, async () => { const pools: SubgraphPoolBase[] = cloneDeep( poolsFromFile.metaStablePool ); - const tokenIn = stETH; + const tokenIn = metaToken; const tokenInPriceRate = ONE.div(2); - const tokenOut = WETH.address; + const tokenOut = baseToken; const tokenOutPriceRate = ONE; const swapType = SwapTypes.SwapExactOut; const swapAmt = parseFixed('2', 18); // Would expect ~ 4 as input @@ -452,6 +443,7 @@ describe(`Tests for MetaStable Pools.`, () => { swapAmt.mul(tokenOutPriceRate).div(ONE) ); + expect(swapInfo.returnAmount.gt(0)).to.be.true; expect(swapInfoStable.tokenAddresses).to.deep.eq( swapInfo.tokenAddresses ); @@ -489,9 +481,8 @@ describe(`Tests for MetaStable Pools.`, () => { context('multihop', () => { it(`Full Swap - swapExactIn, Token>Token`, async () => { // With meta token as hop the result in/out should be same as a normal stable pool - const poolsFromFile = require('./testData/metaStablePools/multihop.json'); const pools: SubgraphPoolBase[] = cloneDeep( - poolsFromFile.metaStablePools + poolsFromFileMultihop.metaStablePools ); const tokenIn = WETH.address; const tokenInPriceRate = ONE; @@ -513,7 +504,8 @@ describe(`Tests for MetaStable Pools.`, () => { { gasPrice, maxPools } ); - const stablePools: SubgraphPoolBase[] = poolsFromFile.stablePools; + const stablePools: SubgraphPoolBase[] = + poolsFromFileMultihop.stablePools; // Same as stable with const swapInfoStable = await getStableComparrison( stablePools, @@ -523,6 +515,7 @@ describe(`Tests for MetaStable Pools.`, () => { swapAmt.mul(tokenInPriceRate).div(ONE) ); + expect(swapInfo.returnAmount.gt(0)).to.be.true; expect(swapInfoStable.tokenAddresses).to.deep.eq( swapInfo.tokenAddresses ); @@ -559,9 +552,8 @@ describe(`Tests for MetaStable Pools.`, () => { it(`Full Swap - swapExactOut, Token>Token`, async () => { // With meta token as hop the result in/out should be same as a normal stable pool - const poolsFromFile = require('./testData/metaStablePools/multihop.json'); const pools: SubgraphPoolBase[] = cloneDeep( - poolsFromFile.metaStablePools + poolsFromFileMultihop.metaStablePools ); const tokenIn = WETH.address; const tokenInPriceRate = ONE; @@ -583,7 +575,8 @@ describe(`Tests for MetaStable Pools.`, () => { { gasPrice, maxPools } ); - const stablePools: SubgraphPoolBase[] = poolsFromFile.stablePools; + const stablePools: SubgraphPoolBase[] = + poolsFromFileMultihop.stablePools; // Same as stable with const swapInfoStable = await getStableComparrison( @@ -594,6 +587,7 @@ describe(`Tests for MetaStable Pools.`, () => { swapAmt.mul(tokenInPriceRate).div(ONE) ); + expect(swapInfo.returnAmount.gt(0)).to.be.true; expect(swapInfoStable.tokenAddresses).to.deep.eq( swapInfo.tokenAddresses ); diff --git a/test/testData/metaStablePools/multihop.json b/test/testData/metaStablePools/multihop.json index ceaaa46b..16cedff4 100644 --- a/test/testData/metaStablePools/multihop.json +++ b/test/testData/metaStablePools/multihop.json @@ -14,6 +14,7 @@ "id": "0xebfed10e11dc08fcda1af1fda146945e8710f22e00000000000000000000007f", "address": "0xebfed10e11dc08fcda1af1fda146945e8710f22e", "swapFee": "0.0003", + "swapEnabled": true, "amp": "10", "tokens": [ { @@ -21,14 +22,16 @@ "balance": "1000", "decimals": 18, "symbol": "WETH", - "priceRate": "1" + "priceRate": "1", + "weight": null }, { "address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", "balance": "4000", "decimals": 18, "symbol": "stETH", - "priceRate": "0.25" + "priceRate": "0.25", + "weight": null } ], "tokensList": [ @@ -43,20 +46,23 @@ "address": "0xa6f548df93de924d73be7d25dc02554c6bd66db5", "swapFee": "0.002", "amp": "10", + "swapEnabled": true, "tokens": [ { "address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", "balance": "8000", "decimals": 18, "symbol": "stETH", - "priceRate": "0.25" + "priceRate": "0.25", + "weight": null }, { "address": "0x42d6622dece394b54999fbd73d108123806f6a18", "balance": "2000", "decimals": 18, "symbol": "randomETH", - "priceRate": "1" + "priceRate": "1", + "weight": null } ], "tokensList": [ @@ -73,18 +79,23 @@ "address": "0xebfed10e11dc08fcda1af1fda146945e8710f22e", "swapFee": "0.0003", "amp": "10", + "swapEnabled": true, "tokens": [ { "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "balance": "1000", "decimals": 18, - "symbol": "WETH" + "symbol": "WETH", + "weight": null, + "priceRate": "1" }, { "address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", "balance": "1000", "decimals": 18, - "symbol": "stETH" + "symbol": "stETH", + "priceRate": "1", + "weight": null } ], "tokensList": [ @@ -99,18 +110,23 @@ "address": "0xa6f548df93de924d73be7d25dc02554c6bd66db5", "swapFee": "0.002", "amp": "10", + "swapEnabled": true, "tokens": [ { "address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", "balance": "2000", "decimals": 18, - "symbol": "stETH" + "symbol": "stETH", + "priceRate": "1", + "weight": null }, { "address": "0x42d6622dece394b54999fbd73d108123806f6a18", "balance": "2000", "decimals": 18, - "symbol": "randomETH" + "symbol": "randomETH", + "priceRate": "1", + "weight": null } ], "tokensList": [ @@ -128,20 +144,23 @@ "id": "0xf216cc947c5ffc2045db16bc98b2535f0689cde80002000000000000000000fa", "poolType": "MetaStable", "swapFee": "0.001", + "swapEnabled": true, "tokens": [ { "address": "0x5f304f6cf88dc76b414f301e05adfb5a429e8b67", "balance": "20246.422732507587057905", "decimals": 18, "priceRate": "1.00239199558952863", - "symbol": "PTSP" + "symbol": "PTSP", + "weight": null }, { "address": "0xc2569dd7d0fd715b054fbf16e75b001e5c0c1115", "balance": "19803.578738", "decimals": 6, "priceRate": "1", - "symbol": "USDC" + "symbol": "USDC", + "weight": null } ], "tokensList": [ @@ -156,27 +175,31 @@ "id": "0x5f304f6cf88dc76b414f301e05adfb5a429e8b670000000000000000000000f4", "poolType": "Stable", "swapFee": "0.03", + "swapEnabled": true, "tokens": [ { "address": "0x04df6e4121c27713ed22341e7c7df330f56f289b", "balance": "11044.697773108789252775", "decimals": 18, - "priceRate": null, - "symbol": "DAI" + "priceRate": "1", + "symbol": "DAI", + "weight": null }, { "address": "0xc2569dd7d0fd715b054fbf16e75b001e5c0c1115", "balance": "10981.662702", "decimals": 6, - "priceRate": null, - "symbol": "USDC" + "priceRate": "1", + "symbol": "USDC", + "weight": null }, { "address": "0xcc08220af469192c53295fdd34cfb8df29aa17ab", "balance": "11010", "decimals": 6, - "priceRate": null, - "symbol": "'USDT'" + "priceRate": "1", + "symbol": "'USDT'", + "weight": null } ], "tokensList": [ @@ -194,20 +217,23 @@ "id": "0xf216cc947c5ffc2045db16bc98b2535f0689cde80002000000000000000000fa", "poolType": "MetaStable", "swapFee": "0.001", + "swapEnabled": true, "tokens": [ { "address": "0x5f304f6cf88dc76b414f301e05adfb5a429e8b67", "balance": "20246.422732507587057905", "decimals": 18, "priceRate": "1.00239199558952863", - "symbol": "PTSP" + "symbol": "PTSP", + "weight": null }, { "address": "0xc2569dd7d0fd715b054fbf16e75b001e5c0c1115", "balance": "19803.578738", "decimals": 6, "priceRate": "1", - "symbol": "USDC" + "symbol": "USDC", + "weight": null } ], "tokensList": [ @@ -222,27 +248,31 @@ "id": "0x6b15a01b5d46a5321b627bd7deef1af57bc629070000000000000000000000d4", "poolType": "Stable", "swapFee": "0.0004", + "swapEnabled": true, "tokens": [ { "address": "0x04df6e4121c27713ed22341e7c7df330f56f289b", "balance": "1010815464.031449904413500383", "decimals": 18, - "priceRate": null, - "symbol": "DAI" + "priceRate": "1", + "symbol": "DAI", + "weight": null }, { "address": "0xc2569dd7d0fd715b054fbf16e75b001e5c0c1115", "balance": "592062193.53414", "decimals": 6, - "priceRate": null, - "symbol": "USDC" + "priceRate": "1", + "symbol": "USDC", + "weight": null }, { "address": "0xcc08220af469192c53295fdd34cfb8df29aa17ab", "balance": "141394.922161", "decimals": 6, - "priceRate": null, - "symbol": "'USDT'" + "priceRate": "1", + "symbol": "'USDT'", + "weight": null } ], "tokensList": [ diff --git a/test/testData/metaStablePools/singlePool.json b/test/testData/metaStablePools/singlePool.json index 838bdfd0..a415a9cc 100644 --- a/test/testData/metaStablePools/singlePool.json +++ b/test/testData/metaStablePools/singlePool.json @@ -15,25 +15,28 @@ "address": "0xebfed10e11dc08fcda1af1fda146945e8710f22e", "swapFee": "0.0004", "amp": "10", + "swapEnabled": true, "tokens": [ { - "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "address": "0x0000000000000000000000000000000000011111", "balance": "10", "decimals": 18, "symbol": "WETH", - "priceRate": "1" + "priceRate": "1", + "weight": null }, { - "address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "address": "0x0000000000000000000000000000000000022222", "balance": "20", "decimals": 18, - "symbol": "stETH", - "priceRate": "0.5" + "symbol": "wstETH", + "priceRate": "0.5", + "weight": null } ], "tokensList": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xae7ab96520de3a18e5e111b5eaab095312d7fe84" + "0x0000000000000000000000000000000000011111", + "0x0000000000000000000000000000000000022222" ], "totalShares": "10", "poolType": "MetaStable" @@ -45,23 +48,28 @@ "address": "0xebfed10e11dc08fcda1af1fda146945e8710f22e", "swapFee": "0.0004", "amp": "10", + "swapEnabled": true, "tokens": [ { - "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "address": "0x0000000000000000000000000000000000011111", "balance": "10", "decimals": 18, - "symbol": "WETH" + "symbol": "WETH", + "weight": null, + "priceRate": "1" }, { - "address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "address": "0x0000000000000000000000000000000000022222", "balance": "10", "decimals": 18, - "symbol": "stETH" + "symbol": "wstETH", + "weight": null, + "priceRate": "1" } ], "tokensList": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xae7ab96520de3a18e5e111b5eaab095312d7fe84" + "0x0000000000000000000000000000000000011111", + "0x0000000000000000000000000000000000022222" ], "totalShares": "10", "poolType": "Stable" diff --git a/test/testData/weightedPools/singlePool.json b/test/testData/weightedPools/singlePool.json index f451da82..ac9ab422 100644 --- a/test/testData/weightedPools/singlePool.json +++ b/test/testData/weightedPools/singlePool.json @@ -14,6 +14,7 @@ "id": "0xebfed10e11dc08fcda1af1fda146945e8710f22e00000000000000000000007f", "address": "0xebfed10e11dc08fcda1af1fda146945e8710f22e", "swapFee": "0.002", + "swapEnabled": true, "tokens": [ { "address": "0x6b175474e89094c44da98b954eedeac495271d0f", diff --git a/test/weightedPools.spec.ts b/test/weightedPools.spec.ts index 2c51c9c5..9f2098ce 100644 --- a/test/weightedPools.spec.ts +++ b/test/weightedPools.spec.ts @@ -1,20 +1,18 @@ require('dotenv').config(); import { expect } from 'chai'; -import { SwapTypes, PoolTypes, SubgraphPoolBase } from '../src/types'; +import { SwapTypes, PoolTypes } from '../src/types'; import { bnum } from '../src/utils/bignumber'; import { WeightedPool, WeightedPoolPairData, } from '../src/pools/weightedPool/weightedPool'; import { parseFixed } from '@ethersproject/bignumber'; +import poolsFromFile from './testData/weightedPools/singlePool.json'; // npx mocha -r ts-node/register test/weightedPools.spec.ts describe(`Tests for Weighted Pools.`, () => { context('limit amounts', () => { it(`tests getLimitAmountSwap SwapExactIn`, async () => { - const poolsFromFile: { - pools: SubgraphPoolBase[]; - } = require('./testData/weightedPools/singlePool.json'); const pool = poolsFromFile.pools[0]; const swapType = SwapTypes.SwapExactIn; @@ -51,9 +49,6 @@ describe(`Tests for Weighted Pools.`, () => { }); it(`tests getLimitAmountSwap SwapExactOut`, async () => { - const poolsFromFile: { - pools: SubgraphPoolBase[]; - } = require('./testData/weightedPools/singlePool.json'); const pool = poolsFromFile.pools[0]; const swapType = SwapTypes.SwapExactOut; From c6be47e7e248c08eae3a751ac7bd44dda4e176d1 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 19 Jan 2022 09:53:29 +0000 Subject: [PATCH 18/43] Remove unused PoolPairData. --- src/pools/phantomStablePool/phantomStablePool.ts | 2 -- src/pools/stablePool/stablePool.ts | 11 +++-------- test/stablePools.spec.ts | 2 -- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/pools/phantomStablePool/phantomStablePool.ts b/src/pools/phantomStablePool/phantomStablePool.ts index 8bbc24c2..d5423a7c 100644 --- a/src/pools/phantomStablePool/phantomStablePool.ts +++ b/src/pools/phantomStablePool/phantomStablePool.ts @@ -153,7 +153,6 @@ export class PhantomStablePool implements PoolBase { } const bptIndex = this.tokensList.indexOf(this.address); - const inv = phantomStableMath._invariant(this.amp, allBalances); // VirtualBPTSupply must be used for the maths // TO DO - SG should be updated to so that totalShares should return VirtualSupply @@ -171,7 +170,6 @@ export class PhantomStablePool implements PoolBase { tokenOut: tokenOut, balanceIn: parseFixed(balanceIn, decimalsIn), balanceOut: parseFixed(balanceOut, decimalsOut), - invariant: inv, swapFee: this.swapFee, allBalances, allBalancesScaled, diff --git a/src/pools/stablePool/stablePool.ts b/src/pools/stablePool/stablePool.ts index 5bf3888b..345b136c 100644 --- a/src/pools/stablePool/stablePool.ts +++ b/src/pools/stablePool/stablePool.ts @@ -18,7 +18,6 @@ import { SubgraphToken, } from '../../types'; import { - _invariant, _spotPriceAfterSwapExactTokenInForTokenOut, _spotPriceAfterSwapTokenInForExactTokenOut, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, @@ -31,7 +30,6 @@ type StablePoolToken = Pick; export type StablePoolPairData = PoolPairBase & { allBalances: OldBigNumber[]; allBalancesScaled: BigNumber[]; // EVM Maths uses everything in 1e18 upscaled format and this avoids repeated scaling - invariant: OldBigNumber; amp: BigNumber; tokenIndexIn: number; tokenIndexOut: number; @@ -110,8 +108,6 @@ export class StablePool implements PoolBase { parseFixed(balance, 18) ); - const inv = _invariant(this.amp, allBalances); - const poolPairData: StablePoolPairData = { id: this.id, address: this.address, @@ -120,10 +116,9 @@ export class StablePool implements PoolBase { tokenOut: tokenOut, balanceIn: parseFixed(balanceIn, decimalsIn), balanceOut: parseFixed(balanceOut, decimalsOut), - invariant: inv, swapFee: this.swapFee, allBalances, - allBalancesScaled, + allBalancesScaled, // TO DO - Change to BigInt?? amp: this.amp, tokenIndexIn: tokenIndexIn, tokenIndexOut: tokenIndexOut, @@ -223,7 +218,7 @@ export class StablePool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaledBn = parseFixed(amount.dp(18).toString(), 18); + const amtScaled = parseFixed(amount.dp(18).toString(), 18); const amt = _calcInGivenOut( this.amp.toBigInt(), @@ -232,7 +227,7 @@ export class StablePool implements PoolBase { ), poolPairData.tokenIndexIn, poolPairData.tokenIndexOut, - amtScaledBn.toBigInt(), + amtScaled.toBigInt(), poolPairData.swapFee.toBigInt() ); // return normalised amount diff --git a/test/stablePools.spec.ts b/test/stablePools.spec.ts index 02f3f3d2..5bbb686e 100644 --- a/test/stablePools.spec.ts +++ b/test/stablePools.spec.ts @@ -62,7 +62,6 @@ describe(`Tests for Stable Pools.`, () => { parseFixed(pool.tokens[0].balance, 18), parseFixed(pool.tokens[1].balance, 18), ], - invariant: bnum(0), tokenIndexIn: 0, tokenIndexOut: 1, }; @@ -105,7 +104,6 @@ describe(`Tests for Stable Pools.`, () => { amp: BigNumber.from(pool.amp as string), allBalances: [], allBalancesScaled: [], - invariant: bnum(0), tokenIndexIn: 0, tokenIndexOut: 1, }; From d84b72997b6b46594ccc0b6ca575c62c54aa09d5 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 19 Jan 2022 10:10:45 +0000 Subject: [PATCH 19/43] Update metaStable with BigInt maths. --- src/pools/metaStablePool/metaStablePool.ts | 77 +++++++++------------- 1 file changed, 32 insertions(+), 45 deletions(-) diff --git a/src/pools/metaStablePool/metaStablePool.ts b/src/pools/metaStablePool/metaStablePool.ts index ffe3e52c..1abcecbc 100644 --- a/src/pools/metaStablePool/metaStablePool.ts +++ b/src/pools/metaStablePool/metaStablePool.ts @@ -1,12 +1,7 @@ import { BigNumber, formatFixed, parseFixed } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; import { isSameAddress } from '../../utils'; -import { - BigNumber as OldBigNumber, - bnum, - scale, - ZERO, -} from '../../utils/bignumber'; +import { BigNumber as OldBigNumber, bnum, ZERO } from '../../utils/bignumber'; import { PoolBase, PoolTypes, @@ -16,14 +11,16 @@ import { SubgraphToken, } from '../../types'; import { getAddress } from '@ethersproject/address'; -import * as SDK from '@georgeroman/balancer-v2-pools'; import { - _invariant, _spotPriceAfterSwapExactTokenInForTokenOut, _spotPriceAfterSwapTokenInForExactTokenOut, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, } from '../stablePool/stableMath'; +import { + _calcOutGivenIn, + _calcInGivenOut, +} from '../stablePool/stableMathBigInt'; import { StablePoolPairData } from 'pools/stablePool/stablePool'; type MetaStablePoolToken = Pick< @@ -124,8 +121,6 @@ export class MetaStablePool implements PoolBase { parseFixed(balance, 18).mul(parseFixed(priceRate, 18)).div(ONE) ); - const inv = _invariant(this.amp, allBalances); - const poolPairData: MetaStablePoolPairData = { id: this.id, address: this.address, @@ -134,7 +129,6 @@ export class MetaStablePool implements PoolBase { tokenOut: tokenOut, balanceIn: parseFixed(balanceIn, decimalsIn), balanceOut: parseFixed(balanceOut, decimalsOut), - invariant: inv, swapFee: this.swapFee, allBalances, allBalancesScaled, @@ -210,29 +204,26 @@ export class MetaStablePool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); - const amountConverted = amtScaled.times( - formatFixed(poolPairData.tokenInPriceRate, 18) - ); + const amountConvertedEvm = parseFixed(amount.dp(18).toString(), 18) + .mul(poolPairData.tokenInPriceRate) + .div(ONE); - const amt = SDK.StableMath._calcOutGivenIn( - bnum(this.amp.toString()), + const returnEvm = _calcOutGivenIn( + this.amp.toBigInt(), poolPairData.allBalancesScaled.map((balance) => - bnum(balance.toString()) + balance.toBigInt() ), poolPairData.tokenIndexIn, poolPairData.tokenIndexOut, - amountConverted, - bnum(poolPairData.swapFee.toString()) + amountConvertedEvm.toBigInt(), + poolPairData.swapFee.toBigInt() ); - // return normalised amount - // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, - // i.e. when using token with 2decimals 0.002 should be returned as 0 - // Uses ROUND_DOWN mode (1) - return scale( - amt.div(formatFixed(poolPairData.tokenOutPriceRate, 18)), - -18 - ).dp(poolPairData.decimalsOut, 1); + + const returnEvmWithRate = BigNumber.from(returnEvm) + .mul(ONE) + .div(poolPairData.tokenOutPriceRate); + + return bnum(formatFixed(returnEvmWithRate, 18)); } catch (err) { console.error(`_evmoutGivenIn: ${err.message}`); return ZERO; @@ -247,30 +238,26 @@ export class MetaStablePool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); - const amountConverted = amtScaled.times( - formatFixed(poolPairData.tokenOutPriceRate, 18) - ); + const amountConvertedEvm = parseFixed(amount.dp(18).toString(), 18) + .mul(poolPairData.tokenOutPriceRate) + .div(ONE); - const amt = SDK.StableMath._calcInGivenOut( - bnum(this.amp.toString()), + const returnEvm = _calcInGivenOut( + this.amp.toBigInt(), poolPairData.allBalancesScaled.map((balance) => - bnum(balance.toString()) + balance.toBigInt() ), poolPairData.tokenIndexIn, poolPairData.tokenIndexOut, - amountConverted, - bnum(poolPairData.swapFee.toString()) + amountConvertedEvm.toBigInt(), + poolPairData.swapFee.toBigInt() ); - // return normalised amount - // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, - // i.e. when using token with 2decimals 0.002 should be returned as 0 - // Uses ROUND_UP mode (0) - return scale( - amt.div(formatFixed(poolPairData.tokenInPriceRate, 18)), - -18 - ).dp(poolPairData.decimalsIn, 0); + const returnEvmWithRate = BigNumber.from(returnEvm) + .mul(ONE) + .div(poolPairData.tokenInPriceRate); + + return bnum(formatFixed(returnEvmWithRate, 18)); } catch (err) { console.error(`_evminGivenOut: ${err.message}`); return ZERO; From b5bdf00bd467b07217fe9b30bacb7284f72108d7 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 19 Jan 2022 10:24:21 +0000 Subject: [PATCH 20/43] Fix missing swapEnabled. --- test/lbp.spec.ts | 14 ++------ test/lido.spec.ts | 4 ++- test/testData/lido/staticPools.json | 32 +++++-------------- .../testData/testPools/gusdBugSinglePath.json | 2 ++ 4 files changed, 15 insertions(+), 37 deletions(-) diff --git a/test/lbp.spec.ts b/test/lbp.spec.ts index 1602603e..a139042b 100644 --- a/test/lbp.spec.ts +++ b/test/lbp.spec.ts @@ -2,9 +2,10 @@ require('dotenv').config(); import { expect } from 'chai'; import { JsonRpcProvider } from '@ethersproject/providers'; import { SOR } from '../src'; -import { SwapInfo, SwapTypes, SubgraphPoolBase } from '../src/types'; +import { SwapInfo, SwapTypes } from '../src/types'; import { parseFixed } from '@ethersproject/bignumber'; import { DAI, USDC } from './lib/constants'; +import poolsFromFile from './testData/lbpPools/singlePool.json'; const gasPrice = parseFixed('30', 9); const maxPools = 4; @@ -13,10 +14,6 @@ const provider = new JsonRpcProvider( `https://mainnet.infura.io/v3/${process.env.INFURA}` ); -// const USDT = '0xdac17f958d2ee523a2206206994597c13d831ec7'; -// const BPT = '0xebfed10e11dc08fcda1af1fda146945e8710f22e'; -// const RANDOM = '0x1456688345527be1f37e9e627da0837d6f08c925'; - // npx mocha -r ts-node/register test/lbp.spec.ts describe(`Tests for LBP Pools.`, () => { /* @@ -26,11 +23,7 @@ describe(`Tests for LBP Pools.`, () => { */ context('lbp pool', () => { it(`Full Swap - swapExactIn, Swaps not paused so should have route`, async () => { - const poolsFromFile: { - pools: SubgraphPoolBase[]; - } = require('./testData/lbpPools/singlePool.json'); const pools = poolsFromFile.pools; - const tokenIn = DAI.address; const tokenOut = USDC.address; const swapType = SwapTypes.SwapExactIn; @@ -55,9 +48,6 @@ describe(`Tests for LBP Pools.`, () => { }); it(`Full Swap - swapExactIn, Swaps paused so should have no route`, async () => { - const poolsFromFile: { - pools: SubgraphPoolBase[]; - } = require('./testData/lbpPools/singlePool.json'); const pools = poolsFromFile.pools; // Set paused to true pools[0].swapEnabled = false; diff --git a/test/lido.spec.ts b/test/lido.spec.ts index d4aacc9f..f653503d 100644 --- a/test/lido.spec.ts +++ b/test/lido.spec.ts @@ -24,11 +24,13 @@ const poolStaBal = Lido.StaticPools.staBal[chainId]; const poolWethDai = Lido.StaticPools.wethDai[chainId]; const poolLido = Lido.StaticPools.wstEthWeth[chainId]; +import poolsFromFile from './testData/lido/staticPools.json'; + const { pools, }: { pools: SubgraphPoolBase[]; -} = require('./testData/lido/staticPools.json'); +} = poolsFromFile; // npx mocha -r ts-node/register test/lido.spec.ts describe(`Tests for Lido USD routes.`, () => { diff --git a/test/testData/lido/staticPools.json b/test/testData/lido/staticPools.json index e9f10891..c0ad95c1 100644 --- a/test/testData/lido/staticPools.json +++ b/test/testData/lido/staticPools.json @@ -13,20 +13,16 @@ { "address": "0x06df3b2bbb68adc8b0e302443692037ed9f91b42", "amp": "200", - "baseToken": null, - "expiryTime": null, "id": "0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063", "poolType": "Stable", - "principalToken": null, "swapEnabled": true, "swapFee": "0.0005", - "swapEnabled": true, "tokens": [ { "address": "0x6b175474e89094c44da98b954eedeac495271d0f", "balance": "45795282.291209453240330327", "decimals": 18, - "priceRate": null, + "priceRate": "1", "symbol": "DAI", "weight": null }, @@ -34,7 +30,7 @@ "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "balance": "51415452.783274", "decimals": 6, - "priceRate": null, + "priceRate": "1", "symbol": "USDC", "weight": null }, @@ -42,7 +38,7 @@ "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", "balance": "48861685.530712", "decimals": 6, - "priceRate": null, + "priceRate": "1", "symbol": "USDT", "weight": null } @@ -53,26 +49,20 @@ "0xdac17f958d2ee523a2206206994597c13d831ec7" ], "totalShares": "145747600.211911429365644832", - "totalWeight": "0", - "unitSeconds": null + "totalWeight": "0" }, { "address": "0x0b09dea16768f0799065c475be02919503cb2a35", - "amp": null, - "baseToken": null, - "expiryTime": null, "id": "0x0b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a", "poolType": "Weighted", - "principalToken": null, "swapEnabled": true, "swapFee": "0.0033", - "swapEnabled": true, "tokens": [ { "address": "0x6b175474e89094c44da98b954eedeac495271d0f", "balance": "41476253.022693257948597201", "decimals": 18, - "priceRate": null, + "priceRate": "1", "symbol": "DAI", "weight": "0.4" }, @@ -80,7 +70,7 @@ "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "balance": "19671.814946560840959771", "decimals": 18, - "priceRate": null, + "priceRate": "1", "symbol": "WETH", "weight": "0.6" } @@ -90,20 +80,15 @@ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" ], "totalShares": "819919.275136494180654341", - "totalWeight": "1", - "unitSeconds": null + "totalWeight": "1" }, { "address": "0xe08590bde837eb9b2d42aa1196469d6e08fe96ec", "amp": "10", - "baseToken": null, - "expiryTime": null, "id": "TODO", "poolType": "MetaStable", - "principalToken": null, "swapEnabled": true, "swapFee": "0.0001", - "swapEnabled": true, "tokens": [ { "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", @@ -127,8 +112,7 @@ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" ], "totalShares": "322.491584799342983787", - "totalWeight": "0", - "unitSeconds": null + "totalWeight": "0" } ] } diff --git a/test/testData/testPools/gusdBugSinglePath.json b/test/testData/testPools/gusdBugSinglePath.json index d832cfff..b89ab207 100644 --- a/test/testData/testPools/gusdBugSinglePath.json +++ b/test/testData/testPools/gusdBugSinglePath.json @@ -19,6 +19,7 @@ "poolType": "Stable", "principalToken": null, "swapFee": "0.005", + "swapEnabled": true, "tokens": [ { "address": "0x04df6e4121c27713ed22341e7c7df330f56f289b", @@ -71,6 +72,7 @@ "poolType": "Weighted", "principalToken": null, "swapFee": "0.03", + "swapEnabled": true, "tokens": [ { "address": "0x22ee6c3b011facc530dd01fe94c58919344d6db5", From 33418ceba04bef753fb908bc8a42ccc49ec6548d Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 19 Jan 2022 12:14:53 +0000 Subject: [PATCH 21/43] Updated PhantomStable to use BigInt. --- .../phantomStablePool/phantomStablePool.ts | 175 ++++++++---------- 1 file changed, 79 insertions(+), 96 deletions(-) diff --git a/src/pools/phantomStablePool/phantomStablePool.ts b/src/pools/phantomStablePool/phantomStablePool.ts index d5423a7c..5519ba96 100644 --- a/src/pools/phantomStablePool/phantomStablePool.ts +++ b/src/pools/phantomStablePool/phantomStablePool.ts @@ -1,12 +1,7 @@ import { BigNumber, formatFixed, parseFixed } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; import { isSameAddress } from '../../utils'; -import { - BigNumber as OldBigNumber, - bnum, - scale, - ZERO, -} from '../../utils/bignumber'; +import { BigNumber as OldBigNumber, bnum, ZERO } from '../../utils/bignumber'; import { PoolBase, PoolTypes, @@ -16,7 +11,14 @@ import { SubgraphToken, } from '../../types'; import { getAddress } from '@ethersproject/address'; -import * as SDK from '@georgeroman/balancer-v2-pools'; +import { + _calcBptOutGivenExactTokensIn, + _calcTokenOutGivenExactBptIn, + _calcOutGivenIn, + _calcTokenInGivenExactBptOut, + _calcBptInGivenExactTokensOut, + _calcInGivenOut, +} from '../stablePool/stableMathBigInt'; import * as phantomStableMath from '../phantomStablePool/phantomStableMath'; import { MetaStablePoolPairData } from '../metaStablePool/metaStablePool'; import cloneDeep from 'lodash.clonedeep'; @@ -243,67 +245,57 @@ export class PhantomStablePool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); - // In Phantom Pools every time there is a swap (token per token, bpt per token or token per bpt), we substract the fee from the amount in - const amtWithFee = this.subtractSwapFeeAmount( - BigNumber.from(amtScaled.toString()), + const amtWithFeeEvm = this.subtractSwapFeeAmount( + parseFixed(amount.dp(18).toString(), 18), poolPairData.swapFee ); - const amountConverted = bnum(amtWithFee.toString()) - .times(formatFixed(poolPairData.tokenInPriceRate, 18)) - .dp(0); + const amountConvertedEvm = amtWithFeeEvm + .mul(poolPairData.tokenInPriceRate) + .div(ONE); - let amt: OldBigNumber; + let returnEvm: BigInt; if (poolPairData.pairType === PairTypes.TokenToBpt) { - const amountsIn = Array( + const amountsInBigInt = Array( poolPairData.allBalancesScaled.length - ).fill(ZERO); - amountsIn[poolPairData.tokenIndexIn] = bnum( - amountConverted.toString() - ); - - amt = SDK.StableMath._calcBptOutGivenExactTokensIn( - bnum(this.amp.toString()), - poolPairData.allBalancesScaled.map((b) => - bnum(b.toString()) - ), - amountsIn, - bnum(poolPairData.virtualBptSupply.toString()), - ZERO // Fee is handled above + ).fill(BigInt(0)); + amountsInBigInt[poolPairData.tokenIndexIn] = + amountConvertedEvm.toBigInt(); + + returnEvm = _calcBptOutGivenExactTokensIn( + this.amp.toBigInt(), + poolPairData.allBalancesScaled.map((b) => b.toBigInt()), + amountsInBigInt, + poolPairData.virtualBptSupply.toBigInt(), + BigInt(0) ); } else if (poolPairData.pairType === PairTypes.BptToToken) { - amt = SDK.StableMath._calcTokenOutGivenExactBptIn( - bnum(this.amp.toString()), - poolPairData.allBalancesScaled.map((b) => - bnum(b.toString()) - ), + returnEvm = _calcTokenOutGivenExactBptIn( + this.amp.toBigInt(), + poolPairData.allBalancesScaled.map((b) => b.toBigInt()), poolPairData.tokenIndexOut, - bnum(amountConverted.toString()), - bnum(poolPairData.virtualBptSupply.toString()), - ZERO // Fee is handled above + amountConvertedEvm.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), + BigInt(0) ); } else { - amt = SDK.StableMath._calcOutGivenIn( - bnum(this.amp.toString()), - poolPairData.allBalancesScaled.map((b) => - bnum(b.toString()) - ), + returnEvm = _calcOutGivenIn( + this.amp.toBigInt(), + poolPairData.allBalancesScaled.map((b) => b.toBigInt()), poolPairData.tokenIndexIn, poolPairData.tokenIndexOut, - bnum(amountConverted.toString()), - ZERO // Fee is handled above + amountConvertedEvm.toBigInt(), + BigInt(0) ); } - // return normalised amount - // Using BigNumber.js decimalPlaces (dp), allows us to consider token decimal accuracy correctly, - // i.e. when using token with 2decimals 0.002 should be returned as 0 - // Uses ROUND_DOWN mode (1) - return scale( - amt.div(formatFixed(poolPairData.tokenOutPriceRate, 18)), - -18 - ).dp(poolPairData.decimalsOut, 1); + + const returnEvmWithRate = BigNumber.from(returnEvm) + .mul(ONE) + .div(poolPairData.tokenOutPriceRate); + + // Return human scaled + return bnum(formatFixed(returnEvmWithRate, 18)); } catch (err) { console.error(`PhantomStable _evmoutGivenIn: ${err.message}`); return ZERO; @@ -318,66 +310,57 @@ export class PhantomStablePool implements PoolBase { try { // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 - const amtScaled = scale(amount, 18); - const amountConverted = amtScaled - .times(formatFixed(poolPairData.tokenOutPriceRate, 18)) - .dp(0); + const amountConvertedEvm = parseFixed(amount.dp(18).toString(), 18) + .mul(poolPairData.tokenOutPriceRate) + .div(ONE); - let amt: OldBigNumber; + let returnEvm: BigInt; if (poolPairData.pairType === PairTypes.TokenToBpt) { - amt = SDK.StableMath._calcTokenInGivenExactBptOut( - bnum(this.amp.toString()), - poolPairData.allBalancesScaled.map((b) => - bnum(b.toString()) - ), + returnEvm = _calcTokenInGivenExactBptOut( + this.amp.toBigInt(), + poolPairData.allBalancesScaled.map((b) => b.toBigInt()), poolPairData.tokenIndexIn, - bnum(amountConverted.toString()), - bnum(poolPairData.virtualBptSupply.toString()), - ZERO // Fee is handled above + amountConvertedEvm.toBigInt(), + poolPairData.virtualBptSupply.toBigInt(), + BigInt(0) ); } else if (poolPairData.pairType === PairTypes.BptToToken) { - const amountsOut = Array( + const amountsOutBigInt = Array( poolPairData.allBalancesScaled.length - ).fill(ZERO); - amountsOut[poolPairData.tokenIndexOut] = bnum( - amountConverted.toString() - ); - - amt = SDK.StableMath._calcBptInGivenExactTokensOut( - bnum(this.amp.toString()), - poolPairData.allBalancesScaled.map((b) => - bnum(b.toString()) - ), - amountsOut, - bnum(poolPairData.virtualBptSupply.toString()), - ZERO // Fee is handled above + ).fill(BigInt(0)); + amountsOutBigInt[poolPairData.tokenIndexOut] = + amountConvertedEvm.toBigInt(); + + returnEvm = _calcBptInGivenExactTokensOut( + this.amp.toBigInt(), + poolPairData.allBalancesScaled.map((b) => b.toBigInt()), + amountsOutBigInt, + poolPairData.virtualBptSupply.toBigInt(), + BigInt(0) // Fee is handled above ); } else { - amt = SDK.StableMath._calcInGivenOut( - bnum(this.amp.toString()), - poolPairData.allBalancesScaled.map((b) => - bnum(b.toString()) - ), + returnEvm = _calcInGivenOut( + this.amp.toBigInt(), + poolPairData.allBalancesScaled.map((b) => b.toBigInt()), poolPairData.tokenIndexIn, poolPairData.tokenIndexOut, - amountConverted, - ZERO // Fee is handled above + amountConvertedEvm.toBigInt(), + BigInt(0) // Fee is handled above ); } - const returnScaled = amt - .div(formatFixed(poolPairData.tokenInPriceRate, 18)) - .dp(0); - // In Phantom Pools every time there is a swap (token per token, bpt per token or token per bpt), we substract the fee from the amount in - const returnWithFee = bnum( - this.addSwapFeeAmount( - BigNumber.from(returnScaled.toString()), - poolPairData.swapFee - ).toString() + const returnEvmWithRate = BigNumber.from(returnEvm) + .mul(ONE) + .div(poolPairData.tokenInPriceRate); + + const returnEvmWithFee = this.addSwapFeeAmount( + returnEvmWithRate, + poolPairData.swapFee ); + // return human number - return scale(returnWithFee, -18); + return bnum(formatFixed(returnEvmWithFee, 18)); } catch (err) { console.error(`PhantomStable _evminGivenOut: ${err.message}`); return ZERO; From 2b39fb7bf4af7f6d34b756aca92fa97b4e3a0f79 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 19 Jan 2022 12:19:08 +0000 Subject: [PATCH 22/43] Move SDK to dev dep. --- package.json | 2 +- yarn.lock | 58 ++++++++++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index eab0ebff..01806c2e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@ethersproject/contracts": "^5.4.1", "@ethersproject/providers": "^5.4.4", "@ethersproject/wallet": "^5.4.0", + "@georgeroman/balancer-v2-pools": "0.0.7", "@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.0.4", @@ -63,7 +64,6 @@ "typescript": "^4.3.5" }, "dependencies": { - "@georgeroman/balancer-v2-pools": "0.0.7", "isomorphic-fetch": "^2.2.1" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index c2351d64..06991cf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -529,10 +529,10 @@ resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.4.0.tgz#f39adadf62ad610c420bcd156fd41270e91b3ca9" integrity sha512-xYdWGGQ9P2cxBayt64d8LC8aPFJk6yWCawQi/4eJ4+oJdMMjEBMrIcIMZ9AxhwpPVmnBPrsB10PcXGmGAqgUEQ== -"@ethersproject/networks@5.5.0", "@ethersproject/networks@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.0.tgz#babec47cab892c51f8dd652ce7f2e3e14283981a" - integrity sha512-KWfP3xOnJeF89Uf/FCJdV1a2aDJe5XTN2N52p4fcQ34QhDqQFkgQKZ39VGtiqUgHcLI8DfT0l9azC3KFTunqtA== +"@ethersproject/networks@5.5.2", "@ethersproject/networks@^5.5.0": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.2.tgz#784c8b1283cd2a931114ab428dae1bd00c07630b" + integrity sha512-NEqPxbGBfy6O3x4ZTISb90SjEDkWYDUbEeIFhJly0F7sZjoQMnj5KYzMSkMkLKZ+1fGpx00EDpHQCy6PrDupkQ== dependencies: "@ethersproject/logger" "^5.5.0" @@ -573,10 +573,10 @@ dependencies: "@ethersproject/logger" "^5.4.0" -"@ethersproject/providers@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.0.tgz#bc2876a8fe5e0053ed9828b1f3767ae46e43758b" - integrity sha512-xqMbDnS/FPy+J/9mBLKddzyLLAQFjrVff5g00efqxPzcAwXiR+SiCGVy6eJ5iAIirBOATjx7QLhDNPGV+AEQsw== +"@ethersproject/providers@5.5.2": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.2.tgz#131ccf52dc17afd0ab69ed444b8c0e3a27297d99" + integrity sha512-hkbx7x/MKcRjyrO4StKXCzCpWer6s97xnm34xkfPiarhtEUVAN4TBBpamM+z66WcTt7H5B53YwbRj1n7i8pZoQ== dependencies: "@ethersproject/abstract-provider" "^5.5.0" "@ethersproject/abstract-signer" "^5.5.0" @@ -623,10 +623,10 @@ bech32 "1.1.4" ws "7.4.6" -"@ethersproject/random@5.5.0", "@ethersproject/random@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.5.0.tgz#305ed9e033ca537735365ac12eed88580b0f81f9" - integrity sha512-egGYZwZ/YIFKMHcoBUo8t3a8Hb/TKYX8BCBoLjudVCZh892welR3jOxgOmb48xznc9bTcMm7Tpwc1gHC1PFNFQ== +"@ethersproject/random@5.5.1", "@ethersproject/random@^5.5.0": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.5.1.tgz#7cdf38ea93dc0b1ed1d8e480ccdaf3535c555415" + integrity sha512-YaU2dQ7DuhL5Au7KbcQLHxcRHfgyNgvFV4sQOo0HrtW3Zkrc9ctWNz8wXQ4uCSfSDsqX2vcjhroxU5RQRV0nqA== dependencies: "@ethersproject/bytes" "^5.5.0" "@ethersproject/logger" "^5.5.0" @@ -808,10 +808,10 @@ "@ethersproject/transactions" "^5.4.0" "@ethersproject/wordlists" "^5.4.0" -"@ethersproject/web@5.5.0", "@ethersproject/web@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.0.tgz#0e5bb21a2b58fb4960a705bfc6522a6acf461e28" - integrity sha512-BEgY0eL5oH4mAo37TNYVrFeHsIXLRxggCRG/ksRIxI2X5uj5IsjGmcNiRN/VirQOlBxcUhCgHhaDLG4m6XAVoA== +"@ethersproject/web@5.5.1", "@ethersproject/web@^5.5.0": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.1.tgz#cfcc4a074a6936c657878ac58917a61341681316" + integrity sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg== dependencies: "@ethersproject/base64" "^5.5.0" "@ethersproject/bytes" "^5.5.0" @@ -1899,9 +1899,9 @@ esutils@^2.0.2: integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== ethers@^5.2.0: - version "5.5.1" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.1.tgz#d3259a95a42557844aa543906c537106c0406fbf" - integrity sha512-RodEvUFZI+EmFcE6bwkuJqpCYHazdzeR1nMzg+YWQSmQEsNtfl1KHGfp/FWZYl48bI/g7cgBeP2IlPthjiVngw== + version "5.5.3" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.3.tgz#1e361516711c0c3244b6210e7e3ecabf0c75fca0" + integrity sha512-fTT4WT8/hTe/BLwRUtl7I5zlpF3XC3P/Xwqxc5AIP2HGlH15qpmjs0Ou78az93b1rLITzXLFxoNX63B8ZbUd7g== dependencies: "@ethersproject/abi" "5.5.0" "@ethersproject/abstract-provider" "5.5.1" @@ -1918,11 +1918,11 @@ ethers@^5.2.0: "@ethersproject/json-wallets" "5.5.0" "@ethersproject/keccak256" "5.5.0" "@ethersproject/logger" "5.5.0" - "@ethersproject/networks" "5.5.0" + "@ethersproject/networks" "5.5.2" "@ethersproject/pbkdf2" "5.5.0" "@ethersproject/properties" "5.5.0" - "@ethersproject/providers" "5.5.0" - "@ethersproject/random" "5.5.0" + "@ethersproject/providers" "5.5.2" + "@ethersproject/random" "5.5.1" "@ethersproject/rlp" "5.5.0" "@ethersproject/sha2" "5.5.0" "@ethersproject/signing-key" "5.5.0" @@ -1931,7 +1931,7 @@ ethers@^5.2.0: "@ethersproject/transactions" "5.5.0" "@ethersproject/units" "5.5.0" "@ethersproject/wallet" "5.5.0" - "@ethersproject/web" "5.5.0" + "@ethersproject/web" "5.5.1" "@ethersproject/wordlists" "5.5.0" execa@^2.1.0: @@ -2222,18 +2222,18 @@ graceful-fs@^4.1.15: integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== graphql-request@^3.4.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-3.6.1.tgz#689cce1da990131b40b05651f9f32bff506a1d8e" - integrity sha512-Nm1EasrAQVZllyNTlHDLnLZjlhC6eRWnWP6KH//ytnAL08pjlLkdI2K+s6OV92p45hn5b/kUlLbDwACmRoLwrQ== + version "3.7.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-3.7.0.tgz#c7406e537084f8b9788541e3e6704340ca13055b" + integrity sha512-dw5PxHCgBneN2DDNqpWu8QkbbJ07oOziy8z+bK/TAXufsOLaETuVO4GkXrbs0WjhdKhBMN3BkpN/RIvUHkmNUQ== dependencies: cross-fetch "^3.0.6" extract-files "^9.0.0" form-data "^3.0.0" graphql@^15.5.0: - version "15.7.2" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef" - integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A== + version "15.8.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" + integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== growl@1.10.5: version "1.10.5" From dd2dcb21cbe190f4632ca4f943ea4573c12b2f79 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 19 Jan 2022 16:20:57 +0000 Subject: [PATCH 23/43] Remove exact param. --- src/pools/elementPool/elementPool.ts | 6 +- src/pools/index.ts | 4 +- src/pools/linearPool/linearPool.ts | 107 +++++------------- src/pools/metaStablePool/metaStablePool.ts | 6 +- .../phantomStablePool/phantomStablePool.ts | 6 +- src/pools/stablePool/stablePool.ts | 6 +- src/pools/weightedPool/weightedPool.ts | 6 +- src/router/helpersClass.ts | 12 +- src/types.ts | 6 +- test/phantomStableMath.spec.ts | 18 +-- 10 files changed, 51 insertions(+), 126 deletions(-) diff --git a/src/pools/elementPool/elementPool.ts b/src/pools/elementPool/elementPool.ts index 2bdd7306..b7c60e23 100644 --- a/src/pools/elementPool/elementPool.ts +++ b/src/pools/elementPool/elementPool.ts @@ -211,8 +211,7 @@ export class ElementPool implements PoolBase { _exactTokenInForTokenOut( poolPairData: ElementPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { poolPairData.currentBlockTimestamp = this.currentBlockTimestamp; return _exactTokenInForTokenOut(amount, poolPairData); @@ -220,8 +219,7 @@ export class ElementPool implements PoolBase { _tokenInForExactTokenOut( poolPairData: ElementPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { poolPairData.currentBlockTimestamp = this.currentBlockTimestamp; return _tokenInForExactTokenOut(amount, poolPairData); diff --git a/src/pools/index.ts b/src/pools/index.ts index 45ea9bc5..a6c24bb7 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -86,7 +86,7 @@ export function getOutputAmountSwap( ) { return ZERO; } else { - return pool._exactTokenInForTokenOut(poolPairData, amount, false); + return pool._exactTokenInForTokenOut(poolPairData, amount); } } else { if (poolPairData.balanceOut.isZero()) { @@ -98,7 +98,7 @@ export function getOutputAmountSwap( ) { return INFINITY; } else { - return pool._tokenInForExactTokenOut(poolPairData, amount, false); + return pool._tokenInForExactTokenOut(poolPairData, amount); } } throw Error('Unsupported swap'); diff --git a/src/pools/linearPool/linearPool.ts b/src/pools/linearPool/linearPool.ts index f74d74c9..6ce0649d 100644 --- a/src/pools/linearPool/linearPool.ts +++ b/src/pools/linearPool/linearPool.ts @@ -228,8 +228,7 @@ export class LinearPool implements PoolBase { poolPairData, balanceOutHuman .times(this.ALMOST_ONE.toString()) - .div(ONE.toString()), - true + .div(ONE.toString()) ); } else if ( linearPoolPairData.pairType === PairTypes.WrappedTokenToBpt @@ -245,8 +244,7 @@ export class LinearPool implements PoolBase { linearPoolPairData, balanceOutHuman .times(this.ALMOST_ONE.toString()) - .div(ONE.toString()), - true + .div(ONE.toString()) ); } else if ( linearPoolPairData.pairType === PairTypes.BptToWrappedToken @@ -255,8 +253,7 @@ export class LinearPool implements PoolBase { poolPairData, balanceOutHuman .times(this.ALMOST_ONE.toString()) - .div(ONE.toString()), - true + .div(ONE.toString()) ); // Returning Human scale return limit; @@ -314,48 +311,30 @@ export class LinearPool implements PoolBase { _exactTokenInForTokenOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { if (poolPairData.pairType === PairTypes.MainTokenToBpt) { - return this._exactMainTokenInForBPTOut(poolPairData, amount, exact); + return this._exactMainTokenInForBPTOut(poolPairData, amount); } else if (poolPairData.pairType === PairTypes.BptToMainToken) { - return this._exactBPTInForMainTokenOut(poolPairData, amount, exact); + return this._exactBPTInForMainTokenOut(poolPairData, amount); } else if (poolPairData.pairType === PairTypes.WrappedTokenToBpt) { - return this._exactWrappedTokenInForBPTOut( - poolPairData, - amount, - exact - ); + return this._exactWrappedTokenInForBPTOut(poolPairData, amount); } else if (poolPairData.pairType === PairTypes.BptToWrappedToken) { - return this._exactBPTInForWrappedTokenOut( - poolPairData, - amount, - exact - ); + return this._exactBPTInForWrappedTokenOut(poolPairData, amount); } else if ( poolPairData.pairType === PairTypes.MainTokenToWrappedToken ) { - return this._exactMainTokenInForWrappedOut( - poolPairData, - amount, - exact - ); + return this._exactMainTokenInForWrappedOut(poolPairData, amount); } else if ( poolPairData.pairType === PairTypes.WrappedTokenToMainToken ) { - return this._exactWrappedTokenInForMainOut( - poolPairData, - amount, - exact - ); + return this._exactWrappedTokenInForMainOut(poolPairData, amount); } else return bnum(0); } _exactWrappedTokenInForMainOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -387,8 +366,7 @@ export class LinearPool implements PoolBase { _exactMainTokenInForWrappedOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -420,8 +398,7 @@ export class LinearPool implements PoolBase { _exactMainTokenInForBPTOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -455,8 +432,7 @@ export class LinearPool implements PoolBase { _exactBPTInForMainTokenOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -490,8 +466,7 @@ export class LinearPool implements PoolBase { _exactWrappedTokenInForBPTOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -527,8 +502,7 @@ export class LinearPool implements PoolBase { _exactBPTInForWrappedTokenOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -566,48 +540,30 @@ export class LinearPool implements PoolBase { _tokenInForExactTokenOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { if (poolPairData.pairType === PairTypes.MainTokenToBpt) { - return this._mainTokenInForExactBPTOut(poolPairData, amount, exact); + return this._mainTokenInForExactBPTOut(poolPairData, amount); } else if (poolPairData.pairType === PairTypes.BptToMainToken) { - return this._BPTInForExactMainTokenOut(poolPairData, amount, exact); + return this._BPTInForExactMainTokenOut(poolPairData, amount); } else if (poolPairData.pairType === PairTypes.WrappedTokenToBpt) { - return this._wrappedTokenInForExactBPTOut( - poolPairData, - amount, - exact - ); + return this._wrappedTokenInForExactBPTOut(poolPairData, amount); } else if (poolPairData.pairType === PairTypes.BptToWrappedToken) { - return this._BPTInForExactWrappedTokenOut( - poolPairData, - amount, - exact - ); + return this._BPTInForExactWrappedTokenOut(poolPairData, amount); } else if ( poolPairData.pairType === PairTypes.MainTokenToWrappedToken ) { - return this._mainTokenInForExactWrappedOut( - poolPairData, - amount, - exact - ); + return this._mainTokenInForExactWrappedOut(poolPairData, amount); } else if ( poolPairData.pairType === PairTypes.WrappedTokenToMainToken ) { - return this._wrappedTokenInForExactMainOut( - poolPairData, - amount, - exact - ); + return this._wrappedTokenInForExactMainOut(poolPairData, amount); } else return bnum(0); // LinearPool does not support TokenToToken } _wrappedTokenInForExactMainOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -639,8 +595,7 @@ export class LinearPool implements PoolBase { _mainTokenInForExactWrappedOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -672,8 +627,7 @@ export class LinearPool implements PoolBase { _mainTokenInForExactBPTOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -708,8 +662,7 @@ export class LinearPool implements PoolBase { _BPTInForExactMainTokenOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -744,8 +697,7 @@ export class LinearPool implements PoolBase { _wrappedTokenInForExactBPTOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -783,8 +735,7 @@ export class LinearPool implements PoolBase { _BPTInForExactWrappedTokenOut( poolPairData: LinearPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point diff --git a/src/pools/metaStablePool/metaStablePool.ts b/src/pools/metaStablePool/metaStablePool.ts index 1abcecbc..b0524f46 100644 --- a/src/pools/metaStablePool/metaStablePool.ts +++ b/src/pools/metaStablePool/metaStablePool.ts @@ -198,8 +198,7 @@ export class MetaStablePool implements PoolBase { _exactTokenInForTokenOut( poolPairData: MetaStablePoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -232,8 +231,7 @@ export class MetaStablePool implements PoolBase { _tokenInForExactTokenOut( poolPairData: MetaStablePoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point diff --git a/src/pools/phantomStablePool/phantomStablePool.ts b/src/pools/phantomStablePool/phantomStablePool.ts index 5519ba96..128df27e 100644 --- a/src/pools/phantomStablePool/phantomStablePool.ts +++ b/src/pools/phantomStablePool/phantomStablePool.ts @@ -239,8 +239,7 @@ export class PhantomStablePool implements PoolBase { _exactTokenInForTokenOut( poolPairData: PhantomStablePoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -304,8 +303,7 @@ export class PhantomStablePool implements PoolBase { _tokenInForExactTokenOut( poolPairData: PhantomStablePoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point diff --git a/src/pools/stablePool/stablePool.ts b/src/pools/stablePool/stablePool.ts index 345b136c..cf654ce1 100644 --- a/src/pools/stablePool/stablePool.ts +++ b/src/pools/stablePool/stablePool.ts @@ -178,8 +178,7 @@ export class StablePool implements PoolBase { _exactTokenInForTokenOut( poolPairData: StablePoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point @@ -212,8 +211,7 @@ export class StablePool implements PoolBase { _tokenInForExactTokenOut( poolPairData: StablePoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { try { // All values should use 1e18 fixed point diff --git a/src/pools/weightedPool/weightedPool.ts b/src/pools/weightedPool/weightedPool.ts index fd5dc08a..23da6e2d 100644 --- a/src/pools/weightedPool/weightedPool.ts +++ b/src/pools/weightedPool/weightedPool.ts @@ -182,8 +182,7 @@ export class WeightedPool implements PoolBase { // calcOutGivenIn _exactTokenInForTokenOut( poolPairData: WeightedPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { if (amount.isNaN()) return amount; @@ -213,8 +212,7 @@ export class WeightedPool implements PoolBase { // calcInGivenOut _tokenInForExactTokenOut( poolPairData: WeightedPoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { if (amount.isNaN()) return amount; diff --git a/src/router/helpersClass.ts b/src/router/helpersClass.ts index f80be7b8..12f33b03 100644 --- a/src/router/helpersClass.ts +++ b/src/router/helpersClass.ts @@ -334,11 +334,7 @@ export function EVMgetOutputAmountSwap( pool.poolType === PoolTypes.Linear ) { // Will accept/return normalised values - returnAmount = pool._exactTokenInForTokenOut( - poolPairData, - amount, - true - ); + returnAmount = pool._exactTokenInForTokenOut(poolPairData, amount); } else if (pool.poolType === PoolTypes.Element) { // TODO this will just be part of above once maths available returnAmount = getOutputAmountSwap( @@ -358,11 +354,7 @@ export function EVMgetOutputAmountSwap( pool.poolType === PoolTypes.MetaStable || pool.poolType === PoolTypes.Linear ) { - returnAmount = pool._tokenInForExactTokenOut( - poolPairData, - amount, - true - ); + returnAmount = pool._tokenInForExactTokenOut(poolPairData, amount); } else if (pool.poolType === PoolTypes.Element) { // TODO this will just be part of above once maths available returnAmount = getOutputAmountSwap( diff --git a/src/types.ts b/src/types.ts index 47c6af09..cc3ef3a9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -162,13 +162,11 @@ export interface PoolBase { updateTokenBalanceForPool: (token: string, newBalance: BigNumber) => void; _exactTokenInForTokenOut: ( poolPairData: PoolPairBase, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ) => OldBigNumber; _tokenInForExactTokenOut: ( poolPairData: PoolPairBase, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ) => OldBigNumber; _spotPriceAfterSwapExactTokenInForTokenOut: ( poolPairData: PoolPairBase, diff --git a/test/phantomStableMath.spec.ts b/test/phantomStableMath.spec.ts index 2a93c5b2..91699070 100644 --- a/test/phantomStableMath.spec.ts +++ b/test/phantomStableMath.spec.ts @@ -166,13 +166,11 @@ function getSwapOutcomes( ): { a1: OldBigNumber; a2: OldBigNumber } { const a1 = phantomStablePool._exactTokenInForTokenOut( poolPairData, - bnum(amount), - true + bnum(amount) ); const a2 = phantomStablePool._tokenInForExactTokenOut( poolPairData, - bnum(amount), - true + bnum(amount) ); return { a1, a2 }; } @@ -227,13 +225,11 @@ function incrementalQuotientExactTokenInForTokenOut( ): OldBigNumber { const f1 = phantomStablePool._exactTokenInForTokenOut( poolPairData, - bnum(amount + delta), - true + bnum(amount + delta) ); const f0 = phantomStablePool._exactTokenInForTokenOut( poolPairData, - bnum(amount), - true + bnum(amount) ); const incrementalQuotient = f1.minus(f0).div(delta); return incrementalQuotient; @@ -247,13 +243,11 @@ function incrementalQuotientTokenInForExactTokenOut( ): OldBigNumber { const f1 = phantomStablePool._tokenInForExactTokenOut( poolPairData, - bnum(amount + delta), - true + bnum(amount + delta) ); const f0 = phantomStablePool._tokenInForExactTokenOut( poolPairData, - bnum(amount), - true + bnum(amount) ); const incrementalQuotient = f1.minus(f0).div(delta); return incrementalQuotient; From f60aa450bac5d4cc41d385221418bec0afdefefb Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Wed, 19 Jan 2022 17:35:56 +0000 Subject: [PATCH 24/43] Gyro2Pool Initial Implementation --- src/pools/gyro2Pool/gyro2Math.ts | 238 +++++++++++++++++++++++++ src/pools/gyro2Pool/gyro2Pool.ts | 286 +++++++++++++++++++++++++++++++ src/types.ts | 1 + 3 files changed, 525 insertions(+) create mode 100644 src/pools/gyro2Pool/gyro2Math.ts create mode 100644 src/pools/gyro2Pool/gyro2Pool.ts diff --git a/src/pools/gyro2Pool/gyro2Math.ts b/src/pools/gyro2Pool/gyro2Math.ts new file mode 100644 index 00000000..29412f85 --- /dev/null +++ b/src/pools/gyro2Pool/gyro2Math.ts @@ -0,0 +1,238 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; + +// Swap limits: amounts swapped may not be larger than this percentage of total balance. +const _MAX_IN_RATIO: BigNumber = BigNumber.from(0.3); +const _MAX_OUT_RATIO: BigNumber = BigNumber.from(0.3); + +// Helpers +function _squareRoot(input: BigNumber): BigNumber { + return input.pow(BigNumber.from(1).div(BigNumber.from(2))); +} + +///////// +/// Fee calculations +///////// + +export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + const feeAmount = amountIn.mul(swapFee); + return amountIn.sub(feeAmount); +} + +export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + return amountIn.div(ONE.sub(swapFee)); +} + +///////// +/// Virtual Parameter calculations +///////// + +export function _findVirtualParams( + invariant: BigNumber, + sqrtAlpha: BigNumber, + sqrtBeta: BigNumber +): [BigNumber, BigNumber] { + return [invariant.div(sqrtBeta), invariant.mul(sqrtAlpha)]; +} + +///////// +/// Invariant Calculation +///////// + +export function _calculateInvariant( + balances: BigNumber[], // balances + sqrtAlpha: BigNumber, + sqrtBeta: BigNumber +): BigNumber { + /********************************************************************************************** + // Calculate with quadratic formula + // 0 = (1-sqrt(alhpa/beta)*L^2 - (y/sqrt(beta)+x*sqrt(alpha))*L - x*y) + // 0 = a*L^2 + b*L + c + // here a > 0, b < 0, and c < 0, which is a special case that works well w/o negative numbers + // taking mb = -b and mc = -c: (1/2) + // mb + (mb^2 + 4 * a * mc)^ // + // L = ------------------------------------------ // + // 2 * a // + // // + **********************************************************************************************/ + const [a, mb, mc] = _calculateQuadraticTerms(balances, sqrtAlpha, sqrtBeta); + + return _calculateQuadratic(a, mb, mc); +} + +function _calculateQuadraticTerms( + balances: BigNumber[], + sqrtAlpha: BigNumber, + sqrtBeta: BigNumber +): [BigNumber, BigNumber, BigNumber] { + const a = BigNumber.from(1).sub(sqrtAlpha.div(sqrtBeta)); + const bterm0 = balances[1].div(sqrtBeta); + const bterm1 = balances[0].mul(sqrtAlpha); + const mb = bterm0.add(bterm1); + const mc = balances[0].mul(balances[1]); + + return [a, mb, mc]; +} + +function _calculateQuadratic( + a: BigNumber, + mb: BigNumber, + mc: BigNumber +): BigNumber { + const denominator = a.mul(BigNumber.from(2)); + const bSquare = mb.mul(mb); + const addTerm = a.mul(mc.mul(BigNumber.from(4))); + // The minus sign in the radicand cancels out in this special case, so we add + const radicand = bSquare.add(addTerm); + const sqrResult = _squareRoot(radicand); + // The minus sign in the numerator cancels out in this special case + const numerator = mb.add(sqrResult); + const invariant = numerator.div(denominator); + + return invariant; +} + +///////// +/// Swap functions +///////// + +// SwapType = 'swapExactIn' +export function _calcOutGivenIn( + balanceIn: BigNumber, + balanceOut: BigNumber, + amountIn: BigNumber, + virtualParamIn: BigNumber, + virtualParamOut: BigNumber, + currentInvariant: BigNumber +): BigNumber { + /********************************************************************************************** + // Described for X = `in' asset and Y = `out' asset, but equivalent for the other case // + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // L = inv.Liq / L^2 \ // + // - dy = y' - | -------------------------- | // + // x' = virtIn \ ( x' + dX) / // + // y' = virtOut // + // Note that -dy > 0 is what the trader receives. // + // We exploit the fact that this formula is symmetric up to virtualParam{X,Y}. // + **********************************************************************************************/ + if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO))) + throw new Error('Swap Amount Too Large'); + + const virtIn = balanceIn.add(virtualParamIn); + const denominator = virtIn.add(amountIn); + const invSquare = currentInvariant.mul(currentInvariant); + const subtrahend = invSquare.div(denominator); + const virtOut = balanceOut.add(virtualParamOut); + return virtOut.sub(subtrahend); +} + +// SwapType = 'swapExactOut' +export function _calcInGivenOut( + balanceIn: BigNumber, + balanceOut: BigNumber, + amountOut: BigNumber, + virtualParamIn: BigNumber, + virtualParamOut: BigNumber, + currentInvariant: BigNumber +): BigNumber { + /********************************************************************************************** + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // x = balanceIn // + // L = inv.Liq / L^2 \ // + // dx = | -------------------------- | - x' // + // x' = virtIn \ ( y' + dy) / // + // y' = virtOut // + // Note that dy < 0 < dx. // + **********************************************************************************************/ + + if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO))) + throw new Error('Swap Amount Too Large'); + + const virtOut = balanceOut.add(virtualParamOut); + const denominator = virtOut.sub(amountOut); + const invSquare = currentInvariant.mul(currentInvariant); + const term = invSquare.div(denominator); + const virtIn = balanceIn.add(virtualParamIn); + return term.sub(virtIn); +} + +// ///////// +// /// Spot price function +// ///////// + +export function _calculateNewSpotPrice( + newBalances: BigNumber[], + sqrtAlpha: BigNumber, + sqrtBeta: BigNumber +): BigNumber { + // Re-compute the liquidity invariant L based on these new balances. + // The invariant will be larger slightly larger than before because there are fees. + const newInvariant = _calculateInvariant(newBalances, sqrtAlpha, sqrtBeta); + + // Compute the offsets a and b based on the new liquidity invariant. + const [newVirtualParameterIn, newVirtualParameterOut] = _findVirtualParams( + newInvariant, + sqrtAlpha, + sqrtBeta + ); + + // Now compute (x + a) / (y + b) for the marginal price of asset y (out) denoted in units of asset x (in) + const numerator = newBalances[0].add(newVirtualParameterIn); + const denominator = newBalances[1].add(newVirtualParameterOut); + const newSpotPrice = numerator.div(denominator); + + return newSpotPrice; +} + +// ///////// +// /// Derivatives of spotPriceAfterSwap +// ///////// + +// // PairType = 'token->token' +// // SwapType = 'swapExactIn' +// export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( +// amount: OldBigNumber, +// poolPairData: WeightedPoolPairData +// ): OldBigNumber { +// const Bi = parseFloat( +// formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) +// ); +// const Bo = parseFloat( +// formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) +// ); +// const wi = parseFloat(formatFixed(poolPairData.weightIn, 18)); +// const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); +// const Ai = amount.toNumber(); +// const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); +// return bnum((wi + wo) / (Bo * (Bi / (Ai + Bi - Ai * f)) ** (wi / wo) * wi)); +// } + +// // PairType = 'token->token' +// // SwapType = 'swapExactOut' +// export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( +// amount: OldBigNumber, +// poolPairData: WeightedPoolPairData +// ): OldBigNumber { +// const Bi = parseFloat( +// formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) +// ); +// const Bo = parseFloat( +// formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) +// ); +// const wi = parseFloat(formatFixed(poolPairData.weightIn, 18)); +// const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); +// const Ao = amount.toNumber(); +// const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); +// return bnum( +// -( +// (Bi * (Bo / (-Ao + Bo)) ** (wo / wi) * wo * (wi + wo)) / +// ((Ao - Bo) ** 2 * (-1 + f) * wi ** 2) +// ) +// ); +// } diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts new file mode 100644 index 00000000..0c15edfb --- /dev/null +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -0,0 +1,286 @@ +import { getAddress } from '@ethersproject/address'; +import { parseFixed, formatFixed, BigNumber } from '@ethersproject/bignumber'; +import { BigNumber as OldBigNumber, bnum } from '../../utils/bignumber'; +import { + PoolBase, + PoolPairBase, + PoolTypes, + SwapPairType, + SubgraphToken, + SwapTypes, + SubgraphPoolBase, +} from 'types'; +import { isSameAddress } from '../../utils'; +import { + _calculateInvariant, + _calcOutGivenIn, + _calcInGivenOut, + _findVirtualParams, + _calculateNewSpotPrice, + _reduceFee, + _addFee, +} from './gyro2Math'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; + +export type Gyro2PoolPairData = PoolPairBase & { + sqrtAlpha: BigNumber; + sqrtBeta: BigNumber; +}; + +export type Gyro2PoolToken = Pick< + SubgraphToken, + 'address' | 'balance' | 'decimals' +>; + +// TODO: getNormalizedLiquidity, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut implementations +export class Gyro2Pool implements PoolBase { + poolType: PoolTypes = PoolTypes.Gyro2; + swapPairType: SwapPairType; + id: string; + address: string; + tokensList: string[]; + tokens: Gyro2PoolToken[]; + swapFee: BigNumber; + totalShares: BigNumber; + + // Max In/Out Ratios + MAX_IN_RATIO = parseFixed('0.3', 18); + MAX_OUT_RATIO = parseFixed('0.3', 18); + + static fromPool(pool: SubgraphPoolBase): Gyro2Pool { + return new Gyro2Pool( + pool.id, + pool.address, + pool.swapFee, + pool.totalShares, + pool.tokens, + pool.tokensList + ); + } + + constructor( + id: string, + address: string, + swapFee: string, + totalShares: string, + tokens: Gyro2PoolToken[], + tokensList: string[] + ) { + this.id = id; + this.address = address; + this.swapFee = parseFixed(swapFee, 18); + this.totalShares = parseFixed(totalShares, 18); + this.tokens = tokens; + this.tokensList = tokensList; + } + + setTypeForSwap(type: SwapPairType): void { + this.swapPairType = type; + } + + parsePoolPairData(tokenIn: string, tokenOut: string): Gyro2PoolPairData { + const tokenInIndex = this.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenIn) + ); + if (tokenInIndex < 0) throw 'Pool does not contain tokenIn'; + const tI = this.tokens[tokenInIndex]; + const balanceIn = tI.balance; + const decimalsIn = tI.decimals; + + const tokenOutIndex = this.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenOut) + ); + if (tokenOutIndex < 0) throw 'Pool does not contain tokenOut'; + const tO = this.tokens[tokenOutIndex]; + const balanceOut = tO.balance; + const decimalsOut = tO.decimals; + + // TODO: sqrtAlpha, sqrtBeta to be added + const poolPairData: Gyro2PoolPairData = { + id: this.id, + address: this.address, + poolType: this.poolType, + tokenIn: tokenIn, + tokenOut: tokenOut, + decimalsIn: Number(decimalsIn), + decimalsOut: Number(decimalsOut), + balanceIn: parseFixed(balanceIn, decimalsIn), + balanceOut: parseFixed(balanceOut, decimalsOut), + swapFee: this.swapFee, + }; + + return poolPairData; + } + + // getNormalizedLiquidity(poolPairData: Gyro2PoolPairData): OldBigNumber { + // } + + getLimitAmountSwap( + poolPairData: PoolPairBase, + swapType: SwapTypes + ): OldBigNumber { + if (swapType === SwapTypes.SwapExactIn) { + return bnum( + formatFixed( + poolPairData.balanceIn.mul(this.MAX_IN_RATIO).div(ONE), + poolPairData.decimalsIn + ) + ); + } else { + return bnum( + formatFixed( + poolPairData.balanceOut.mul(this.MAX_OUT_RATIO).div(ONE), + poolPairData.decimalsOut + ) + ); + } + } + + // Updates the balance of a given token for the pool + updateTokenBalanceForPool(token: string, newBalance: BigNumber): void { + // token is BPT + if (this.address == token) { + this.totalShares = newBalance; + } else { + // token is underlying in the pool + const T = this.tokens.find((t) => isSameAddress(t.address, token)); + if (!T) throw Error('Pool does not contain this token'); + T.balance = formatFixed(newBalance, T.decimals); + } + } + + _exactTokenInForTokenOut( + poolPairData: Gyro2PoolPairData, + amount: OldBigNumber, + exact: boolean + ): OldBigNumber { + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const currentInvariant = _calculateInvariant( + balances, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const [virtualParamIn, virtualParamOut] = _findVirtualParams( + currentInvariant, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); + + const outAmount = _calcOutGivenIn( + poolPairData.balanceIn, + poolPairData.balanceOut, + inAmountLessFee, + virtualParamIn, + virtualParamOut, + currentInvariant + ); + + return bnum(formatFixed(outAmount, poolPairData.decimalsOut)); + } + + _tokenInForExactTokenOut( + poolPairData: Gyro2PoolPairData, + amount: OldBigNumber, + exact: boolean + ): OldBigNumber { + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const currentInvariant = _calculateInvariant( + balances, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const [virtualParamIn, virtualParamOut] = _findVirtualParams( + currentInvariant, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + + const inAmountLessFee = _calcInGivenOut( + poolPairData.balanceIn, + poolPairData.balanceOut, + parseFixed(amount.toString(), poolPairData.decimalsOut), + virtualParamIn, + virtualParamOut, + currentInvariant + ); + + const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); + + return bnum(formatFixed(inAmount, poolPairData.decimalsIn)); + } + + _spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: Gyro2PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + // Compute the reserve balances of the two assets after the swap (say x and y). This should include any fees. + const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + + const outAmount = parseFixed( + this._exactTokenInForTokenOut( + poolPairData, + amount, + false + ).toString(), + poolPairData.decimalsOut + ); + + const newBalances = [ + poolPairData.balanceIn.add(inAmount), + poolPairData.balanceOut.sub(outAmount), + ]; + + const newSpotPrice = _calculateNewSpotPrice( + newBalances, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + + return bnum(formatFixed(newSpotPrice, poolPairData.decimalsIn)); + } + + _spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: Gyro2PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + // Compute the reserve balances of the two assets after the swap (say x and y). This should include any fees. + const outAmount = parseFixed( + amount.toString(), + poolPairData.decimalsOut + ); + + const inAmount = parseFixed( + this._tokenInForExactTokenOut( + poolPairData, + amount, + false + ).toString(), + poolPairData.decimalsIn + ); + + const newBalances = [ + poolPairData.balanceIn.add(inAmount), + poolPairData.balanceOut.sub(outAmount), + ]; + + const newSpotPrice = _calculateNewSpotPrice( + newBalances, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + + return bnum(formatFixed(newSpotPrice, poolPairData.decimalsIn)); + } + + // _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + // poolPairData: Gyro2PoolPairData, + // amount: OldBigNumber + // ): OldBigNumber {} + + // _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + // poolPairData: Gyro2PoolPairData, + // amount: OldBigNumber + // ): OldBigNumber {} +} diff --git a/src/types.ts b/src/types.ts index 3748bb59..0545e1db 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,6 +25,7 @@ export enum PoolTypes { Element, MetaStable, Linear, + Gyro2, } export enum SwapPairType { From ef02b3d44139b14251b81629e68382b659626539 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 20 Jan 2022 09:28:54 +0000 Subject: [PATCH 25/43] Check for 0 amounts in Stable pools. --- src/pools/metaStablePool/metaStablePool.ts | 2 ++ src/pools/phantomStablePool/phantomStablePool.ts | 2 ++ src/pools/stablePool/stablePool.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/pools/metaStablePool/metaStablePool.ts b/src/pools/metaStablePool/metaStablePool.ts index b0524f46..369a7aa9 100644 --- a/src/pools/metaStablePool/metaStablePool.ts +++ b/src/pools/metaStablePool/metaStablePool.ts @@ -201,6 +201,7 @@ export class MetaStablePool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { + if (amount.isZero()) return ZERO; // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 const amountConvertedEvm = parseFixed(amount.dp(18).toString(), 18) @@ -234,6 +235,7 @@ export class MetaStablePool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { + if (amount.isZero()) return ZERO; // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 const amountConvertedEvm = parseFixed(amount.dp(18).toString(), 18) diff --git a/src/pools/phantomStablePool/phantomStablePool.ts b/src/pools/phantomStablePool/phantomStablePool.ts index 128df27e..ae5e766b 100644 --- a/src/pools/phantomStablePool/phantomStablePool.ts +++ b/src/pools/phantomStablePool/phantomStablePool.ts @@ -242,6 +242,7 @@ export class PhantomStablePool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { + if (amount.isZero()) return ZERO; // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 // In Phantom Pools every time there is a swap (token per token, bpt per token or token per bpt), we substract the fee from the amount in @@ -306,6 +307,7 @@ export class PhantomStablePool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { + if (amount.isZero()) return ZERO; // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 const amountConvertedEvm = parseFixed(amount.dp(18).toString(), 18) diff --git a/src/pools/stablePool/stablePool.ts b/src/pools/stablePool/stablePool.ts index cf654ce1..a328eea1 100644 --- a/src/pools/stablePool/stablePool.ts +++ b/src/pools/stablePool/stablePool.ts @@ -181,6 +181,7 @@ export class StablePool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { + if (amount.isZero()) return ZERO; // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 const amtScaled = parseFixed(amount.dp(18).toString(), 18); @@ -214,6 +215,7 @@ export class StablePool implements PoolBase { amount: OldBigNumber ): OldBigNumber { try { + if (amount.isZero()) return ZERO; // All values should use 1e18 fixed point // i.e. 1USDC => 1e18 not 1e6 const amtScaled = parseFixed(amount.dp(18).toString(), 18); From 19f9a18ad49d86d26b42552169a8b2e3b9907e0a Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 20 Jan 2022 09:34:14 +0000 Subject: [PATCH 26/43] Fix OldBigNumber import. --- src/pools/linearPool/linearMath.ts | 77 ++++++++++++++++++------------ 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/src/pools/linearPool/linearMath.ts b/src/pools/linearPool/linearMath.ts index cc1c9296..c1680179 100644 --- a/src/pools/linearPool/linearMath.ts +++ b/src/pools/linearPool/linearMath.ts @@ -1,4 +1,4 @@ -import { BigNumber as linear } from '../../utils/bignumber'; +import { BigNumber as OldBigNumber } from '../../utils/bignumber'; import { bnum } from '../../utils/bignumber'; import { formatFixed } from '@ethersproject/bignumber'; import { MathSol } from '../../utils/basicOperations'; @@ -533,7 +533,7 @@ export function _spotPriceAfterSwapBptInPerMainOut( export function _spotPriceAfterSwapExactTokenInForTokenOut( amount, poolPairData -): linear { +): OldBigNumber { // This is not expected to be used by SOR // but could still be implemented throw new Error('Function not implemented.'); @@ -544,7 +544,7 @@ export function _spotPriceAfterSwapExactTokenInForTokenOut( export function _spotPriceAfterSwapTokenInForExactTokenOut( amount, poolPairData -): linear { +): OldBigNumber { // This is not expected to be used by SOR // but could still be implemented throw new Error('Function not implemented.'); @@ -553,9 +553,9 @@ export function _spotPriceAfterSwapTokenInForExactTokenOut( // PairType = 'token->BPT' // SwapType = 'swapExactIn' export function _spotPriceAfterSwapExactTokenInForBPTOut( - amount: linear, + amount: OldBigNumber, poolPairData: LinearPoolPairData -): linear { +): OldBigNumber { const mainIn = bnum(amount.toString()); const mainBalance = bnum( formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) @@ -570,7 +570,7 @@ export function _spotPriceAfterSwapExactTokenInForBPTOut( const virtualBptSupply = bnum( formatFixed(poolPairData.virtualBptSupply, 18) ); - const params: linear[] = [ + const params: OldBigNumber[] = [ bnum(formatFixed(poolPairData.swapFee, 18)), bnum(formatFixed(poolPairData.rate.toString(), 18)), bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), @@ -593,9 +593,9 @@ export function _spotPriceAfterSwapExactTokenInForBPTOut( // PairType = 'token->BPT' // SwapType = 'swapExactOut' export function _spotPriceAfterSwapTokenInForExactBPTOut( - amount: linear, + amount: OldBigNumber, poolPairData: LinearPoolPairData -): linear { +): OldBigNumber { const bptOut = bnum(amount.toString()); const virtualBptSupply = bnum( formatFixed(poolPairData.virtualBptSupply, 18) @@ -609,7 +609,7 @@ export function _spotPriceAfterSwapTokenInForExactBPTOut( poolPairData.wrappedDecimals ) ); - const params: linear[] = [ + const params: OldBigNumber[] = [ bnum(formatFixed(poolPairData.swapFee, 18)), bnum(formatFixed(poolPairData.rate.toString(), 18)), bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), @@ -636,9 +636,9 @@ export function _spotPriceAfterSwapTokenInForExactBPTOut( // PairType = 'BPT->token' // SwapType = 'swapExactIn' export function _spotPriceAfterSwapExactBPTInForTokenOut( - amount: linear, + amount: OldBigNumber, poolPairData: LinearPoolPairData -): linear { +): OldBigNumber { const bptIn = bnum(amount.toString()); const mainBalance = bnum( formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) @@ -652,7 +652,7 @@ export function _spotPriceAfterSwapExactBPTInForTokenOut( const virtualBptSupply = bnum( formatFixed(poolPairData.virtualBptSupply, 18) ); - const params: linear[] = [ + const params: OldBigNumber[] = [ bnum(formatFixed(poolPairData.swapFee, 18)), bnum(formatFixed(poolPairData.rate.toString(), 18)), bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), @@ -676,9 +676,9 @@ export function _spotPriceAfterSwapExactBPTInForTokenOut( // PairType = 'BPT->token' // SwapType = 'swapExactOut' export function _spotPriceAfterSwapBPTInForExactTokenOut( - amount: linear, + amount: OldBigNumber, poolPairData: LinearPoolPairData -): linear { +): OldBigNumber { const mainOut = bnum(amount.toString()); const mainBalance = bnum( formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) @@ -693,7 +693,7 @@ export function _spotPriceAfterSwapBPTInForExactTokenOut( formatFixed(poolPairData.virtualBptSupply, 18) ); const finalMainBalance = mainBalance.minus(mainOut); - const params: linear[] = [ + const params: OldBigNumber[] = [ bnum(formatFixed(poolPairData.swapFee, 18)), bnum(formatFixed(poolPairData.rate.toString(), 18)), bnum(formatFixed(poolPairData.lowerTarget.toString(), 18)), @@ -723,7 +723,7 @@ export function _spotPriceAfterSwapBPTInForExactTokenOut( export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( amount, poolPairData -): linear { +): OldBigNumber { // This is not expected to be used by SOR // but could still be implemented throw new Error('Function not implemented.'); @@ -734,7 +734,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( amount, poolPairData -): linear { +): OldBigNumber { // This is not expected to be used by SOR // but could still be implemented throw new Error('Function not implemented.'); @@ -745,7 +745,7 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( export function _derivativeSpotPriceAfterSwapExactTokenInForBPTOut( amount, poolPairData -): linear { +): OldBigNumber { return bnum(0); } @@ -754,7 +754,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForBPTOut( export function _derivativeSpotPriceAfterSwapTokenInForExactBPTOut( amount, poolPairData -): linear { +): OldBigNumber { return bnum(0); } @@ -763,7 +763,7 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactBPTOut( export function _derivativeSpotPriceAfterSwapExactBPTInForTokenOut( amount, poolPairData -): linear { +): OldBigNumber { return bnum(0); } @@ -772,20 +772,20 @@ export function _derivativeSpotPriceAfterSwapExactBPTInForTokenOut( export function _derivativeSpotPriceAfterSwapBPTInForExactTokenOut( amount, poolPairData -): linear { +): OldBigNumber { return bnum(0); } function calcInvariant( - nominalMainBalance: linear, - wrappedBalance: linear, - params: linear[] -): linear { + nominalMainBalance: OldBigNumber, + wrappedBalance: OldBigNumber, + params: OldBigNumber[] +): OldBigNumber { const rate = params[1]; return nominalMainBalance.plus(wrappedBalance.times(rate)); } -function toNominal(amount: linear, params: linear[]): linear { +function toNominal(amount: OldBigNumber, params: OldBigNumber[]): OldBigNumber { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -805,7 +805,10 @@ function toNominal(amount: linear, params: linear[]): linear { } } -function leftDerivativeToNominal(amount: linear, params: linear[]): linear { +function leftDerivativeToNominal( + amount: OldBigNumber, + params: OldBigNumber[] +): OldBigNumber { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -820,7 +823,10 @@ function leftDerivativeToNominal(amount: linear, params: linear[]): linear { } } -function rightDerivativeToNominal(amount: linear, params: linear[]): linear { +function rightDerivativeToNominal( + amount: OldBigNumber, + params: OldBigNumber[] +): OldBigNumber { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -835,7 +841,10 @@ function rightDerivativeToNominal(amount: linear, params: linear[]): linear { } } -function fromNominal(nominal: linear, params: linear[]): linear { +function fromNominal( + nominal: OldBigNumber, + params: OldBigNumber[] +): OldBigNumber { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -849,7 +858,10 @@ function fromNominal(nominal: linear, params: linear[]): linear { return nominal.minus(upperTarget.times(fee)).div(oneMinusFee); } } -function leftDerivativeFromNominal(amount: linear, params: linear[]): linear { +function leftDerivativeFromNominal( + amount: OldBigNumber, + params: OldBigNumber[] +): OldBigNumber { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; @@ -864,7 +876,10 @@ function leftDerivativeFromNominal(amount: linear, params: linear[]): linear { } } -function rightDerivativeFromNominal(amount: linear, params: linear[]): linear { +function rightDerivativeFromNominal( + amount: OldBigNumber, + params: OldBigNumber[] +): OldBigNumber { const fee = params[0]; const lowerTarget = params[2]; const upperTarget = params[3]; From 679a5abda6b8cf9e36fa8a3dbcd04bdf1cc2d38c Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Mon, 24 Jan 2022 19:58:07 -0300 Subject: [PATCH 27/43] introduce algorithm to get paths for multiple boosted pools --- src/pools/index.ts | 2 +- src/routeProposal/filtering.ts | 206 +++++++- src/routeProposal/index.ts | 1 + src/types.ts | 4 +- test/boostedPaths.spec.ts | 161 ++++++ test/lib/constants.ts | 8 + .../boostedPools/multipleBoosted.json | 463 ++++++++++++++++++ .../boostedPools/multipleBoostedContent.txt | 27 + 8 files changed, 869 insertions(+), 3 deletions(-) create mode 100644 test/boostedPaths.spec.ts create mode 100644 test/testData/boostedPools/multipleBoosted.json create mode 100644 test/testData/boostedPools/multipleBoostedContent.txt diff --git a/src/pools/index.ts b/src/pools/index.ts index 45ea9bc5..f1ca5ec0 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -54,7 +54,7 @@ export function parseNewPool( } else if (pool.poolType === 'Element') { newPool = ElementPool.fromPool(pool); newPool.setCurrentBlockTimestamp(currentBlockTimestamp); - } else if (pool.poolType === 'AaveLinear') + } else if (pool.poolType.toString().includes('Linear')) newPool = LinearPool.fromPool(pool); else if (pool.poolType === 'StablePhantom') newPool = PhantomStablePool.fromPool(pool); diff --git a/src/routeProposal/filtering.ts b/src/routeProposal/filtering.ts index 41cd138d..58bc6325 100644 --- a/src/routeProposal/filtering.ts +++ b/src/routeProposal/filtering.ts @@ -15,6 +15,7 @@ import { MetaStablePool } from '../pools/metaStablePool/metaStablePool'; import { ZERO } from '../utils/bignumber'; import { parseNewPool } from '../pools'; import { Zero } from '@ethersproject/constants'; +import { concat } from 'lodash'; export const filterPoolsByType = ( pools: SubgraphPoolBase[], @@ -203,10 +204,213 @@ export function filterHopPools( paths.push(path); } } - return [filteredPoolsOfInterest, paths]; } +/* +Return paths using boosted pools +*/ +export function getBoostedPaths( + tokenIn: string, + tokenOut: string, + poolsAllDict: PoolDictionary, + poolsFilteredDict: PoolDictionary, + config: SorConfig +): NewPath[] { + // To do: ensure that no duplicate paths are sent to the second part of the SOR. + const [semiPathsInToWeth, semiPathsInToBBausd] = getSemiPaths( + tokenIn.toLowerCase(), + true, + poolsAllDict, + config + ); + const [semiPathsWethToOut, semiPathsBBausdToOut] = getSemiPaths( + tokenOut.toLowerCase(), + false, + poolsAllDict, + config + ); + const paths1 = combineSemiPaths(semiPathsInToWeth, semiPathsWethToOut); + const paths2 = combineSemiPaths(semiPathsInToBBausd, semiPathsBBausdToOut); + let paths = paths1.concat(paths2); + + if (config.wethBBausd && config.BBausd) { + const WethBBausdPool = poolsAllDict[config.wethBBausd.id]; + const WethBBausdPath = createPath( + [config.weth, config.BBausd.address], + [WethBBausdPool] + ); + const BBausdWethPath = createPath( + [config.BBausd.address, config.weth], + [WethBBausdPool] + ); + const paths3 = combineSemiPaths( + semiPathsInToWeth, + semiPathsBBausdToOut, + WethBBausdPath + ); + const paths4 = combineSemiPaths( + semiPathsInToBBausd, + semiPathsWethToOut, + BBausdWethPath + ); + paths = paths.concat(paths3, paths4); + } + return paths; +} + +function combineSemiPaths( + semiPathsIn: NewPath[], + semiPathsOut: NewPath[], + intermediatePath?: NewPath +): NewPath[] { + const paths: NewPath[] = []; + if (intermediatePath) { + semiPathsIn = semiPathsIn.map((semiPathIn) => + composePaths([semiPathIn, intermediatePath]) + ); + } + for (const semiPathIn of semiPathsIn) { + for (const semiPathOut of semiPathsOut) { + paths.push(composePaths([semiPathIn, semiPathOut])); + } + } + return paths; +} + +function getSemiPaths( + token: string, + isTokenIn: boolean, + poolsAllDict: PoolDictionary, + config: SorConfig +): [semiPathsWeth: NewPath[], semiPathsBBausd: NewPath[]] { + let semiPathsWeth: NewPath[] = []; + let semiPathsBBausd: NewPath[] = []; + const linearPoolsWithThisToken: PoolDictionary = {}; + + for (const id in poolsAllDict) { + const pool = poolsAllDict[id]; + const tokensList = pool.tokensList.map((address) => + address.toLowerCase() + ); + if (tokensList.includes(token)) { + if (pool.poolType == PoolTypes.Linear) { + linearPoolsWithThisToken[id] = pool; + } + } + } + + if (token == config.weth.toLowerCase()) { + semiPathsWeth.push(getEmptyPath()); + } else if (token == config.BBausd?.address.toLowerCase()) { + semiPathsBBausd.push(getEmptyPath()); + } else { + const [directConnectionsWeth, directConnectionsBBausd] = + searchConnections(token, isTokenIn, poolsAllDict, config); + semiPathsWeth = directConnectionsWeth; + semiPathsBBausd = directConnectionsBBausd; + + for (const id in linearPoolsWithThisToken) { + const linearBpt = linearPoolsWithThisToken[id].address; + const linearPool = linearPoolsWithThisToken[id]; + const [linearBptConnectionsWeth, linearBptConnectionsBBausd] = + searchConnections(linearBpt, isTokenIn, poolsAllDict, config); + let newConnectionsBBausd: NewPath[]; + let newConnectionsWeth: NewPath[]; + if (isTokenIn) { + const linearPart = createPath([token, linearBpt], [linearPool]); + newConnectionsBBausd = linearBptConnectionsBBausd.map( + (connection) => composePaths([linearPart, connection]) + ); + newConnectionsWeth = linearBptConnectionsWeth.map( + (connection) => composePaths([linearPart, connection]) + ); + } else { + const linearPart = createPath([linearBpt, token], [linearPool]); + newConnectionsBBausd = linearBptConnectionsBBausd.map( + (connection) => composePaths([connection, linearPart]) + ); + newConnectionsWeth = linearBptConnectionsWeth.map( + (connection) => composePaths([connection, linearPart]) + ); + } + semiPathsWeth = concat(semiPathsWeth, newConnectionsWeth); + semiPathsBBausd = concat(semiPathsBBausd, newConnectionsBBausd); + } + } + return [semiPathsWeth, semiPathsBBausd]; +} + +function searchConnections( + token: string, + isTokenIn: boolean, + poolsAllDict: PoolDictionary, + config: SorConfig +): [NewPath[], NewPath[]] { + const poolsWithThisToken: PoolDictionary = {}; + for (const id in poolsAllDict) { + const pool = poolsAllDict[id]; + const tokensList = pool.tokensList.map((address) => + address.toLowerCase() + ); + if (tokensList.includes(token)) { + poolsWithThisToken[id] = pool; + } + } + const connectionsWeth: NewPath[] = []; + const connectionsBBausd: NewPath[] = []; + for (const id in poolsWithThisToken) { + const tokensList = poolsWithThisToken[id].tokensList.map((address) => + address.toLowerCase() + ); + if (tokensList.includes(config.weth.toLowerCase())) { + let semipath: NewPath; + if (isTokenIn) + semipath = createPath( + [token, config.weth], + [poolsWithThisToken[id]] + ); + else + semipath = createPath( + [config.weth, token], + [poolsWithThisToken[id]] + ); + connectionsWeth.push(semipath); + } + if ( + config.BBausd && + poolsWithThisToken[id].tokensList.includes( + config.BBausd.address.toLowerCase() + ) + ) { + let semipath: NewPath; + if (isTokenIn) + semipath = createPath( + [token, config.BBausd.address], + [poolsWithThisToken[id]] + ); + else + semipath = createPath( + [config.BBausd.address, token], + [poolsWithThisToken[id]] + ); + connectionsBBausd.push(semipath); + } + } + return [connectionsWeth, connectionsBBausd]; +} + +function getEmptyPath(): NewPath { + const emptyPath: NewPath = { + id: '', + swaps: [], + poolPairData: [], + limitAmount: Zero, // logically this should be infinity, but no practical difference expected + pools: [], + }; + return emptyPath; +} + /* Return paths through StaBal3 Linear pools. Paths can be created for any token that is paired to staBAL3 diff --git a/src/routeProposal/index.ts b/src/routeProposal/index.ts index 1e0a347e..dd187775 100644 --- a/src/routeProposal/index.ts +++ b/src/routeProposal/index.ts @@ -59,6 +59,7 @@ export class RouteProposer { poolsFilteredDict ); + // This must be changed to getBoostedPaths const pathsUsingLinear: NewPath[] = getLinearStaBal3Paths( tokenIn, tokenOut, diff --git a/src/types.ts b/src/types.ts index 3748bb59..0a8b78e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,8 +3,10 @@ import { BigNumber as OldBigNumber } from './utils/bignumber'; export interface SorConfig { chainId: number; - weth: string; vault: string; + weth: string; + BBausd?: { id: string; address: string }; + wethBBausd?: { id: string; address: string }; staBal3Pool?: { id: string; address: string }; wethStaBal3?: { id: string; address: string }; usdcConnectingPool?: { id: string; usdc: string }; diff --git a/test/boostedPaths.spec.ts b/test/boostedPaths.spec.ts new file mode 100644 index 00000000..5246e090 --- /dev/null +++ b/test/boostedPaths.spec.ts @@ -0,0 +1,161 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/linear.spec.ts +import { assert, expect } from 'chai'; +import cloneDeep from 'lodash.clonedeep'; +import { PoolDictionary, NewPath, SwapTypes, SubgraphPoolBase } from '../src'; +import { + filterPoolsOfInterest, + filterHopPools, + parseToPoolsDict, + getBoostedPaths, +} from '../src/routeProposal/filtering'; +import { calculatePathLimits } from '../src/routeProposal/pathLimits'; +import { checkPath } from './lib/testHelpers'; +import { + DAI, + aDAI, + bDAI, + USDC, + bUSDC, + BAL, + STABAL3PHANTOM, + TestToken, + MKR, + GUSD, + WETH, + TUSD, + bTUSD, + USDT, + LINEAR_AUSDT, + LINEAR_ADAI, + aUSDT, + KOVAN_BAL, + AAVE_USDT, + sorConfigTest, + sorConfigKovan, +} from './lib/constants'; + +// Multiple boosted pools +import boostedPools from './testData/boostedPools/multipleBoosted.json'; + +const maxPools = 10; +describe('Multiple boosted pools, path creation test', () => { + context('Case with no linear pools', () => { + it('TUSD-BAL', () => { + const tokenIn = TUSD.address; + const tokenOut = BAL.address; + const [, , paths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools + ); + assert.equal(paths.length, 4); + }); + it('BAL-TUSD', () => { + const tokenIn = BAL.address; + const tokenOut = TUSD.address; + const [, , paths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools + ); + assert.equal(paths.length, 4); + }); + }); + context('Case with linear pools', () => { + it('DAI-BAL', () => { + const tokenIn = DAI.address; + const tokenOut = BAL.address; + const [, , paths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools + ); + assert.equal(paths.length, 4); + }); + it('BAL-DAI', () => { + const tokenIn = BAL.address; + const tokenOut = DAI.address; + const [, , paths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools + ); + assert.equal(paths.length, 4); + }); + }); + context('BBausd and Weth to Dai', () => { + it('four combinations', () => { + const binaryOption = [true, false]; + for (const reverse of binaryOption) { + const tokens = [ + [WETH.address, sorConfigTest.BBausd.address], + [DAI.address], + ]; + if (reverse) tokens.reverse(); + for (const tokenIn of tokens[0]) { + for (const tokenOut of tokens[1]) { + const [, , paths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools + ); + assert.equal(paths.length, 2); + } + } + } + }); + }); + // To do: more thorough tests should be applied to verify correctness of paths +}); + +function getPaths( + tokenIn: string, + tokenOut: string, + swapType: SwapTypes, + pools: SubgraphPoolBase[], + maxPools: number +): [NewPath[], PoolDictionary, NewPath[]] { + const poolsAll = parseToPoolsDict(cloneDeep(pools), 0); + + const [poolsFilteredDict, hopTokens] = filterPoolsOfInterest( + poolsAll, + tokenIn, + tokenOut, + maxPools + ); + + let pathData: NewPath[] = []; + [, pathData] = filterHopPools( + tokenIn, + tokenOut, + hopTokens, + poolsFilteredDict + ); + + const boostedPaths = getBoostedPaths( + tokenIn, + tokenOut, + poolsAll, + poolsFilteredDict, + sorConfigTest + ); + for (const path of boostedPaths) { + console.log('Path begins'); + for (const swap of path.swaps) { + console.log(swap.tokenIn, ' ', swap.tokenOut); + } + } + pathData = pathData.concat(boostedPaths); + const [paths] = calculatePathLimits(pathData, swapType); + return [paths, poolsAll, boostedPaths]; +} diff --git a/test/lib/constants.ts b/test/lib/constants.ts index 398a1afa..9772c9bb 100644 --- a/test/lib/constants.ts +++ b/test/lib/constants.ts @@ -17,6 +17,14 @@ export const sorConfigTest = { id: 'staBal3Id', address: '0x06df3b2bbb68adc8b0e302443692037ed9f91b42', }, + BBausd: { + id: '0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8', + address: '0x8fd162f338b770f7e879030830cde9173367f301', + }, + wethBBausd: { + id: 'weightedWeth-BBausd', + address: '0x0000000000000000000000000000000000000004', + }, wethStaBal3: { id: 'weightedWethStaBal3Id', address: 'weightedWethStaBal3', diff --git a/test/testData/boostedPools/multipleBoosted.json b/test/testData/boostedPools/multipleBoosted.json new file mode 100644 index 00000000..91584bb7 --- /dev/null +++ b/test/testData/boostedPools/multipleBoosted.json @@ -0,0 +1,463 @@ +{ + "pools": [ + { + "address": "0x8fd162f338b770f7e879030830cde9173367f301", + "amp": "400", + "id": "0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8", + "mainIndex": 0, + "poolType": "StablePhantom", + "swapEnabled": true, + "swapFee": "0.01", + "tokens": [ + { + "address": "0x8fd162f338b770f7e879030830cde9173367f301", + "balance": "5192296858520354.535602194932501075", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-aUSD", + "weight": null + }, + { + "address": "0x3d1b554f1b1d1b6108b601ff22fea9c90fdfe50d", + "balance": "4817.7627626993040385", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-AUSDC", + "weight": null + }, + { + "address": "0x6a8c3239695613c0710dc971310b36f9b81e115e", + "balance": "4803.335614256025401", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-AUSDT", + "weight": null + }, + { + "address": "0xcd32a460b6fecd053582e43b07ed6e2c04e15369", + "balance": "4851.9948738150390637", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-ADAI", + "weight": null + } + ], + "tokensList": [ + "0x8fd162f338b770f7e879030830cde9173367f301", + "0x3d1b554f1b1d1b6108b601ff22fea9c90fdfe50d", + "0x6a8c3239695613c0710dc971310b36f9b81e115e", + "0xcd32a460b6fecd053582e43b07ed6e2c04e15369" + ], + "totalShares": "0", + "totalWeight": "0", + "wrappedIndex": 0 + }, + { + "address": "0x3d1b554f1b1d1b6108b601ff22fea9c90fdfe50d", + "id": "0x3d1b554f1b1d1b6108b601ff22fea9c90fdfe50d00000000000000000000023d", + "lowerTarget": "0", + "mainIndex": 2, + "poolType": "AaveLinear", + "swapEnabled": true, + "swapFee": "0.0001", + "tokens": [ + { + "address": "0x0fbddc06a4720408a2f5eb78e62bc31ac6e2a3c4", + "balance": "4900", + "decimals": 6, + "priceRate": "1.003625053612102865", + "symbol": "aUSDC", + "weight": null + }, + { + "address": "0x3d1b554f1b1d1b6108b601ff22fea9c90fdfe50d", + "balance": "5192296858529909.865767797025181595", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-aUSDC", + "weight": null + }, + { + "address": "0xe22da380ee6b445bb8273c81944adeb6e8450422", + "balance": "0", + "decimals": 6, + "priceRate": "1", + "symbol": "USDC", + "weight": null + } + ], + "tokensList": [ + "0x0fbddc06a4720408a2f5eb78e62bc31ac6e2a3c4", + "0x3d1b554f1b1d1b6108b601ff22fea9c90fdfe50d", + "0xe22da380ee6b445bb8273c81944adeb6e8450422" + ], + "totalShares": "4917.7627626993040385", + "totalWeight": "0", + "upperTarget": "2100000000000000000000000", + "wrappedIndex": 0 + }, + { + "address": "0x6a8c3239695613c0710dc971310b36f9b81e115e", + "id": "0x6a8c3239695613c0710dc971310b36f9b81e115e00000000000000000000023e", + "lowerTarget": "1000", + "mainIndex": 0, + "poolType": "AaveLinear", + "swapEnabled": true, + "swapFee": "0.0001", + "tokens": [ + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "balance": "100.060001", + "decimals": 6, + "priceRate": "1", + "symbol": "USDT", + "weight": null + }, + { + "address": "0x6a8c3239695613c0710dc971310b36f9b81e115e", + "balance": "5192296858529924.23290923961439575", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-aUSDT", + "weight": null + }, + { + "address": "0xe8191aacfcdb32260cda25830dc6c9342142f310", + "balance": "4800.058034", + "decimals": 6, + "priceRate": "1.00068073760327049", + "symbol": "aUSDT", + "weight": null + } + ], + "tokensList": [ + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0x6a8c3239695613c0710dc971310b36f9b81e115e", + "0xe8191aacfcdb32260cda25830dc6c9342142f310" + ], + "totalShares": "4903.395621256714824345", + "totalWeight": "0", + "upperTarget": "2100000000000000000000000", + "wrappedIndex": 2 + }, + { + "address": "0xcd32a460b6fecd053582e43b07ed6e2c04e15369", + "id": "0xcd32a460b6fecd053582e43b07ed6e2c04e1536900000000000000000000023c", + "lowerTarget": "0", + "mainIndex": 2, + "poolType": "AaveLinear", + "swapEnabled": true, + "swapFee": "0.0001", + "tokens": [ + { + "address": "0x4811a7bb9061a46753486e5e84b3cd3d668fb596", + "balance": "4300", + "decimals": 18, + "priceRate": "1.151626714840706759", + "symbol": "aDAI", + "weight": null + }, + { + "address": "0xcd32a460b6fecd053582e43b07ed6e2c04e15369", + "balance": "5192296858529875.633656681290156395", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-aDAI", + "weight": null + }, + { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "balance": "0", + "decimals": 18, + "priceRate": "1", + "symbol": "DAI", + "weight": null + } + ], + "tokensList": [ + "0x4811a7bb9061a46753486e5e84b3cd3d668fb596", + "0xcd32a460b6fecd053582e43b07ed6e2c04e15369", + "0x6b175474e89094c44da98b954eedeac495271d0f" + ], + "totalShares": "4951.9948738150390637", + "totalWeight": "0", + "upperTarget": "2100000000000000000000000", + "wrappedIndex": 0 + }, + { + "address": "0x0000000000000000000000000000000000000000", + "id": "0x0000000000000000000000000000000000000000000000000000000000000000", + "lowerTarget": "0", + "mainIndex": 2, + "poolType": "FuseLinear", + "swapEnabled": true, + "swapFee": "0.0001", + "tokens": [ + { + "address": "0x0000000000000000000000000000000000000001", + "balance": "13300", + "decimals": 18, + "priceRate": "1.051626714840706759", + "symbol": "fDAI", + "weight": null + }, + { + "address": "0x0000000000000000000000000000000000000000", + "balance": "5192296858529875.633656681290156395", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-fDAI", + "weight": null + }, + { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "balance": "0", + "decimals": 18, + "priceRate": "1", + "symbol": "DAI", + "weight": null + } + ], + "tokensList": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000", + "0x6b175474e89094c44da98b954eedeac495271d0f" + ], + "totalShares": "4951.9948738150390637", + "totalWeight": "0", + "upperTarget": "2100000000000000000000000", + "wrappedIndex": 0 + }, + { + "address": "0x0000000000000000000000000000000000000002", + "amp": "400", + "id": "0x0000000000000000000000000000000000000002000000000000000000000002", + "mainIndex": 0, + "poolType": "StablePhantom", + "swapEnabled": true, + "swapFee": "0.01", + "tokens": [ + { + "address": "0x0000000000000000000000000000000000000002", + "balance": "5192296858520354.535602194932501075", + "decimals": 18, + "priceRate": "1", + "symbol": "BBaUSD-BBfDAI", + "weight": null + }, + { + "address": "0x8fd162f338b770f7e879030830cde9173367f301", + "balance": "4817.7627626993040385", + "decimals": 18, + "priceRate": "1.2", + "symbol": "BB-aUSD", + "weight": null + }, + { + "address": "0x0000000000000000000000000000000000000000", + "balance": "4803.335614256025401", + "decimals": 18, + "priceRate": "1", + "symbol": "BB-fDAI", + "weight": null + } + ], + "tokensList": [ + "0x0000000000000000000000000000000000000002", + "0x8fd162f338b770f7e879030830cde9173367f301", + "0x0000000000000000000000000000000000000000" + ], + "totalShares": "14473.092928301396719020", + "totalWeight": "0", + "wrappedIndex": 0 + }, + { + "address": "0x0000000000000000000000000000000000000003", + "amp": "400", + "id": "0x0000000000000000000000000000000000000003000000000000000000000003", + "mainIndex": 0, + "poolType": "StablePhantom", + "swapEnabled": true, + "swapFee": "0.01", + "tokens": [ + { + "address": "0x0000000000000000000000000000000000000003", + "balance": "5192296858520354.535602194932501075", + "decimals": 18, + "priceRate": "1", + "symbol": "BBaUSD-TUSD", + "weight": null + }, + { + "address": "0x8fd162f338b770f7e879030830cde9173367f301", + "balance": "4817.7627626993040385", + "decimals": 18, + "priceRate": "1.2", + "symbol": "BB-aUSD", + "weight": null + }, + { + "address": "0x0000000000085d4780B73119b644AE5ecd22b376", + "balance": "4803.335614256025401", + "decimals": 18, + "priceRate": "1", + "symbol": "TUSD", + "weight": null + } + ], + "tokensList": [ + "0x0000000000000000000000000000000000000003", + "0x8fd162f338b770f7e879030830cde9173367f301", + "0x0000000000085d4780B73119b644AE5ecd22b376" + ], + "totalShares": "14473.092928301396719020", + "totalWeight": "0", + "wrappedIndex": 0 + }, + { + "address": "0x0000000000000000000000000000000000000013", + "amp": "400", + "id": "0x0000000000000000000000000000000000000013000000000000000000000013", + "mainIndex": 0, + "poolType": "StablePhantom", + "swapEnabled": true, + "swapFee": "0.01", + "tokens": [ + { + "address": "0x0000000000000000000000000000000000000013", + "balance": "5192296858520354.535602194932501075", + "decimals": 18, + "priceRate": "1", + "symbol": "BBaUSD-BAL", + "weight": null + }, + { + "address": "0x8fd162f338b770f7e879030830cde9173367f301", + "balance": "4817.7627626993040385", + "decimals": 18, + "priceRate": "1.2", + "symbol": "BB-aUSD", + "weight": null + }, + { + "address": "0xba100000625a3754423978a60c9317c58a424e3d", + "balance": "4803.335614256025401", + "decimals": 18, + "priceRate": "1", + "symbol": "BAL", + "weight": null + } + ], + "tokensList": [ + "0x0000000000000000000000000000000000000013", + "0x8fd162f338b770f7e879030830cde9173367f301", + "0xba100000625a3754423978a60c9317c58a424e3d" + ], + "totalShares": "14473.092928301396719020", + "totalWeight": "0", + "wrappedIndex": 0 + }, + { + "address": "0x0000000000000000000000000000000000000004", + "id": "weightedWeth-BBausd", + "poolType": "Weighted", + "publicSwap": true, + "swapFee": "0.01", + "swapEnabled": true, + "tokens": [ + { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "balance": "100000.000000", + "decimals": 18, + "weight": "50", + "id": "weightedWeth-BBausd-0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "symbol": "WETH", + "priceRate": "1" + }, + { + "address": "0x8fd162f338b770f7e879030830cde9173367f301", + "balance": "20000000.78213445", + "decimals": 18, + "weight": "50", + "id": "weightedWeth-BBausd-0x8fd162f338b770f7e879030830cde9173367f301", + "symbol": "BB-aUSD", + "priceRate": "1" + } + ], + "tokensList": [ + "0x8fd162f338b770f7e879030830cde9173367f301", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + ], + "totalWeight": "100", + "totalShares": "10000" + }, + { + "address": "0x0000000000000000000000000000000000000005", + "id": "weightedBalWeth", + "poolType": "Weighted", + "publicSwap": true, + "swapFee": "0.01", + "swapEnabled": true, + "tokens": [ + { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "balance": "100000.000000", + "decimals": 18, + "weight": "50", + "id": "weightedBalWeth-0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "symbol": "WETH", + "priceRate": "1" + }, + { + "address": "0xba100000625a3754423978a60c9317c58a424e3d", + "balance": "20000000.78213445", + "decimals": 18, + "weight": "50", + "id": "weightedBalWeth-0xba100000625a3754423978a60c9317c58a424e3d", + "symbol": "BAL", + "priceRate": "1" + } + ], + "tokensList": [ + "0xba100000625a3754423978a60c9317c58a424e3d", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + ], + "totalWeight": "100", + "totalShares": "10000" + }, + { + "address": "0x0000000000000000000000000000000000000006", + "id": "weightedTusdWeth", + "poolType": "Weighted", + "publicSwap": true, + "swapFee": "0.01", + "swapEnabled": true, + "tokens": [ + { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "balance": "100000.000000", + "decimals": 18, + "weight": "50", + "id": "weightedTusdWeth-0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "symbol": "WETH", + "priceRate": "1" + }, + { + "address": "0x0000000000085d4780B73119b644AE5ecd22b376", + "balance": "20000000.78213445", + "decimals": 18, + "weight": "50", + "id": "weightedTusdWeth-0x0000000000085d4780B73119b644AE5ecd22b376", + "symbol": "TUSD", + "priceRate": "1" + } + ], + "tokensList": [ + "0x0000000000085d4780B73119b644AE5ecd22b376", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + ], + "totalWeight": "100", + "totalShares": "10000" + } + ] +} diff --git a/test/testData/boostedPools/multipleBoostedContent.txt b/test/testData/boostedPools/multipleBoostedContent.txt new file mode 100644 index 00000000..59682d20 --- /dev/null +++ b/test/testData/boostedPools/multipleBoostedContent.txt @@ -0,0 +1,27 @@ +Pools by BPT name, or pair of tokens: + +BB-aUSDC +BB-aUSDT +BB-aDAI +BB-aUSD + +BB-fDAI +BBaUSD-BBfDAI [3] + +BBaUSD-TUSD [2] (StablePhantom) +BBaUSD-BAL (StablePhantom) + +WETH-BBaUSD [1] (weighted) +BAL-WETH (weighted) +TUSD-WETH (weighted) +______ + +path examples: + +TUSD > [2] > bbaUSD > [1] > WETH > [balWethPool] > BAL + +TUSD > [TusdWethPool] > WETH > [balWethPool] > BAL + +DAI > [2] > bbfDai > [3] > bbaUSD > [1] > weth > [balWethPool] > Bal + + From a7493a6d38aa87a2378f5c658e4bf984edf85a74 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Wed, 26 Jan 2022 17:26:55 -0300 Subject: [PATCH 28/43] major getBoostedPaths refactor --- src/routeProposal/filtering.ts | 293 +++++++++++++++++---------------- test/boostedPaths.spec.ts | 4 +- 2 files changed, 156 insertions(+), 141 deletions(-) diff --git a/src/routeProposal/filtering.ts b/src/routeProposal/filtering.ts index 58bc6325..b099f046 100644 --- a/src/routeProposal/filtering.ts +++ b/src/routeProposal/filtering.ts @@ -214,49 +214,137 @@ export function getBoostedPaths( tokenIn: string, tokenOut: string, poolsAllDict: PoolDictionary, - poolsFilteredDict: PoolDictionary, config: SorConfig ): NewPath[] { + tokenIn = tokenIn.toLowerCase(); + tokenOut = tokenOut.toLowerCase(); + // Should we consider the case (config.BBausd && !config.wethBBausd) ? + if (!config.BBausd || !config.wethBBausd) return []; // To do: ensure that no duplicate paths are sent to the second part of the SOR. - const [semiPathsInToWeth, semiPathsInToBBausd] = getSemiPaths( - tokenIn.toLowerCase(), - true, - poolsAllDict, - config + + const weth = config.weth.toLowerCase(); + const bbausd = config.BBausd.address.toLowerCase(); + + const linearPoolsTokenIn = getLinearPools(tokenIn, poolsAllDict); + const linearPoolsTokenOut = getLinearPools(tokenOut, poolsAllDict); + + const wethPoolsDict = getPoolsWith(weth, poolsAllDict); + // This avoids duplicate paths when weth is a token to trade + delete wethPoolsDict[config.wethBBausd.id]; + const bbausdPoolsDict = getPoolsWith(bbausd, poolsAllDict); + delete bbausdPoolsDict[config.wethBBausd.id]; + const semiPathsInToWeth: NewPath[] = getSemiPaths( + tokenIn, + linearPoolsTokenIn, + wethPoolsDict, + weth ); - const [semiPathsWethToOut, semiPathsBBausdToOut] = getSemiPaths( - tokenOut.toLowerCase(), - false, - poolsAllDict, - config + const semiPathsInToBBausd: NewPath[] = getSemiPaths( + tokenIn, + linearPoolsTokenIn, + bbausdPoolsDict, + bbausd + ); + const semiPathsOutToWeth: NewPath[] = getSemiPaths( + tokenOut, + linearPoolsTokenOut, + wethPoolsDict, + weth + ); + const semiPathsOutToBBausd: NewPath[] = getSemiPaths( + tokenOut, + linearPoolsTokenOut, + bbausdPoolsDict, + bbausd + ); + const semiPathsWethToOut = semiPathsOutToWeth.map((path) => + reversePath(path) + ); + const semiPathsBBausdToOut = semiPathsOutToBBausd.map((path) => + reversePath(path) ); const paths1 = combineSemiPaths(semiPathsInToWeth, semiPathsWethToOut); const paths2 = combineSemiPaths(semiPathsInToBBausd, semiPathsBBausdToOut); - let paths = paths1.concat(paths2); - if (config.wethBBausd && config.BBausd) { - const WethBBausdPool = poolsAllDict[config.wethBBausd.id]; - const WethBBausdPath = createPath( - [config.weth, config.BBausd.address], - [WethBBausdPool] + const WethBBausdPool = poolsAllDict[config.wethBBausd.id]; + const WethBBausdPath = createPath( + [config.weth, config.BBausd.address], + [WethBBausdPool] + ); + const BBausdWethPath = createPath( + [config.BBausd.address, config.weth], + [WethBBausdPool] + ); + const paths3 = combineSemiPaths( + semiPathsInToWeth, + semiPathsBBausdToOut, + WethBBausdPath + ); + const paths4 = combineSemiPaths( + semiPathsInToBBausd, + semiPathsWethToOut, + BBausdWethPath + ); + const paths = paths1.concat(paths2, paths3, paths4); + return paths; +} + +function getLinearPools( + token: string, + poolsAllDict: PoolDictionary +): PoolDictionary { + const linearPools: PoolDictionary = {}; + for (const id in poolsAllDict) { + const pool = poolsAllDict[id]; + const tokensList = pool.tokensList.map((address) => + address.toLowerCase() + ); + if (tokensList.includes(token) && pool.poolType == PoolTypes.Linear) { + linearPools[id] = pool; + } + } + return linearPools; +} + +function getPoolsWith(token: string, poolsDict: PoolDictionary) { + const poolsWithToken: PoolDictionary = {}; + for (const id in poolsDict) { + const pool = poolsDict[id]; + const tokensList = pool.tokensList.map((address) => + address.toLowerCase() ); - const BBausdWethPath = createPath( - [config.BBausd.address, config.weth], - [WethBBausdPool] + if (tokensList.includes(token)) { + poolsWithToken[id] = pool; + } + } + return poolsWithToken; +} + +function getSemiPaths( + token: string, + linearPoolstoken: PoolDictionary, + poolsDict: PoolDictionary, + toToken: string +): NewPath[] { + if (token == toToken) return [getEmptyPath()]; + let semiPaths = searchConnectionsTo(token, poolsDict, toToken); + for (const id in linearPoolstoken) { + const linearPool = linearPoolstoken[id]; + const simpleLinearPath = createPath( + [token, linearPool.address], + [linearPool] ); - const paths3 = combineSemiPaths( - semiPathsInToWeth, - semiPathsBBausdToOut, - WethBBausdPath + const connections = searchConnectionsTo( + linearPool.address, + poolsDict, + toToken ); - const paths4 = combineSemiPaths( - semiPathsInToBBausd, - semiPathsWethToOut, - BBausdWethPath + const newSemiPaths = connections.map((connection) => + composePaths([simpleLinearPath, connection]) ); - paths = paths.concat(paths3, paths4); + semiPaths = semiPaths.concat(newSemiPaths); } - return paths; + return semiPaths; } function combineSemiPaths( @@ -278,126 +366,53 @@ function combineSemiPaths( return paths; } -function getSemiPaths( +function searchConnectionsTo( token: string, - isTokenIn: boolean, - poolsAllDict: PoolDictionary, - config: SorConfig -): [semiPathsWeth: NewPath[], semiPathsBBausd: NewPath[]] { - let semiPathsWeth: NewPath[] = []; - let semiPathsBBausd: NewPath[] = []; - const linearPoolsWithThisToken: PoolDictionary = {}; - - for (const id in poolsAllDict) { - const pool = poolsAllDict[id]; + poolsDict: PoolDictionary, + toToken: string +): NewPath[] { + // this assumes that every pool in poolsDict contains toToken + const connections: NewPath[] = []; + for (const id in poolsDict) { + const pool = poolsDict[id]; const tokensList = pool.tokensList.map((address) => address.toLowerCase() ); if (tokensList.includes(token)) { - if (pool.poolType == PoolTypes.Linear) { - linearPoolsWithThisToken[id] = pool; - } + const connection = createPath([token, toToken], [pool]); + connections.push(connection); } } + return connections; +} - if (token == config.weth.toLowerCase()) { - semiPathsWeth.push(getEmptyPath()); - } else if (token == config.BBausd?.address.toLowerCase()) { - semiPathsBBausd.push(getEmptyPath()); - } else { - const [directConnectionsWeth, directConnectionsBBausd] = - searchConnections(token, isTokenIn, poolsAllDict, config); - semiPathsWeth = directConnectionsWeth; - semiPathsBBausd = directConnectionsBBausd; - - for (const id in linearPoolsWithThisToken) { - const linearBpt = linearPoolsWithThisToken[id].address; - const linearPool = linearPoolsWithThisToken[id]; - const [linearBptConnectionsWeth, linearBptConnectionsBBausd] = - searchConnections(linearBpt, isTokenIn, poolsAllDict, config); - let newConnectionsBBausd: NewPath[]; - let newConnectionsWeth: NewPath[]; - if (isTokenIn) { - const linearPart = createPath([token, linearBpt], [linearPool]); - newConnectionsBBausd = linearBptConnectionsBBausd.map( - (connection) => composePaths([linearPart, connection]) - ); - newConnectionsWeth = linearBptConnectionsWeth.map( - (connection) => composePaths([linearPart, connection]) - ); - } else { - const linearPart = createPath([linearBpt, token], [linearPool]); - newConnectionsBBausd = linearBptConnectionsBBausd.map( - (connection) => composePaths([connection, linearPart]) - ); - newConnectionsWeth = linearBptConnectionsWeth.map( - (connection) => composePaths([connection, linearPart]) - ); - } - semiPathsWeth = concat(semiPathsWeth, newConnectionsWeth); - semiPathsBBausd = concat(semiPathsBBausd, newConnectionsBBausd); - } +function reversePath(path: NewPath): NewPath { + let swaps = path.swaps.map((swap) => reverseSwap(swap)); + swaps = swaps.reverse(); + const poolPairData = path.poolPairData.reverse(); + const pools = path.pools.reverse(); + let id = ''; + for (const pool of pools) { + id += pool.address; } - return [semiPathsWeth, semiPathsBBausd]; + const result: NewPath = { + id: id, + swaps: swaps, + poolPairData: poolPairData, + limitAmount: Zero, // this is expected to be computed later + pools: pools, + }; + return result; } -function searchConnections( - token: string, - isTokenIn: boolean, - poolsAllDict: PoolDictionary, - config: SorConfig -): [NewPath[], NewPath[]] { - const poolsWithThisToken: PoolDictionary = {}; - for (const id in poolsAllDict) { - const pool = poolsAllDict[id]; - const tokensList = pool.tokensList.map((address) => - address.toLowerCase() - ); - if (tokensList.includes(token)) { - poolsWithThisToken[id] = pool; - } - } - const connectionsWeth: NewPath[] = []; - const connectionsBBausd: NewPath[] = []; - for (const id in poolsWithThisToken) { - const tokensList = poolsWithThisToken[id].tokensList.map((address) => - address.toLowerCase() - ); - if (tokensList.includes(config.weth.toLowerCase())) { - let semipath: NewPath; - if (isTokenIn) - semipath = createPath( - [token, config.weth], - [poolsWithThisToken[id]] - ); - else - semipath = createPath( - [config.weth, token], - [poolsWithThisToken[id]] - ); - connectionsWeth.push(semipath); - } - if ( - config.BBausd && - poolsWithThisToken[id].tokensList.includes( - config.BBausd.address.toLowerCase() - ) - ) { - let semipath: NewPath; - if (isTokenIn) - semipath = createPath( - [token, config.BBausd.address], - [poolsWithThisToken[id]] - ); - else - semipath = createPath( - [config.BBausd.address, token], - [poolsWithThisToken[id]] - ); - connectionsBBausd.push(semipath); - } - } - return [connectionsWeth, connectionsBBausd]; +function reverseSwap(swap: Swap): Swap { + return { + pool: swap.pool, + tokenIn: swap.tokenOut, + tokenOut: swap.tokenIn, + tokenInDecimals: swap.tokenOutDecimals, + tokenOutDecimals: swap.tokenInDecimals, + }; } function getEmptyPath(): NewPath { diff --git a/test/boostedPaths.spec.ts b/test/boostedPaths.spec.ts index 5246e090..a955e4a4 100644 --- a/test/boostedPaths.spec.ts +++ b/test/boostedPaths.spec.ts @@ -38,7 +38,7 @@ import { import boostedPools from './testData/boostedPools/multipleBoosted.json'; const maxPools = 10; -describe('Multiple boosted pools, path creation test', () => { +describe('multiple boosted pools, path creation test', () => { context('Case with no linear pools', () => { it('TUSD-BAL', () => { const tokenIn = TUSD.address; @@ -102,6 +102,7 @@ describe('Multiple boosted pools, path creation test', () => { if (reverse) tokens.reverse(); for (const tokenIn of tokens[0]) { for (const tokenOut of tokens[1]) { + console.log('getPaths begins'); const [, , paths] = getPaths( tokenIn, tokenOut, @@ -146,7 +147,6 @@ function getPaths( tokenIn, tokenOut, poolsAll, - poolsFilteredDict, sorConfigTest ); for (const path of boostedPaths) { From b03ae029b192aa8d08b32411263f84d107eeb0f6 Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Tue, 1 Feb 2022 11:57:05 +0000 Subject: [PATCH 29/43] Complete Gyro2 implementation --- src/pools/gyro2Pool/gyro2Math.ts | 243 ++++++++++++++++++------------ src/pools/gyro2Pool/gyro2Pool.ts | 245 +++++++++++++++++++++++-------- src/types.ts | 10 ++ 3 files changed, 347 insertions(+), 151 deletions(-) diff --git a/src/pools/gyro2Pool/gyro2Math.ts b/src/pools/gyro2Pool/gyro2Math.ts index 29412f85..2f1c5551 100644 --- a/src/pools/gyro2Pool/gyro2Math.ts +++ b/src/pools/gyro2Pool/gyro2Math.ts @@ -1,28 +1,29 @@ import { BigNumber } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; +import bn from 'bignumber.js'; // Swap limits: amounts swapped may not be larger than this percentage of total balance. const _MAX_IN_RATIO: BigNumber = BigNumber.from(0.3); const _MAX_OUT_RATIO: BigNumber = BigNumber.from(0.3); // Helpers -function _squareRoot(input: BigNumber): BigNumber { - return input.pow(BigNumber.from(1).div(BigNumber.from(2))); +function _squareRoot(value: BigNumber): BigNumber { + return BigNumber.from( + new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] + ); } - ///////// /// Fee calculations ///////// export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - const feeAmount = amountIn.mul(swapFee); + const feeAmount = amountIn.mul(swapFee).div(ONE); return amountIn.sub(feeAmount); } export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - return amountIn.div(ONE.sub(swapFee)); + return amountIn.mul(ONE).div(ONE.sub(swapFee)); } - ///////// /// Virtual Parameter calculations ///////// @@ -32,7 +33,10 @@ export function _findVirtualParams( sqrtAlpha: BigNumber, sqrtBeta: BigNumber ): [BigNumber, BigNumber] { - return [invariant.div(sqrtBeta), invariant.mul(sqrtAlpha)]; + return [ + invariant.mul(ONE).div(sqrtBeta), + invariant.mul(sqrtAlpha).div(ONE), + ]; } ///////// @@ -46,7 +50,7 @@ export function _calculateInvariant( ): BigNumber { /********************************************************************************************** // Calculate with quadratic formula - // 0 = (1-sqrt(alhpa/beta)*L^2 - (y/sqrt(beta)+x*sqrt(alpha))*L - x*y) + // 0 = (1-sqrt(alpha/beta)*L^2 - (y/sqrt(beta)+x*sqrt(alpha))*L - x*y) // 0 = a*L^2 + b*L + c // here a > 0, b < 0, and c < 0, which is a special case that works well w/o negative numbers // taking mb = -b and mc = -c: (1/2) @@ -57,7 +61,9 @@ export function _calculateInvariant( **********************************************************************************************/ const [a, mb, mc] = _calculateQuadraticTerms(balances, sqrtAlpha, sqrtBeta); - return _calculateQuadratic(a, mb, mc); + const invariant = _calculateQuadratic(a, mb, mc); + + return invariant; } function _calculateQuadraticTerms( @@ -65,11 +71,11 @@ function _calculateQuadraticTerms( sqrtAlpha: BigNumber, sqrtBeta: BigNumber ): [BigNumber, BigNumber, BigNumber] { - const a = BigNumber.from(1).sub(sqrtAlpha.div(sqrtBeta)); - const bterm0 = balances[1].div(sqrtBeta); - const bterm1 = balances[0].mul(sqrtAlpha); + const a = ONE.sub(sqrtAlpha.mul(ONE).div(sqrtBeta)); + const bterm0 = balances[1].mul(ONE).div(sqrtBeta); + const bterm1 = balances[0].mul(sqrtAlpha).div(ONE); const mb = bterm0.add(bterm1); - const mc = balances[0].mul(balances[1]); + const mc = balances[0].mul(balances[1]).div(ONE); return [a, mb, mc]; } @@ -80,14 +86,14 @@ function _calculateQuadratic( mc: BigNumber ): BigNumber { const denominator = a.mul(BigNumber.from(2)); - const bSquare = mb.mul(mb); - const addTerm = a.mul(mc.mul(BigNumber.from(4))); + const bSquare = mb.mul(mb).div(ONE); + const addTerm = a.mul(mc.mul(BigNumber.from(4))).div(ONE); // The minus sign in the radicand cancels out in this special case, so we add const radicand = bSquare.add(addTerm); const sqrResult = _squareRoot(radicand); // The minus sign in the numerator cancels out in this special case const numerator = mb.add(sqrResult); - const invariant = numerator.div(denominator); + const invariant = numerator.mul(ONE).div(denominator); return invariant; } @@ -106,29 +112,28 @@ export function _calcOutGivenIn( currentInvariant: BigNumber ): BigNumber { /********************************************************************************************** - // Described for X = `in' asset and Y = `out' asset, but equivalent for the other case // - // dX = incrX = amountIn > 0 // - // dY = incrY = amountOut < 0 // - // x = balanceIn x' = x + virtualParamX // - // y = balanceOut y' = y + virtualParamY // - // L = inv.Liq / L^2 \ // - // - dy = y' - | -------------------------- | // - // x' = virtIn \ ( x' + dX) / // - // y' = virtOut // - // Note that -dy > 0 is what the trader receives. // - // We exploit the fact that this formula is symmetric up to virtualParam{X,Y}. // - **********************************************************************************************/ - if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO))) + // Described for X = `in' asset and Y = `out' asset, but equivalent for the other case // + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // L = inv.Liq / L^2 \ // + // - dy = y' - | -------------------------- | // + // x' = virtIn \ ( x' + dX) / // + // y' = virtOut // + // Note that -dy > 0 is what the trader receives. // + // We exploit the fact that this formula is symmetric up to virtualParam{X,Y}. // + **********************************************************************************************/ + if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) throw new Error('Swap Amount Too Large'); const virtIn = balanceIn.add(virtualParamIn); const denominator = virtIn.add(amountIn); - const invSquare = currentInvariant.mul(currentInvariant); - const subtrahend = invSquare.div(denominator); + const invSquare = currentInvariant.mul(currentInvariant).div(ONE); + const subtrahend = invSquare.mul(ONE).div(denominator); const virtOut = balanceOut.add(virtualParamOut); return virtOut.sub(subtrahend); } - // SwapType = 'swapExactOut' export function _calcInGivenOut( balanceIn: BigNumber, @@ -151,13 +156,13 @@ export function _calcInGivenOut( // Note that dy < 0 < dx. // **********************************************************************************************/ - if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO))) + if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) throw new Error('Swap Amount Too Large'); const virtOut = balanceOut.add(virtualParamOut); const denominator = virtOut.sub(amountOut); - const invSquare = currentInvariant.mul(currentInvariant); - const term = invSquare.div(denominator); + const invSquare = currentInvariant.mul(currentInvariant).div(ONE); + const term = invSquare.mul(ONE).div(denominator); const virtIn = balanceIn.add(virtualParamIn); return term.sub(virtIn); } @@ -167,24 +172,31 @@ export function _calcInGivenOut( // ///////// export function _calculateNewSpotPrice( - newBalances: BigNumber[], - sqrtAlpha: BigNumber, - sqrtBeta: BigNumber + balances: BigNumber[], + inAmount: BigNumber, + outAmount: BigNumber, + virtualParamIn: BigNumber, + virtualParamOut: BigNumber, + swapFee: BigNumber ): BigNumber { - // Re-compute the liquidity invariant L based on these new balances. - // The invariant will be larger slightly larger than before because there are fees. - const newInvariant = _calculateInvariant(newBalances, sqrtAlpha, sqrtBeta); - - // Compute the offsets a and b based on the new liquidity invariant. - const [newVirtualParameterIn, newVirtualParameterOut] = _findVirtualParams( - newInvariant, - sqrtAlpha, - sqrtBeta - ); + /********************************************************************************************** + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // s = swapFee // + // L = inv.Liq 1 / x' + (1 - s) * dx \ // + // p_y = --- | -------------------------- | // + // x' = virtIn 1-s \ y' + dy / // + // y' = virtOut // + // Note that dy < 0 < dx. // + **********************************************************************************************/ - // Now compute (x + a) / (y + b) for the marginal price of asset y (out) denoted in units of asset x (in) - const numerator = newBalances[0].add(newVirtualParameterIn); - const denominator = newBalances[1].add(newVirtualParameterOut); + const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s + const virtIn = balances[0].add(virtualParamIn); // x + virtualParamX = x' + const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount)); // x' + (1 - s) * dx + const virtOut = balances[1].add(virtualParamOut); // y + virtualParamY = y' + const denominator = afterFeeMultiplier.mul(virtOut.sub(outAmount)); // (1 - s) * (y' + dy) const newSpotPrice = numerator.div(denominator); return newSpotPrice; @@ -194,45 +206,90 @@ export function _calculateNewSpotPrice( // /// Derivatives of spotPriceAfterSwap // ///////// -// // PairType = 'token->token' -// // SwapType = 'swapExactIn' -// export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( -// amount: OldBigNumber, -// poolPairData: WeightedPoolPairData -// ): OldBigNumber { -// const Bi = parseFloat( -// formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) -// ); -// const Bo = parseFloat( -// formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) -// ); -// const wi = parseFloat(formatFixed(poolPairData.weightIn, 18)); -// const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); -// const Ai = amount.toNumber(); -// const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); -// return bnum((wi + wo) / (Bo * (Bi / (Ai + Bi - Ai * f)) ** (wi / wo) * wi)); -// } - -// // PairType = 'token->token' -// // SwapType = 'swapExactOut' -// export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( -// amount: OldBigNumber, -// poolPairData: WeightedPoolPairData -// ): OldBigNumber { -// const Bi = parseFloat( -// formatFixed(poolPairData.balanceIn, poolPairData.decimalsIn) -// ); -// const Bo = parseFloat( -// formatFixed(poolPairData.balanceOut, poolPairData.decimalsOut) -// ); -// const wi = parseFloat(formatFixed(poolPairData.weightIn, 18)); -// const wo = parseFloat(formatFixed(poolPairData.weightOut, 18)); -// const Ao = amount.toNumber(); -// const f = parseFloat(formatFixed(poolPairData.swapFee, 18)); -// return bnum( -// -( -// (Bi * (Bo / (-Ao + Bo)) ** (wo / wi) * wo * (wi + wo)) / -// ((Ao - Bo) ** 2 * (-1 + f) * wi ** 2) -// ) -// ); -// } +// SwapType = 'swapExactIn' +export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + balances: BigNumber[], + outAmount: BigNumber, + virtualParamOut: BigNumber +): BigNumber { + /********************************************************************************************** + // dy = incrY = amountOut < 0 // + // + // y = balanceOut y' = y + virtualParamY = virtOut // + // // + // / 1 \ // + // (p_y)' = 2 | -------------------------- | // + // \ y' + dy / // + // // + // Note that dy < 0 // + **********************************************************************************************/ + + const TWO = BigNumber.from(2).mul(ONE); + const virtOut = balances[1].add(virtualParamOut); // y' = y + virtualParamY + const denominator = virtOut.sub(outAmount); // y' + dy + + const derivative = TWO.mul(ONE).div(denominator); + + return derivative; +} + +// SwapType = 'swapExactOut' +export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + balances: BigNumber[], + inAmount: BigNumber, + virtualParamIn: BigNumber, + outAmount: BigNumber, + virtualParamOut: BigNumber, + swapFee: BigNumber +): BigNumber { + /********************************************************************************************** + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // s = swapFee // + // L = inv.Liq 1 / x' + (1 - s) * dx \ // + // p_y = --- (2) | -------------------------- | // + // x' = virtIn 1-s \ (y' + dy)^2 / // + // y' = virtOut // + // Note that dy < 0 < dx. // + **********************************************************************************************/ + + const TWO = BigNumber.from(2).mul(ONE); + const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s + const virtIn = balances[0].add(virtualParamIn); // x + virtualParamX = x' + const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const virtOut = balances[1].add(virtualParamOut); // y + virtualParamY = y' + const denominator = virtOut + .sub(outAmount) + .mul(virtOut.sub(outAmount)) + .div(ONE); // (y' + dy)^2 + const factor = TWO.mul(ONE).div(afterFeeMultiplier); // 2 / (1 - s) + + const derivative = factor.mul(numerator.mul(ONE).div(denominator)).div(ONE); + + return derivative; +} + +// Normalized Liquidity +export function _getNormalizedLiquidity( + balances: BigNumber[], + virtualParamIn: BigNumber, + swapFee: BigNumber +): BigNumber { + /********************************************************************************************** + // x = balanceIn x' = x + virtualParamX // + // s = swapFee // + // 1 // + // normalizedLiquidity = --- x' // + // 1-s // + // x' = virtIn // + **********************************************************************************************/ + + const virtIn = balances[0].add(virtualParamIn); + const afterFeeMultiplier = ONE.sub(swapFee); + + const normalizedLiquidity = virtIn.mul(ONE).div(afterFeeMultiplier); + + return normalizedLiquidity; +} diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index 0c15edfb..44357092 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -1,6 +1,8 @@ import { getAddress } from '@ethersproject/address'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; import { parseFixed, formatFixed, BigNumber } from '@ethersproject/bignumber'; import { BigNumber as OldBigNumber, bnum } from '../../utils/bignumber'; + import { PoolBase, PoolPairBase, @@ -9,6 +11,7 @@ import { SubgraphToken, SwapTypes, SubgraphPoolBase, + PriceBoundData, } from 'types'; import { isSameAddress } from '../../utils'; import { @@ -19,8 +22,10 @@ import { _calculateNewSpotPrice, _reduceFee, _addFee, + _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, + _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, + _getNormalizedLiquidity, } from './gyro2Math'; -import { WeiPerEther as ONE } from '@ethersproject/constants'; export type Gyro2PoolPairData = PoolPairBase & { sqrtAlpha: BigNumber; @@ -32,7 +37,6 @@ export type Gyro2PoolToken = Pick< 'address' | 'balance' | 'decimals' >; -// TODO: getNormalizedLiquidity, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut implementations export class Gyro2Pool implements PoolBase { poolType: PoolTypes = PoolTypes.Gyro2; swapPairType: SwapPairType; @@ -42,19 +46,28 @@ export class Gyro2Pool implements PoolBase { tokens: Gyro2PoolToken[]; swapFee: BigNumber; totalShares: BigNumber; + priceBounds: PriceBoundData; // Max In/Out Ratios MAX_IN_RATIO = parseFixed('0.3', 18); MAX_OUT_RATIO = parseFixed('0.3', 18); static fromPool(pool: SubgraphPoolBase): Gyro2Pool { + if (!pool.priceBounds) throw new Error('Gyro2Pool missing priceBounds'); + const addresses = pool.tokens.map((t) => t.address); + if (!addresses.includes(pool.priceBounds.tokenInAddress)) + throw new Error('Gyro2Pool priceBounds tokenIn not in tokens'); + if (!addresses.includes(pool.priceBounds.tokenOutAddress)) + throw new Error('Gyro2Pool priceBounds tokenOut not in tokens'); + return new Gyro2Pool( pool.id, pool.address, pool.swapFee, pool.totalShares, - pool.tokens, - pool.tokensList + pool.tokens as Gyro2PoolToken[], + pool.tokensList, + pool.priceBounds as PriceBoundData ); } @@ -64,7 +77,8 @@ export class Gyro2Pool implements PoolBase { swapFee: string, totalShares: string, tokens: Gyro2PoolToken[], - tokensList: string[] + tokensList: string[], + priceBounds: PriceBoundData ) { this.id = id; this.address = address; @@ -72,6 +86,7 @@ export class Gyro2Pool implements PoolBase { this.totalShares = parseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; + this.priceBounds = priceBounds; } setTypeForSwap(type: SwapPairType): void { @@ -95,6 +110,20 @@ export class Gyro2Pool implements PoolBase { const balanceOut = tO.balance; const decimalsOut = tO.decimals; + const sqrtAlpha = + tI.address === this.priceBounds.tokenInAddress + ? parseFixed(this.priceBounds.lowerBound, 18).pow(1 / 2) + : ONE.div(parseFixed(this.priceBounds.upperBound, 18)).pow( + 1 / 2 + ); + + const sqrtBeta = + tI.address === this.priceBounds.tokenInAddress + ? parseFixed(this.priceBounds.upperBound, 18).pow(1 / 2) + : ONE.div(parseFixed(this.priceBounds.lowerBound, 18)).pow( + 1 / 2 + ); + // TODO: sqrtAlpha, sqrtBeta to be added const poolPairData: Gyro2PoolPairData = { id: this.id, @@ -107,13 +136,33 @@ export class Gyro2Pool implements PoolBase { balanceIn: parseFixed(balanceIn, decimalsIn), balanceOut: parseFixed(balanceOut, decimalsOut), swapFee: this.swapFee, + sqrtAlpha, + sqrtBeta, }; return poolPairData; } - // getNormalizedLiquidity(poolPairData: Gyro2PoolPairData): OldBigNumber { - // } + getNormalizedLiquidity(poolPairData: Gyro2PoolPairData): OldBigNumber { + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const invariant = _calculateInvariant( + balances, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const [virtualParamIn] = _findVirtualParams( + invariant, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const normalisedLiquidity = _getNormalizedLiquidity( + balances, + virtualParamIn, + poolPairData.swapFee + ); + + return bnum(formatFixed(normalisedLiquidity, poolPairData.decimalsOut)); + } getLimitAmountSwap( poolPairData: PoolPairBase, @@ -155,13 +204,13 @@ export class Gyro2Pool implements PoolBase { exact: boolean ): OldBigNumber { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const currentInvariant = _calculateInvariant( + const invariant = _calculateInvariant( balances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); const [virtualParamIn, virtualParamOut] = _findVirtualParams( - currentInvariant, + invariant, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); @@ -174,7 +223,7 @@ export class Gyro2Pool implements PoolBase { inAmountLessFee, virtualParamIn, virtualParamOut, - currentInvariant + invariant ); return bnum(formatFixed(outAmount, poolPairData.decimalsOut)); @@ -185,27 +234,29 @@ export class Gyro2Pool implements PoolBase { amount: OldBigNumber, exact: boolean ): OldBigNumber { + const outAmount = parseFixed( + amount.toString(), + poolPairData.decimalsOut + ); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const currentInvariant = _calculateInvariant( + const invariant = _calculateInvariant( balances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); const [virtualParamIn, virtualParamOut] = _findVirtualParams( - currentInvariant, + invariant, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); - const inAmountLessFee = _calcInGivenOut( poolPairData.balanceIn, poolPairData.balanceOut, - parseFixed(amount.toString(), poolPairData.decimalsOut), + outAmount, virtualParamIn, virtualParamOut, - currentInvariant + invariant ); - const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); return bnum(formatFixed(inAmount, poolPairData.decimalsIn)); @@ -215,72 +266,150 @@ export class Gyro2Pool implements PoolBase { poolPairData: Gyro2PoolPairData, amount: OldBigNumber ): OldBigNumber { - // Compute the reserve balances of the two assets after the swap (say x and y). This should include any fees. + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const invariant = _calculateInvariant( + balances, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const [virtualParamIn, virtualParamOut] = _findVirtualParams( + invariant, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); + const outAmount = _calcOutGivenIn( + poolPairData.balanceIn, + poolPairData.balanceOut, + inAmountLessFee, + virtualParamIn, + virtualParamOut, + invariant + ); + const newSpotPrice = _calculateNewSpotPrice( + balances, + inAmount, + outAmount, + virtualParamIn, + virtualParamOut, + poolPairData.swapFee + ); + return bnum(formatFixed(newSpotPrice, poolPairData.decimalsIn)); + } + _spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: Gyro2PoolPairData, + amount: OldBigNumber + ): OldBigNumber { const outAmount = parseFixed( - this._exactTokenInForTokenOut( - poolPairData, - amount, - false - ).toString(), + amount.toString(), poolPairData.decimalsOut ); + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const invariant = _calculateInvariant( + balances, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const [virtualParamIn, virtualParamOut] = _findVirtualParams( + invariant, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const inAmountLessFee = _calcInGivenOut( + poolPairData.balanceIn, + poolPairData.balanceOut, + outAmount, + virtualParamIn, + virtualParamOut, + invariant + ); + const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); + const newSpotPrice = _calculateNewSpotPrice( + balances, + inAmount, + outAmount, + virtualParamIn, + virtualParamOut, + poolPairData.swapFee + ); - const newBalances = [ - poolPairData.balanceIn.add(inAmount), - poolPairData.balanceOut.sub(outAmount), - ]; + return bnum(formatFixed(newSpotPrice, poolPairData.decimalsIn)); + } - const newSpotPrice = _calculateNewSpotPrice( - newBalances, + _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: Gyro2PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const invariant = _calculateInvariant( + balances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); + const [virtualParamIn, virtualParamOut] = _findVirtualParams( + invariant, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); + const outAmount = _calcOutGivenIn( + poolPairData.balanceIn, + poolPairData.balanceOut, + inAmountLessFee, + virtualParamIn, + virtualParamOut, + invariant + ); + const derivative = _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + balances, + outAmount, + virtualParamOut + ); - return bnum(formatFixed(newSpotPrice, poolPairData.decimalsIn)); + return bnum(formatFixed(derivative, poolPairData.decimalsIn)); } - _spotPriceAfterSwapTokenInForExactTokenOut( + _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( poolPairData: Gyro2PoolPairData, amount: OldBigNumber ): OldBigNumber { - // Compute the reserve balances of the two assets after the swap (say x and y). This should include any fees. const outAmount = parseFixed( amount.toString(), poolPairData.decimalsOut ); - - const inAmount = parseFixed( - this._tokenInForExactTokenOut( - poolPairData, - amount, - false - ).toString(), - poolPairData.decimalsIn + const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; + const invariant = _calculateInvariant( + balances, + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta ); - - const newBalances = [ - poolPairData.balanceIn.add(inAmount), - poolPairData.balanceOut.sub(outAmount), - ]; - - const newSpotPrice = _calculateNewSpotPrice( - newBalances, + const [virtualParamIn, virtualParamOut] = _findVirtualParams( + invariant, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); + const inAmountLessFee = _calcInGivenOut( + poolPairData.balanceIn, + poolPairData.balanceOut, + outAmount, + virtualParamIn, + virtualParamOut, + invariant + ); + const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); - return bnum(formatFixed(newSpotPrice, poolPairData.decimalsIn)); - } - - // _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( - // poolPairData: Gyro2PoolPairData, - // amount: OldBigNumber - // ): OldBigNumber {} + const derivative = _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + balances, + inAmount, + virtualParamIn, + outAmount, + virtualParamOut, + poolPairData.swapFee + ); - // _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( - // poolPairData: Gyro2PoolPairData, - // amount: OldBigNumber - // ): OldBigNumber {} + return bnum(formatFixed(derivative, poolPairData.decimalsIn)); + } } diff --git a/src/types.ts b/src/types.ts index 0545e1db..30d3f994 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,6 +94,9 @@ export interface SubgraphPoolBase { wrappedIndex?: number; lowerTarget?: string; upperTarget?: string; + + // Gyro2 specific field + priceBounds?: PriceBoundData; } export type SubgraphToken = { @@ -214,3 +217,10 @@ export interface TokenPriceService { export interface PoolDataService { getPools(): Promise; } + +export type PriceBoundData = { + lowerBound: string; + upperBound: string; + tokenInAddress: string; + tokenOutAddress: string; +}; From 90542c64c58e0d9e5ef3e3e0080501efc44ce5a0 Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Tue, 1 Feb 2022 13:49:38 +0000 Subject: [PATCH 30/43] Add balances decimal normalization --- src/pools/gyro2Pool/gyro2Math.ts | 55 +++++++++----- src/pools/gyro2Pool/gyro2Pool.ts | 123 ++++++++++++++++++++++--------- 2 files changed, 123 insertions(+), 55 deletions(-) diff --git a/src/pools/gyro2Pool/gyro2Math.ts b/src/pools/gyro2Pool/gyro2Math.ts index 2f1c5551..813ba1d2 100644 --- a/src/pools/gyro2Pool/gyro2Math.ts +++ b/src/pools/gyro2Pool/gyro2Math.ts @@ -1,17 +1,34 @@ -import { BigNumber } from '@ethersproject/bignumber'; +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; import bn from 'bignumber.js'; // Swap limits: amounts swapped may not be larger than this percentage of total balance. -const _MAX_IN_RATIO: BigNumber = BigNumber.from(0.3); -const _MAX_OUT_RATIO: BigNumber = BigNumber.from(0.3); + +const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); +const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); // Helpers -function _squareRoot(value: BigNumber): BigNumber { +export function _squareRoot(value: BigNumber): BigNumber { return BigNumber.from( new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] ); } + +export function _normalizeBalances( + balances: BigNumber[], + decimalsIn: number, + decimalsOut: number +): BigNumber[] { + const scalingFactors = [ + parseFixed('1', decimalsIn), + parseFixed('1', decimalsOut), + ]; + + return balances.map((bal, index) => + bal.mul(ONE).div(scalingFactors[index]) + ); +} + ///////// /// Fee calculations ///////// @@ -180,24 +197,24 @@ export function _calculateNewSpotPrice( swapFee: BigNumber ): BigNumber { /********************************************************************************************** - // dX = incrX = amountIn > 0 // - // dY = incrY = amountOut < 0 // - // x = balanceIn x' = x + virtualParamX // - // y = balanceOut y' = y + virtualParamY // - // s = swapFee // - // L = inv.Liq 1 / x' + (1 - s) * dx \ // - // p_y = --- | -------------------------- | // - // x' = virtIn 1-s \ y' + dy / // - // y' = virtOut // - // Note that dy < 0 < dx. // - **********************************************************************************************/ + // dX = incrX = amountIn > 0 // + // dY = incrY = amountOut < 0 // + // x = balanceIn x' = x + virtualParamX // + // y = balanceOut y' = y + virtualParamY // + // s = swapFee // + // L = inv.Liq 1 / x' + (1 - s) * dx \ // + // p_y = --- | -------------------------- | // + // x' = virtIn 1-s \ y' + dy / // + // y' = virtOut // + // Note that dy < 0 < dx. // + **********************************************************************************************/ const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualParamIn); // x + virtualParamX = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount)); // x' + (1 - s) * dx + const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualParamOut); // y + virtualParamY = y' - const denominator = afterFeeMultiplier.mul(virtOut.sub(outAmount)); // (1 - s) * (y' + dy) - const newSpotPrice = numerator.div(denominator); + const denominator = afterFeeMultiplier.mul(virtOut.sub(outAmount)).div(ONE); // (1 - s) * (y' + dy) + const newSpotPrice = numerator.mul(ONE).div(denominator); return newSpotPrice; } @@ -237,8 +254,8 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( balances: BigNumber[], inAmount: BigNumber, - virtualParamIn: BigNumber, outAmount: BigNumber, + virtualParamIn: BigNumber, virtualParamOut: BigNumber, swapFee: BigNumber ): BigNumber { diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index 44357092..b52eb2b3 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -15,6 +15,8 @@ import { } from 'types'; import { isSameAddress } from '../../utils'; import { + _squareRoot, + _normalizeBalances, _calculateInvariant, _calcOutGivenIn, _calcInGivenOut, @@ -54,10 +56,20 @@ export class Gyro2Pool implements PoolBase { static fromPool(pool: SubgraphPoolBase): Gyro2Pool { if (!pool.priceBounds) throw new Error('Gyro2Pool missing priceBounds'); - const addresses = pool.tokens.map((t) => t.address); - if (!addresses.includes(pool.priceBounds.tokenInAddress)) + const tokenInAddress = pool.priceBounds.tokenInAddress; + const tokenOutAddress = pool.priceBounds.tokenOutAddress; + + const tokenInIndex = pool.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenInAddress) + ); + const tokenOutIndex = pool.tokens.findIndex( + (t) => getAddress(t.address) === getAddress(tokenOutAddress) + ); + + if (tokenInIndex < 0) throw new Error('Gyro2Pool priceBounds tokenIn not in tokens'); - if (!addresses.includes(pool.priceBounds.tokenOutAddress)) + + if (tokenOutIndex < 0) throw new Error('Gyro2Pool priceBounds tokenOut not in tokens'); return new Gyro2Pool( @@ -110,19 +122,23 @@ export class Gyro2Pool implements PoolBase { const balanceOut = tO.balance; const decimalsOut = tO.decimals; - const sqrtAlpha = - tI.address === this.priceBounds.tokenInAddress - ? parseFixed(this.priceBounds.lowerBound, 18).pow(1 / 2) - : ONE.div(parseFixed(this.priceBounds.upperBound, 18)).pow( - 1 / 2 - ); - - const sqrtBeta = - tI.address === this.priceBounds.tokenInAddress - ? parseFixed(this.priceBounds.upperBound, 18).pow(1 / 2) - : ONE.div(parseFixed(this.priceBounds.lowerBound, 18)).pow( - 1 / 2 - ); + const sqrtAlpha = isSameAddress( + tI.address, + this.priceBounds.tokenInAddress + ) + ? _squareRoot(parseFixed(this.priceBounds.lowerBound, 18)) + : _squareRoot( + ONE.mul(ONE).div(parseFixed(this.priceBounds.upperBound, 18)) + ); + + const sqrtBeta = isSameAddress( + tI.address, + this.priceBounds.tokenInAddress + ) + ? _squareRoot(parseFixed(this.priceBounds.upperBound, 18)) + : _squareRoot( + ONE.mul(ONE).div(parseFixed(this.priceBounds.lowerBound, 18)) + ); // TODO: sqrtAlpha, sqrtBeta to be added const poolPairData: Gyro2PoolPairData = { @@ -145,8 +161,13 @@ export class Gyro2Pool implements PoolBase { getNormalizedLiquidity(poolPairData: Gyro2PoolPairData): OldBigNumber { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const invariant = _calculateInvariant( + const normalizedBalances = _normalizeBalances( balances, + poolPairData.decimalsIn, + poolPairData.decimalsOut + ); + const invariant = _calculateInvariant( + normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); @@ -156,12 +177,12 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtBeta ); const normalisedLiquidity = _getNormalizedLiquidity( - balances, + normalizedBalances, virtualParamIn, poolPairData.swapFee ); - return bnum(formatFixed(normalisedLiquidity, poolPairData.decimalsOut)); + return bnum(formatFixed(normalisedLiquidity, 18)); } getLimitAmountSwap( @@ -204,8 +225,13 @@ export class Gyro2Pool implements PoolBase { exact: boolean ): OldBigNumber { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const invariant = _calculateInvariant( + const normalizedBalances = _normalizeBalances( balances, + poolPairData.decimalsIn, + poolPairData.decimalsOut + ); + const invariant = _calculateInvariant( + normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); @@ -226,7 +252,7 @@ export class Gyro2Pool implements PoolBase { invariant ); - return bnum(formatFixed(outAmount, poolPairData.decimalsOut)); + return bnum(formatFixed(outAmount, 18)); } _tokenInForExactTokenOut( @@ -239,8 +265,13 @@ export class Gyro2Pool implements PoolBase { poolPairData.decimalsOut ); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const invariant = _calculateInvariant( + const normalizedBalances = _normalizeBalances( balances, + poolPairData.decimalsIn, + poolPairData.decimalsOut + ); + const invariant = _calculateInvariant( + normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); @@ -259,7 +290,7 @@ export class Gyro2Pool implements PoolBase { ); const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); - return bnum(formatFixed(inAmount, poolPairData.decimalsIn)); + return bnum(formatFixed(inAmount, 18)); } _spotPriceAfterSwapExactTokenInForTokenOut( @@ -267,8 +298,13 @@ export class Gyro2Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const invariant = _calculateInvariant( + const normalizedBalances = _normalizeBalances( balances, + poolPairData.decimalsIn, + poolPairData.decimalsOut + ); + const invariant = _calculateInvariant( + normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); @@ -288,14 +324,14 @@ export class Gyro2Pool implements PoolBase { invariant ); const newSpotPrice = _calculateNewSpotPrice( - balances, + normalizedBalances, inAmount, outAmount, virtualParamIn, virtualParamOut, poolPairData.swapFee ); - return bnum(formatFixed(newSpotPrice, poolPairData.decimalsIn)); + return bnum(formatFixed(newSpotPrice, 18)); } _spotPriceAfterSwapTokenInForExactTokenOut( @@ -307,8 +343,13 @@ export class Gyro2Pool implements PoolBase { poolPairData.decimalsOut ); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const invariant = _calculateInvariant( + const normalizedBalances = _normalizeBalances( balances, + poolPairData.decimalsIn, + poolPairData.decimalsOut + ); + const invariant = _calculateInvariant( + normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); @@ -327,7 +368,7 @@ export class Gyro2Pool implements PoolBase { ); const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); const newSpotPrice = _calculateNewSpotPrice( - balances, + normalizedBalances, inAmount, outAmount, virtualParamIn, @@ -335,7 +376,7 @@ export class Gyro2Pool implements PoolBase { poolPairData.swapFee ); - return bnum(formatFixed(newSpotPrice, poolPairData.decimalsIn)); + return bnum(formatFixed(newSpotPrice, 18)); } _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( @@ -343,8 +384,13 @@ export class Gyro2Pool implements PoolBase { amount: OldBigNumber ): OldBigNumber { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const invariant = _calculateInvariant( + const normalizedBalances = _normalizeBalances( balances, + poolPairData.decimalsIn, + poolPairData.decimalsOut + ); + const invariant = _calculateInvariant( + normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); @@ -364,12 +410,12 @@ export class Gyro2Pool implements PoolBase { invariant ); const derivative = _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( - balances, + normalizedBalances, outAmount, virtualParamOut ); - return bnum(formatFixed(derivative, poolPairData.decimalsIn)); + return bnum(formatFixed(derivative, 18)); } _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( @@ -381,8 +427,13 @@ export class Gyro2Pool implements PoolBase { poolPairData.decimalsOut ); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; - const invariant = _calculateInvariant( + const normalizedBalances = _normalizeBalances( balances, + poolPairData.decimalsIn, + poolPairData.decimalsOut + ); + const invariant = _calculateInvariant( + normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); @@ -402,14 +453,14 @@ export class Gyro2Pool implements PoolBase { const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); const derivative = _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( - balances, + normalizedBalances, inAmount, - virtualParamIn, outAmount, + virtualParamIn, virtualParamOut, poolPairData.swapFee ); - return bnum(formatFixed(derivative, poolPairData.decimalsIn)); + return bnum(formatFixed(derivative, 18)); } } From cd25dbcfdfe56817b8982939de0fea28aa9eec9a Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Thu, 3 Feb 2022 18:47:07 -0300 Subject: [PATCH 31/43] improve boosted paths algorithm, adapt test cases --- src/routeProposal/filtering.ts | 123 ++++++++++--- src/types.ts | 2 +- test/boostedPaths.spec.ts | 44 +++-- test/lib/constants.ts | 13 +- test/lib/testHelpers.ts | 20 +++ test/linear.spec.ts | 165 ++++++++++-------- test/phantomStableMath.spec.ts | 6 +- test/phantomStablePools.spec.ts | 18 +- .../boostedPools/multipleBoosted.json | 40 ++--- test/testData/linearPools/smallLinear.json | 34 ++-- 10 files changed, 290 insertions(+), 175 deletions(-) diff --git a/src/routeProposal/filtering.ts b/src/routeProposal/filtering.ts index b099f046..f2110507 100644 --- a/src/routeProposal/filtering.ts +++ b/src/routeProposal/filtering.ts @@ -16,6 +16,7 @@ import { ZERO } from '../utils/bignumber'; import { parseNewPool } from '../pools'; import { Zero } from '@ethersproject/constants'; import { concat } from 'lodash'; +import path from 'path'; export const filterPoolsByType = ( pools: SubgraphPoolBase[], @@ -208,7 +209,34 @@ export function filterHopPools( } /* -Return paths using boosted pools +Returns paths using boosted pools. +Relevant paths using boosted pools have length greater than 2, so we need +a separate algorithm to create those paths. +We consider two central tokens: WETH and bbaUSD (which is the BPT of aave boosted-stable +pool). We want to consider paths in which token_in and token_out are connected +(each of them) to either WETH or bbaUSD. Here for a token A to be "connected" to +a token B means that it satisfies one of the following: +(a) A is B. +(b) A and B belong to the same pool. +(c) A has a linear pool whose BPT belongs to a pool jointly with B. +For the case of LBPs we will probably consider extra connections. + +Thus for token_in and token_out we generate every semipath connecting them +to one of the central tokens. After that we combine semipaths to form +paths from token_in to token_out. We expect to have a central pool +WETH/bbaUSD. We use this pool to combine a semipath connecting to WETH with a +semipath connecting to bbaUSD. + + Issues + + a) when trading DAI/USDC, this finds the path + DAI-bbaDAI-bbaUSD-bbaUSDC-USDC instead of directly + DAI-bbaDAI-bbaUSDC-USDC + Possible solution: add a patch to combineSemiPaths for the case where + the end pool of the first semipath matches the first pool of the second one. + + b) For DAI/aDAI it finds complicated paths through bbaUSD pool + (to do, just in case: inspect those paths) */ export function getBoostedPaths( tokenIn: string, @@ -218,21 +246,27 @@ export function getBoostedPaths( ): NewPath[] { tokenIn = tokenIn.toLowerCase(); tokenOut = tokenOut.toLowerCase(); - // Should we consider the case (config.BBausd && !config.wethBBausd) ? - if (!config.BBausd || !config.wethBBausd) return []; + // We assume consistency between config and poolsAllDict. + // If they are not consistent, there will be errors. + if (!config.bbausd) return []; // To do: ensure that no duplicate paths are sent to the second part of the SOR. const weth = config.weth.toLowerCase(); - const bbausd = config.BBausd.address.toLowerCase(); + const bbausd = config.bbausd.address.toLowerCase(); + // getLinearPools could receive an array of tokens so that we search + // over poolsAllDict once instead of twice. Similarly for getPoolsWith. + // This is a matter of code simplicity vs. efficiency const linearPoolsTokenIn = getLinearPools(tokenIn, poolsAllDict); const linearPoolsTokenOut = getLinearPools(tokenOut, poolsAllDict); const wethPoolsDict = getPoolsWith(weth, poolsAllDict); - // This avoids duplicate paths when weth is a token to trade - delete wethPoolsDict[config.wethBBausd.id]; const bbausdPoolsDict = getPoolsWith(bbausd, poolsAllDict); - delete bbausdPoolsDict[config.wethBBausd.id]; + if (config.wethBBausd) { + // This avoids duplicate paths when weth is a token to trade + delete wethPoolsDict[config.wethBBausd.id]; + delete bbausdPoolsDict[config.wethBBausd.id]; + } const semiPathsInToWeth: NewPath[] = getSemiPaths( tokenIn, linearPoolsTokenIn, @@ -263,16 +297,26 @@ export function getBoostedPaths( const semiPathsBBausdToOut = semiPathsOutToBBausd.map((path) => reversePath(path) ); - const paths1 = combineSemiPaths(semiPathsInToWeth, semiPathsWethToOut); - const paths2 = combineSemiPaths(semiPathsInToBBausd, semiPathsBBausdToOut); - + // Here we assume that every short path (short means length 1 and 2) is included + // in filterHopPools. This should include things like DAI->bbfDAI->WETH or + // BAL->WETH->TUSD, WETH->bbaUSD, etc. + const paths1 = removeShortPaths( + combineSemiPaths(semiPathsInToWeth, semiPathsWethToOut) + ); + const paths2 = removeShortPaths( + combineSemiPaths(semiPathsInToBBausd, semiPathsBBausdToOut) + ); + if (!config.wethBBausd) { + const paths = paths1.concat(paths2); + return paths; + } const WethBBausdPool = poolsAllDict[config.wethBBausd.id]; const WethBBausdPath = createPath( - [config.weth, config.BBausd.address], + [config.weth, config.bbausd.address], [WethBBausdPool] ); const BBausdWethPath = createPath( - [config.BBausd.address, config.weth], + [config.bbausd.address, config.weth], [WethBBausdPool] ); const paths3 = combineSemiPaths( @@ -360,9 +404,12 @@ function combineSemiPaths( } for (const semiPathIn of semiPathsIn) { for (const semiPathOut of semiPathsOut) { - paths.push(composePaths([semiPathIn, semiPathOut])); + const path = composeSimplifyPath(semiPathIn, semiPathOut); + if (path) paths.push(path); + // paths.push(composePaths([semiPathIn, semiPathOut])); } } + return paths; } @@ -386,23 +433,18 @@ function searchConnectionsTo( return connections; } +function removeShortPaths(paths: NewPath[]): NewPath[] { + // return paths; + const answer = paths.filter((path) => path.swaps.length > 2); + return answer; +} + function reversePath(path: NewPath): NewPath { - let swaps = path.swaps.map((swap) => reverseSwap(swap)); - swaps = swaps.reverse(); - const poolPairData = path.poolPairData.reverse(); + if (path.pools.length == 0) return getEmptyPath(); const pools = path.pools.reverse(); - let id = ''; - for (const pool of pools) { - id += pool.address; - } - const result: NewPath = { - id: id, - swaps: swaps, - poolPairData: poolPairData, - limitAmount: Zero, // this is expected to be computed later - pools: pools, - }; - return result; + const tokens = path.swaps.map((swap) => swap.tokenOut).reverse(); + tokens.push(path.swaps[0].tokenIn); + return createPath(tokens, pools); } function reverseSwap(swap: Swap): Swap { @@ -856,3 +898,28 @@ export function parseToPoolsDict( .filter(([, pool]) => pool !== undefined) ); } + +function composeSimplifyPath(semiPathIn: NewPath, semiPathOut: NewPath) { + let path: NewPath; + if (semiPathIn.pools[semiPathIn.pools.length - 1] == semiPathOut.pools[0]) { + const newPoolsIn = semiPathIn.pools.slice(0, -1); + const newTokensIn = semiPathIn.swaps.map((swap) => swap.tokenIn); + const tokensOut = semiPathOut.swaps.map((swap) => swap.tokenOut); + path = createPath( + newTokensIn.concat(tokensOut), + newPoolsIn.concat(semiPathOut.pools) + ); + for (const leftPool of newPoolsIn) { + for (const rightPool of semiPathOut.pools) { + if (leftPool == rightPool) { + console.log('leftPool: ', leftPool); + console.log('rightPool: ', rightPool); + return null; + } + } + } + } else { + path = composePaths([semiPathIn, semiPathOut]); + } + return path; +} diff --git a/src/types.ts b/src/types.ts index 0a8b78e8..df9b68c8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,7 @@ export interface SorConfig { chainId: number; vault: string; weth: string; - BBausd?: { id: string; address: string }; + bbausd?: { id: string; address: string }; wethBBausd?: { id: string; address: string }; staBal3Pool?: { id: string; address: string }; wethStaBal3?: { id: string; address: string }; diff --git a/test/boostedPaths.spec.ts b/test/boostedPaths.spec.ts index a955e4a4..2e81fb11 100644 --- a/test/boostedPaths.spec.ts +++ b/test/boostedPaths.spec.ts @@ -9,7 +9,7 @@ import { getBoostedPaths, } from '../src/routeProposal/filtering'; import { calculatePathLimits } from '../src/routeProposal/pathLimits'; -import { checkPath } from './lib/testHelpers'; +import { simpleCheckPath } from './lib/testHelpers'; import { DAI, aDAI, @@ -17,7 +17,6 @@ import { USDC, bUSDC, BAL, - STABAL3PHANTOM, TestToken, MKR, GUSD, @@ -32,6 +31,7 @@ import { AAVE_USDT, sorConfigTest, sorConfigKovan, + bbaUSD, } from './lib/constants'; // Multiple boosted pools @@ -43,19 +43,27 @@ describe('multiple boosted pools, path creation test', () => { it('TUSD-BAL', () => { const tokenIn = TUSD.address; const tokenOut = BAL.address; - const [, , paths] = getPaths( + const [paths, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, boostedPools.pools, maxPools ); + assert.isTrue( + simpleCheckPath( + paths[1], + ['weightedTusdWeth', 'weightedWeth-BBausd', 'bbaUSD-BAL'], + [TUSD.address, WETH.address, bbaUSD.address, BAL.address] + ) + ); + assert.equal(boostedPaths.length, 2); assert.equal(paths.length, 4); }); it('BAL-TUSD', () => { const tokenIn = BAL.address; const tokenOut = TUSD.address; - const [, , paths] = getPaths( + const [paths, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, @@ -69,7 +77,7 @@ describe('multiple boosted pools, path creation test', () => { it('DAI-BAL', () => { const tokenIn = DAI.address; const tokenOut = BAL.address; - const [, , paths] = getPaths( + const [paths, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, @@ -81,7 +89,7 @@ describe('multiple boosted pools, path creation test', () => { it('BAL-DAI', () => { const tokenIn = BAL.address; const tokenOut = DAI.address; - const [, , paths] = getPaths( + const [paths, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, @@ -91,19 +99,18 @@ describe('multiple boosted pools, path creation test', () => { assert.equal(paths.length, 4); }); }); - context('BBausd and Weth to Dai', () => { + context('bbausd and Weth to Dai', () => { it('four combinations', () => { const binaryOption = [true, false]; for (const reverse of binaryOption) { const tokens = [ - [WETH.address, sorConfigTest.BBausd.address], + [WETH.address, sorConfigTest.bbausd.address], [DAI.address], ]; if (reverse) tokens.reverse(); for (const tokenIn of tokens[0]) { for (const tokenOut of tokens[1]) { - console.log('getPaths begins'); - const [, , paths] = getPaths( + const [paths, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, @@ -111,12 +118,21 @@ describe('multiple boosted pools, path creation test', () => { maxPools ); assert.equal(paths.length, 2); + if ( + tokenIn == WETH.address || + tokenOut == WETH.address + ) { + assert.equal(boostedPaths.length, 2); + } else { + assert.equal(boostedPaths.length, 0); + } } } } }); }); - // To do: more thorough tests should be applied to verify correctness of paths + // To do: consider the case WETH to bbaUSD, + // verify correctness of some more paths using simpleCheckPath }); function getPaths( @@ -149,12 +165,6 @@ function getPaths( poolsAll, sorConfigTest ); - for (const path of boostedPaths) { - console.log('Path begins'); - for (const swap of path.swaps) { - console.log(swap.tokenIn, ' ', swap.tokenOut); - } - } pathData = pathData.concat(boostedPaths); const [paths] = calculatePathLimits(pathData, swapType); return [paths, poolsAll, boostedPaths]; diff --git a/test/lib/constants.ts b/test/lib/constants.ts index 9772c9bb..6b4614aa 100644 --- a/test/lib/constants.ts +++ b/test/lib/constants.ts @@ -7,7 +7,7 @@ export interface TestToken { export const sorConfigTest = { chainId: 99, - weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', usdcConnectingPool: { id: 'usdcConnecting', @@ -17,7 +17,7 @@ export const sorConfigTest = { id: 'staBal3Id', address: '0x06df3b2bbb68adc8b0e302443692037ed9f91b42', }, - BBausd: { + bbausd: { id: '0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8', address: '0x8fd162f338b770f7e879030830cde9173367f301', }, @@ -33,7 +33,7 @@ export const sorConfigTest = { export const sorConfigEth: SorConfig = { chainId: 1, - weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', staBal3Pool: { id: '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb20000000000000000000000fe', @@ -56,7 +56,7 @@ export const sorConfigKovan: SorConfig = { }; export const WETH: TestToken = { - address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase(), + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'.toLowerCase(), decimals: 18, }; export const DAI: TestToken = { @@ -143,6 +143,11 @@ export const STABAL3PHANTOM: TestToken = { decimals: 18, }; +export const bbaUSD: TestToken = { + address: '0x8fd162f338b770f7e879030830cde9173367f301', + decimals: 18, +}; + export const aUSDT: TestToken = { address: '0xe8191aacfcdb32260cda25830dc6c9342142f310', decimals: 6, diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index d95c911f..8a877b14 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -14,6 +14,7 @@ import { PoolTypes, NewPath, SorConfig, + PoolBase, } from '../../src'; import { bnum } from '../../src/utils/bignumber'; import * as fs from 'fs'; @@ -465,3 +466,22 @@ export function checkPath( expect(path.swaps[0].tokenIn).to.eq(tokenIn); expect(path.swaps[path.swaps.length - 1].tokenOut).to.eq(tokenOut); } + +/* +Checks path for: +- tokens chain +- pools chain +*/ +export function simpleCheckPath( + path: NewPath, + poolsIds: string[], + tokens: string[] +): boolean { + for (let i = 0; i < path.swaps.length; i++) { + if (poolsIds[i] !== path.pools[i].id) return false; + if (tokens[i] !== path.swaps[i].tokenIn.toLowerCase()) return false; + if (tokens[i + 1] !== path.swaps[i].tokenOut.toLowerCase()) + return false; + } + return true; +} diff --git a/test/linear.spec.ts b/test/linear.spec.ts index e68ae59f..e0e5b302 100644 --- a/test/linear.spec.ts +++ b/test/linear.spec.ts @@ -15,8 +15,8 @@ import { import { filterPoolsOfInterest, filterHopPools, - getLinearStaBal3Paths, parseToPoolsDict, + getBoostedPaths, } from '../src/routeProposal/filtering'; import { calculatePathLimits } from '../src/routeProposal/pathLimits'; import { LinearPool, PairTypes } from '../src/pools/linearPool/linearPool'; @@ -28,7 +28,7 @@ import { USDC, bUSDC, BAL, - STABAL3PHANTOM, + bbaUSD, TestToken, MKR, GUSD, @@ -128,7 +128,7 @@ describe('linear pool tests', () => { expect(amount.toString()).to.eq('1485000000.122222221232222221'); }); - it(`debug getLimitAmountSwap, SwapExactIn, TokenToBpt should return valid limit`, async () => { + it(`getLimitAmountSwap, SwapExactIn, TokenToBpt should return valid limit`, async () => { const tokenIn = DAI.address; const tokenOut = bDAI.address; const swapType = SwapTypes.SwapExactIn; @@ -194,35 +194,39 @@ describe('linear pool tests', () => { context('Considering Linear Paths Only', () => { context('Using Single Linear Pool', () => { + const config = { + chainId: 99, + weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', + }; it('getPathsUsingLinearPool return empty paths', () => { const tokenIn = DAI.address; const tokenOut = USDC.address; const maxPools = 4; - - const [, , pathsUsingLinear] = getPaths( + const [, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, singleLinear.pools, - maxPools + maxPools, + config ); - expect(pathsUsingLinear).to.be.empty; + expect(boostedPaths).to.be.empty; }); it('getPathsUsingLinearPool return empty paths', () => { const tokenIn = DAI.address; const tokenOut = USDC.address; const maxPools = 4; - - const [, , pathsUsingLinear] = getPaths( + const [, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, singleLinear.pools, - maxPools + maxPools, + config ); - - expect(pathsUsingLinear).to.be.empty; + expect(boostedPaths).to.be.empty; }); }); @@ -233,7 +237,7 @@ describe('linear pool tests', () => { const tokenOut = bTUSD.address; const maxPools = 4; - const [allPaths, poolsAllDict, pathsUsingLinear] = getPaths( + const [allPaths, poolsAllDict, boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, @@ -241,7 +245,7 @@ describe('linear pool tests', () => { maxPools ); - expect(pathsUsingLinear).to.be.empty; + expect(boostedPaths).to.be.empty; expect(allPaths.length).to.equal(1); checkPath( ['linearTUSD'], @@ -257,7 +261,7 @@ describe('linear pool tests', () => { const tokenOut = USDC.address; const maxPools = 4; - const [allPaths, , pathsUsingLinear] = getPaths( + const [allPaths, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, @@ -265,18 +269,18 @@ describe('linear pool tests', () => { maxPools ); - expect(pathsUsingLinear).to.be.empty; + expect(boostedPaths).to.be.empty; expect(allPaths).to.be.empty; }); }); context('Stable<>Token with no staBal or WETH paired pool', () => { - it('Stable>Token, getPathsUsingLinearPool return empty paths', async () => { + it('Stable>Token, getBoostedPaths return empty paths', async () => { const tokenIn = MKR.address; const tokenOut = DAI.address; const maxPools = 10; - const [, , pathsUsingLinear] = getPaths( + const [, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, @@ -284,15 +288,15 @@ describe('linear pool tests', () => { maxPools ); - assert.equal(pathsUsingLinear.length, 0); + assert.equal(boostedPaths.length, 0); }); - it('Token>Stable, getPathsUsingLinearPool return empty paths', async () => { + it('Token>Stable, getBoostedPaths return empty paths', async () => { const tokenIn = USDC.address; const tokenOut = MKR.address; const maxPools = 10; - const [, , pathsUsingLinear] = getPaths( + const [, , boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, @@ -300,48 +304,66 @@ describe('linear pool tests', () => { maxPools ); - assert.equal(pathsUsingLinear.length, 0); + assert.equal(boostedPaths.length, 0); }); }); - context('getPathsUsingLinearPools - stable pair', () => { - it('should return 1 valid linear path', async () => { + context('getBoostedPaths - stable pair', () => { + it('should return 3 valid paths', async () => { const tokenIn = DAI.address; const tokenOut = USDC.address; const maxPools = 10; - const [, poolsAllDict, pathsUsingLinear] = getPaths( + const [, poolsAllDict, boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, smallLinear.pools, maxPools ); - - assert.equal(pathsUsingLinear.length, 1); - checkPath( - ['linearDAI', 'staBal3Id', 'linearUSDC'], - poolsAllDict, - pathsUsingLinear[0], - tokenIn, - tokenOut - ); + assert.equal(boostedPaths.length, 3); + const expectedPoolsIdsArray = [ + ['linearDAI', 'bbaUSD-Pool', 'linearUSDC'], + // eslint-disable-next-line prettier/prettier + [ + 'weightedDaiWeth', + 'weightedWeth-BBausd', + 'bbaUSD-Pool', + 'linearUSDC', + ], + // eslint-disable-next-line prettier/prettier + [ + 'linearDAI', + 'bbaUSD-Pool', + 'weightedWeth-BBausd', + 'weightedUsdcWeth', + ], + ]; + for (let i = 0; i < 3; i++) { + checkPath( + expectedPoolsIdsArray[i], + poolsAllDict, + boostedPaths[i], + tokenIn, + tokenOut + ); + } }); it('tokenIn and tokenOut belong to same linear pool should have standard single hop path', async () => { - const tokenOut = DAI.address; const tokenIn = aDAI.address; + const tokenOut = DAI.address; const maxPools = 10; - const [allPaths, poolsAllDict, pathsUsingLinear] = getPaths( + const [allPaths, poolsAllDict, boostedPaths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, smallLinear.pools, maxPools ); - expect(pathsUsingLinear).to.be.empty; - assert.equal(allPaths.length, 1); + assert.equal(boostedPaths.length, 1); + assert.equal(allPaths.length, 2); checkPath( ['linearDAI'], poolsAllDict, @@ -367,26 +389,19 @@ describe('linear pool tests', () => { smallLinear.pools, maxPools ); - - assert.equal(paths.length, 3); - checkPath( - ['linearDAI', 'staBal3Id', 'linearUSDC'], - poolAllDict, - paths[0], - tokenIn, - tokenOut - ); + // boosted paths for DAI/USDC were tested in a previous case + assert.equal(paths.length, 5); checkPath( ['weightedDaiWeth', 'weightedUsdcWeth'], poolAllDict, - paths[1], + paths[3], tokenIn, tokenOut ); checkPath( ['weightedDaiUsdc'], poolAllDict, - paths[2], + paths[4], tokenIn, tokenOut ); @@ -432,15 +447,17 @@ describe('linear pool tests', () => { maxPools ); - assert.equal(paths.length, 1); + assert.equal(paths.length, 2); // TokenIn>[weightedBalStaBal3]>bDAI>[staBAL3]>staBal3>[linearDAI]>DAI checkPath( - ['staBal3Gusd', 'staBal3Id', 'linearDAI'], + ['bbaUsdGusd', 'bbaUSD-Pool', 'linearDAI'], poolsAllDict, paths[0], tokenIn, tokenOut ); + // The other path id is: + // bbaUsdGusdweightedWeth-BBausdweightedDaiWeth }); it('should return 1 valid linear paths', async () => { @@ -455,16 +472,17 @@ describe('linear pool tests', () => { smallLinear.pools, maxPools ); - - assert.equal(paths.length, 1); + assert.equal(paths.length, 2); // TokenIn>[linearUSDC]>bUSDC>[staBAL3]>staBal3>[staBal3Gusd]>TokenOut checkPath( - ['linearUSDC', 'staBal3Id', 'staBal3Gusd'], + ['linearUSDC', 'bbaUSD-Pool', 'bbaUsdGusd'], poolsAllDict, paths[0], tokenIn, tokenOut ); + // The other path id is: + // weightedUsdcWethweightedWeth-BBausdbbaUsdGusd }); }); }); @@ -487,8 +505,8 @@ describe('linear pool tests', () => { checkPath( [ 'linearUSDC', - 'staBal3Id', - 'weightedWethStaBal3Id', + 'bbaUSD-Pool', + 'weightedWeth-BBausd', 'weightedBalWeth', ], poolsAllDict, @@ -523,8 +541,8 @@ describe('linear pool tests', () => { checkPath( [ 'weightedBalWeth', - 'weightedWethStaBal3Id', - 'staBal3Id', + 'weightedWeth-BBausd', + 'bbaUSD-Pool', 'linearUSDC', ], poolsAllDict, @@ -740,7 +758,7 @@ describe('linear pool tests', () => { pools[3].tokens[0].priceRate = '1.151626716671767642'; const returnAmount = await testFullSwap( DAI.address, - STABAL3PHANTOM.address, + bbaUSD.address, SwapTypes.SwapExactIn, parseFixed('1', DAI.decimals), pools, @@ -752,9 +770,9 @@ describe('linear pool tests', () => { it('USDT>staBAL3, SwapExactOut', async () => { const returnAmount = await testFullSwap( USDT.address, - STABAL3PHANTOM.address, + bbaUSD.address, SwapTypes.SwapExactOut, - parseFixed('1', STABAL3PHANTOM.decimals), + parseFixed('1', bbaUSD.decimals), kovanPools.pools, sorConfigKovan ); @@ -763,10 +781,10 @@ describe('linear pool tests', () => { it('staBAL3>USDT, SwapExactIn', async () => { const returnAmount = await testFullSwap( - STABAL3PHANTOM.address, + bbaUSD.address, USDT.address, SwapTypes.SwapExactIn, - parseFixed('1', STABAL3PHANTOM.decimals), + parseFixed('1', bbaUSD.decimals), kovanPools.pools, sorConfigKovan ); @@ -775,7 +793,7 @@ describe('linear pool tests', () => { it('staBAL3>USDT, SwapExactOut', async () => { const returnAmount = await testFullSwap( - STABAL3PHANTOM.address, + bbaUSD.address, USDT.address, SwapTypes.SwapExactOut, parseFixed('1', USDT.decimals), @@ -788,7 +806,7 @@ describe('linear pool tests', () => { // it('aUSDT>staBAL3, SwapExactIn', async () => { // const returnAmount = await testFullSwap( // aUSDT.address, - // STABAL3PHANTOM.address, + // bbaUSD.address, // SwapTypes.SwapExactIn, // parseFixed('1', aUSDT.decimals), // kovanPools.pools, @@ -816,7 +834,8 @@ function getPaths( tokenOut: string, swapType: SwapTypes, pools: SubgraphPoolBase[], - maxPools: number + maxPools: number, + config?: SorConfig ): [NewPath[], PoolDictionary, NewPath[]] { const poolsAll = parseToPoolsDict(cloneDeep(pools), 0); @@ -834,17 +853,11 @@ function getPaths( hopTokens, poolsFilteredDict ); - - const pathsUsingLinear = getLinearStaBal3Paths( - tokenIn, - tokenOut, - poolsAll, - poolsFilteredDict, - sorConfigTest - ); - pathData = pathData.concat(pathsUsingLinear); + const conf = config || sorConfigTest; + const boostedPaths = getBoostedPaths(tokenIn, tokenOut, poolsAll, conf); + pathData = pathData.concat(boostedPaths); const [paths] = calculatePathLimits(pathData, swapType); - return [paths, poolsAll, pathsUsingLinear]; + return [paths, poolsAll, boostedPaths]; } async function testFullSwap( diff --git a/test/phantomStableMath.spec.ts b/test/phantomStableMath.spec.ts index 2a93c5b2..4f00f8cf 100644 --- a/test/phantomStableMath.spec.ts +++ b/test/phantomStableMath.spec.ts @@ -9,7 +9,7 @@ import { PhantomStablePoolPairData, } from '../src/pools/phantomStablePool/phantomStablePool'; import * as phantomStableMath from '../src/pools/phantomStablePool/phantomStableMath'; -import { STABAL3PHANTOM, LINEAR_AUSDT, LINEAR_AUSDC } from './lib/constants'; +import { bbaUSD, LINEAR_AUSDT, LINEAR_AUSDC } from './lib/constants'; describe('phantomStable pools tests', () => { // For the moment we tolerate a moderate relative error until @@ -77,7 +77,7 @@ describe('phantomStable pools tests', () => { it('phantomStable BPT -> token', () => { const poolPairData = phantomStablePool.parsePoolPairData( - STABAL3PHANTOM.address, + bbaUSD.address, LINEAR_AUSDC.address ); const priceRateOut = bnum( @@ -119,7 +119,7 @@ describe('phantomStable pools tests', () => { it('phantomStable token -> BPT', () => { const poolPairData = phantomStablePool.parsePoolPairData( LINEAR_AUSDC.address, - STABAL3PHANTOM.address + bbaUSD.address ); const { a1, a2 } = getSwapOutcomes( phantomStablePool, diff --git a/test/phantomStablePools.spec.ts b/test/phantomStablePools.spec.ts index de11185d..451d5b07 100644 --- a/test/phantomStablePools.spec.ts +++ b/test/phantomStablePools.spec.ts @@ -10,7 +10,7 @@ import { MKR, stETH, LINEAR_AUSDT, - STABAL3PHANTOM, + bbaUSD, LINEAR_ADAI, sorConfigKovan, } from './lib/constants'; @@ -40,7 +40,7 @@ describe(`Tests for PhantomStable Pools.`, () => { it(`TokenToBpt should return valid limit`, async () => { testLimit( LINEAR_ADAI.address, - STABAL3PHANTOM.address, + bbaUSD.address, SwapTypes.SwapExactIn, [poolsFromFile.STABAL3[0] as SubgraphPoolBase], 0, @@ -50,7 +50,7 @@ describe(`Tests for PhantomStable Pools.`, () => { it(`BptToToken should return valid limit`, async () => { testLimit( - STABAL3PHANTOM.address, + bbaUSD.address, LINEAR_ADAI.address, // '0xcd32a460b6fecd053582e43b07ed6e2c04e15369' SwapTypes.SwapExactIn, [poolsFromFile.STABAL3[0] as SubgraphPoolBase], @@ -75,7 +75,7 @@ describe(`Tests for PhantomStable Pools.`, () => { it(`TokenToBpt should return valid limit`, async () => { testLimit( LINEAR_ADAI.address, - STABAL3PHANTOM.address, + bbaUSD.address, SwapTypes.SwapExactOut, [poolsFromFile.STABAL3[0] as SubgraphPoolBase], 0, @@ -85,7 +85,7 @@ describe(`Tests for PhantomStable Pools.`, () => { it(`BptToToken should return valid limit`, async () => { testLimit( - STABAL3PHANTOM.address, + bbaUSD.address, LINEAR_ADAI.address, SwapTypes.SwapExactOut, [poolsFromFile.STABAL3[0] as SubgraphPoolBase], @@ -169,7 +169,7 @@ describe(`Tests for PhantomStable Pools.`, () => { it('Token>BPT, SwapExactIn', async () => { const returnAmount = await testFullSwap( LINEAR_AUSDT.address, - STABAL3PHANTOM.address, + bbaUSD.address, SwapTypes.SwapExactIn, parseFixed('0.010001000098489046', 18), [pool] @@ -179,7 +179,7 @@ describe(`Tests for PhantomStable Pools.`, () => { it('BPT>Token, SwapExactIn', async () => { const returnAmount = await testFullSwap( - STABAL3PHANTOM.address, + bbaUSD.address, LINEAR_AUSDT.address, SwapTypes.SwapExactIn, parseFixed('401.873', 18), @@ -204,7 +204,7 @@ describe(`Tests for PhantomStable Pools.`, () => { it('Token>BPT, SwapExactOut', async () => { const returnAmount = await testFullSwap( LINEAR_AUSDT.address, - STABAL3PHANTOM.address, + bbaUSD.address, SwapTypes.SwapExactOut, parseFixed('654.98', 18), [pool] @@ -214,7 +214,7 @@ describe(`Tests for PhantomStable Pools.`, () => { it('BPT>Token, SwapExactIn', async () => { const returnAmount = await testFullSwap( - STABAL3PHANTOM.address, + bbaUSD.address, LINEAR_AUSDT.address, SwapTypes.SwapExactOut, parseFixed('0.007321', 18), diff --git a/test/testData/boostedPools/multipleBoosted.json b/test/testData/boostedPools/multipleBoosted.json index 91584bb7..2244382e 100644 --- a/test/testData/boostedPools/multipleBoosted.json +++ b/test/testData/boostedPools/multipleBoosted.json @@ -3,7 +3,7 @@ { "address": "0x8fd162f338b770f7e879030830cde9173367f301", "amp": "400", - "id": "0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8", + "id": "bbaUSD-Pool", "mainIndex": 0, "poolType": "StablePhantom", "swapEnabled": true, @@ -14,7 +14,7 @@ "balance": "5192296858520354.535602194932501075", "decimals": 18, "priceRate": "1", - "symbol": "BB-aUSD", + "symbol": "bbaUSD", "weight": null }, { @@ -22,7 +22,7 @@ "balance": "4817.7627626993040385", "decimals": 18, "priceRate": "1", - "symbol": "BB-AUSDC", + "symbol": "bbaUSDC", "weight": null }, { @@ -30,7 +30,7 @@ "balance": "4803.335614256025401", "decimals": 18, "priceRate": "1", - "symbol": "BB-AUSDT", + "symbol": "bbaUSDT", "weight": null }, { @@ -38,7 +38,7 @@ "balance": "4851.9948738150390637", "decimals": 18, "priceRate": "1", - "symbol": "BB-ADAI", + "symbol": "bbaDAI", "weight": null } ], @@ -74,7 +74,7 @@ "balance": "5192296858529909.865767797025181595", "decimals": 18, "priceRate": "1", - "symbol": "BB-aUSDC", + "symbol": "bbaUSDC", "weight": null }, { @@ -118,7 +118,7 @@ "balance": "5192296858529924.23290923961439575", "decimals": 18, "priceRate": "1", - "symbol": "BB-aUSDT", + "symbol": "bbaUSDT", "weight": null }, { @@ -162,7 +162,7 @@ "balance": "5192296858529875.633656681290156395", "decimals": 18, "priceRate": "1", - "symbol": "BB-aDAI", + "symbol": "bbaDAI", "weight": null }, { @@ -250,7 +250,7 @@ "balance": "4817.7627626993040385", "decimals": 18, "priceRate": "1.2", - "symbol": "BB-aUSD", + "symbol": "bbaUSD", "weight": null }, { @@ -274,7 +274,7 @@ { "address": "0x0000000000000000000000000000000000000003", "amp": "400", - "id": "0x0000000000000000000000000000000000000003000000000000000000000003", + "id": "BBaUSD-TUSD", "mainIndex": 0, "poolType": "StablePhantom", "swapEnabled": true, @@ -293,11 +293,11 @@ "balance": "4817.7627626993040385", "decimals": 18, "priceRate": "1.2", - "symbol": "BB-aUSD", + "symbol": "bbaUSD", "weight": null }, { - "address": "0x0000000000085d4780B73119b644AE5ecd22b376", + "address": "0x0000000000085d4780b73119b644ae5ecd22b376", "balance": "4803.335614256025401", "decimals": 18, "priceRate": "1", @@ -308,7 +308,7 @@ "tokensList": [ "0x0000000000000000000000000000000000000003", "0x8fd162f338b770f7e879030830cde9173367f301", - "0x0000000000085d4780B73119b644AE5ecd22b376" + "0x0000000000085d4780b73119b644ae5ecd22b376" ], "totalShares": "14473.092928301396719020", "totalWeight": "0", @@ -317,7 +317,7 @@ { "address": "0x0000000000000000000000000000000000000013", "amp": "400", - "id": "0x0000000000000000000000000000000000000013000000000000000000000013", + "id": "bbaUSD-BAL", "mainIndex": 0, "poolType": "StablePhantom", "swapEnabled": true, @@ -328,7 +328,7 @@ "balance": "5192296858520354.535602194932501075", "decimals": 18, "priceRate": "1", - "symbol": "BBaUSD-BAL", + "symbol": "bbaUSD-BAL", "weight": null }, { @@ -336,7 +336,7 @@ "balance": "4817.7627626993040385", "decimals": 18, "priceRate": "1.2", - "symbol": "BB-aUSD", + "symbol": "bbaUSD", "weight": null }, { @@ -380,7 +380,7 @@ "decimals": 18, "weight": "50", "id": "weightedWeth-BBausd-0x8fd162f338b770f7e879030830cde9173367f301", - "symbol": "BB-aUSD", + "symbol": "bbaUSD", "priceRate": "1" } ], @@ -443,17 +443,17 @@ "priceRate": "1" }, { - "address": "0x0000000000085d4780B73119b644AE5ecd22b376", + "address": "0x0000000000085d4780b73119b644ae5ecd22b376", "balance": "20000000.78213445", "decimals": 18, "weight": "50", - "id": "weightedTusdWeth-0x0000000000085d4780B73119b644AE5ecd22b376", + "id": "weightedTusdWeth-0x0000000000085d4780b73119b644ae5ecd22b376", "symbol": "TUSD", "priceRate": "1" } ], "tokensList": [ - "0x0000000000085d4780B73119b644AE5ecd22b376", + "0x0000000000085d4780b73119b644ae5ecd22b376", "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" ], "totalWeight": "100", diff --git a/test/testData/linearPools/smallLinear.json b/test/testData/linearPools/smallLinear.json index 0d478ffb..71749820 100644 --- a/test/testData/linearPools/smallLinear.json +++ b/test/testData/linearPools/smallLinear.json @@ -1,18 +1,18 @@ { "pools": [ { - "id": "weightedWethStaBal3Id", + "id": "weightedWeth-BBausd", "publicSwap": true, "swapFee": "0.01", "swapEnabled": true, "tokens": [ { - "address": "0x06df3b2bbb68adc8b0e302443692037ed9f91b42", + "address": "0x8fd162f338b770f7e879030830cde9173367f301", "balance": "20000000", "decimals": 18, "weight": "50", - "id": "weightedWethStaBal3Id-0x06df3b2bbb68adc8b0e302443692037ed9f91b42", - "symbol": "staBal3", + "id": "weightedWeth-BBausd-0x8fd162f338b770f7e879030830cde9173367f301", + "symbol": "bbaUSD", "priceRate": "1" }, { @@ -20,7 +20,7 @@ "balance": "10000.12345", "decimals": 18, "weight": "50", - "id": "weightedWethStaBal3Id-0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "id": "weightedWeth-BBausd-0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "symbol": "WETH", "priceRate": "1" } @@ -32,7 +32,7 @@ "totalWeight": "100", "totalShares": "1000000", "poolType": "Weighted", - "address": "weightedWethStaBal3" + "address": "0x0000000000000000000000000000000000000004" }, { "id": "weightedUsdcWeth", @@ -278,8 +278,8 @@ "upperTarget": "150000.000000" }, { - "id": "staBal3Id", - "address": "0x06df3b2bbb68adc8b0e302443692037ed9f91b42", + "id": "bbaUSD-Pool", + "address": "0x8fd162f338b770f7e879030830cde9173367f301", "swapFee": "0.0004", "swapEnabled": true, "amp": "10", @@ -309,10 +309,10 @@ "weight": null }, { - "address": "0x06df3b2bbb68adc8b0e302443692037ed9f91b42", + "address": "0x8fd162f338b770f7e879030830cde9173367f301", "balance": "1500000000000000", "decimals": 18, - "symbol": "staBal3", + "symbol": "bbaUSD", "priceRate": "1", "weight": null } @@ -321,23 +321,23 @@ "0x0000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000003", - "0x06df3b2bbb68adc8b0e302443692037ed9f91b42" + "0x8fd162f338b770f7e879030830cde9173367f301" ], "totalShares": "10000000", - "poolType": "MetaStable" + "poolType": "StablePhantom" }, { - "id": "staBal3Gusd", + "id": "bbaUsdGusd", "address": "0xebfed10e11dc08fcda1af1fda146945e8710f22e", "swapFee": "0.0004", "swapEnabled": true, "amp": "10", "tokens": [ { - "address": "0x06df3b2bbb68adc8b0e302443692037ed9f91b42", + "address": "0x8fd162f338b770f7e879030830cde9173367f301", "balance": "100000000", "decimals": 18, - "symbol": "staBal3", + "symbol": "bbaUSD", "priceRate": "1", "weight": null }, @@ -351,7 +351,7 @@ } ], "tokensList": [ - "0x06df3b2bbb68adc8b0e302443692037ed9f91b42", + "0x8fd162f338b770f7e879030830cde9173367f301", "0x056fd409e1d7a124bd7017459dfea2f387b6d5cd" ], "totalShares": "10", @@ -369,7 +369,7 @@ "decimals": 18, "weight": "50", "id": "weightedBalWeth-0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "symbol": "staBal3", + "symbol": "WETH", "priceRate": "1" }, { From e866abcc7507723549105032edb48f607df09046 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Wed, 9 Feb 2022 16:18:54 -0300 Subject: [PATCH 32/43] Remove old staBalPaths function; best boosted path tests --- src/routeProposal/filtering.ts | 33 ++- src/routeProposal/index.ts | 8 +- test/boostedPaths.spec.ts | 240 +++++++++++++++--- test/lib/constants.ts | 42 ++- test/linear.spec.ts | 72 ++++-- test/staBalPaths.spec.ts | 22 +- .../boostedPools/multipleBoosted.json | 16 +- 7 files changed, 337 insertions(+), 96 deletions(-) diff --git a/src/routeProposal/filtering.ts b/src/routeProposal/filtering.ts index f2110507..944e5c63 100644 --- a/src/routeProposal/filtering.ts +++ b/src/routeProposal/filtering.ts @@ -16,7 +16,6 @@ import { ZERO } from '../utils/bignumber'; import { parseNewPool } from '../pools'; import { Zero } from '@ethersproject/constants'; import { concat } from 'lodash'; -import path from 'path'; export const filterPoolsByType = ( pools: SubgraphPoolBase[], @@ -298,8 +297,7 @@ export function getBoostedPaths( reversePath(path) ); // Here we assume that every short path (short means length 1 and 2) is included - // in filterHopPools. This should include things like DAI->bbfDAI->WETH or - // BAL->WETH->TUSD, WETH->bbaUSD, etc. + // in filterHopPools. const paths1 = removeShortPaths( combineSemiPaths(semiPathsInToWeth, semiPathsWethToOut) ); @@ -319,15 +317,19 @@ export function getBoostedPaths( [config.bbausd.address, config.weth], [WethBBausdPool] ); - const paths3 = combineSemiPaths( - semiPathsInToWeth, - semiPathsBBausdToOut, - WethBBausdPath + const paths3 = removeShortPaths( + combineSemiPaths( + semiPathsInToWeth, + semiPathsBBausdToOut, + WethBBausdPath + ) ); - const paths4 = combineSemiPaths( - semiPathsInToBBausd, - semiPathsWethToOut, - BBausdWethPath + const paths4 = removeShortPaths( + combineSemiPaths( + semiPathsInToBBausd, + semiPathsWethToOut, + BBausdWethPath + ) ); const paths = paths1.concat(paths2, paths3, paths4); return paths; @@ -406,7 +408,6 @@ function combineSemiPaths( for (const semiPathOut of semiPathsOut) { const path = composeSimplifyPath(semiPathIn, semiPathOut); if (path) paths.push(path); - // paths.push(composePaths([semiPathIn, semiPathOut])); } } @@ -690,6 +691,12 @@ export function createPath(tokens: string[], pools: PoolBase[]): NewPath { for (let i = 0; i < pools.length; i++) { tI = tokens[i]; tO = tokens[i + 1]; + /* console.log(i); + console.log(pools.length); + console.log(pools); + console.log(tokens); + console.log(pools[i]); + console.log(pools[i].id);*/ const poolPair = pools[i].parsePoolPairData(tI, tO); poolPairData.push(poolPair); id = id + poolPair.id; @@ -912,8 +919,6 @@ function composeSimplifyPath(semiPathIn: NewPath, semiPathOut: NewPath) { for (const leftPool of newPoolsIn) { for (const rightPool of semiPathOut.pools) { if (leftPool == rightPool) { - console.log('leftPool: ', leftPool); - console.log('rightPool: ', rightPool); return null; } } diff --git a/src/routeProposal/index.ts b/src/routeProposal/index.ts index dd187775..717d702b 100644 --- a/src/routeProposal/index.ts +++ b/src/routeProposal/index.ts @@ -1,9 +1,9 @@ import { filterPoolsOfInterest, filterHopPools, - getLinearStaBal3Paths, getPathsUsingStaBalPool, parseToPoolsDict, + getBoostedPaths, } from './filtering'; import { calculatePathLimits } from './pathLimits'; import { @@ -59,12 +59,10 @@ export class RouteProposer { poolsFilteredDict ); - // This must be changed to getBoostedPaths - const pathsUsingLinear: NewPath[] = getLinearStaBal3Paths( + const boostedPaths = getBoostedPaths( tokenIn, tokenOut, poolsAllDict, - poolsFilteredDict, this.config ); @@ -77,7 +75,7 @@ export class RouteProposer { ); const combinedPathData = pathData - .concat(...pathsUsingLinear) + .concat(...boostedPaths) .concat(...pathsUsingStaBal); const [paths] = calculatePathLimits(combinedPathData, swapType); diff --git a/test/boostedPaths.spec.ts b/test/boostedPaths.spec.ts index 2e81fb11..97dda1ca 100644 --- a/test/boostedPaths.spec.ts +++ b/test/boostedPaths.spec.ts @@ -1,19 +1,26 @@ // TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/linear.spec.ts import { assert, expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { PoolDictionary, NewPath, SwapTypes, SubgraphPoolBase } from '../src'; +import { + PoolDictionary, + NewPath, + SwapTypes, + SubgraphPoolBase, + SorConfig, +} from '../src'; import { filterPoolsOfInterest, filterHopPools, parseToPoolsDict, getBoostedPaths, } from '../src/routeProposal/filtering'; +import { bnum } from '../src/utils/bignumber'; import { calculatePathLimits } from '../src/routeProposal/pathLimits'; -import { simpleCheckPath } from './lib/testHelpers'; +import { getFullSwap, simpleCheckPath } from './lib/testHelpers'; import { DAI, aDAI, - bDAI, + bbaDAI, USDC, bUSDC, BAL, @@ -29,13 +36,16 @@ import { aUSDT, KOVAN_BAL, AAVE_USDT, - sorConfigTest, + sorConfigTestBoosted, sorConfigKovan, bbaUSD, } from './lib/constants'; // Multiple boosted pools import boostedPools from './testData/boostedPools/multipleBoosted.json'; +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { getOutputAmountSwapForPath } from '../src/router/helpersClass'; +import { JsonRpcProvider } from '@ethersproject/providers'; const maxPools = 10; describe('multiple boosted pools, path creation test', () => { @@ -48,55 +58,131 @@ describe('multiple boosted pools, path creation test', () => { tokenOut, SwapTypes.SwapExactIn, boostedPools.pools, - maxPools - ); - assert.isTrue( - simpleCheckPath( - paths[1], - ['weightedTusdWeth', 'weightedWeth-BBausd', 'bbaUSD-BAL'], - [TUSD.address, WETH.address, bbaUSD.address, BAL.address] - ) + maxPools, + sorConfigTestBoosted ); + const tokensChains = [ + [TUSD.address, WETH.address, BAL.address], + [TUSD.address, WETH.address, bbaUSD.address, BAL.address], + [TUSD.address, bbaUSD.address, WETH.address, BAL.address], + [TUSD.address, bbaUSD.address, BAL.address], + ]; + const poolsChains = [ + ['weightedTusdWeth', 'weightedBalWeth'], + ['weightedTusdWeth', 'weightedWeth-BBausd', 'bbaUSD-BAL'], + ['BBaUSD-TUSD', 'weightedWeth-BBausd', 'weightedBalWeth'], + ['BBaUSD-TUSD', 'bbaUSD-BAL'], + ]; + for (let i = 0; i < 4; i++) { + assert.isTrue( + simpleCheckPath(paths[i], poolsChains[i], tokensChains[i]) + ); + } assert.equal(boostedPaths.length, 2); assert.equal(paths.length, 4); }); it('BAL-TUSD', () => { const tokenIn = BAL.address; const tokenOut = TUSD.address; - const [paths, , boostedPaths] = getPaths( + const [paths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, boostedPools.pools, - maxPools + maxPools, + sorConfigTestBoosted ); assert.equal(paths.length, 4); + assert.isTrue(checkNoDuplicate(paths)); }); }); context('Case with linear pools', () => { it('DAI-BAL', () => { const tokenIn = DAI.address; const tokenOut = BAL.address; - const [paths, , boostedPaths] = getPaths( + const [paths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, boostedPools.pools, - maxPools + maxPools, + sorConfigTestBoosted ); assert.equal(paths.length, 4); + assert.isTrue(checkNoDuplicate(paths)); }); it('BAL-DAI', () => { const tokenIn = BAL.address; const tokenOut = DAI.address; - const [paths, , boostedPaths] = getPaths( + const [paths] = getPaths( tokenIn, tokenOut, SwapTypes.SwapExactIn, boostedPools.pools, - maxPools + maxPools, + sorConfigTestBoosted ); assert.equal(paths.length, 4); + const bbfDaiAddress = '0x0000000000000000000000000000000000000000'; + const tokensChains = [ + [BAL.address, bbaUSD.address, bbfDaiAddress, DAI.address], + [BAL.address, bbaUSD.address, bbaDAI.address, DAI.address], + // eslint-disable-next-line prettier/prettier + [ + BAL.address, + WETH.address, + bbaUSD.address, + bbfDaiAddress, + DAI.address, + ], + // eslint-disable-next-line prettier/prettier + [ + BAL.address, + WETH.address, + bbaUSD.address, + bbaDAI.address, + DAI.address, + ], + ]; + const poolsChains = [ + ['bbaUSD-BAL', 'bbaUSD-bbfDAI', 'FuseLinearDai'], + ['bbaUSD-BAL', 'bbaUSD-Pool', 'AaveLinearDai'], + // eslint-disable-next-line prettier/prettier + [ + 'weightedBalWeth', + 'weightedWeth-BBausd', + 'bbaUSD-bbfDAI', + 'FuseLinearDai', + ], + // eslint-disable-next-line prettier/prettier + [ + 'weightedBalWeth', + 'weightedWeth-BBausd', + 'bbaUSD-Pool', + 'AaveLinearDai', + ], + ]; + for (let i = 0; i < 4; i++) { + assert.isTrue( + simpleCheckPath(paths[i], poolsChains[i], tokensChains[i]) + ); + } + }); + }); + context('Direct Weth - bbaUSD', () => { + it('WETH-bbaUSD', () => { + const tokenIn = WETH.address; + const tokenOut = bbaUSD.address; + const [paths, , boostedPaths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools, + sorConfigTestBoosted + ); + assert.equal(paths.length, 3); + assert.equal(boostedPaths.length, 0); }); }); context('bbausd and Weth to Dai', () => { @@ -104,7 +190,7 @@ describe('multiple boosted pools, path creation test', () => { const binaryOption = [true, false]; for (const reverse of binaryOption) { const tokens = [ - [WETH.address, sorConfigTest.bbausd.address], + [WETH.address, sorConfigTestBoosted.bbausd.address], [DAI.address], ]; if (reverse) tokens.reverse(); @@ -115,9 +201,11 @@ describe('multiple boosted pools, path creation test', () => { tokenOut, SwapTypes.SwapExactIn, boostedPools.pools, - maxPools + maxPools, + sorConfigTestBoosted ); assert.equal(paths.length, 2); + assert.isTrue(checkNoDuplicate(paths)); if ( tokenIn == WETH.address || tokenOut == WETH.address @@ -131,16 +219,50 @@ describe('multiple boosted pools, path creation test', () => { } }); }); - // To do: consider the case WETH to bbaUSD, - // verify correctness of some more paths using simpleCheckPath + context('debug-best Verify that SOR chooses best path', () => { + it('swapExactIn case', async () => { + // For small enough amounts, the best route must typically be + // formed exclusively by only one path. + assert( + checkBestPath( + TUSD, + BAL, + SwapTypes.SwapExactIn, + 91.23098, + boostedPools.pools, + sorConfigTestBoosted + ), + 'SOR path is not the best one' + ); + /* + assert( + checkBestPath(BAL, WETH, SwapTypes.SwapExactIn, 910000.23098), + 'SOR path is not the best one' + ); + */ + }); + }); }); +// The following code has been tested to detect duplicate paths +// when paths are created by filterHopPools and getBoostedPaths without "removeShortPaths" +function checkNoDuplicate(paths: NewPath[]): boolean { + const n = paths.length; + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (paths[i] == paths[j]) return false; + } + } + return true; +} + function getPaths( tokenIn: string, tokenOut: string, swapType: SwapTypes, pools: SubgraphPoolBase[], - maxPools: number + maxPools: number, + config: SorConfig ): [NewPath[], PoolDictionary, NewPath[]] { const poolsAll = parseToPoolsDict(cloneDeep(pools), 0); @@ -159,13 +281,73 @@ function getPaths( poolsFilteredDict ); - const boostedPaths = getBoostedPaths( - tokenIn, - tokenOut, - poolsAll, - sorConfigTest - ); + const boostedPaths = getBoostedPaths(tokenIn, tokenOut, poolsAll, config); pathData = pathData.concat(boostedPaths); const [paths] = calculatePathLimits(pathData, swapType); return [paths, poolsAll, boostedPaths]; } + +export async function checkBestPath( + tokenIn: TestToken, + tokenOut: TestToken, + swapType: SwapTypes, + amount: number, + pools: SubgraphPoolBase[], + config: SorConfig +): Promise { + const [paths] = getPaths( + tokenIn.address, + tokenOut.address, + swapType, + pools, + maxPools, + config + ); + const bnumSwapAmount = bnum(amount); + let bestPath = paths[0]; + let bestOutcome = bnum(0); + for (const path of paths) { + const outcome = getOutputAmountSwapForPath( + path, + swapType, + bnumSwapAmount, + tokenIn.decimals + ); + if (!bestOutcome || outcome.toNumber() > bestOutcome.toNumber()) { + bestPath = path; + bestOutcome = outcome; + } + } + const swapAmount = parseFixed(amount.toString(), tokenIn.decimals); + const costOutputToken = BigNumber.from('0'); + const gasPrice = BigNumber.from(`10000000000`); + const provider = new JsonRpcProvider( + `https://mainnet.infura.io/v3/${process.env.INFURA}` + ); + const swapGas = BigNumber.from(`32500`); + const swapInfo = await getFullSwap( + cloneDeep(pools), + tokenIn.address, + tokenOut.address, + tokenOut.decimals, + maxPools, + swapType, + swapAmount, + costOutputToken, + gasPrice, + provider, + swapGas, + config + ); + let swapPathId = ''; + for (const swap of swapInfo.swaps) { + swapPathId += swap.poolId; + } + let areEqual = swapPathId == bestPath.id; + for (let i = 0; i < bestPath.swaps.length; i++) { + if (bestPath.swaps[i].tokenIn !== swapInfo.tokenAddresses[i]) { + areEqual = false; + } + } + return areEqual; +} diff --git a/test/lib/constants.ts b/test/lib/constants.ts index 6b4614aa..6e7783e0 100644 --- a/test/lib/constants.ts +++ b/test/lib/constants.ts @@ -9,6 +9,12 @@ export const sorConfigTest = { chainId: 99, weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', +}; + +export const sorConfigTestStaBal = { + chainId: 99, + weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', usdcConnectingPool: { id: 'usdcConnecting', usdc: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', @@ -17,6 +23,16 @@ export const sorConfigTest = { id: 'staBal3Id', address: '0x06df3b2bbb68adc8b0e302443692037ed9f91b42', }, + wethStaBal3: { + id: 'weightedWethStaBal3Id', + address: 'weightedWethStaBal3', + }, +}; + +export const sorConfigTestBoosted = { + chainId: 99, + weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', bbausd: { id: '0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8', address: '0x8fd162f338b770f7e879030830cde9173367f301', @@ -25,17 +41,13 @@ export const sorConfigTest = { id: 'weightedWeth-BBausd', address: '0x0000000000000000000000000000000000000004', }, - wethStaBal3: { - id: 'weightedWethStaBal3Id', - address: 'weightedWethStaBal3', - }, }; export const sorConfigEth: SorConfig = { chainId: 1, weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', - staBal3Pool: { + bbausd: { id: '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb20000000000000000000000fe', address: '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2', }, @@ -43,13 +55,23 @@ export const sorConfigEth: SorConfig = { export const sorConfigKovan: SorConfig = { chainId: 42, - weth: '0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1', + weth: '0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1'.toLowerCase(), vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', - staBal3Pool: { + bbausd: { id: '0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8', address: '0x8fd162f338b770f7e879030830cde9173367f301', }, - wethStaBal3: { +}; + +export const sorConfigFullKovan: SorConfig = { + chainId: 42, + weth: '0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1'.toLowerCase(), + vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + bbausd: { + id: '0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8', + address: '0x8fd162f338b770f7e879030830cde9173367f301', + }, + wethBBausd: { id: '0x6be79a54f119dbf9e8ebd9ded8c5bd49205bc62d00020000000000000000033c', address: '0x6be79a54f119dbf9e8ebd9ded8c5bd49205bc62d', }, @@ -79,6 +101,10 @@ export const BAL: TestToken = { address: '0xba100000625a3754423978a60c9317c58a424e3D'.toLowerCase(), decimals: 18, }; +export const bbaDAI: TestToken = { + address: '0xcd32a460b6fecd053582e43b07ed6e2c04e15369', + decimals: 18, +}; export const bDAI: TestToken = { address: '0x0000000000000000000000000000000000000002', decimals: 18, diff --git a/test/linear.spec.ts b/test/linear.spec.ts index e0e5b302..2164b4ea 100644 --- a/test/linear.spec.ts +++ b/test/linear.spec.ts @@ -41,8 +41,9 @@ import { aUSDT, KOVAN_BAL, AAVE_USDT, - sorConfigTest, + sorConfigTestBoosted, sorConfigKovan, + sorConfigFullKovan, } from './lib/constants'; // Single Linear pool DAI/aDAI/bDAI @@ -51,6 +52,8 @@ import singleLinear from './testData/linearPools/singleLinear.json'; import smallLinear from './testData/linearPools/smallLinear.json'; import kovanPools from './testData/linearPools/kovan.json'; import fullKovanPools from './testData/linearPools/fullKovan.json'; +import { getOutputAmountSwapForPath } from '../src/router/helpersClass'; +import { checkBestPath } from './boostedPaths.spec'; describe('linear pool tests', () => { context('parsePoolPairData', () => { @@ -569,7 +572,8 @@ describe('linear pool tests', () => { LINEAR_AUSDT.address, SwapTypes.SwapExactIn, parseFixed('25.001542', USDT.decimals), - kovanPools.pools + kovanPools.pools, + sorConfigKovan ); expect(returnAmount).to.eq('25004552099099202302'); }); @@ -580,7 +584,8 @@ describe('linear pool tests', () => { LINEAR_AUSDT.address, SwapTypes.SwapExactOut, parseFixed('0.981028', LINEAR_AUSDT.decimals), - kovanPools.pools + kovanPools.pools, + sorConfigKovan ); expect(returnAmount).to.eq('980910'); }); @@ -591,7 +596,8 @@ describe('linear pool tests', () => { USDT.address, SwapTypes.SwapExactIn, parseFixed('26.0872140', LINEAR_AUSDT.decimals), - kovanPools.pools + kovanPools.pools, + sorConfigKovan ); expect(returnAmount).to.eq('26084073'); }); @@ -602,7 +608,8 @@ describe('linear pool tests', () => { USDT.address, SwapTypes.SwapExactOut, parseFixed('71.204293', USDT.decimals), - kovanPools.pools + kovanPools.pools, + sorConfigKovan ); expect(returnAmount).to.eq('71212865750361503175'); }); @@ -615,7 +622,8 @@ describe('linear pool tests', () => { LINEAR_ADAI.address, SwapTypes.SwapExactIn, parseFixed('491.23098', DAI.decimals), - pools + pools, + sorConfigKovan ); expect(returnAmount).to.eq('491230979220188637567'); }); @@ -628,7 +636,8 @@ describe('linear pool tests', () => { LINEAR_AUSDT.address, SwapTypes.SwapExactIn, parseFixed('25.001542', aUSDT.decimals), - kovanPools.pools + kovanPools.pools, + sorConfigKovan ); expect(returnAmount).to.eq('25002051893909811321'); }); @@ -639,7 +648,8 @@ describe('linear pool tests', () => { LINEAR_AUSDT.address, SwapTypes.SwapExactOut, parseFixed('0.981028', LINEAR_AUSDT.decimals), - kovanPools.pools + kovanPools.pools, + sorConfigKovan ); expect(returnAmount).to.eq('981007'); }); @@ -650,7 +660,8 @@ describe('linear pool tests', () => { aUSDT.address, SwapTypes.SwapExactIn, parseFixed('26.0872140', LINEAR_AUSDT.decimals), - kovanPools.pools + kovanPools.pools, + sorConfigKovan ); expect(returnAmount).to.eq('26086681'); }); @@ -661,7 +672,8 @@ describe('linear pool tests', () => { aUSDT.address, SwapTypes.SwapExactOut, parseFixed('71.204293', aUSDT.decimals), - kovanPools.pools + kovanPools.pools, + sorConfigKovan ); expect(returnAmount).to.eq('71205745000000000000'); }); @@ -706,23 +718,36 @@ describe('linear pool tests', () => { SwapTypes.SwapExactIn, parseFixed('7.21', AAVE_USDT.decimals), fullKovanPools.pools, - sorConfigKovan + sorConfigFullKovan ); // 6605808981785744500 expect(returnAmount).to.eq('6606146264948964392'); }); it('BAL>USDT, SwapExactIn', async () => { + const tokenIn = KOVAN_BAL; + const tokenOut = AAVE_USDT; const returnAmount = await testFullSwap( - KOVAN_BAL.address, - AAVE_USDT.address, + tokenIn.address, + tokenOut.address, SwapTypes.SwapExactIn, parseFixed('10.8248', KOVAN_BAL.decimals), fullKovanPools.pools, - sorConfigKovan + sorConfigFullKovan + ); + expect(returnAmount).to.eq('70169832'); + // from worse path: 11061470 + assert( + checkBestPath( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + 10.8248, + fullKovanPools.pools, + sorConfigFullKovan + ), + 'SOR path is not the best one' ); - // 11062044 - expect(returnAmount).to.eq('11061470'); }); it('USDT>BAL, SwapExactOut', async () => { @@ -732,7 +757,7 @@ describe('linear pool tests', () => { SwapTypes.SwapExactOut, parseFixed('0.652413919893769122', KOVAN_BAL.decimals), fullKovanPools.pools, - sorConfigKovan + sorConfigFullKovan ); expect(returnAmount).to.eq('702055'); }); @@ -744,11 +769,10 @@ describe('linear pool tests', () => { SwapTypes.SwapExactOut, parseFixed('71.990116', AAVE_USDT.decimals), fullKovanPools.pools, - sorConfigKovan + sorConfigFullKovan ); - - // 81894035538462519296 - expect(returnAmount).to.eq('81899098582251741376'); + // from worse path: 81899098582251741376 + expect(returnAmount).to.eq('653098636918112'); }); }); @@ -853,20 +877,20 @@ function getPaths( hopTokens, poolsFilteredDict ); - const conf = config || sorConfigTest; + const conf = config || sorConfigTestBoosted; const boostedPaths = getBoostedPaths(tokenIn, tokenOut, poolsAll, conf); pathData = pathData.concat(boostedPaths); const [paths] = calculatePathLimits(pathData, swapType); return [paths, poolsAll, boostedPaths]; } -async function testFullSwap( +export async function testFullSwap( tokenIn: string, tokenOut: string, swapType: SwapTypes, swapAmount: BigNumber, pools: SubgraphPoolBase[], - config: SorConfig = sorConfigTest + config: SorConfig = sorConfigTestBoosted ) { const returnAmountDecimals = 18; // TO DO Remove? const maxPools = 4; diff --git a/test/staBalPaths.spec.ts b/test/staBalPaths.spec.ts index 52eeb430..6df0da70 100644 --- a/test/staBalPaths.spec.ts +++ b/test/staBalPaths.spec.ts @@ -19,7 +19,13 @@ import { } from '../src/routeProposal/filtering'; import staBalPools from './testData/staBal/staBalPools.json'; import { checkPath } from './lib/testHelpers'; -import { BAL, TUSD, MKR, sorConfigTest, sorConfigEth } from './lib/constants'; +import { + BAL, + TUSD, + MKR, + sorConfigTestStaBal, + sorConfigEth, +} from './lib/constants'; const maxPools = 4; @@ -117,7 +123,7 @@ describe(`staBalPaths.`, () => { // Hop out as it is USDC > tokenOut const mostLiquidPool = getHighestLiquidityPool( - sorConfigTest.usdcConnectingPool.usdc, + sorConfigTestStaBal.usdcConnectingPool.usdc, tokenOut, SwapPairType.HopOut, poolsAll @@ -136,15 +142,15 @@ describe(`staBalPaths.`, () => { const staBalPoolIdIn = 'staBalPair1'; const staBalPoolIn = poolsOfInterest[staBalPoolIdIn]; - const hopTokenStaBal = sorConfigTest.staBal3Pool.address; + const hopTokenStaBal = sorConfigTestStaBal.staBal3Pool.address; const usdcConnectingPool = - poolsAll[sorConfigTest.usdcConnectingPool.id]; + poolsAll[sorConfigTestStaBal.usdcConnectingPool.id]; const multihopPath = createPath( [ tokenIn, hopTokenStaBal, - sorConfigTest.usdcConnectingPool.usdc, + sorConfigTestStaBal.usdcConnectingPool.usdc, ], [staBalPoolIn, usdcConnectingPool] ); @@ -154,7 +160,7 @@ describe(`staBalPaths.`, () => { poolsAll, multihopPath, tokenIn, - sorConfigTest.usdcConnectingPool.usdc + sorConfigTestStaBal.usdcConnectingPool.usdc ); }); }); @@ -206,7 +212,7 @@ describe(`staBalPaths.`, () => { // Hop in as it is tokenIn > USDC const mostLiquidPool = getHighestLiquidityPool( tokenIn, - sorConfigTest.usdcConnectingPool.usdc, + sorConfigTestStaBal.usdcConnectingPool.usdc, SwapPairType.HopIn, poolsAll ); @@ -237,7 +243,7 @@ function itCreatesCorrectPath( tokenOut: string, pools: SubgraphPoolBase[], expectedPoolIds: string[], - config: SorConfig = sorConfigTest + config: SorConfig = sorConfigTestStaBal ): [PoolDictionary, string[], PoolDictionary] { const poolsAll = parseToPoolsDict(pools, 0); diff --git a/test/testData/boostedPools/multipleBoosted.json b/test/testData/boostedPools/multipleBoosted.json index 2244382e..ddd9b112 100644 --- a/test/testData/boostedPools/multipleBoosted.json +++ b/test/testData/boostedPools/multipleBoosted.json @@ -79,7 +79,7 @@ }, { "address": "0xe22da380ee6b445bb8273c81944adeb6e8450422", - "balance": "0", + "balance": "100000", "decimals": 6, "priceRate": "1", "symbol": "USDC", @@ -107,7 +107,7 @@ "tokens": [ { "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", - "balance": "100.060001", + "balance": "10000.060001", "decimals": 6, "priceRate": "1", "symbol": "USDT", @@ -142,7 +142,7 @@ }, { "address": "0xcd32a460b6fecd053582e43b07ed6e2c04e15369", - "id": "0xcd32a460b6fecd053582e43b07ed6e2c04e1536900000000000000000000023c", + "id": "AaveLinearDai", "lowerTarget": "0", "mainIndex": 2, "poolType": "AaveLinear", @@ -167,7 +167,7 @@ }, { "address": "0x6b175474e89094c44da98b954eedeac495271d0f", - "balance": "0", + "balance": "10000", "decimals": 18, "priceRate": "1", "symbol": "DAI", @@ -186,7 +186,7 @@ }, { "address": "0x0000000000000000000000000000000000000000", - "id": "0x0000000000000000000000000000000000000000000000000000000000000000", + "id": "FuseLinearDai", "lowerTarget": "0", "mainIndex": 2, "poolType": "FuseLinear", @@ -206,12 +206,12 @@ "balance": "5192296858529875.633656681290156395", "decimals": 18, "priceRate": "1", - "symbol": "BB-fDAI", + "symbol": "bbfDAI", "weight": null }, { "address": "0x6b175474e89094c44da98b954eedeac495271d0f", - "balance": "0", + "balance": "100000", "decimals": 18, "priceRate": "1", "symbol": "DAI", @@ -231,7 +231,7 @@ { "address": "0x0000000000000000000000000000000000000002", "amp": "400", - "id": "0x0000000000000000000000000000000000000002000000000000000000000002", + "id": "bbaUSD-bbfDAI", "mainIndex": 0, "poolType": "StablePhantom", "swapEnabled": true, From 3d3aaadc22bb515a96e771fd3c4dce2772aa7d20 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Thu, 10 Feb 2022 15:23:01 -0300 Subject: [PATCH 33/43] update checkBestPath at boostedPaths.spec.ts --- test/boostedPaths.spec.ts | 23 ++++++++++++++++------- test/lib/constants.ts | 5 ----- test/linear.spec.ts | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/test/boostedPaths.spec.ts b/test/boostedPaths.spec.ts index 97dda1ca..f9d55ae8 100644 --- a/test/boostedPaths.spec.ts +++ b/test/boostedPaths.spec.ts @@ -22,14 +22,12 @@ import { aDAI, bbaDAI, USDC, - bUSDC, BAL, TestToken, MKR, GUSD, WETH, TUSD, - bTUSD, USDT, LINEAR_AUSDT, LINEAR_ADAI, @@ -37,7 +35,6 @@ import { KOVAN_BAL, AAVE_USDT, sorConfigTestBoosted, - sorConfigKovan, bbaUSD, } from './lib/constants'; @@ -234,12 +231,17 @@ describe('multiple boosted pools, path creation test', () => { ), 'SOR path is not the best one' ); - /* assert( - checkBestPath(BAL, WETH, SwapTypes.SwapExactIn, 910000.23098), + checkBestPath( + BAL, + WETH, + SwapTypes.SwapExactIn, + 910000.23098, + boostedPools.pools, + sorConfigTestBoosted + ), 'SOR path is not the best one' ); - */ }); }); }); @@ -313,7 +315,13 @@ export async function checkBestPath( bnumSwapAmount, tokenIn.decimals ); - if (!bestOutcome || outcome.toNumber() > bestOutcome.toNumber()) { + if ( + !bestOutcome || + (swapType == SwapTypes.SwapExactIn && + outcome.toNumber() > bestOutcome.toNumber()) || + (swapType == SwapTypes.SwapExactOut && + outcome.toNumber() < bestOutcome.toNumber()) + ) { bestPath = path; bestOutcome = outcome; } @@ -349,5 +357,6 @@ export async function checkBestPath( areEqual = false; } } + console.log(swapInfo.swaps.length); return areEqual; } diff --git a/test/lib/constants.ts b/test/lib/constants.ts index 6e7783e0..241ab2f2 100644 --- a/test/lib/constants.ts +++ b/test/lib/constants.ts @@ -164,11 +164,6 @@ export const LINEAR_ADAI: TestToken = { decimals: 18, }; -export const STABAL3PHANTOM: TestToken = { - address: '0x8fd162f338b770f7e879030830cde9173367f301', - decimals: 18, -}; - export const bbaUSD: TestToken = { address: '0x8fd162f338b770f7e879030830cde9173367f301', decimals: 18, diff --git a/test/linear.spec.ts b/test/linear.spec.ts index 2164b4ea..d50ac8ff 100644 --- a/test/linear.spec.ts +++ b/test/linear.spec.ts @@ -724,7 +724,7 @@ describe('linear pool tests', () => { expect(returnAmount).to.eq('6606146264948964392'); }); - it('BAL>USDT, SwapExactIn', async () => { + it('debug-1 BAL>USDT, SwapExactIn', async () => { const tokenIn = KOVAN_BAL; const tokenOut = AAVE_USDT; const returnAmount = await testFullSwap( @@ -735,7 +735,7 @@ describe('linear pool tests', () => { fullKovanPools.pools, sorConfigFullKovan ); - expect(returnAmount).to.eq('70169832'); + // expect(returnAmount).to.eq('70169832'); // from worse path: 11061470 assert( checkBestPath( From f981950a2dd164071324f3507b6428be299d8699 Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Fri, 11 Feb 2022 13:53:15 +0000 Subject: [PATCH 34/43] Add tests for Gyro2Pool --- src/pools/gyro2Pool/gyro2Math.ts | 8 +- src/pools/gyro2Pool/gyro2Pool.ts | 29 ++-- src/types.ts | 13 +- test/gyro2Math.spec.ts | 67 +++++++++ test/gyro2Pool.spec.ts | 147 ++++++++++++++++++++ test/testData/gyro2Pools/gyro2TestPool.json | 51 +++++++ 6 files changed, 297 insertions(+), 18 deletions(-) create mode 100644 test/gyro2Math.spec.ts create mode 100644 test/gyro2Pool.spec.ts create mode 100644 test/testData/gyro2Pools/gyro2TestPool.json diff --git a/src/pools/gyro2Pool/gyro2Math.ts b/src/pools/gyro2Pool/gyro2Math.ts index 813ba1d2..c4b88fe0 100644 --- a/src/pools/gyro2Pool/gyro2Math.ts +++ b/src/pools/gyro2Pool/gyro2Math.ts @@ -83,7 +83,7 @@ export function _calculateInvariant( return invariant; } -function _calculateQuadraticTerms( +export function _calculateQuadraticTerms( balances: BigNumber[], sqrtAlpha: BigNumber, sqrtBeta: BigNumber @@ -97,7 +97,7 @@ function _calculateQuadraticTerms( return [a, mb, mc]; } -function _calculateQuadratic( +export function _calculateQuadratic( a: BigNumber, mb: BigNumber, mc: BigNumber @@ -288,7 +288,9 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( return derivative; } -// Normalized Liquidity +// ///////// +// /// Normalized Liquidity +// ///////// export function _getNormalizedLiquidity( balances: BigNumber[], virtualParamIn: BigNumber, diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index b52eb2b3..f2400136 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -11,8 +11,8 @@ import { SubgraphToken, SwapTypes, SubgraphPoolBase, - PriceBoundData, -} from 'types'; + Gyro2PriceBounds, +} from '../../types'; import { isSameAddress } from '../../utils'; import { _squareRoot, @@ -48,16 +48,22 @@ export class Gyro2Pool implements PoolBase { tokens: Gyro2PoolToken[]; swapFee: BigNumber; totalShares: BigNumber; - priceBounds: PriceBoundData; + priceBounds: Gyro2PriceBounds; // Max In/Out Ratios MAX_IN_RATIO = parseFixed('0.3', 18); MAX_OUT_RATIO = parseFixed('0.3', 18); static fromPool(pool: SubgraphPoolBase): Gyro2Pool { - if (!pool.priceBounds) throw new Error('Gyro2Pool missing priceBounds'); - const tokenInAddress = pool.priceBounds.tokenInAddress; - const tokenOutAddress = pool.priceBounds.tokenOutAddress; + if (!pool.gyro2PriceBounds) + throw new Error('Pool missing gyro2PriceBounds'); + + const { lowerBound, upperBound } = pool.gyro2PriceBounds; + if (Number(lowerBound) <= 0 || Number(upperBound) <= 0) + throw new Error('Invalid price bounds in gyro2PriceBounds'); + + const tokenInAddress = pool.gyro2PriceBounds.tokenInAddress; + const tokenOutAddress = pool.gyro2PriceBounds.tokenOutAddress; const tokenInIndex = pool.tokens.findIndex( (t) => getAddress(t.address) === getAddress(tokenInAddress) @@ -79,7 +85,7 @@ export class Gyro2Pool implements PoolBase { pool.totalShares, pool.tokens as Gyro2PoolToken[], pool.tokensList, - pool.priceBounds as PriceBoundData + pool.gyro2PriceBounds as Gyro2PriceBounds ); } @@ -90,7 +96,7 @@ export class Gyro2Pool implements PoolBase { totalShares: string, tokens: Gyro2PoolToken[], tokensList: string[], - priceBounds: PriceBoundData + priceBounds: Gyro2PriceBounds ) { this.id = id; this.address = address; @@ -140,7 +146,6 @@ export class Gyro2Pool implements PoolBase { ONE.mul(ONE).div(parseFixed(this.priceBounds.lowerBound, 18)) ); - // TODO: sqrtAlpha, sqrtBeta to be added const poolPairData: Gyro2PoolPairData = { id: this.id, address: this.address, @@ -221,8 +226,7 @@ export class Gyro2Pool implements PoolBase { _exactTokenInForTokenOut( poolPairData: Gyro2PoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; const normalizedBalances = _normalizeBalances( @@ -257,8 +261,7 @@ export class Gyro2Pool implements PoolBase { _tokenInForExactTokenOut( poolPairData: Gyro2PoolPairData, - amount: OldBigNumber, - exact: boolean + amount: OldBigNumber ): OldBigNumber { const outAmount = parseFixed( amount.toString(), diff --git a/src/types.ts b/src/types.ts index 30d3f994..a0dd8b52 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,7 @@ export enum PoolTypes { MetaStable, Linear, Gyro2, + // Gyro3, } export enum SwapPairType { @@ -96,7 +97,10 @@ export interface SubgraphPoolBase { upperTarget?: string; // Gyro2 specific field - priceBounds?: PriceBoundData; + gyro2PriceBounds?: Gyro2PriceBounds; + + // // Gyro3 specific field + // gyro3PriceBounds?: Gyro3PriceBounds; } export type SubgraphToken = { @@ -218,9 +222,14 @@ export interface PoolDataService { getPools(): Promise; } -export type PriceBoundData = { +export type Gyro2PriceBounds = { lowerBound: string; upperBound: string; tokenInAddress: string; tokenOutAddress: string; }; + +// export type Gyro3PriceBounds = { +// alpha: string; // Assume symmetric price bounds for Gyro 3 pool +// // (The price range for any asset pair is equal to [alpha, 1/alpha]) +// }; diff --git a/test/gyro2Math.spec.ts b/test/gyro2Math.spec.ts new file mode 100644 index 00000000..408dca89 --- /dev/null +++ b/test/gyro2Math.spec.ts @@ -0,0 +1,67 @@ +import { expect } from 'chai'; +import cloneDeep from 'lodash.clonedeep'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { USDC, DAI } from './lib/constants'; +// Add new PoolType +import { Gyro2Pool } from '../src/pools/gyro2Pool/gyro2Pool'; +// Add new pool test data in Subgraph Schema format +import testPools from './testData/gyro2Pools/gyro2TestPool.json'; +import { + _addFee, + _calculateInvariant, + _reduceFee, + _calculateQuadratic, + _calculateQuadraticTerms, + _findVirtualParams, +} from '../src/pools/gyro2Pool/gyro2Math'; + +describe('gyro2Math tests', () => { + const testPool = cloneDeep(testPools).pools[0]; + const pool = Gyro2Pool.fromPool(testPool); + + const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); + + context('add and remove swap fee', () => { + const amountIn = parseFixed('28492.48453', 18); + const swapFee = poolPairData.swapFee; + it(`should correctly add swap fee`, async () => { + expect( + Number(formatFixed(_addFee(amountIn, swapFee), 18)) + ).to.be.approximately(28751.24575, 0.00001); + }); + it(`should correctly reduce by swap fee`, async () => { + expect( + Number(formatFixed(_reduceFee(amountIn, swapFee), 18)) + ).to.be.approximately(28236.05217, 0.00001); + }); + }); + + context('invariant and virtual parameters', () => { + it(`should correctly calculate invariant`, async () => { + const [a, mb, mc] = _calculateQuadraticTerms( + [poolPairData.balanceIn, poolPairData.balanceOut], + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + + expect(formatFixed(a, 18)).to.equal('0.000999500499625375'); + expect(formatFixed(mb, 18)).to.equal('2230.884220610725033295'); + expect(formatFixed(mc, 18)).to.equal('1232000.0'); + + const L = _calculateQuadratic(a, mb, mc); + + expect(formatFixed(L, 18)).to.equal('2232551.215824107930236259'); + }); + + it(`should correctly calculate virtual parameters`, async () => { + const [a, b] = _findVirtualParams( + parseFixed('2232551.215824107930236259', 18), + poolPairData.sqrtAlpha, + poolPairData.sqrtBeta + ); + + expect(formatFixed(a, 18)).to.equal('2231434.661007672178972479'); + expect(formatFixed(b, 18)).to.equal('2231435.776725839468265783'); + }); + }); +}); diff --git a/test/gyro2Pool.spec.ts b/test/gyro2Pool.spec.ts new file mode 100644 index 00000000..74a7c53e --- /dev/null +++ b/test/gyro2Pool.spec.ts @@ -0,0 +1,147 @@ +import { expect } from 'chai'; +import cloneDeep from 'lodash.clonedeep'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { bnum } from '../src/utils/bignumber'; +import { USDC, DAI } from './lib/constants'; +import { SwapTypes } from '../src'; +// Add new PoolType +import { Gyro2Pool } from '../src/pools/gyro2Pool/gyro2Pool'; +// Add new pool test data in Subgraph Schema format +import testPools from './testData/gyro2Pools/gyro2TestPool.json'; + +describe('Gyro2Pool tests USDC > DAI', () => { + const testPool = cloneDeep(testPools).pools[0]; + const pool = Gyro2Pool.fromPool(testPool); + + const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); + + const poolPairData2 = pool.parsePoolPairData(DAI.address, USDC.address); + + context('parsePoolPairData', () => { + it(`should correctly parse USDC > DAI`, async () => { + // Tests that compare poolPairData to known results with correct number scaling, etc, i.e.: + expect(poolPairData.swapFee.toString()).to.eq( + parseFixed(testPool.swapFee, 18).toString() + ); + expect(poolPairData.id).to.eq(testPool.id); + }); + + it(`should correctly calculate price bounds USDC > DAI`, async () => { + expect( + Number(formatFixed(poolPairData.sqrtAlpha, 18)) + ).to.be.approximately(0.9995003747, 0.00000001); + + expect( + Number(formatFixed(poolPairData.sqrtBeta, 18)) + ).to.be.approximately(1.000500375, 0.00000001); + }); + + it(`should correctly calculate price bounds DAI > USDC`, async () => { + expect( + Number(formatFixed(poolPairData2.sqrtAlpha, 18)) + ).to.be.approximately(0.9994998749, 0.00000001); + + expect( + Number(formatFixed(poolPairData2.sqrtBeta, 18)) + ).to.be.approximately(1.000499875, 0.00000001); + }); + }); + + context('limit amounts', () => { + it(`should correctly calculate limit amounts, USDC > DAI`, async () => { + let amount = pool.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactIn + ); + + expect(amount.toString()).to.eq('300'); + + amount = pool.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactOut + ); + + expect(amount.toString()).to.eq('369.6'); + }); + }); + + context('normalized liquidity', () => { + it(`should correctly calculate normalized liquidity, USDC > DAI`, async () => { + const normalizedLiquidity = + pool.getNormalizedLiquidity(poolPairData); + + expect(Number(normalizedLiquidity.toString())).to.be.approximately( + 2252709.0423891, + 0.00001 + ); + }); + + it(`should correctly calculate normalized liquidity, DAI > USDC`, async () => { + const normalizedLiquidity = + pool.getNormalizedLiquidity(poolPairData2); + + expect(Number(normalizedLiquidity.toString())).to.be.approximately( + 2252944.2752, + 0.00001 + ); + }); + }); + + context('Test Swaps', () => { + context('SwapExactIn', () => { + const amountIn = bnum('13.5'); + + it('should correctly calculate amountOut given amountIn', async () => { + const amountOut = pool._exactTokenInForTokenOut( + poolPairData, + amountIn + ); + expect(amountOut.toString()).to.eq('13.379816829921106482'); + }); + it('should correctly calculate newSpotPrice', async () => { + const newSpotPrice = + pool._spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + amountIn + ); + expect(newSpotPrice.toString()).to.eq('1.008988469289267733'); + }); + it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { + const derivative = + pool._derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + amountIn + ); + expect(derivative.toString()).to.eq('0.000000895794710891'); + }); + }); + + context('SwapExactOut', () => { + const amountOut = bnum('45.568'); + + it('should correctly calculate amountIn given amountOut', async () => { + const amountIn = pool._tokenInForExactTokenOut( + poolPairData, + amountOut + ); + expect(amountIn.toString()).to.eq('45.977973900999006919'); + }); + it('should correctly calculate newSpotPrice', async () => { + const newSpotPrice = + pool._spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData, + amountOut + ); + expect(newSpotPrice.toString()).to.eq('1.009017563096232974'); + }); + it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { + const derivative = + pool._derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + poolPairData, + amountOut + ); + expect(derivative.toString()).to.eq('0.000000903885627537'); + }); + }); + }); +}); diff --git a/test/testData/gyro2Pools/gyro2TestPool.json b/test/testData/gyro2Pools/gyro2TestPool.json new file mode 100644 index 00000000..8e400934 --- /dev/null +++ b/test/testData/gyro2Pools/gyro2TestPool.json @@ -0,0 +1,51 @@ +{ + "tradeInfo": { + "SwapType": "n/a", + "TokenIn": "n/a", + "TokenOut": "n/a", + "NoPools": 1, + "SwapAmount": "n/a", + "GasPrice": "n/a", + "SwapAmountDecimals": "n/a", + "ReturnAmountDecimals": "n/a" + }, + "pools": [ + { + "id": "0xebfed10e11dc08fcda1af1fda146945e8710f22e0000000000000000000000ff", + "address": "0xebfed10e11dc08fcda1af1fda146945e8710f22e", + "swapFee": "0.009", + "tokens": [ + { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "balance": "1232", + "decimals": 18, + "symbol": "DAI", + "weight": null, + "priceRate": "1" + }, + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "balance": "1000", + "decimals": 18, + "weight": null, + "priceRate": "1", + "symbol": "USDC" + } + ], + "tokensList": [ + "0x6b175474e89094c44da98b954eedeac495271d0f", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + ], + "totalWeight": "15", + "totalShares": "100", + "poolType": "Gyro2Pool", + "swapEnabled": true, + "gyro2PriceBounds": { + "lowerBound": "0.999", + "upperBound": "1.001", + "tokenInAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", + "tokenOutAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + } + } + ] +} From 93c768738bf867553193c09fcedd6d90256096d5 Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Sun, 13 Feb 2022 16:55:16 +0000 Subject: [PATCH 35/43] Add Gyro3Pool with tests --- src/pools/gyro3Pool/gyro3Math.ts | 439 ++++++++++++++++++ src/pools/gyro3Pool/gyro3Pool.ts | 472 ++++++++++++++++++++ src/types.ts | 14 +- test/gyro3Math.spec.ts | 145 ++++++ test/gyro3Pool.spec.ts | 122 +++++ test/testData/gyro3Pools/gyro3TestPool.json | 57 +++ yarn.lock | 28 +- 7 files changed, 1258 insertions(+), 19 deletions(-) create mode 100644 src/pools/gyro3Pool/gyro3Math.ts create mode 100644 src/pools/gyro3Pool/gyro3Pool.ts create mode 100644 test/gyro3Math.spec.ts create mode 100644 test/gyro3Pool.spec.ts create mode 100644 test/testData/gyro3Pools/gyro3TestPool.json diff --git a/src/pools/gyro3Pool/gyro3Math.ts b/src/pools/gyro3Pool/gyro3Math.ts new file mode 100644 index 00000000..2737f0c8 --- /dev/null +++ b/src/pools/gyro3Pool/gyro3Math.ts @@ -0,0 +1,439 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import bn from 'bignumber.js'; + +// Swap limits: amounts swapped may not be larger than this percentage of total balance. + +const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); +const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); + +// Helpers +export function _squareRoot(value: BigNumber): BigNumber { + return BigNumber.from( + new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] + ); +} + +export function _normalizeBalances( + balances: BigNumber[], + decimals: number[] +): BigNumber[] { + const scalingFactors = decimals.map((d) => parseFixed('1', d)); + + return balances.map((bal, index) => + bal.mul(ONE).div(scalingFactors[index]) + ); +} + +///////// +/// Fee calculations +///////// + +export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + const feeAmount = amountIn.mul(swapFee).div(ONE); + return amountIn.sub(feeAmount); +} + +export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + return amountIn.mul(ONE).div(ONE.sub(swapFee)); +} + +///////// +/// Invariant Calculation +///////// + +// Stopping criterion for the Newton iteration that computes the invariant: +// - Stop if the step width doesn't shrink anymore by at least a factor _INVARIANT_SHRINKING_FACTOR_PER_STEP. +// - ... but in any case, make at least _INVARIANT_MIN_ITERATIONS iterations. This is useful to compensate for a +// less-than-ideal starting point, which is important when alpha is small. +const _INVARIANT_SHRINKING_FACTOR_PER_STEP = 10; +const _INVARIANT_MIN_ITERATIONS = 2; + +// Invariant is used to collect protocol swap fees by comparing its value between two times. +// So we can round always to the same direction. It is also used to initiate the BPT amount +// and, because there is a minimum BPT, we round down the invariant. +// Argument root3Alpha = cube root of the lower price bound (symmetric across assets) +// Note: all price bounds for the pool are alpha and 1/alpha + +export function _calculateInvariant( + balances: BigNumber[], + root3Alpha: BigNumber +): BigNumber { + /********************************************************************************************** +// Calculate root of cubic: +// (1-alpha)L^3 - (x+y+z) * alpha^(2/3) * L^2 - (x*y + y*z + x*z) * alpha^(1/3) * L - x*y*z = 0 +// These coefficients are a,b,c,d respectively +// here, a > 0, b < 0, c < 0, and d < 0 +// taking mb = -b and mc = -c +/**********************************************************************************************/ + const [a, mb, mc, md] = _calculateCubicTerms(balances, root3Alpha); + return _calculateCubic(a, mb, mc, md); +} + +/** @dev Prepares quadratic terms for input to _calculateCubic + * assumes a > 0, b < 0, c <= 0, and d <= 0 and returns a, -b, -c, -d + * terms come from cubic in Section 3.1.1 + * argument root3Alpha = cube root of alpha + */ +export function _calculateCubicTerms( + balances: BigNumber[], + root3Alpha: BigNumber +): [BigNumber, BigNumber, BigNumber, BigNumber] { + const alpha23: BigNumber = root3Alpha.mul(root3Alpha).div(ONE); // alpha to the power of (2/3) + const alpha = alpha23.mul(root3Alpha).div(ONE); + const a = ONE.sub(alpha); + const bterm = balances[0].add(balances[1]).add(balances[2]); + const mb = bterm.mul(alpha23).div(ONE); + const cterm = balances[0] + .mul(balances[1]) + .div(ONE) + .add(balances[1].mul(balances[2]).div(ONE)) + .add(balances[2].mul(balances[0]).div(ONE)); + const mc = cterm.mul(root3Alpha).div(ONE); + const md = balances[0].mul(balances[1]).div(ONE).mul(balances[2]).div(ONE); + + return [a, mb, mc, md]; +} + +/** @dev Calculate the maximal root of the polynomial a L^3 - mb L^2 - mc L - md. + * This root is always non-negative, and it is the unique positive root unless mb == mc == md == 0. */ +export function _calculateCubic( + a: BigNumber, + mb: BigNumber, + mc: BigNumber, + md: BigNumber +): BigNumber { + let rootEst: BigNumber; + if (md.isZero()) { + // lower-order special case + const radic = mb + .mul(mb) + .div(ONE) + .add(a.mul(mc).div(ONE).mul(BigNumber.from(4))); + rootEst = mb.add(_squareRoot(radic)).div(BigNumber.from(2).mul(a)); + } else { + rootEst = _calculateCubicStartingPoint(a, mb, mc); + rootEst = _runNewtonIteration(a, mb, mc, md, rootEst); + } + + return rootEst; +} + +/** @dev Starting point for Newton iteration. Safe with all cubic polynomials where the coefficients have the appropriate + * signs, but calibrated to the particular polynomial for computing the invariant. */ +export function _calculateCubicStartingPoint( + a: BigNumber, + mb: BigNumber, + mc: BigNumber +): BigNumber { + const radic: BigNumber = mb + .mul(mb) + .div(ONE) + .add(a.mul(mc).div(ONE).mul(BigNumber.from(3))); + const lmin = mb + .mul(ONE) + .div(a.mul(BigNumber.from(3))) + .add(_squareRoot(radic).mul(ONE).div(BigNumber.from(3).mul(a))); + // The factor 3/2 is a magic number found experimentally for our invariant. All factors > 1 are safe. + const l0 = lmin.mul(BigNumber.from(3)).div(BigNumber.from(2)); + + return l0; +} + +/** @dev Find a root of the given polynomial with the given starting point l. + * Safe iff l > the local minimum. + * Note that f(l) may be negative for the first iteration and will then be positive (up to rounding errors). + * f'(l) is always positive for the range of values we consider. + * See write-up, Appendix A. */ +export function _runNewtonIteration( + a: BigNumber, + mb: BigNumber, + mc: BigNumber, + md: BigNumber, + rootEst: BigNumber +): BigNumber { + let deltaAbsPrev = BigNumber.from(0); + for (let iteration = 0; iteration < 255; ++iteration) { + // The delta to the next step can be positive or negative, so we represent a positive and a negative part + // separately. The signed delta is delta_plus - delta_minus, but we only ever consider its absolute value. + const [deltaAbs, deltaIsPos] = _calcNewtonDelta(a, mb, mc, md, rootEst); + // ^ Note: If we ever set _INVARIANT_MIN_ITERATIONS=0, the following should include `iteration >= 1`. + if ( + deltaAbs.isZero() || + (iteration >= _INVARIANT_MIN_ITERATIONS && deltaIsPos) + ) + // Iteration literally stopped or numerical error dominates + return rootEst; + if ( + iteration >= _INVARIANT_MIN_ITERATIONS && + deltaAbs.gte( + deltaAbsPrev.div( + BigNumber.from(_INVARIANT_SHRINKING_FACTOR_PER_STEP) + ) + ) + ) { + // stalled + // Move one more step to the left to ensure we're underestimating, rather than overestimating, L + return rootEst.sub(deltaAbs); + } + deltaAbsPrev = deltaAbs; + if (deltaIsPos) rootEst = rootEst.add(deltaAbs); + else rootEst = rootEst.sub(deltaAbs); + } + + throw new Error( + 'Gyro3Pool: Newton Method did not converge on required invariant' + ); +} +let first = 0; +// -f(l)/f'(l), represented as an absolute value and a sign. Require that l is sufficiently large so that f is strictly increasing. +export function _calcNewtonDelta( + a: BigNumber, + mb: BigNumber, + mc: BigNumber, + md: BigNumber, + rootEst: BigNumber +): [BigNumber, boolean] { + const dfRootEst = BigNumber.from(3) + .mul(a) + .mul(rootEst) + .div(ONE) + .sub(BigNumber.from(2).mul(mb)) + .mul(rootEst) + .div(ONE) + .sub(mc); // Does not underflow since rootEst >> 0 by assumption. + // We know that a rootEst^2 / dfRootEst ~ 1. (this is pretty exact actually, see the Mathematica notebook). We use this + // multiplication order to prevent overflows that can otherwise occur when computing l^3 for very large + // reserves. + + let deltaMinus = a.mul(rootEst).div(ONE).mul(rootEst).div(ONE); + deltaMinus = deltaMinus.mul(ONE).div(dfRootEst).mul(rootEst).div(ONE); + // use multiple statements to prevent 'stack too deep'. The order of operations is chosen to prevent overflows + // for very large numbers. + let deltaPlus = mb.mul(rootEst).div(ONE).add(mc).mul(ONE).div(dfRootEst); + deltaPlus = deltaPlus.mul(rootEst).div(ONE).add(md.mul(ONE).div(dfRootEst)); + + const deltaIsPos = deltaPlus.gte(deltaMinus); + const deltaAbs = deltaIsPos + ? deltaPlus.sub(deltaMinus) + : deltaMinus.sub(deltaPlus); + + return [deltaAbs, deltaIsPos]; +} + +///////// +/// Swap Amount Calculations +///////// + +/** @dev Computes how many tokens can be taken out of a pool if `amountIn` are sent, given the + * current balances and weights. + * Changed signs compared to original algorithm to account for amountOut < 0. + * See Proposition 12 in 3.1.4.*/ +export function _calcOutGivenIn( + balanceIn: BigNumber, + balanceOut: BigNumber, + amountIn: BigNumber, + virtualOffsetInOut: BigNumber +): BigNumber { + /********************************************************************************************** + // Described for X = `in' asset and Z = `out' asset, but equivalent for the other case // + // dX = incrX = amountIn > 0 // + // dZ = incrZ = amountOut < 0 // + // x = balanceIn x' = x + virtualOffset // + // z = balanceOut z' = z + virtualOffset // + // L = inv.Liq / x' * z' \ // + // - dZ = z' - | -------------------------- | // + // x' = virtIn \ ( x' + dX) / // + // z' = virtOut // + // Note that -dz > 0 is what the trader receives. // + // We exploit the fact that this formula is symmetric up to virtualParam{X,Y,Z}. // + **********************************************************************************************/ + if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) + throw new Error('Swap Amount In Too Large'); + + const virtIn = balanceIn.add(virtualOffsetInOut); + const virtOut = balanceOut.add(virtualOffsetInOut); + const denominator = virtIn.add(amountIn); + const subtrahend = virtIn.mul(virtOut).div(denominator); + const amountOut = virtOut.sub(subtrahend); + + // Note that this in particular reverts if amountOut > balanceOut, i.e., if the out-amount would be more than + // the balance. + + if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) + throw new Error('Resultant Swap Amount Out Too Large'); + + return amountOut; +} + +/** @dev Computes how many tokens must be sent to a pool in order to take `amountOut`, given the + * currhent balances and weights. + * Similar to the one before but adapting bc negative values (amountOut would be negative).*/ +export function _calcInGivenOut( + balanceIn: BigNumber, + balanceOut: BigNumber, + amountOut: BigNumber, + virtualOffsetInOut: BigNumber +): BigNumber { + /********************************************************************************************** + // Described for X = `in' asset and Z = `out' asset, but equivalent for the other case // + // dX = incrX = amountIn > 0 // + // dZ = incrZ = amountOut < 0 // + // x = balanceIn x' = x + virtualOffset // + // z = balanceOut z' = z + virtualOffset // + // L = inv.Liq / x' * z' \ // + // dX = | -------------------------- | - x' // + // x' = virtIn \ ( z' + dZ) / // + // z' = virtOut // + // Note that dz < 0 < dx. // + // We exploit the fact that this formula is symmetric up to virtualParam{X,Y,Z}. // + **********************************************************************************************/ + + // Note that this in particular reverts if amountOut > balanceOut, i.e., if the trader tries to take more out of + // the pool than is in it. + if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) + throw new Error('Swap Amount Out Too Large'); + + const virtIn = balanceIn.add(virtualOffsetInOut); + const virtOut = balanceOut.add(virtualOffsetInOut); + const denominator = virtOut.sub(amountOut); + const minuend = virtIn.mul(virtOut).div(denominator); + + const amountIn = minuend.sub(virtIn); + + if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) + throw new Error('Resultant Swap Amount In Too Large'); + + return amountIn; +} + +// ///////// +// /// Spot price function +// ///////// + +export function _calculateNewSpotPrice( + balances: BigNumber[], + inAmount: BigNumber, + outAmount: BigNumber, + virtualOffsetInOut: BigNumber, + swapFee: BigNumber +): BigNumber { + /********************************************************************************************** + // dX = incrX = amountIn > 0 // + // dZ = incrZ = amountOut < 0 // + // x = balanceIn x' = x + virtualOffsetInOut // + // z = balanceOut z' = z + virtualOffsetInOut // + // s = swapFee // + // L = inv.Liq 1 / x' + (1 - s) * dx \ // + // p_z = --- | -------------------------- | // + // x' = virtIn 1-s \ z' + dz / // + // z' = virtOut // + // Note that dz < 0 < dx. // + **********************************************************************************************/ + + const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s + const virtIn = balances[0].add(virtualOffsetInOut); // x + virtualOffsetInOut = x' + const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + + const virtOut = balances[1].add(virtualOffsetInOut); // z + virtualOffsetInOut = y' + const denominator = afterFeeMultiplier.mul(virtOut.sub(outAmount)).div(ONE); // (1 - s) * (z' + dz) + + const newSpotPrice = numerator.mul(ONE).div(denominator); + + return newSpotPrice; +} + +// ///////// +// /// Derivatives of spotPriceAfterSwap +// ///////// + +// SwapType = 'swapExactIn' +export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + balances: BigNumber[], + outAmount: BigNumber, + virtualOffsetInOut: BigNumber +): BigNumber { + /********************************************************************************************** + // dz = incrZ = amountOut < 0 // + // + // z = balanceOut z' = z + virtualOffsetInOut = virtOut // + // // + // / 1 \ // + // (p_z)' = 2 | -------------------------- | // + // \ z' + dz / // + // // + // Note that dz < 0 // + **********************************************************************************************/ + + const TWO = BigNumber.from(2).mul(ONE); + const virtOut = balances[1].add(virtualOffsetInOut); // z' = z + virtualOffsetInOut + const denominator = virtOut.sub(outAmount); // z' + dz + + const derivative = TWO.mul(ONE).div(denominator); + + return derivative; +} + +// SwapType = 'swapExactOut' +export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + balances: BigNumber[], + inAmount: BigNumber, + outAmount: BigNumber, + virtualOffsetInOut: BigNumber, + swapFee: BigNumber +): BigNumber { + /********************************************************************************************** + // dX = incrX = amountIn > 0 // + // dZ = incrZ = amountOut < 0 // + // x = balanceIn x' = x + virtualOffsetInOut // + // z = balanceOut z' = z + virtualOffsetInOut // + // s = swapFee // + // L = inv.Liq 1 / x' + (1 - s) * dx \ // + // p_z = --- (2) | -------------------------- | // + // x' = virtIn 1-s \ (z' + dz)^2 / // + // z' = virtOut // + // Note that dz < 0 < dx. // + **********************************************************************************************/ + + const TWO = BigNumber.from(2).mul(ONE); + const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s + const virtIn = balances[0].add(virtualOffsetInOut); // x + virtualOffsetInOut = x' + const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const virtOut = balances[1].add(virtualOffsetInOut); // z + virtualOffsetInOut = z' + const denominator = virtOut + .sub(outAmount) + .mul(virtOut.sub(outAmount)) + .div(ONE); // (z' + dz)^2 + const factor = TWO.mul(ONE).div(afterFeeMultiplier); // 2 / (1 - s) + + const derivative = factor.mul(numerator.mul(ONE).div(denominator)).div(ONE); + + return derivative; +} + +// ///////// +// /// Normalized Liquidity +// ///////// + +export function _getNormalizedLiquidity( + balances: BigNumber[], + virtualParamIn: BigNumber, + swapFee: BigNumber +): BigNumber { + /********************************************************************************************** + // x = balanceIn x' = x + virtualParamX // + // s = swapFee // + // 1 // + // normalizedLiquidity = --- x' // + // 1-s // + // x' = virtIn // + **********************************************************************************************/ + + const virtIn = balances[0].add(virtualParamIn); + + const afterFeeMultiplier = ONE.sub(swapFee); + + const normalizedLiquidity = virtIn.mul(ONE).div(afterFeeMultiplier); + + return normalizedLiquidity; +} diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts new file mode 100644 index 00000000..165ef9f2 --- /dev/null +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -0,0 +1,472 @@ +import { getAddress } from '@ethersproject/address'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { parseFixed, formatFixed, BigNumber } from '@ethersproject/bignumber'; +import { BigNumber as OldBigNumber, bnum } from '../../utils/bignumber'; + +import { + PoolBase, + PoolPairBase, + PoolTypes, + SwapPairType, + SubgraphToken, + SwapTypes, + SubgraphPoolBase, + Gyro3PriceBounds, +} from '../../types'; +import { isSameAddress } from '../../utils'; +import { + _normalizeBalances, + _calculateInvariant, + _calcOutGivenIn, + _calcInGivenOut, + _reduceFee, + _addFee, + _calculateNewSpotPrice, + _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, + _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, + _getNormalizedLiquidity, +} from './gyro3Math'; + +export type Gyro3PoolPairData = PoolPairBase & { + balanceTertiary: BigNumber; // Balance of the unchanged asset + decimalsTertiary: number; // Decimals of the unchanged asset +}; + +export type Gyro3PoolToken = Pick< + SubgraphToken, + 'address' | 'balance' | 'decimals' +>; + +export class Gyro3Pool implements PoolBase { + poolType: PoolTypes = PoolTypes.Gyro3; + swapPairType: SwapPairType; + id: string; + address: string; + tokensList: string[]; + tokens: Gyro3PoolToken[]; + swapFee: BigNumber; + totalShares: BigNumber; + root3Alpha: BigNumber; + + // Max In/Out Ratios + MAX_IN_RATIO = parseFixed('0.3', 18); + MAX_OUT_RATIO = parseFixed('0.3', 18); + + private static findToken(list, tokenAddress, error) { + const token = list.find( + (t) => getAddress(t.address) === getAddress(tokenAddress) + ); + if (!token) throw new Error(error); + return token; + } + + static fromPool(pool: SubgraphPoolBase): Gyro3Pool { + if (!pool.gyro3PriceBounds) + throw new Error('Pool missing gyro3PriceBounds'); + + const { alpha } = pool.gyro3PriceBounds; + if (!(Number(alpha) > 0 && Number(alpha) < 1)) + throw new Error('Invalid alpha price bound in gyro3PriceBounds'); + + if (pool.tokens.length !== 3) + throw new Error('Gyro3Pool must contain three tokens only'); + + return new Gyro3Pool( + pool.id, + pool.address, + pool.swapFee, + pool.totalShares, + pool.tokens as Gyro3PoolToken[], + pool.tokensList, + pool.gyro3PriceBounds as Gyro3PriceBounds + ); + } + + constructor( + id: string, + address: string, + swapFee: string, + totalShares: string, + tokens: Gyro3PoolToken[], + tokensList: string[], + priceBounds: Gyro3PriceBounds + ) { + this.id = id; + this.address = address; + this.swapFee = parseFixed(swapFee, 18); + this.totalShares = parseFixed(totalShares, 18); + this.tokens = tokens; + this.tokensList = tokensList; + + const root3Alpha = parseFixed( + Math.pow(Number(priceBounds.alpha), 1 / 3).toString(), + 18 + ); + + this.root3Alpha = root3Alpha; + } + + setTypeForSwap(type: SwapPairType): void { + this.swapPairType = type; + } + + parsePoolPairData(tokenIn: string, tokenOut: string): Gyro3PoolPairData { + const tI = Gyro3Pool.findToken( + this.tokens, + tokenIn, + 'Pool does not contain tokenIn' + ); + const balanceIn = tI.balance; + const decimalsIn = tI.decimals; + + const tO = Gyro3Pool.findToken( + this.tokens, + tokenOut, + 'Pool does not contain tokenOut' + ); + const balanceOut = tO.balance; + const decimalsOut = tO.decimals; + + const tokenTertiary = this.tokens.find( + (t) => + getAddress(t.address) !== getAddress(tokenOut) && + getAddress(t.address) !== getAddress(tokenIn) + ); + + if (!tokenTertiary) + throw new Error('Pool does not contain a valid third token'); + + const balanceTertiary = tokenTertiary.balance; + const decimalsTertiary = tokenTertiary.decimals; + + const poolPairData: Gyro3PoolPairData = { + id: this.id, + address: this.address, + poolType: this.poolType, + tokenIn: tokenIn, + tokenOut: tokenOut, + decimalsIn: Number(decimalsIn), + decimalsOut: Number(decimalsOut), + decimalsTertiary: Number(decimalsTertiary), + balanceIn: parseFixed(balanceIn, decimalsIn), + balanceOut: parseFixed(balanceOut, decimalsOut), + balanceTertiary: parseFixed(balanceTertiary, decimalsTertiary), + swapFee: this.swapFee, + }; + + return poolPairData; + } + + getNormalizedLiquidity(poolPairData: Gyro3PoolPairData): OldBigNumber { + const balances = [ + poolPairData.balanceIn, + poolPairData.balanceOut, + poolPairData.balanceTertiary, + ]; + const decimals = [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + poolPairData.decimalsTertiary, + ]; + const normalizedBalances = _normalizeBalances(balances, decimals); + + const invariant = _calculateInvariant( + normalizedBalances, + this.root3Alpha + ); + + const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + + const normalisedLiquidity = _getNormalizedLiquidity( + normalizedBalances, + virtualOffsetInOut, + poolPairData.swapFee + ); + + return bnum(formatFixed(normalisedLiquidity, 18)); + } + + getLimitAmountSwap( + poolPairData: PoolPairBase, + swapType: SwapTypes + ): OldBigNumber { + if (swapType === SwapTypes.SwapExactIn) { + return bnum( + formatFixed( + poolPairData.balanceIn.mul(this.MAX_IN_RATIO).div(ONE), + poolPairData.decimalsIn + ) + ); + } else { + return bnum( + formatFixed( + poolPairData.balanceOut.mul(this.MAX_OUT_RATIO).div(ONE), + poolPairData.decimalsOut + ) + ); + } + } + + // Updates the balance of a given token for the pool + updateTokenBalanceForPool(token: string, newBalance: BigNumber): void { + // token is BPT + if (this.address == token) { + this.totalShares = newBalance; + } else { + // token is underlying in the pool + const T = this.tokens.find((t) => isSameAddress(t.address, token)); + if (!T) throw Error('Pool does not contain this token'); + T.balance = formatFixed(newBalance, T.decimals); + } + } + + _exactTokenInForTokenOut( + poolPairData: Gyro3PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const balances = [ + poolPairData.balanceIn, + poolPairData.balanceOut, + poolPairData.balanceTertiary, + ]; + const decimals = [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + poolPairData.decimalsTertiary, + ]; + const normalizedBalances = _normalizeBalances(balances, decimals); + + const invariant = _calculateInvariant( + normalizedBalances, + this.root3Alpha + ); + + const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + + const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); + + const outAmount = _calcOutGivenIn( + poolPairData.balanceIn, + poolPairData.balanceOut, + inAmountLessFee, + virtualOffsetInOut + ); + + return bnum(formatFixed(outAmount, 18)); + } + + _tokenInForExactTokenOut( + poolPairData: Gyro3PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const outAmount = parseFixed( + amount.toString(), + poolPairData.decimalsOut + ); + const balances = [ + poolPairData.balanceIn, + poolPairData.balanceOut, + poolPairData.balanceTertiary, + ]; + const decimals = [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + poolPairData.decimalsTertiary, + ]; + const normalizedBalances = _normalizeBalances(balances, decimals); + + const invariant = _calculateInvariant( + normalizedBalances, + this.root3Alpha + ); + + const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + + const inAmountLessFee = _calcInGivenOut( + poolPairData.balanceIn, + poolPairData.balanceOut, + outAmount, + virtualOffsetInOut + ); + const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); + + return bnum(formatFixed(inAmount, 18)); + } + + _spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: Gyro3PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const balances = [ + poolPairData.balanceIn, + poolPairData.balanceOut, + poolPairData.balanceTertiary, + ]; + const decimals = [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + poolPairData.decimalsTertiary, + ]; + const normalizedBalances = _normalizeBalances(balances, decimals); + + const invariant = _calculateInvariant( + normalizedBalances, + this.root3Alpha + ); + + const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + + const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); + + const outAmount = _calcOutGivenIn( + poolPairData.balanceIn, + poolPairData.balanceOut, + inAmountLessFee, + virtualOffsetInOut + ); + + const newSpotPrice = _calculateNewSpotPrice( + normalizedBalances, + inAmount, + outAmount, + virtualOffsetInOut, + poolPairData.swapFee + ); + return bnum(formatFixed(newSpotPrice, 18)); + } + + _spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: Gyro3PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const outAmount = parseFixed( + amount.toString(), + poolPairData.decimalsOut + ); + const balances = [ + poolPairData.balanceIn, + poolPairData.balanceOut, + poolPairData.balanceTertiary, + ]; + const decimals = [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + poolPairData.decimalsTertiary, + ]; + const normalizedBalances = _normalizeBalances(balances, decimals); + + const invariant = _calculateInvariant( + normalizedBalances, + this.root3Alpha + ); + + const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + + const inAmountLessFee = _calcInGivenOut( + poolPairData.balanceIn, + poolPairData.balanceOut, + outAmount, + virtualOffsetInOut + ); + const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); + + const newSpotPrice = _calculateNewSpotPrice( + normalizedBalances, + inAmount, + outAmount, + virtualOffsetInOut, + poolPairData.swapFee + ); + + return bnum(formatFixed(newSpotPrice, 18)); + } + + _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData: Gyro3PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const balances = [ + poolPairData.balanceIn, + poolPairData.balanceOut, + poolPairData.balanceTertiary, + ]; + const decimals = [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + poolPairData.decimalsTertiary, + ]; + const normalizedBalances = _normalizeBalances(balances, decimals); + + const invariant = _calculateInvariant( + normalizedBalances, + this.root3Alpha + ); + + const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + + const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); + + const outAmount = _calcOutGivenIn( + poolPairData.balanceIn, + poolPairData.balanceOut, + inAmountLessFee, + virtualOffsetInOut + ); + const derivative = _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + normalizedBalances, + outAmount, + virtualOffsetInOut + ); + + return bnum(formatFixed(derivative, 18)); + } + + _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + poolPairData: Gyro3PoolPairData, + amount: OldBigNumber + ): OldBigNumber { + const outAmount = parseFixed( + amount.toString(), + poolPairData.decimalsOut + ); + const balances = [ + poolPairData.balanceIn, + poolPairData.balanceOut, + poolPairData.balanceTertiary, + ]; + const decimals = [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + poolPairData.decimalsTertiary, + ]; + const normalizedBalances = _normalizeBalances(balances, decimals); + + const invariant = _calculateInvariant( + normalizedBalances, + this.root3Alpha + ); + + const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + + const inAmountLessFee = _calcInGivenOut( + poolPairData.balanceIn, + poolPairData.balanceOut, + outAmount, + virtualOffsetInOut + ); + const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); + + const derivative = _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + normalizedBalances, + inAmount, + outAmount, + virtualOffsetInOut, + poolPairData.swapFee + ); + + return bnum(formatFixed(derivative, 18)); + } +} diff --git a/src/types.ts b/src/types.ts index a0dd8b52..c4084c50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,7 +26,7 @@ export enum PoolTypes { MetaStable, Linear, Gyro2, - // Gyro3, + Gyro3, } export enum SwapPairType { @@ -99,8 +99,8 @@ export interface SubgraphPoolBase { // Gyro2 specific field gyro2PriceBounds?: Gyro2PriceBounds; - // // Gyro3 specific field - // gyro3PriceBounds?: Gyro3PriceBounds; + // Gyro3 specific field + gyro3PriceBounds?: Gyro3PriceBounds; } export type SubgraphToken = { @@ -229,7 +229,7 @@ export type Gyro2PriceBounds = { tokenOutAddress: string; }; -// export type Gyro3PriceBounds = { -// alpha: string; // Assume symmetric price bounds for Gyro 3 pool -// // (The price range for any asset pair is equal to [alpha, 1/alpha]) -// }; +export type Gyro3PriceBounds = { + alpha: string; // Assume symmetric price bounds for Gyro 3 pool + // (The price range for any asset pair is equal to [alpha, 1/alpha]) +}; diff --git a/test/gyro3Math.spec.ts b/test/gyro3Math.spec.ts new file mode 100644 index 00000000..d8ade66e --- /dev/null +++ b/test/gyro3Math.spec.ts @@ -0,0 +1,145 @@ +import { expect } from 'chai'; +import cloneDeep from 'lodash.clonedeep'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { USDC, DAI } from './lib/constants'; +// Add new PoolType +import { Gyro3Pool } from '../src/pools/gyro3Pool/gyro3Pool'; +// Add new pool test data in Subgraph Schema format +import testPools from './testData/gyro3Pools/gyro3TestPool.json'; +import { + _calcNewtonDelta, + _calculateCubicStartingPoint, + _calculateCubicTerms, + _runNewtonIteration, +} from '../src/pools/gyro3Pool/gyro3Math'; + +describe('gyro3Math tests', () => { + const testPool = cloneDeep(testPools).pools[0]; + const pool = Gyro3Pool.fromPool(testPool); + + const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); + + context('Calculate Invariant', () => { + it(`should correctly calculate the terms of the cubic expression`, async () => { + const [a, mb, mc, md] = _calculateCubicTerms( + [ + poolPairData.balanceIn, + poolPairData.balanceOut, + poolPairData.balanceTertiary, + ], + pool.root3Alpha + ); + + expect(formatFixed(a, 18)).to.equal('0.013000000000000127'); + expect(formatFixed(mb, 18)).to.equal('245387.995391323689889516'); + expect(formatFixed(mc, 18)).to.equal( + '20335328582.7366683195765956' + ); + expect(formatFixed(md, 18)).to.equal('561707977531810.0'); + }); + + it(`should correctly calculate the starting point for the Newton method approximation of L`, async () => { + const a = parseFixed('0.013000000000000127', 18); + const mb = parseFixed('245387.995391323689889516', 18); + const mc = parseFixed('20335328582.7366683195765956', 18); + + const l0 = _calculateCubicStartingPoint(a, mb, mc); + + expect(formatFixed(l0, 18)).to.equal('18937948.911434007702525325'); + }); + + it(`should correctly calculate deltas for Newton method`, async () => { + const a = parseFixed('0.013000000000000127', 18); + const mb = parseFixed('245387.995391323689889516', 18); + const mc = parseFixed('20335328582.7366683195765956', 18); + const md = parseFixed('561707977531810.0', 18); + + const rootEst0 = parseFixed('18937948.911434007702525325', 18); + + const [deltaAbs1, deltaIsPos1] = _calcNewtonDelta( + a, + mb, + mc, + md, + rootEst0 + ); + + expect(formatFixed(deltaAbs1, 18)).to.equal( + '20725.034790223169767955' + ); + expect(deltaIsPos1).to.equal(true); + + const rootEst1 = parseFixed('18958673.94622423087229328', 18); + + const [deltaAbs2, deltaIsPos2] = _calcNewtonDelta( + a, + mb, + mc, + md, + rootEst1 + ); + + expect(formatFixed(deltaAbs2, 18)).to.equal( + '45.163851832290322917' + ); + expect(deltaIsPos2).to.equal(false); + + const rootEst2 = parseFixed('18958628.782372398581970363', 18); + + const [deltaAbs3, deltaIsPos3] = _calcNewtonDelta( + a, + mb, + mc, + md, + rootEst2 + ); + + expect(formatFixed(deltaAbs3, 18)).to.equal('0.000214713810115934'); + expect(deltaIsPos3).to.equal(false); + + const rootEst3 = parseFixed('18958628.782157684771854429', 18); + + const [deltaAbs4, deltaIsPos4] = _calcNewtonDelta( + a, + mb, + mc, + md, + rootEst3 + ); + + expect(formatFixed(deltaAbs4, 18)).to.equal('0.000000000004454138'); + expect(deltaIsPos4).to.equal(false); + + const rootEst4 = parseFixed('18958628.782157684767400291', 18); + + const [deltaAbs5, deltaIsPos5] = _calcNewtonDelta( + a, + mb, + mc, + md, + rootEst4 + ); + + expect(formatFixed(deltaAbs5, 18)).to.equal('0.000000000004453998'); + expect(deltaIsPos5).to.equal(false); + + const finalRootEst = _runNewtonIteration(a, mb, mc, md, rootEst0); + + expect(formatFixed(finalRootEst, 18)).to.equal( + '18958628.782157684762946293' + ); + }); + }); +}); + +// a = 0.013000000000000127 +// mb = 245387.995391323689889516 +// mc = 20335328582.7366683195765956 +// md = 561707977531810.0 + +// l0 = 18937948.911434007702525325 +// l1 = 18,958,673.94622423087229328 +// l2 = 18,958,628.782372398581970363 +// l3 = 18,958,628.782157684771854429 +// l4 = 18,958,628.782157684767400291 +// lFinal = 18958628.782157684762946293 diff --git a/test/gyro3Pool.spec.ts b/test/gyro3Pool.spec.ts new file mode 100644 index 00000000..5cc3d6c8 --- /dev/null +++ b/test/gyro3Pool.spec.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import cloneDeep from 'lodash.clonedeep'; +import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { bnum } from '../src/utils/bignumber'; +import { USDC, USDT } from './lib/constants'; +import { SwapTypes } from '../src'; +// Add new PoolType +import { Gyro3Pool } from '../src/pools/gyro3Pool/gyro3Pool'; +// Add new pool test data in Subgraph Schema format +import testPools from './testData/gyro3Pools/gyro3TestPool.json'; + +describe('Gyro3Pool tests USDC > DAI', () => { + const testPool = cloneDeep(testPools).pools[0]; + const pool = Gyro3Pool.fromPool(testPool); + + const poolPairData = pool.parsePoolPairData(USDT.address, USDC.address); + + context('parsePoolPairData', () => { + it(`should correctly parse USDT > USDC`, async () => { + // Tests that compare poolPairData to known results with correct number scaling, etc, i.e.: + expect(poolPairData.swapFee.toString()).to.eq( + parseFixed(testPool.swapFee, 18).toString() + ); + expect(poolPairData.id).to.eq(testPool.id); + }); + + it(`should correctly calculate symmetric price bound parameter (cube root alpha)`, async () => { + expect( + Number(formatFixed(pool.root3Alpha, 18)) + ).to.be.approximately(0.995647752, 0.0000001); + }); + }); + + context('limit amounts', () => { + it(`should correctly calculate limit amounts, USDT > USDC`, async () => { + let amount = pool.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactIn + ); + + expect(amount.toString()).to.eq('24935.7'); + + amount = pool.getLimitAmountSwap( + poolPairData, + SwapTypes.SwapExactOut + ); + + expect(amount.toString()).to.eq('24445.5'); + }); + }); + + context('normalized liquidity', () => { + it(`should correctly calculate normalized liquidity, USDT > USDC`, async () => { + const normalizedLiquidity = + pool.getNormalizedLiquidity(poolPairData); + + expect(normalizedLiquidity.toString()).to.equal( + '19016283.981512596845077192' + ); + }); + }); + + context('Test Swaps', () => { + context('SwapExactIn', () => { + const amountIn = bnum('234.3543'); + + it('should correctly calculate amountOut given amountIn', async () => { + const amountOut = pool._exactTokenInForTokenOut( + poolPairData, + amountIn + ); + expect(amountOut.toString()).to.eq('233.62822068392501182'); + }); + it('should correctly calculate newSpotPrice', async () => { + const newSpotPrice = + pool._spotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + amountIn + ); + expect(newSpotPrice.toString()).to.eq('1.003120202974438177'); + }); + it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { + const derivative = + pool._derivativeSpotPriceAfterSwapExactTokenInForTokenOut( + poolPairData, + amountIn + ); + expect(derivative.toString()).to.eq('0.000000105499880184'); + }); + }); + + context('SwapExactOut', () => { + const amountOut = bnum('4523.5334368'); + + it('should correctly calculate amountIn given amountOut', async () => { + const amountIn = pool._tokenInForExactTokenOut( + poolPairData, + amountOut + ); + expect(amountIn.toString()).to.eq('4538.618912825809655998'); + }); + + it('should correctly calculate newSpotPrice', async () => { + const newSpotPrice = + pool._spotPriceAfterSwapTokenInForExactTokenOut( + poolPairData, + amountOut + ); + expect(newSpotPrice.toString()).to.eq('1.003574353766587852'); + }); + + it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { + const derivative = + pool._derivativeSpotPriceAfterSwapTokenInForExactTokenOut( + poolPairData, + amountOut + ); + expect(derivative.toString()).to.eq('0.000000105900938637'); + }); + }); + }); +}); diff --git a/test/testData/gyro3Pools/gyro3TestPool.json b/test/testData/gyro3Pools/gyro3TestPool.json new file mode 100644 index 00000000..1873839a --- /dev/null +++ b/test/testData/gyro3Pools/gyro3TestPool.json @@ -0,0 +1,57 @@ +{ + "tradeInfo": { + "SwapType": "n/a", + "TokenIn": "n/a", + "TokenOut": "n/a", + "NoPools": 1, + "SwapAmount": "n/a", + "GasPrice": "n/a", + "SwapAmountDecimals": "n/a", + "ReturnAmountDecimals": "n/a" + }, + "pools": [ + { + "id": "0xebfed10e11dc08fcda1af1fda146945e8710f22e0000000000000000000000ff", + "address": "0xebfed10e11dc08fcda1af1fda146945e8710f22e", + "swapFee": "0.003", + "tokens": [ + { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "balance": "82934", + "decimals": 18, + "symbol": "DAI", + "weight": null, + "priceRate": "1" + }, + { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "balance": "81485", + "decimals": 18, + "weight": null, + "priceRate": "1", + "symbol": "USDC" + }, + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "balance": "83119", + "decimals": 18, + "weight": "5", + "priceRate": "1", + "symbol": "USDT" + } + ], + "tokensList": [ + "0x6b175474e89094c44da98b954eedeac495271d0f", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xdac17f958d2ee523a2206206994597c13d831ec7" + ], + "totalWeight": "15", + "totalShares": "100", + "poolType": "Gyro3Pool", + "swapEnabled": true, + "gyro3PriceBounds": { + "alpha": "0.987" + } + } + ] +} diff --git a/yarn.lock b/yarn.lock index c2351d64..993d47a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2159,6 +2159,11 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-stream@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" @@ -2543,10 +2548,10 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.0.tgz#d4d16d035db99581b6194e119bbf36c963c5eb70" - integrity sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A== +istanbul-reports@^3.0.2: + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -2891,7 +2896,7 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" -node-preload@^0.2.0: +node-preload@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301" integrity sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ== @@ -2911,9 +2916,9 @@ npm-run-path@^3.0.0: path-key "^3.0.0" nyc@^15.0.0: - version "15.0.0" - resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.0.tgz#eb32db2c0f29242c2414fe46357f230121cfc162" - integrity sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg== + version "15.1.0" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.1.0.tgz#1335dae12ddc87b6e249d5a1994ca4bdaea75f02" + integrity sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A== dependencies: "@istanbuljs/load-nyc-config" "^1.0.0" "@istanbuljs/schema" "^0.1.2" @@ -2923,6 +2928,7 @@ nyc@^15.0.0: find-cache-dir "^3.2.0" find-up "^4.1.0" foreground-child "^2.0.0" + get-package-type "^0.1.0" glob "^7.1.6" istanbul-lib-coverage "^3.0.0" istanbul-lib-hook "^3.0.0" @@ -2930,10 +2936,9 @@ nyc@^15.0.0: istanbul-lib-processinfo "^2.0.2" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.0" - js-yaml "^3.13.1" + istanbul-reports "^3.0.2" make-dir "^3.0.0" - node-preload "^0.2.0" + node-preload "^0.2.1" p-map "^3.0.0" process-on-spawn "^1.0.0" resolve-from "^5.0.0" @@ -2941,7 +2946,6 @@ nyc@^15.0.0: signal-exit "^3.0.2" spawn-wrap "^2.0.0" test-exclude "^6.0.0" - uuid "^3.3.3" yargs "^15.0.2" oauth-sign@~0.9.0: From 9e4756c98c5dc236cb494089c04450cd3dff14d4 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Tue, 15 Feb 2022 20:39:50 -0300 Subject: [PATCH 36/43] LBP case: consider boosted paths composed with LBPs --- src/pools/index.ts | 10 +- src/pools/weightedPool/weightedPool.ts | 7 +- src/routeProposal/filtering.ts | 144 +++++++++++------- src/types.ts | 2 + test/boostedPaths.spec.ts | 133 +++++++++++++++- test/lib/constants.ts | 10 ++ test/linear.spec.ts | 3 +- .../boostedPools/multipleBoosted.json | 68 +++++++++ 8 files changed, 311 insertions(+), 66 deletions(-) diff --git a/src/pools/index.ts b/src/pools/index.ts index f1ca5ec0..0a273030 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -41,12 +41,10 @@ export function parseNewPool( | PhantomStablePool; try { - if ( - pool.poolType === 'Weighted' || - pool.poolType === 'LiquidityBootstrapping' || - pool.poolType === 'Investment' - ) { - newPool = WeightedPool.fromPool(pool); + if (pool.poolType === 'Weighted' || pool.poolType === 'Investment') { + newPool = WeightedPool.fromPool(pool, false); + } else if (pool.poolType === 'LiquidityBootstrapping') { + newPool = WeightedPool.fromPool(pool, true); } else if (pool.poolType === 'Stable') { newPool = StablePool.fromPool(pool); } else if (pool.poolType === 'MetaStable') { diff --git a/src/pools/weightedPool/weightedPool.ts b/src/pools/weightedPool/weightedPool.ts index b9fdc524..3cafa0d7 100644 --- a/src/pools/weightedPool/weightedPool.ts +++ b/src/pools/weightedPool/weightedPool.ts @@ -50,11 +50,12 @@ export class WeightedPool implements PoolBase { tokensList: string[]; MAX_IN_RATIO = parseFixed('0.3', 18); MAX_OUT_RATIO = parseFixed('0.3', 18); + isLBP = false; - static fromPool(pool: SubgraphPoolBase): WeightedPool { + static fromPool(pool: SubgraphPoolBase, isLBP?: boolean): WeightedPool { if (!pool.totalWeight) throw new Error('WeightedPool missing totalWeight'); - return new WeightedPool( + const weightedPool = new WeightedPool( pool.id, pool.address, pool.swapFee, @@ -63,6 +64,8 @@ export class WeightedPool implements PoolBase { pool.tokens as WeightedPoolToken[], pool.tokensList ); + if (isLBP) weightedPool.isLBP = true; + return weightedPool; } constructor( diff --git a/src/routeProposal/filtering.ts b/src/routeProposal/filtering.ts index 944e5c63..a61181cf 100644 --- a/src/routeProposal/filtering.ts +++ b/src/routeProposal/filtering.ts @@ -15,7 +15,6 @@ import { MetaStablePool } from '../pools/metaStablePool/metaStablePool'; import { ZERO } from '../utils/bignumber'; import { parseNewPool } from '../pools'; import { Zero } from '@ethersproject/constants'; -import { concat } from 'lodash'; export const filterPoolsByType = ( pools: SubgraphPoolBase[], @@ -208,9 +207,9 @@ export function filterHopPools( } /* -Returns paths using boosted pools. -Relevant paths using boosted pools have length greater than 2, so we need -a separate algorithm to create those paths. +Returns relevant paths using boosted pools, called "boosted paths". +Boosted paths tipically have length greater than 2, so we need +a separate algorithm to create them. We consider two central tokens: WETH and bbaUSD (which is the BPT of aave boosted-stable pool). We want to consider paths in which token_in and token_out are connected (each of them) to either WETH or bbaUSD. Here for a token A to be "connected" to @@ -218,7 +217,6 @@ a token B means that it satisfies one of the following: (a) A is B. (b) A and B belong to the same pool. (c) A has a linear pool whose BPT belongs to a pool jointly with B. -For the case of LBPs we will probably consider extra connections. Thus for token_in and token_out we generate every semipath connecting them to one of the central tokens. After that we combine semipaths to form @@ -226,17 +224,23 @@ paths from token_in to token_out. We expect to have a central pool WETH/bbaUSD. We use this pool to combine a semipath connecting to WETH with a semipath connecting to bbaUSD. - Issues +If either of token_in our token_out is a token being offered at an LBP, +we consider the boosted paths from the corresponding "raising token" +composed with the LBP. + +Two issues that had to be addressed: - a) when trading DAI/USDC, this finds the path + a) when trading for instance DAI/USDC, the algorithm described above finds + the path whose tokens chain is DAI-bbaDAI-bbaUSD-bbaUSDC-USDC instead of directly DAI-bbaDAI-bbaUSDC-USDC - Possible solution: add a patch to combineSemiPaths for the case where - the end pool of the first semipath matches the first pool of the second one. - b) For DAI/aDAI it finds complicated paths through bbaUSD pool - (to do, just in case: inspect those paths) + b) For DAI/aDAI it finds a path through bbaUSD pool using twice Aave-DAI linear pool. + + To deal with both of these we call the function composeSimplifyPath when + combining semipaths at combineSemiPaths function. */ + export function getBoostedPaths( tokenIn: string, tokenOut: string, @@ -248,16 +252,25 @@ export function getBoostedPaths( // We assume consistency between config and poolsAllDict. // If they are not consistent, there will be errors. if (!config.bbausd) return []; - // To do: ensure that no duplicate paths are sent to the second part of the SOR. const weth = config.weth.toLowerCase(); const bbausd = config.bbausd.address.toLowerCase(); + // Letter 'i' in iTokenIn and iTokenOut stands for "internal", + // lacking of a better name for that so far. + const [lbpPathIn, iTokenIn] = getLBP(tokenIn, poolsAllDict, true, config); + // eslint-disable-next-line prettier/prettier + const [lbpPathOut, iTokenOut] = getLBP( + tokenOut, + poolsAllDict, + false, + config + ); // getLinearPools could receive an array of tokens so that we search - // over poolsAllDict once instead of twice. Similarly for getPoolsWith. - // This is a matter of code simplicity vs. efficiency - const linearPoolsTokenIn = getLinearPools(tokenIn, poolsAllDict); - const linearPoolsTokenOut = getLinearPools(tokenOut, poolsAllDict); + // over poolsAllDict once instead of twice. Similarly for getPoolsWith + // and getLBP. This is a matter of code simplicity vs. efficiency. + const linearPoolsIn = getLinearPools(iTokenIn, poolsAllDict); + const linearPoolsOut = getLinearPools(iTokenOut, poolsAllDict); const wethPoolsDict = getPoolsWith(weth, poolsAllDict); const bbausdPoolsDict = getPoolsWith(bbausd, poolsAllDict); @@ -267,26 +280,26 @@ export function getBoostedPaths( delete bbausdPoolsDict[config.wethBBausd.id]; } const semiPathsInToWeth: NewPath[] = getSemiPaths( - tokenIn, - linearPoolsTokenIn, + iTokenIn, + linearPoolsIn, wethPoolsDict, weth ); const semiPathsInToBBausd: NewPath[] = getSemiPaths( - tokenIn, - linearPoolsTokenIn, + iTokenIn, + linearPoolsIn, bbausdPoolsDict, bbausd ); const semiPathsOutToWeth: NewPath[] = getSemiPaths( - tokenOut, - linearPoolsTokenOut, + iTokenOut, + linearPoolsOut, wethPoolsDict, weth ); const semiPathsOutToBBausd: NewPath[] = getSemiPaths( - tokenOut, - linearPoolsTokenOut, + iTokenOut, + linearPoolsOut, bbausdPoolsDict, bbausd ); @@ -296,17 +309,13 @@ export function getBoostedPaths( const semiPathsBBausdToOut = semiPathsOutToBBausd.map((path) => reversePath(path) ); - // Here we assume that every short path (short means length 1 and 2) is included - // in filterHopPools. - const paths1 = removeShortPaths( - combineSemiPaths(semiPathsInToWeth, semiPathsWethToOut) - ); - const paths2 = removeShortPaths( - combineSemiPaths(semiPathsInToBBausd, semiPathsBBausdToOut) - ); + + const paths1 = combineSemiPaths(semiPathsInToWeth, semiPathsWethToOut); + const paths2 = combineSemiPaths(semiPathsInToBBausd, semiPathsBBausdToOut); if (!config.wethBBausd) { const paths = paths1.concat(paths2); - return paths; + // Every short path (short means length 1 and 2) is included in filterHopPools. + return removeShortPaths(paths); } const WethBBausdPool = poolsAllDict[config.wethBBausd.id]; const WethBBausdPath = createPath( @@ -317,22 +326,26 @@ export function getBoostedPaths( [config.bbausd.address, config.weth], [WethBBausdPool] ); - const paths3 = removeShortPaths( - combineSemiPaths( - semiPathsInToWeth, - semiPathsBBausdToOut, - WethBBausdPath - ) + const paths3 = combineSemiPaths( + semiPathsInToWeth, + semiPathsBBausdToOut, + WethBBausdPath ); - const paths4 = removeShortPaths( - combineSemiPaths( - semiPathsInToBBausd, - semiPathsWethToOut, - BBausdWethPath - ) + const paths4 = combineSemiPaths( + semiPathsInToBBausd, + semiPathsWethToOut, + BBausdWethPath ); - const paths = paths1.concat(paths2, paths3, paths4); - return paths; + let paths = paths1.concat(paths2, paths3, paths4); + + // If there is a nontrivial LBP path, compose every path with the lbp paths + // in and out. One of them might be the empty path. + if (lbpPathIn.pools.length > 0 || lbpPathOut.pools.length > 0) { + paths = paths.map((path) => + composePaths([lbpPathIn, path, lbpPathOut]) + ); + } + return removeShortPaths(paths); } function getLinearPools( @@ -410,7 +423,6 @@ function combineSemiPaths( if (path) paths.push(path); } } - return paths; } @@ -691,12 +703,6 @@ export function createPath(tokens: string[], pools: PoolBase[]): NewPath { for (let i = 0; i < pools.length; i++) { tI = tokens[i]; tO = tokens[i + 1]; - /* console.log(i); - console.log(pools.length); - console.log(pools); - console.log(tokens); - console.log(pools[i]); - console.log(pools[i].id);*/ const poolPair = pools[i].parsePoolPairData(tI, tO); poolPairData.push(poolPair); id = id + poolPair.id; @@ -928,3 +934,33 @@ function composeSimplifyPath(semiPathIn: NewPath, semiPathOut: NewPath) { } return path; } + +function getLBP( + token: string, + poolsAllDict: PoolDictionary, + isInitial: boolean, + config: SorConfig +): [NewPath, string] { + if (config.lbpRaisingTokens) { + if (config.lbpRaisingTokens.includes(token)) { + return [getEmptyPath(), token]; + } + } + for (const id in poolsAllDict) { + const pool = poolsAllDict[id]; + if (!pool.isLBP) continue; + const tokensList = pool.tokensList; + // We assume that the LBP has two tokens. + for (let i = 0; i < 2; i++) { + if (tokensList[i] == token) { + let path = createPath( + [tokensList[i], tokensList[1 - i]], + [pool] + ); + if (!isInitial) path = reversePath(path); + return [path, tokensList[1 - i]]; + } + } + } + return [getEmptyPath(), token]; +} diff --git a/src/types.ts b/src/types.ts index df9b68c8..7917a0f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,7 @@ export interface SorConfig { staBal3Pool?: { id: string; address: string }; wethStaBal3?: { id: string; address: string }; usdcConnectingPool?: { id: string; usdc: string }; + lbpRaisingTokens?: string[]; } export type NoNullableField = { @@ -163,6 +164,7 @@ export interface PoolBase { address: string; tokensList: string[]; mainIndex?: number; + isLBP?: boolean; setTypeForSwap: (type: SwapPairType) => void; parsePoolPairData: (tokenIn: string, tokenOut: string) => PoolPairBase; getNormalizedLiquidity: (poolPairData: PoolPairBase) => OldBigNumber; diff --git a/test/boostedPaths.spec.ts b/test/boostedPaths.spec.ts index f9d55ae8..018d5736 100644 --- a/test/boostedPaths.spec.ts +++ b/test/boostedPaths.spec.ts @@ -216,7 +216,7 @@ describe('multiple boosted pools, path creation test', () => { } }); }); - context('debug-best Verify that SOR chooses best path', () => { + context('Verify that SOR chooses best path', () => { it('swapExactIn case', async () => { // For small enough amounts, the best route must typically be // formed exclusively by only one path. @@ -244,6 +244,136 @@ describe('multiple boosted pools, path creation test', () => { ); }); }); + + context('LBP cases', () => { + it('OHM-BAL', () => { + const tokenIn = '0x0000000000000000000000000000000000000009'; + const tokenOut = BAL.address; + const [paths, , boostedPaths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools, + sorConfigTestBoosted + ); + assert.equal( + paths[0].id, + 'LBPweightedTusdOhmweightedTusdWethweightedBalWeth' + ); + assert.equal( + paths[3].id, + 'LBPweightedTusdOhmBBaUSD-TUSDbbaUSD-BAL' + ); + const OHM = tokenIn; + const tokensChains = [ + [OHM, TUSD.address, WETH.address, bbaUSD.address, BAL.address], + [OHM, TUSD.address, bbaUSD.address, WETH.address, BAL.address], + ]; + const poolsChains = [ + [ + 'LBPweightedTusdOhm', + 'weightedTusdWeth', + 'weightedWeth-BBausd', + 'bbaUSD-BAL', + ], + [ + 'LBPweightedTusdOhm', + 'BBaUSD-TUSD', + 'weightedWeth-BBausd', + 'weightedBalWeth', + ], + ]; + for (const path of paths) { + console.log(path.id); + } + for (let i = 0; i < 2; i++) { + assert.isTrue( + // eslint-disable-next-line prettier/prettier + simpleCheckPath( + paths[i + 1], + poolsChains[i], + tokensChains[i] + ) + ); + } + assert.equal(boostedPaths.length, 4); + assert.equal(paths.length, 4); + }); + it('DAI-OHM', () => { + const tokenIn = DAI.address; + const tokenOut = '0x0000000000000000000000000000000000000009'; + const [paths, , boostedPaths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools, + sorConfigTestBoosted + ); + const tokensChains = [ + [ + DAI.address, + bbaDAI.address, + bbaUSD.address, + WETH.address, + TUSD.address, + tokenOut, + ], + ]; + const poolsChains = [ + [ + 'AaveLinearDai', + 'bbaUSD-Pool', + 'weightedWeth-BBausd', + 'weightedTusdWeth', + 'LBPweightedTusdOhm', + ], + ]; + assert.isTrue( + simpleCheckPath(paths[0], poolsChains[0], tokensChains[0]) + ); + assert.equal( + paths[1].id, + 'FuseLinearDaibbaUSD-bbfDAIweightedWeth-BBausdweightedTusdWethLBPweightedTusdOhm' + ); + assert.equal( + paths[2].id, + 'FuseLinearDaibbaUSD-bbfDAIBBaUSD-TUSDLBPweightedTusdOhm' + ); + assert.equal( + paths[3].id, + 'AaveLinearDaibbaUSD-PoolBBaUSD-TUSDLBPweightedTusdOhm' + ); + assert.equal(boostedPaths.length, 4); + assert.equal(paths.length, 4); + }); + it('OHM-QRE', () => { + const tokenIn = '0x0000000000000000000000000000000000000009'; + const tokenOut = '0x0000000000000000000000000000000000000011'; + const [paths, , boostedPaths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools, + sorConfigTestBoosted + ); + for (const path of paths) { + console.log(path.id); + } + assert.equal( + paths[0].id, + 'LBPweightedTusdOhmweightedTusdWethLBPweightedWethQre' + ); + assert.equal( + paths[1].id, + 'LBPweightedTusdOhmBBaUSD-TUSDweightedWeth-BBausdLBPweightedWethQre' + ); + assert.equal(boostedPaths.length, 2); + assert.equal(paths.length, 2); + }); + }); }); // The following code has been tested to detect duplicate paths @@ -357,6 +487,5 @@ export async function checkBestPath( areEqual = false; } } - console.log(swapInfo.swaps.length); return areEqual; } diff --git a/test/lib/constants.ts b/test/lib/constants.ts index 241ab2f2..1ac187bb 100644 --- a/test/lib/constants.ts +++ b/test/lib/constants.ts @@ -41,6 +41,10 @@ export const sorConfigTestBoosted = { id: 'weightedWeth-BBausd', address: '0x0000000000000000000000000000000000000004', }, + lbpRaisingTokens: [ + '0x0000000000085d4780b73119b644ae5ecd22b376', + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + ], }; export const sorConfigEth: SorConfig = { @@ -75,6 +79,12 @@ export const sorConfigFullKovan: SorConfig = { id: '0x6be79a54f119dbf9e8ebd9ded8c5bd49205bc62d00020000000000000000033c', address: '0x6be79a54f119dbf9e8ebd9ded8c5bd49205bc62d', }, + lbpRaisingTokens: [ + '0xdfcea9088c8a88a76ff74892c1457c17dfeef9c1', + '0xc2569dd7d0fd715b054fbf16e75b001e5c0c1115', + '0x41286bb1d3e870f3f750eb7e1c25d7e48c8a1ac7', + '0x04df6e4121c27713ed22341e7c7df330f56f289b', + ], }; export const WETH: TestToken = { diff --git a/test/linear.spec.ts b/test/linear.spec.ts index d50ac8ff..5a88362b 100644 --- a/test/linear.spec.ts +++ b/test/linear.spec.ts @@ -52,7 +52,6 @@ import singleLinear from './testData/linearPools/singleLinear.json'; import smallLinear from './testData/linearPools/smallLinear.json'; import kovanPools from './testData/linearPools/kovan.json'; import fullKovanPools from './testData/linearPools/fullKovan.json'; -import { getOutputAmountSwapForPath } from '../src/router/helpersClass'; import { checkBestPath } from './boostedPaths.spec'; describe('linear pool tests', () => { @@ -724,7 +723,7 @@ describe('linear pool tests', () => { expect(returnAmount).to.eq('6606146264948964392'); }); - it('debug-1 BAL>USDT, SwapExactIn', async () => { + it('BAL>USDT, SwapExactIn', async () => { const tokenIn = KOVAN_BAL; const tokenOut = AAVE_USDT; const returnAmount = await testFullSwap( diff --git a/test/testData/boostedPools/multipleBoosted.json b/test/testData/boostedPools/multipleBoosted.json index ddd9b112..8003fd87 100644 --- a/test/testData/boostedPools/multipleBoosted.json +++ b/test/testData/boostedPools/multipleBoosted.json @@ -458,6 +458,74 @@ ], "totalWeight": "100", "totalShares": "10000" + }, + { + "address": "0x0000000000000000000000000000000000000010", + "id": "LBPweightedTusdOhm", + "poolType": "LiquidityBootstrapping", + "publicSwap": true, + "swapFee": "0.01", + "swapEnabled": true, + "tokens": [ + { + "address": "0x0000000000000000000000000000000000000009", + "balance": "1000000.000000", + "decimals": 18, + "weight": "50", + "id": "LBPweightedTusdOhm-0x0000000000000000000000000000000000000009", + "symbol": "OHM", + "priceRate": "1" + }, + { + "address": "0x0000000000085d4780b73119b644ae5ecd22b376", + "balance": "20000000.78213445", + "decimals": 18, + "weight": "50", + "id": "LBPweightedTusdOhm-0x0000000000085d4780b73119b644ae5ecd22b376", + "symbol": "TUSD", + "priceRate": "1" + } + ], + "tokensList": [ + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0x0000000000000000000000000000000000000009" + ], + "totalWeight": "100", + "totalShares": "10000" + }, + { + "address": "0x0000000000000000000000000000000000000012", + "id": "LBPweightedWethQre", + "poolType": "LiquidityBootstrapping", + "publicSwap": true, + "swapFee": "0.01", + "swapEnabled": true, + "tokens": [ + { + "address": "0x0000000000000000000000000000000000000011", + "balance": "1000000.000000", + "decimals": 18, + "weight": "50", + "id": "LBPweightedWethQre-0x0000000000000000000000000000000000000011", + "symbol": "QRE", + "priceRate": "1" + }, + { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "balance": "100000.000000", + "decimals": 18, + "weight": "50", + "id": "LBPweightedWethQre-0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "symbol": "WETH", + "priceRate": "1" + } + ], + "tokensList": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0x0000000000000000000000000000000000000011" + ], + "totalWeight": "100", + "totalShares": "10000" } ] } From 4e65a2613e1a38578ed9955f824cad910ccd627f Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Mon, 21 Feb 2022 11:48:01 +0000 Subject: [PATCH 37/43] Fix decimals normalization bugs for Gyro2 pools --- src/pools/gyro2Pool/gyro2Pool.ts | 45 +++++++++------------ test/gyro2Math.spec.ts | 8 +++- test/gyro2Pool.spec.ts | 1 + test/testData/gyro2Pools/gyro2TestPool.json | 2 +- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index f2400136..511750b2 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -244,12 +244,12 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); - const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], inAmountLessFee, virtualParamIn, virtualParamOut, @@ -263,10 +263,7 @@ export class Gyro2Pool implements PoolBase { poolPairData: Gyro2PoolPairData, amount: OldBigNumber ): OldBigNumber { - const outAmount = parseFixed( - amount.toString(), - poolPairData.decimalsOut - ); + const outAmount = parseFixed(amount.toString(), 18); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; const normalizedBalances = _normalizeBalances( balances, @@ -284,8 +281,8 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtBeta ); const inAmountLessFee = _calcInGivenOut( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], outAmount, virtualParamIn, virtualParamOut, @@ -316,11 +313,11 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); - const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], inAmountLessFee, virtualParamIn, virtualParamOut, @@ -341,10 +338,7 @@ export class Gyro2Pool implements PoolBase { poolPairData: Gyro2PoolPairData, amount: OldBigNumber ): OldBigNumber { - const outAmount = parseFixed( - amount.toString(), - poolPairData.decimalsOut - ); + const outAmount = parseFixed(amount.toString(), 18); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; const normalizedBalances = _normalizeBalances( balances, @@ -362,8 +356,8 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtBeta ); const inAmountLessFee = _calcInGivenOut( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], outAmount, virtualParamIn, virtualParamOut, @@ -402,11 +396,11 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); - const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], inAmountLessFee, virtualParamIn, virtualParamOut, @@ -425,10 +419,7 @@ export class Gyro2Pool implements PoolBase { poolPairData: Gyro2PoolPairData, amount: OldBigNumber ): OldBigNumber { - const outAmount = parseFixed( - amount.toString(), - poolPairData.decimalsOut - ); + const outAmount = parseFixed(amount.toString(), 18); const balances = [poolPairData.balanceIn, poolPairData.balanceOut]; const normalizedBalances = _normalizeBalances( balances, @@ -446,8 +437,8 @@ export class Gyro2Pool implements PoolBase { poolPairData.sqrtBeta ); const inAmountLessFee = _calcInGivenOut( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], outAmount, virtualParamIn, virtualParamOut, diff --git a/test/gyro2Math.spec.ts b/test/gyro2Math.spec.ts index 408dca89..6aad772a 100644 --- a/test/gyro2Math.spec.ts +++ b/test/gyro2Math.spec.ts @@ -13,6 +13,7 @@ import { _calculateQuadratic, _calculateQuadraticTerms, _findVirtualParams, + _normalizeBalances, } from '../src/pools/gyro2Pool/gyro2Math'; describe('gyro2Math tests', () => { @@ -38,8 +39,13 @@ describe('gyro2Math tests', () => { context('invariant and virtual parameters', () => { it(`should correctly calculate invariant`, async () => { - const [a, mb, mc] = _calculateQuadraticTerms( + const normalizedBalances = _normalizeBalances( [poolPairData.balanceIn, poolPairData.balanceOut], + poolPairData.decimalsIn, + poolPairData.decimalsOut + ); + const [a, mb, mc] = _calculateQuadraticTerms( + normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta ); diff --git a/test/gyro2Pool.spec.ts b/test/gyro2Pool.spec.ts index 74a7c53e..23426779 100644 --- a/test/gyro2Pool.spec.ts +++ b/test/gyro2Pool.spec.ts @@ -1,3 +1,4 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyro2Pool.spec.ts import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; import { formatFixed, parseFixed } from '@ethersproject/bignumber'; diff --git a/test/testData/gyro2Pools/gyro2TestPool.json b/test/testData/gyro2Pools/gyro2TestPool.json index 8e400934..554e0108 100644 --- a/test/testData/gyro2Pools/gyro2TestPool.json +++ b/test/testData/gyro2Pools/gyro2TestPool.json @@ -26,7 +26,7 @@ { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "balance": "1000", - "decimals": 18, + "decimals": 6, "weight": null, "priceRate": "1", "symbol": "USDC" From 71d05466c229dc78cfa2703b8782b61c74725b3a Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Mon, 21 Feb 2022 12:07:24 +0000 Subject: [PATCH 38/43] Fix decimals normalization bugs for Gyro3 pools --- src/pools/gyro3Pool/gyro3Pool.ts | 45 +++++++++------------ test/gyro3Math.spec.ts | 11 ++++- test/testData/gyro3Pools/gyro3TestPool.json | 2 +- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index 165ef9f2..73840846 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -243,12 +243,12 @@ export class Gyro3Pool implements PoolBase { const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); - const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], inAmountLessFee, virtualOffsetInOut ); @@ -260,10 +260,7 @@ export class Gyro3Pool implements PoolBase { poolPairData: Gyro3PoolPairData, amount: OldBigNumber ): OldBigNumber { - const outAmount = parseFixed( - amount.toString(), - poolPairData.decimalsOut - ); + const outAmount = parseFixed(amount.toString(), 18); const balances = [ poolPairData.balanceIn, poolPairData.balanceOut, @@ -284,8 +281,8 @@ export class Gyro3Pool implements PoolBase { const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); const inAmountLessFee = _calcInGivenOut( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], outAmount, virtualOffsetInOut ); @@ -317,12 +314,12 @@ export class Gyro3Pool implements PoolBase { const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); - const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], inAmountLessFee, virtualOffsetInOut ); @@ -341,10 +338,7 @@ export class Gyro3Pool implements PoolBase { poolPairData: Gyro3PoolPairData, amount: OldBigNumber ): OldBigNumber { - const outAmount = parseFixed( - amount.toString(), - poolPairData.decimalsOut - ); + const outAmount = parseFixed(amount.toString(), 18); const balances = [ poolPairData.balanceIn, poolPairData.balanceOut, @@ -365,8 +359,8 @@ export class Gyro3Pool implements PoolBase { const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); const inAmountLessFee = _calcInGivenOut( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], outAmount, virtualOffsetInOut ); @@ -406,12 +400,12 @@ export class Gyro3Pool implements PoolBase { const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); - const inAmount = parseFixed(amount.toString(), poolPairData.decimalsIn); + const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); const outAmount = _calcOutGivenIn( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], inAmountLessFee, virtualOffsetInOut ); @@ -428,10 +422,7 @@ export class Gyro3Pool implements PoolBase { poolPairData: Gyro3PoolPairData, amount: OldBigNumber ): OldBigNumber { - const outAmount = parseFixed( - amount.toString(), - poolPairData.decimalsOut - ); + const outAmount = parseFixed(amount.toString(), 18); const balances = [ poolPairData.balanceIn, poolPairData.balanceOut, @@ -452,8 +443,8 @@ export class Gyro3Pool implements PoolBase { const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); const inAmountLessFee = _calcInGivenOut( - poolPairData.balanceIn, - poolPairData.balanceOut, + normalizedBalances[0], + normalizedBalances[1], outAmount, virtualOffsetInOut ); diff --git a/test/gyro3Math.spec.ts b/test/gyro3Math.spec.ts index d8ade66e..64c1dc0c 100644 --- a/test/gyro3Math.spec.ts +++ b/test/gyro3Math.spec.ts @@ -11,6 +11,7 @@ import { _calculateCubicStartingPoint, _calculateCubicTerms, _runNewtonIteration, + _normalizeBalances, } from '../src/pools/gyro3Pool/gyro3Math'; describe('gyro3Math tests', () => { @@ -21,12 +22,20 @@ describe('gyro3Math tests', () => { context('Calculate Invariant', () => { it(`should correctly calculate the terms of the cubic expression`, async () => { - const [a, mb, mc, md] = _calculateCubicTerms( + const normalizedBalances = _normalizeBalances( [ poolPairData.balanceIn, poolPairData.balanceOut, poolPairData.balanceTertiary, ], + [ + poolPairData.decimalsIn, + poolPairData.decimalsOut, + poolPairData.decimalsTertiary, + ] + ); + const [a, mb, mc, md] = _calculateCubicTerms( + normalizedBalances, pool.root3Alpha ); diff --git a/test/testData/gyro3Pools/gyro3TestPool.json b/test/testData/gyro3Pools/gyro3TestPool.json index 1873839a..066b4d90 100644 --- a/test/testData/gyro3Pools/gyro3TestPool.json +++ b/test/testData/gyro3Pools/gyro3TestPool.json @@ -26,7 +26,7 @@ { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "balance": "81485", - "decimals": 18, + "decimals": 6, "weight": null, "priceRate": "1", "symbol": "USDC" From 48d93f7ba931a815292b0dba3c5e651d52882d27 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Tue, 1 Mar 2022 20:25:47 -0300 Subject: [PATCH 39/43] getLBP correctly handles abscence of lbpRaisingTokens in config --- src/routeProposal/filtering.ts | 88 +++++++++++++++++---------------- test/testScripts/swapExample.ts | 10 ++-- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/routeProposal/filtering.ts b/src/routeProposal/filtering.ts index a61181cf..7ca71680 100644 --- a/src/routeProposal/filtering.ts +++ b/src/routeProposal/filtering.ts @@ -255,6 +255,7 @@ export function getBoostedPaths( const weth = config.weth.toLowerCase(); const bbausd = config.bbausd.address.toLowerCase(); + // Letter 'i' in iTokenIn and iTokenOut stands for "internal", // lacking of a better name for that so far. const [lbpPathIn, iTokenIn] = getLBP(tokenIn, poolsAllDict, true, config); @@ -266,7 +267,7 @@ export function getBoostedPaths( config ); - // getLinearPools could receive an array of tokens so that we search + // getLinearPools might instead receive an array of tokens so that we search // over poolsAllDict once instead of twice. Similarly for getPoolsWith // and getLBP. This is a matter of code simplicity vs. efficiency. const linearPoolsIn = getLinearPools(iTokenIn, poolsAllDict); @@ -312,32 +313,29 @@ export function getBoostedPaths( const paths1 = combineSemiPaths(semiPathsInToWeth, semiPathsWethToOut); const paths2 = combineSemiPaths(semiPathsInToBBausd, semiPathsBBausdToOut); - if (!config.wethBBausd) { - const paths = paths1.concat(paths2); - // Every short path (short means length 1 and 2) is included in filterHopPools. - return removeShortPaths(paths); + let paths = paths1.concat(paths2); + if (config.wethBBausd) { + const WethBBausdPool = poolsAllDict[config.wethBBausd.id]; + const WethBBausdPath = createPath( + [config.weth, config.bbausd.address], + [WethBBausdPool] + ); + const BBausdWethPath = createPath( + [config.bbausd.address, config.weth], + [WethBBausdPool] + ); + const paths3 = combineSemiPaths( + semiPathsInToWeth, + semiPathsBBausdToOut, + WethBBausdPath + ); + const paths4 = combineSemiPaths( + semiPathsInToBBausd, + semiPathsWethToOut, + BBausdWethPath + ); + paths = paths.concat(paths3, paths4); } - const WethBBausdPool = poolsAllDict[config.wethBBausd.id]; - const WethBBausdPath = createPath( - [config.weth, config.bbausd.address], - [WethBBausdPool] - ); - const BBausdWethPath = createPath( - [config.bbausd.address, config.weth], - [WethBBausdPool] - ); - const paths3 = combineSemiPaths( - semiPathsInToWeth, - semiPathsBBausdToOut, - WethBBausdPath - ); - const paths4 = combineSemiPaths( - semiPathsInToBBausd, - semiPathsWethToOut, - BBausdWethPath - ); - let paths = paths1.concat(paths2, paths3, paths4); - // If there is a nontrivial LBP path, compose every path with the lbp paths // in and out. One of them might be the empty path. if (lbpPathIn.pools.length > 0 || lbpPathOut.pools.length > 0) { @@ -345,6 +343,7 @@ export function getBoostedPaths( composePaths([lbpPathIn, path, lbpPathOut]) ); } + // Every short path (short means length 1 and 2) is included in filterHopPools. return removeShortPaths(paths); } @@ -944,23 +943,26 @@ function getLBP( if (config.lbpRaisingTokens) { if (config.lbpRaisingTokens.includes(token)) { return [getEmptyPath(), token]; - } - } - for (const id in poolsAllDict) { - const pool = poolsAllDict[id]; - if (!pool.isLBP) continue; - const tokensList = pool.tokensList; - // We assume that the LBP has two tokens. - for (let i = 0; i < 2; i++) { - if (tokensList[i] == token) { - let path = createPath( - [tokensList[i], tokensList[1 - i]], - [pool] - ); - if (!isInitial) path = reversePath(path); - return [path, tokensList[1 - i]]; + } else { + for (const id in poolsAllDict) { + const pool = poolsAllDict[id]; + if (!pool.isLBP) continue; + const tokensList = pool.tokensList; + // We assume that the LBP has two tokens. + for (let i = 0; i < 2; i++) { + if (tokensList[i] == token) { + const theOtherToken = tokensList[1 - i]; + let path = createPath( + [tokensList[i], theOtherToken], + [pool] + ); + if (!isInitial) path = reversePath(path); + if (config.lbpRaisingTokens.includes(theOtherToken)) + return [path, theOtherToken]; + } + } } + return [getEmptyPath(), token]; } - } - return [getEmptyPath(), token]; + } else return [getEmptyPath(), token]; } diff --git a/test/testScripts/swapExample.ts b/test/testScripts/swapExample.ts index cf9bdacf..bcbe879e 100644 --- a/test/testScripts/swapExample.ts +++ b/test/testScripts/swapExample.ts @@ -39,7 +39,7 @@ export const SOR_CONFIG: Record = { chainId: Network.MAINNET, //1 vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', - staBal3Pool: { + bbausd: { id: '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb20000000000000000000000fe', address: '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2', }, @@ -654,11 +654,11 @@ async function makeRelayerTrade( } export async function simpleSwap() { - const networkId = Network.KOVAN; + const networkId = Network.MAINNET; // Pools source can be Subgraph URL or pools data set passed directly // Update pools list with most recent onchain balances - const tokenIn = ADDRESSES[networkId].DAI_from_AAVE; - const tokenOut = ADDRESSES[networkId].USDC_from_AAVE; + const tokenIn = ADDRESSES[networkId].DAI; + const tokenOut = ADDRESSES[networkId].USDC; const swapType = SwapTypes.SwapExactIn; const swapAmount = parseFixed('100', 18); const executeTrade = true; @@ -694,7 +694,7 @@ export async function simpleSwap() { const swapInfo = await getSwap( provider, - SOR_CONFIG[Network.KOVAN], + SOR_CONFIG[networkId], subgraphPoolDataService, // mockPoolDataService, coingeckoTokenPriceService, From 941428ef2ec430d3dd4cd9e19cb13f8367a1d67c Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Wed, 2 Mar 2022 08:36:30 -0300 Subject: [PATCH 40/43] restore uppercase characters in test/lib/constants.ts --- test/lib/constants.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/lib/constants.ts b/test/lib/constants.ts index 1ac187bb..6ba747ff 100644 --- a/test/lib/constants.ts +++ b/test/lib/constants.ts @@ -7,7 +7,7 @@ export interface TestToken { export const sorConfigTest = { chainId: 99, - weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', }; @@ -49,7 +49,7 @@ export const sorConfigTestBoosted = { export const sorConfigEth: SorConfig = { chainId: 1, - weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', bbausd: { id: '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb20000000000000000000000fe', @@ -59,7 +59,7 @@ export const sorConfigEth: SorConfig = { export const sorConfigKovan: SorConfig = { chainId: 42, - weth: '0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1'.toLowerCase(), + weth: '0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1', vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', bbausd: { id: '0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8', @@ -69,7 +69,7 @@ export const sorConfigKovan: SorConfig = { export const sorConfigFullKovan: SorConfig = { chainId: 42, - weth: '0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1'.toLowerCase(), + weth: '0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1', vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', bbausd: { id: '0x8fd162f338b770f7e879030830cde9173367f3010000000000000000000004d8', @@ -88,7 +88,7 @@ export const sorConfigFullKovan: SorConfig = { }; export const WETH: TestToken = { - address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'.toLowerCase(), + address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase(), decimals: 18, }; export const DAI: TestToken = { From 90039c8ecb78b65ac81819c90d64826cca469ab3 Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Wed, 2 Mar 2022 18:41:05 -0300 Subject: [PATCH 41/43] add test case: absence of LBP raising tokens info --- test/boostedPaths.spec.ts | 34 ++++++++++++++++++++++++++++++++++ test/lib/constants.ts | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/test/boostedPaths.spec.ts b/test/boostedPaths.spec.ts index 018d5736..69ae11f6 100644 --- a/test/boostedPaths.spec.ts +++ b/test/boostedPaths.spec.ts @@ -185,6 +185,7 @@ describe('multiple boosted pools, path creation test', () => { context('bbausd and Weth to Dai', () => { it('four combinations', () => { const binaryOption = [true, false]; + if (!sorConfigTestBoosted.bbausd) return; for (const reverse of binaryOption) { const tokens = [ [WETH.address, sorConfigTestBoosted.bbausd.address], @@ -373,6 +374,39 @@ describe('multiple boosted pools, path creation test', () => { assert.equal(boostedPaths.length, 2); assert.equal(paths.length, 2); }); + it('Test correctness in absence of LBP raising info at config', () => { + const sorConfigNoLbpRaising = cloneDeep(sorConfigTestBoosted); + delete sorConfigNoLbpRaising['lbpRaisingTokens']; + const sorConfigNoRaisingTusd = cloneDeep(sorConfigNoLbpRaising); + sorConfigNoRaisingTusd['lbpRaisingTokens'] = [ + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + ]; + const tokenIn = TUSD.address; + const tokenOut = BAL.address; + const pathsCases: NewPath[][] = []; + const sorConfigCases: SorConfig[] = [ + sorConfigTestBoosted, + sorConfigNoLbpRaising, + sorConfigNoRaisingTusd, + ]; + for (let i = 0; i < 3; i++) { + const [paths] = getPaths( + tokenIn, + tokenOut, + SwapTypes.SwapExactIn, + boostedPools.pools, + maxPools, + sorConfigCases[i] + ); + pathsCases.push(paths); + } + assert.equal(pathsCases[0].length, pathsCases[1].length); + assert.equal(pathsCases[0].length, pathsCases[2].length); + for (let i = 0; i < pathsCases[0].length; i++) { + assert.equal(pathsCases[0][i].id, pathsCases[1][i].id); + assert.equal(pathsCases[0][i].id, pathsCases[2][i].id); + } + }); }); }); diff --git a/test/lib/constants.ts b/test/lib/constants.ts index 6ba747ff..83fd92a0 100644 --- a/test/lib/constants.ts +++ b/test/lib/constants.ts @@ -29,7 +29,7 @@ export const sorConfigTestStaBal = { }, }; -export const sorConfigTestBoosted = { +export const sorConfigTestBoosted: SorConfig = { chainId: 99, weth: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', vault: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', From 08d8501e1b7a138df61d051a8be7b8fda936bc1f Mon Sep 17 00:00:00 2001 From: sergioyuhjtman Date: Fri, 4 Mar 2022 14:13:55 -0300 Subject: [PATCH 42/43] remove unused functions --- src/routeProposal/filtering.ts | 222 --------------------------------- 1 file changed, 222 deletions(-) diff --git a/src/routeProposal/filtering.ts b/src/routeProposal/filtering.ts index 7ca71680..4ace25b5 100644 --- a/src/routeProposal/filtering.ts +++ b/src/routeProposal/filtering.ts @@ -11,7 +11,6 @@ import { PoolPairBase, SorConfig, } from '../types'; -import { MetaStablePool } from '../pools/metaStablePool/metaStablePool'; import { ZERO } from '../utils/bignumber'; import { parseNewPool } from '../pools'; import { Zero } from '@ethersproject/constants'; @@ -459,16 +458,6 @@ function reversePath(path: NewPath): NewPath { return createPath(tokens, pools); } -function reverseSwap(swap: Swap): Swap { - return { - pool: swap.pool, - tokenIn: swap.tokenOut, - tokenOut: swap.tokenIn, - tokenInDecimals: swap.tokenOutDecimals, - tokenOutDecimals: swap.tokenInDecimals, - }; -} - function getEmptyPath(): NewPath { const emptyPath: NewPath = { id: '', @@ -480,217 +469,6 @@ function getEmptyPath(): NewPath { return emptyPath; } -/* -Return paths through StaBal3 Linear pools. -Paths can be created for any token that is paired to staBAL3 -or WETH via a WETH/staBAL3 connecting pool -LinearStable <> LinearStable: LinearStableIn>[LINEARPOOL_IN]>bStable_IN>[staBAL3]>bStable_OUT>[LINEARPOOL_OUT]>LinearStableOut -LinearStable <> token paired to staBAL3: LinearStableIn>[LINEARPOOL]>bStable>[staBAL3]>staBal3>[staBal3-TokenOut]>TokenOut -LinearStable <> token paired to WETH: LinearStableIn>[LINEARPOOL]>bStable>[staBAL3]>staBal3>[staBal3-WETH]>WETH>[WETH-TokenOut]>TokenOut -*/ -export function getLinearStaBal3Paths( - tokenIn: string, - tokenOut: string, - poolsAllDict: PoolDictionary, - poolsFilteredDict: PoolDictionary, - config: SorConfig -): NewPath[] { - // This is the top level Metastable pool containing bUSDC/bDAI/bUSDT - const staBal3PoolInfo = config.staBal3Pool; - if (!staBal3PoolInfo) return []; - const staBal3Pool: MetaStablePool = poolsAllDict[ - staBal3PoolInfo.id - ] as MetaStablePool; - - if (!staBal3Pool) return []; - - if ( - tokenIn === staBal3PoolInfo.address || - tokenOut === staBal3PoolInfo.address - ) - return []; - - // Find Linear Pools that are connected to staBal3 - const linearStaBal3Pools = getLinearStaBal3Pools(poolsAllDict, staBal3Pool); - // Find Linear Pools that have tokenIn/Out as main token - const linearPoolIn = getPoolWithMainToken(linearStaBal3Pools, tokenIn); - const linearPoolOut = getPoolWithMainToken(linearStaBal3Pools, tokenOut); - - const pathsUsingLinear: NewPath[] = []; - - // If neither of tokenIn and tokenOut have linear pools, return an empty array. - if (!linearPoolIn && !linearPoolOut) return []; - // If both tokenIn and tokenOut belong to linear pools - else if (linearPoolIn && linearPoolOut) { - // tokenIn/tokenOut belong to same Linear Pool and don't need routed via staBAL3 - if (linearPoolIn.id === linearPoolOut.id) return []; - - // TokenIn>[LINEARPOOL_IN]>BPT_IN>[staBAL3]>BPT_OUT>[LINEARPOOL_OUT]>TokenOut - const linearPathway = createPath( - [tokenIn, linearPoolIn.address, linearPoolOut.address, tokenOut], - [linearPoolIn, staBal3Pool, linearPoolOut] - ); - pathsUsingLinear.push(linearPathway); - return pathsUsingLinear; - } else if (linearPoolIn && !linearPoolOut) { - // Creates first part of path: TokenIn>[LINEARPOOL]>bStable>[staBAL3]>staBal3Bpt - const linearPathway = createPath( - [tokenIn, linearPoolIn.address, staBal3Pool.address], - [linearPoolIn, staBal3Pool] - ); - - // Creates a path through most liquid staBal3/Token pool - // staBal3Bpt>[staBal3Bpt-TokenOut]>TokenOut - const staBal3TokenOutPath = getMostLiquidPath( - [staBal3Pool.address, tokenOut], - 0, - [], - poolsFilteredDict, - SwapPairType.HopOut - ); - - if (staBal3TokenOutPath.swaps) { - const shortPath = composePaths([ - linearPathway, - staBal3TokenOutPath, - ]); - pathsUsingLinear.push(shortPath); - } - - // Creates a path through most liquid WETH/TokenOut pool via WETH/staBal3 connecting pool - // staBal3Bpt>[staBal3Bpt-WETH]>WETH>[WETH-TokenOut]>TokenOut - const wethStaBal3Info = config.wethStaBal3; - if (!wethStaBal3Info) return pathsUsingLinear; - const wethStaBal3Pool = poolsAllDict[wethStaBal3Info.id]; - if (!wethStaBal3Pool) return pathsUsingLinear; - - const wethTokenOutPath = getMostLiquidPath( - [staBal3Pool.address, config.weth, tokenOut], - 1, - [wethStaBal3Pool], - poolsFilteredDict, - SwapPairType.HopOut - ); - - if (wethTokenOutPath.swaps) { - const longPath = composePaths([linearPathway, wethTokenOutPath]); - pathsUsingLinear.push(longPath); - } - - return pathsUsingLinear; - } else { - // here we have the condition (!linearPoolIn && linearPoolOut) - // Creates second part of path: staBal3Bpt>[staBAL3]>bStable>[LINEARPOOL]>TokenOut - const linearPathway = createPath( - [staBal3Pool.address, linearPoolOut.address, tokenOut], - [staBal3Pool, linearPoolOut] - ); - - // Creates a path through most liquid staBal3/Token pool - // TokenIn>[TokenIn-staBal3Bpt]>staBal3Bpt - const tokenInStaBal3Path = getMostLiquidPath( - [tokenIn, staBal3Pool.address], - 0, - [], - poolsFilteredDict, - SwapPairType.HopIn - ); - if (tokenInStaBal3Path.swaps) { - const shortPath = composePaths([tokenInStaBal3Path, linearPathway]); - pathsUsingLinear.push(shortPath); - } - - // Creates a path through most liquid WETH paired pool and staBal3/WETH pool - // TokenIn>[WETH-TokenIn]>WETH>[staBal3Bpt-WETH]>staBal3Bpt> - const wethStaBal3Info = config.wethStaBal3; - if (!wethStaBal3Info) return pathsUsingLinear; - const wethStaBal3Pool = poolsAllDict[wethStaBal3Info.id]; - if (!wethStaBal3Pool) return pathsUsingLinear; - - const tokenInWethPath = getMostLiquidPath( - [tokenIn, config.weth, staBal3Pool.address], - 0, - [wethStaBal3Pool], - poolsFilteredDict, - SwapPairType.HopIn - ); - - if (tokenInWethPath.swaps) { - const longPath = composePaths([tokenInWethPath, linearPathway]); - pathsUsingLinear.push(longPath); - } - - return pathsUsingLinear; - } -} - -/* -Returns Linear pools that are part of staBal3 -*/ -function getLinearStaBal3Pools( - pools: PoolDictionary, - staBal3Pool: PoolBase -): PoolDictionary { - const linearStaBal3Pools = {}; - for (const id in pools) { - if (pools[id].poolType === PoolTypes.Linear) { - // Check if pool is part of staBal3 - if (staBal3Pool.tokensList.includes(pools[id].address)) - linearStaBal3Pools[id] = pools[id]; - } - } - - return linearStaBal3Pools; -} - -function getPoolWithMainToken(pools: PoolDictionary, token: string): PoolBase { - let pool; - for (const id in pools) { - const mainIndex = pools[id].mainIndex; - if (mainIndex !== undefined) { - // TO DO - // Currently maths doesn't support wrapped tokens (TBC if there is a workaround) - // This means we can only support main token - // See Linear https://linear.app/balancer/issue/SOR-26/finalise-wrappedtoken-support - // If can workaround then replace below with: if (pools[id].tokensList.includes(token.toLowerCase())) - if (pools[id].tokensList[mainIndex] === token.toLowerCase()) - return pools[id]; - } - } - return pool; -} - -/* -Creates a path for a set of token hops with one unknown pool. -Finds most liquid pool for tokenIn/tokenOut where tokenInIndex is position of tokenIn in tokens array. -*/ -function getMostLiquidPath( - tokens: string[], - tokenInIndex: number, - pathPools: PoolBase[], - pools: PoolDictionary, - swapPairType: SwapPairType -): NewPath { - const tokenIn = tokens[tokenInIndex]; - const tokenOut = tokens[tokenInIndex + 1]; - - // Finds pool with highest liquidity for tokenIn/Out - const mostLiquidPool = getHighestLiquidityPool( - tokenIn.toLowerCase(), - tokenOut.toLowerCase(), - swapPairType, - pools - ); - // If there is a paired pool create a path - if (mostLiquidPool === null) return {} as NewPath; - - // Add most liquid pool to array in correct hop position - pathPools.splice(tokenInIndex, 0, pools[mostLiquidPool]); - - const path = createPath(tokens, pathPools); - return path; -} - // Creates a path with pools.length hops // i.e. tokens[0]>[Pool0]>tokens[1]>[Pool1]>tokens[2]>[Pool2]>tokens[3] export function createPath(tokens: string[], pools: PoolBase[]): NewPath { From 26f0f6ed23c7ad6c21b22fc5170224fbd66de8c7 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 8 Mar 2022 15:13:37 +0000 Subject: [PATCH 43/43] Update package version to 4.0.0-beta.0. Updated path finding algo and additional SorConfig fields required. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2110aaa3..bf11da1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sor", - "version": "3.0.0-beta.1", + "version": "4.0.0-beta.0", "license": "GPL-3.0-only", "main": "dist/index.js", "module": "dist/index.esm.js",