Skip to content

Commit

Permalink
test: major refactor, compute expected values dynamically
Browse files Browse the repository at this point in the history
build: add "mathjs" and "@types/mathjs" as deps
refactor: delete stale constants
refactor: turn BigNumber constants into strings
test: new ethers.math module for ethers bn math extensions
test: new math helper functions, which build on top of math.js
  • Loading branch information
PaulRBerg committed May 18, 2021
1 parent a5bbb4d commit d7d4a1f
Show file tree
Hide file tree
Showing 52 changed files with 1,440 additions and 1,488 deletions.
42 changes: 17 additions & 25 deletions helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
import { BigNumber } from "@ethersproject/bignumber";

import { fp, maxInt, maxUint, minInt, solidityModByScale } from "./numbers";
import { bn, fp } from "./numbers";

export const E: BigNumber = fp("2.718281828459045235");
export const LN_E: BigNumber = fp("0.999999999999999990");
export const LN_MAX_SD59x18: BigNumber = fp("135.305999368893231615");
export const LN_MAX_UD60x18: BigNumber = fp("135.999146549453176925");
export const LOG10_MAX_SD59x18: BigNumber = fp("587.626488943152049156");
export const LOG10_MAX_UD60x18: BigNumber = fp("590.636788899791861115");
export const LOG2_MAX_SD59x18: BigNumber = fp("195.205294292027477728");
export const LOG2_MAX_UD60x18: BigNumber = fp("196.205294292027477728");
export const HALF_SCALE: BigNumber = fp("0.5");
export const MAX_SD59x18: BigNumber = maxInt(256); // Equivalent to max int256
export const MAX_UD60x18: BigNumber = maxUint(256); // Equivalent to max uint256
export const MAX_WHOLE_SD59x18: BigNumber = MAX_SD59x18.sub(solidityModByScale(MAX_SD59x18));
export const MAX_WHOLE_UD60x18: BigNumber = MAX_UD60x18.sub(solidityModByScale(MAX_UD60x18));
export const MIN_SD59x18: BigNumber = minInt(256); // Equivalent to min int256
export const MIN_WHOLE_SD59x18: BigNumber = MIN_SD59x18.sub(solidityModByScale(MIN_SD59x18));
export const PI: BigNumber = fp("3.141592653589793238");
export const SCALE: BigNumber = BigNumber.from(10).pow(18);
export const SQRT_2 = fp("1.414213562373095048");
export const SQRT_MAX_SD59x18 = fp("240615969168004511545033772477.625056927114980741");
export const SQRT_MAX_UD60x18 = fp("340282366920938463463374607431.768211455999999999");
export const SQRT_MAX_SD59x18_DIV_BY_SCALE = fp("240615969168004511545.033772477625056927"); // biggest number whose square fits within int256
export const SQRT_MAX_UD60x18_DIV_BY_SCALE = fp("340282366920938463463.374607431768211455"); // biggest number whose square fits within uint256
export const ZERO = BigNumber.from(0);
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
export const E: string = "2.718281828459045235";
export const EPSILON: BigNumber = fp("1e-6");
export const EPSILON_MAGNITUDE: BigNumber = bn("1e6");
export const HALF_SCALE: string = "0.5";
export const MAX_SD59x18: string = "57896044618658097711785492504343953926634992332820282019728.792003956564819967";
export const MAX_UD60x18: string = "115792089237316195423570985008687907853269984665640564039457.584007913129639935";
export const MAX_WHOLE_SD59x18: string = "57896044618658097711785492504343953926634992332820282019728";
export const MAX_WHOLE_UD60x18: string = "115792089237316195423570985008687907853269984665640564039457";
export const MIN_SD59x18: string = "-57896044618658097711785492504343953926634992332820282019728.792003956564819968";
export const MIN_WHOLE_SD59x18: string = "-57896044618658097711785492504343953926634992332820282019728";
export const PI: string = "3.141592653589793238";
export const SCALE: string = "1";
export const SQRT_MAX_SD59x18: string = "240615969168004511545033772477.625056927114980741";
export const SQRT_MAX_UD60x18: string = "340282366920938463463374607431.768211455999999999";
export const SQRT_MAX_SD59x18_DIV_BY_SCALE: string = "240615969168004511545.033772477625056927"; // biggest number whose square fits within int256
export const SQRT_MAX_UD60x18_DIV_BY_SCALE: string = "340282366920938463463.374607431768211455"; // biggest number whose square fits within uint256
10 changes: 10 additions & 0 deletions helpers/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { BigNumber } from "@ethersproject/bignumber";

import { SCALE } from "./constants";
import { fp } from "./numbers";

const foo: BigNumber = fp(SCALE).mul(fp(SCALE));
console.log({ foo: foo.toString() });

const bar: BigNumber = BigNumber.from(10).pow(18).mul(BigNumber.from(10).pow(18));
console.log({ bar: bar.toString() });
54 changes: 54 additions & 0 deletions helpers/ethers.math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { BigNumber } from "@ethersproject/bignumber";

import { HALF_SCALE, SCALE } from "./constants";
import { fp } from "./numbers";

export function avg(x: BigNumber, y: BigNumber): BigNumber {
let result: BigNumber = x.div(2).add(y.div(2));
if (x.mod(2).eq(1) && y.mod(2).eq(1)) {
result = result.add(1);
}
return result;
}

export function frac(x: BigNumber): BigNumber {
return solidityModByScale(x);
}

export function inv(x: BigNumber): BigNumber {
return fp(SCALE).mul(fp(SCALE)).div(x);
}

export function max(x: BigNumber, y: BigNumber): BigNumber {
if (x.gte(y)) {
return x;
} else {
return y;
}
}

export function mul(x: BigNumber, y: BigNumber): BigNumber {
const doubleScaledProduct = x.mul(y);
let doubleScaledProductWithHalfScale: BigNumber;
if (doubleScaledProduct.isNegative()) {
doubleScaledProductWithHalfScale = doubleScaledProduct.sub(fp(HALF_SCALE));
} else {
doubleScaledProductWithHalfScale = doubleScaledProduct.add(fp(HALF_SCALE));
}
const result: BigNumber = doubleScaledProductWithHalfScale.div(fp(SCALE));
return result;
}

export function solidityMod(x: BigNumber, n: BigNumber): BigNumber {
let result = x.mod(n);
if (!result.isZero() && x.isNegative()) {
result = result.sub(n);
}
return result;
}

// See https://github.com/ethers-io/ethers.js/discussions/1408.
export function solidityModByScale(x: BigNumber): BigNumber {
const scale: BigNumber = BigNumber.from(10).pow(18);
return solidityMod(x, scale);
}
53 changes: 53 additions & 0 deletions helpers/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { all, create } from "mathjs";
import { BigNumber as MathjsBigNumber } from "mathjs";

const config = {
number: "BigNumber",
precision: 79,
};

const math = create(all, config)!;
const mbn = math.bignumber!;

export function ceil(x: string): MathjsBigNumber {
return <MathjsBigNumber>mbn(x).ceil();
}

export function exp(x: string): MathjsBigNumber {
return <MathjsBigNumber>mbn(x).exp();
}

export function exp2(x: string): MathjsBigNumber {
return <MathjsBigNumber>math.pow!(mbn("2"), mbn(x));
}

export function floor(x: string): MathjsBigNumber {
return <MathjsBigNumber>mbn(x).floor();
}

export function gm(x: string, y: string): MathjsBigNumber {
return <MathjsBigNumber>math.sqrt!(mbn(x).mul(mbn(y)));
}

export function ln(x: string): MathjsBigNumber {
return <MathjsBigNumber>math.log!(mbn(x));
}

export function log10(x: string): MathjsBigNumber {
return <MathjsBigNumber>math.log10!(mbn(x));
}

export function log2(x: string): MathjsBigNumber {
return <MathjsBigNumber>math.log2!(mbn(x));
}

export function pow(x: string, y: string): MathjsBigNumber {
return <MathjsBigNumber>math.pow!(mbn(x), mbn(y));
}

export function sqrt(x: string): MathjsBigNumber {
return <MathjsBigNumber>math.sqrt!(mbn(x));
}

export { mbn };
83 changes: 32 additions & 51 deletions helpers/numbers.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,42 @@
import { BigNumber, parseFixed } from "@ethersproject/bignumber";
import fromExponential from "from-exponential";
import { BigNumber as MathjsBigNumber } from "mathjs";

export function bn(x: string): BigNumber {
return BigNumber.from(x);
}

export function fp(x: string): BigNumber {
// Check if x is either a whole number with up to 60 digits or a fixed-point number with up to 60 digits and up to 18 decimals.
if (!/^[-+]?(\d{1,60}|(?=\d+\.\d+)\d{1,60}\.\d{1,18})$/.test(x)) {
throw new Error(`Unknown format for fixed-point number: ${x}`);
let xs: string = x;
if (x.includes("e")) {
xs = fromExponential(x);
}

const precision: number = 18;
return parseFixed(x, precision);
}

export function sfp(x: string): BigNumber {
// Check if the input is in scientific notation.
if (!/^(-?\d+)(\.\d+)?(e|e-)(\d+)$/.test(x)) {
throw new Error(`Unknown format for fixed-point number in scientific notation: ${x}`);
return BigNumber.from(xs);
}

/// 1. Get the stringified value of x, but limit the number of decimals to 18.
/// 2. Check if x is a whole number with up to 60 digits or a fixed-point number with up to 60 digits and up to 18
/// decimals. If yes, convert the number to fixed-point representation and return it.
/// 3. Otherwise, throw an error.
export function fp(x: string | MathjsBigNumber): BigNumber {
let xs: string;
if (typeof x === "string") {
xs = x;
if (xs.includes("e")) {
xs = fromExponential(xs);
}
} else {
xs = String(x);
if (xs.includes("e")) {
xs = fromExponential(xs);
}
if (xs.includes(".")) {
const parts: string[] = xs.split(".");
parts[1] = parts[1].slice(0, 18);
xs = parts[0] + "." + parts[1];
}
}

const precision: number = 18;
return parseFixed(fromExponential(x), precision);
}

export function fpPowOfTwo(exp: number | BigNumber): BigNumber {
const scale: BigNumber = BigNumber.from(10).pow(18);
return powOfTwo(exp).mul(scale);
}

export function maxInt(exp: number): BigNumber {
return powOfTwo(exp - 1).sub(1);
}

export function minInt(exp: number): BigNumber {
return powOfTwo(exp - 1).mul(-1);
}

export function maxUint(exp: number): BigNumber {
return BigNumber.from(2).pow(exp).sub(1);
}

export function powOfTwo(exp: number | BigNumber): BigNumber {
return BigNumber.from(2).pow(BigNumber.from(exp));
}

export function solidityMod(x: BigNumber, n: BigNumber): BigNumber {
let result = x.mod(n);
if (x.isNegative()) {
result = result.sub(n);
if (/^[-+]?(\d{1,60}|(?=\d+\.\d+)\d{1,60}\.\d{1,18})$/.test(xs)) {
return parseFixed(xs, precision);
} else {
throw new Error(`Unknown format for fixed-point number: ${xs}`);
}
return result;
}

// See https://github.com/ethers-io/ethers.js/issues/1402.
export function solidityModByScale(x: BigNumber): BigNumber {
const scale: BigNumber = BigNumber.from(10).pow(18);
return solidityMod(x, scale);
}
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@typechain/hardhat": "^1.0.1",
"@types/chai": "^4.2.13",
"@types/fs-extra": "^9.0.1",
"@types/mathjs": "^6.0.12",
"@types/mocha": "^7.0.2",
"@types/mocha-each": "^2.0.0",
"@types/node": "^14.11.8",
Expand All @@ -42,6 +43,7 @@
"husky": "^4.3.0",
"import-sort-parser-typescript": "^6.0.0",
"import-sort-style-module": "^6.0.0",
"mathjs": "^9.4.0",
"mocha": "^8.1.3",
"mocha-each": "^2.0.1",
"prettier": "^2.1.2",
Expand All @@ -58,13 +60,6 @@
"files": [
"/contracts"
],
"keywords": [
"blockchain",
"ethereum",
"hardhat",
"smart-contracts",
"solidity"
],
"homepage": "https://github.com/hifi-finance/prb-math#readme",
"importSort": {
".js, .jsx": {
Expand All @@ -76,6 +71,13 @@
"style": "module"
}
},
"keywords": [
"blockchain",
"ethereum",
"hardhat",
"smart-contracts",
"solidity"
],
"license": "WTFPL",
"publishConfig": {
"access": "public"
Expand Down
13 changes: 4 additions & 9 deletions test/unit/prbMathSd59x18/PRBMathSD59x18.behavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import shouldBehaveLikeFromInt from "./pure/fromInt";
import shouldBehaveLikeGm from "./pure/gm";
import shouldBehaveLikeInv from "./pure/inv";
import shouldBehaveLikeLn from "./pure/ln";
import shouldBehaveLikeLog10 from "./pure/log10";
import shouldBehaveLikeLog2 from "./pure/log2";
import shouldBehaveLikeMul from "./pure/mul";
import shouldBehaveLikePiGetter from "./pure/pi";
Expand Down Expand Up @@ -55,14 +54,14 @@ export function shouldBehaveLikePrbMathSd59x18(): void {
shouldBehaveLikeFloor();
});

describe("frac", function () {
shouldBehaveLikeFrac();
});

describe("fromInt", function () {
shouldBehaveLikeFromInt();
});

describe("frac", function () {
shouldBehaveLikeFrac();
});

describe("gm", function () {
shouldBehaveLikeGm();
});
Expand All @@ -75,10 +74,6 @@ export function shouldBehaveLikePrbMathSd59x18(): void {
shouldBehaveLikeLn();
});

describe("log10", function () {
shouldBehaveLikeLog10();
});

describe("log2", function () {
shouldBehaveLikeLog2();
});
Expand Down
Loading

0 comments on commit d7d4a1f

Please sign in to comment.