@0x/contracts-staking: Update LibFixedMath ln() and exp() input domains and improve precision.

`@0x/contracts-staking`: Add `_invert()` and `_mulDiv()` to `LibFixedMath`.
`@0x/contracts-staking`: Update `MixinExchangeFees._cobbDouglas()` to work with `LibFixedMath`.
`@0x/contracts-staking`: Add unit and fuzz tests for `_cobbDouglas()` and remaining `LibFixedMath` functions.
This commit is contained in:
Lawrence Forman
2019-08-29 05:28:34 -04:00
committed by Lawrence Forman
parent 0c6a6743ab
commit 9a63bea763
6 changed files with 535 additions and 149 deletions

View File

@@ -308,10 +308,12 @@ contract MixinExchangeFees is
pure
returns (uint256 ownerRewards)
{
assert(alphaNumerator > alphaDenominator);
assert(alphaNumerator <= alphaDenominator);
int256 feeRatio = LibFixedMath._toFixed(ownerFees, totalFees);
int256 stakeRatio = LibFixedMath._toFixed(ownerStake, totalStake);
int256 alpha = LibFixedMath._toFixed(alphaNumerator, alphaDenominator);
if (feeRatio == 0 || stakeRatio == 0) {
return ownerRewards = 0;
}
// The cobb-doublas function has the form:
// totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)
@@ -319,12 +321,27 @@ contract MixinExchangeFees is
// totalRewards * stakeRatio * e^(alpha * (ln(feeRatio) - ln(stakeRatio)))
// Compute e^(alpha * (ln(feeRatio) - ln(stakeRatio)))
int256 n = LibFixedMath._exp(
LibFixedMath._mul(
alpha,
LibFixedMath._sub(LibFixedMath._ln(feeRatio), LibFixedMath._ln(stakeRatio))
)
);
int256 logFeeRatio = LibFixedMath._ln(feeRatio);
int256 logStakeRatio = LibFixedMath._ln(stakeRatio);
int256 n;
if (logFeeRatio <= logStakeRatio) {
n = LibFixedMath._exp(
LibFixedMath._mulDiv(
LibFixedMath._sub(logFeeRatio, logStakeRatio),
int256(alphaNumerator),
int256(alphaDenominator)
)
);
} else {
n = LibFixedMath._exp(
LibFixedMath._mulDiv(
LibFixedMath._sub(logStakeRatio, logFeeRatio),
int256(alphaNumerator),
int256(alphaDenominator)
)
);
n = LibFixedMath._invert(n);
}
// Multiply the above with totalRewards * stakeRatio
ownerRewards = LibFixedMath._uintMul(
LibFixedMath._mul(n, stakeRatio),

View File

@@ -25,11 +25,16 @@ library LibFixedMath {
// 1
int256 private constant FIXED_1 = int256(0x0000000000000000000000000000000080000000000000000000000000000000);
// 1^2 (in fixed-point)
int256 private constant FIXED_1_SQUARED = int256(0x4000000000000000000000000000000000000000000000000000000000000000);
// 1
int256 private constant LN_MAX_VAL = FIXED_1;
// -69.07755278982137043720332303294494434957608235919531845909733017243007692132225692604324818191230406
int256 private constant EXP_MIN_VAL = int256(0xffffffffffffffffffffffffffffffdd7612c00c0077ada1b83518e8cafc0e90);
// e ^ -63.875
int256 private constant LN_MIN_VAL = int256(0x0000000000000000000000000000000000000000000000000000000733048c5a);
// 0
int256 private constant EXP_MAX_VAL = 0;
// -63.875
int256 private constant EXP_MIN_VAL = -int256(0x0000000000000000000000000000001ff0000000000000000000000000000000);
/// @dev Get one as a fixed-point number.
function _one() internal pure returns (int256 f) {
@@ -56,6 +61,11 @@ library LibFixedMath {
c = __div(__mul(a, FIXED_1), b);
}
/// @dev Performs (a * n) / d, without scaling for precision.
function _mulDiv(int256 a, int256 n, int256 d) internal pure returns (int256 c) {
c = __div(__mul(a, n), d);
}
/// @dev Returns the unsigned integer result of multiplying a fixed-point
/// number with an integer, reverting if the multiplication overflows.
/// Negative results are clamped to zero.
@@ -74,14 +84,19 @@ library LibFixedMath {
}
/// @dev Returns the absolute value of a fixed point number.
function _abs(int256 a) internal pure returns (int256 c) {
if (a >= 0) {
c = a;
function _abs(int256 f) internal pure returns (int256 c) {
if (f >= 0) {
c = f;
} else {
c = -a;
c = -f;
}
}
/// @dev Returns 1 / `x`, where `x` is a fixed-point number.
function _invert(int256 f) internal pure returns (int256 c) {
c = __div(FIXED_1_SQUARED, f);
}
/// @dev Convert signed `n` / 1 to a fixed-point number.
function _toFixed(int256 n) internal pure returns (int256 f) {
f = __mul(n, FIXED_1);
@@ -129,9 +144,6 @@ library LibFixedMath {
/// @dev Get the natural logarithm of a fixed-point number 0 < `x` <= LN_MAX_VAL
function _ln(int256 x) internal pure returns (int256 r) {
if (x == FIXED_1) {
return 0;
}
if (x > LN_MAX_VAL) {
LibRichErrors.rrevert(LibFixedMathRichErrors.FixedMathSignedValueError(
LibFixedMathRichErrors.ValueErrorCodes.TOO_LARGE,
@@ -144,58 +156,65 @@ library LibFixedMath {
x
));
}
if (x == FIXED_1) {
return 0;
}
if (x <= LN_MIN_VAL) {
return EXP_MIN_VAL;
}
int256 y;
int256 z;
int256 w;
// Rewrite the input as a quotient of negative natural exponents and a single residual q, such that 1 < q < 2
// For example: log(0.3) = log(e^-1 * e^-0.25 * 1.0471028872385522) = 1 - 0.25 - log(1 + 0.0471028872385522)
// e ^ -64
if (x <= int256(0x000000000000000000000000000000000000000000000000000000065a751cc8)) {
r += int256(0xffffffffffffffffffffffffffffffe000000000000000000000000000000000); // - 64
x = x * FIXED_1 / int256(0x000000000000000000000000000000000000000000000000000000065a751cc8); // / e ^ -64
}
// For example: log(0.3) = log(e^-1 * e^-0.25 * 1.0471028872385522)
// = 1 - 0.25 - log(1 + 0.0471028872385522)
// e ^ -32
if (x <= int256(0x00000000000000000000000000000000000000000001c8464f76164760000000)) {
r += int256(0xfffffffffffffffffffffffffffffff000000000000000000000000000000000); // - 32
r -= int256(0x0000000000000000000000000000001000000000000000000000000000000000); // - 32
x = x * FIXED_1 / int256(0x00000000000000000000000000000000000000000001c8464f76164760000000); // / e ^ -32
}
// e ^ -16
if (x <= int256(0x00000000000000000000000000000000000000f1aaddd7742e90000000000000)) {
r += int256(0xfffffffffffffffffffffffffffffff800000000000000000000000000000000); // - 16
r -= int256(0x0000000000000000000000000000000800000000000000000000000000000000); // - 16
x = x * FIXED_1 / int256(0x00000000000000000000000000000000000000f1aaddd7742e90000000000000); // / e ^ -16
}
// e ^ -8
if (x <= int256(0x00000000000000000000000000000000000afe10820813d78000000000000000)) {
r += int256(0xfffffffffffffffffffffffffffffffc00000000000000000000000000000000); // - 8
r -= int256(0x0000000000000000000000000000000400000000000000000000000000000000); // - 8
x = x * FIXED_1 / int256(0x00000000000000000000000000000000000afe10820813d78000000000000000); // / e ^ -8
}
// e ^ -4
if (x <= int256(0x0000000000000000000000000000000002582ab704279ec00000000000000000)) {
r += int256(0xfffffffffffffffffffffffffffffffe00000000000000000000000000000000); // - 4
r -= int256(0x0000000000000000000000000000000200000000000000000000000000000000); // - 4
x = x * FIXED_1 / int256(0x0000000000000000000000000000000002582ab704279ec00000000000000000); // / e ^ -4
}
// e ^ -2
if (x <= int256(0x000000000000000000000000000000001152aaa3bf81cc000000000000000000)) {
r += int256(0xffffffffffffffffffffffffffffffff00000000000000000000000000000000); // - 2
r -= int256(0x0000000000000000000000000000000100000000000000000000000000000000); // - 2
x = x * FIXED_1 / int256(0x000000000000000000000000000000001152aaa3bf81cc000000000000000000); // / e ^ -2
}
// e ^ -1
if (x <= int256(0x000000000000000000000000000000002f16ac6c59de70000000000000000000)) {
r += int256(0xffffffffffffffffffffffffffffffff80000000000000000000000000000000); // - 1
r -= int256(0x0000000000000000000000000000000080000000000000000000000000000000); // - 1
x = x * FIXED_1 / int256(0x000000000000000000000000000000002f16ac6c59de70000000000000000000); // / e ^ -1
}
// e ^ -0.5
if (x <= int256(0x000000000000000000000000000000004da2cbf1be5828000000000000000000)) {
r += int256(0xffffffffffffffffffffffffffffffffc0000000000000000000000000000000); // - 0.5
r -= int256(0x0000000000000000000000000000000040000000000000000000000000000000); // - 0.5
x = x * FIXED_1 / int256(0x000000000000000000000000000000004da2cbf1be5828000000000000000000); // / e ^ -0.5
}
// e ^ -0.25
if (x <= int256(0x0000000000000000000000000000000063afbe7ab2082c000000000000000000)) {
r += int256(0xffffffffffffffffffffffffffffffffe0000000000000000000000000000000); // - 0.25
r -= int256(0x0000000000000000000000000000000020000000000000000000000000000000); // - 0.25
x = x * FIXED_1 / int256(0x0000000000000000000000000000000063afbe7ab2082c000000000000000000); // / e ^ -0.25
}
// e ^ -0.125
if (x <= int256(0x0000000000000000000000000000000070f5a893b608861e1f58934f97aea57d)) {
r -= int256(0x0000000000000000000000000000000010000000000000000000000000000000); // - 0.125
x = x * FIXED_1 / int256(0x0000000000000000000000000000000070f5a893b608861e1f58934f97aea57d); // / e ^ -0.125
}
// `x` is now our residual in the range of 1 <= x <= 2 (or close enough).
// Add the taylor series for log(1 + z), where z = x - 1
@@ -213,102 +232,99 @@ library LibFixedMath {
/// @dev Compute the natural exponent for a fixed-point number EXP_MIN_VAL <= `x` <= 1
function _exp(int256 x) internal pure returns (int256 r) {
if (x <= EXP_MIN_VAL) {
if (x < EXP_MIN_VAL) {
// Saturate to zero below EXP_MIN_VAL.
return 0;
}
if (x == 0) {
return FIXED_1;
}
if (x > 0) {
if (x > EXP_MAX_VAL) {
LibRichErrors.rrevert(LibFixedMathRichErrors.FixedMathSignedValueError(
LibFixedMathRichErrors.ValueErrorCodes.TOO_LARGE,
x
));
}
// Rewrite the input as a product of positive natural exponents and a
// Rewrite the input as a product of natural exponents and a
// single residual q, where q is a number of small magnitude.
// For example: e^-34.419 = e^(-32 - 2 - 0.419) = e^-32 * e^-2 * e^-0.419
r = FIXED_1;
// e ^ 64
if (x <= -int256(0x0000000000000000000000000000002000000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000002000000000000000000000000000000000); // + 64
r = r * int256(0x000000000000000000000000000000000000000000000000000000065a751cc8) / FIXED_1; // * e ^ 64
}
// e ^ 32
if (x <= -int256(0x0000000000000000000000000000001000000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000001000000000000000000000000000000000); // + 32
r = r * int256(0x00000000000000000000000000000000000000000001c8464f76164681e299a0) / FIXED_1; // * e ^ 32
}
// e ^ 16
if (x <= -int256(0x0000000000000000000000000000000800000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000000800000000000000000000000000000000); // + 16
r = r * int256(0x00000000000000000000000000000000000000f1aaddd7742e56d32fb9f99744) / FIXED_1; // * e ^ 16
}
// e ^ 8
if (x <= -int256(0x0000000000000000000000000000000400000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000000400000000000000000000000000000000); // + 8
r = r * int256(0x00000000000000000000000000000000000afe10820813d65dfe6a33c07f738f) / FIXED_1; // * e ^ 8
}
// e ^ 4
if (x <= -int256(0x0000000000000000000000000000000200000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000000200000000000000000000000000000000); // + 4
r = r * int256(0x0000000000000000000000000000000002582ab704279e8efd15e0265855c47a) / FIXED_1; // * e ^ 4
}
// e ^ 2
if (x <= -int256(0x0000000000000000000000000000000100000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000000100000000000000000000000000000000); // + 2
r = r * int256(0x000000000000000000000000000000001152aaa3bf81cb9fdb76eae12d029571) / FIXED_1; // * e ^ 2
}
// e ^ 1
if (x <= -int256(0x0000000000000000000000000000000080000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000000080000000000000000000000000000000); // + 1
r = r * int256(0x000000000000000000000000000000002f16ac6c59de6f8d5d6f63c1482a7c86) / FIXED_1; // * e ^ 1
}
// e ^ 0.5
if (x <= -int256(0x0000000000000000000000000000000040000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000000040000000000000000000000000000000); // + 0.5
r = r * int256(0x000000000000000000000000000000004da2cbf1be5827f9eb3ad1aa9866ebb3) / FIXED_1; // * e ^ 0.5
}
// e ^ 0.25
if (x <= -int256(0x0000000000000000000000000000000020000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000000020000000000000000000000000000000); // + 0.25
r = r * int256(0x0000000000000000000000000000000063afbe7ab2082ba1a0ae5e4eb1b479dc) / FIXED_1; // * e ^ 0.25
}
// e ^ 0.125
if (x <= -int256(0x0000000000000000000000000000000010000000000000000000000000000000)) {
x += int256(0x0000000000000000000000000000000010000000000000000000000000000000); // + 0.125
r = r * int256(0x0000000000000000000000000000000070f5a893b608861e1f58934f97aea57d) / FIXED_1; // * e ^ 0.125
}
// x is now a small residual close to 0
// For example: e^-34.419 = e^(-32 - 2 - 0.25 - 0.125 - 0.044)
// = e^-32 * e^-2 * e^-0.25 * e^-0.125 * e^-0.044
// -> q = -0.044
// Multiply with the taylor series for e^q
int256 y;
int256 z;
int256 t;
z = y = x;
z = z * y / FIXED_1; t += z * 0x10e1b3be415a0000; // add y^02 * (20! / 02!)
z = z * y / FIXED_1; t += z * 0x05a0913f6b1e0000; // add y^03 * (20! / 03!)
z = z * y / FIXED_1; t += z * 0x0168244fdac78000; // add y^04 * (20! / 04!)
z = z * y / FIXED_1; t += z * 0x004807432bc18000; // add y^05 * (20! / 05!)
z = z * y / FIXED_1; t += z * 0x000c0135dca04000; // add y^06 * (20! / 06!)
z = z * y / FIXED_1; t += z * 0x0001b707b1cdc000; // add y^07 * (20! / 07!)
z = z * y / FIXED_1; t += z * 0x000036e0f639b800; // add y^08 * (20! / 08!)
z = z * y / FIXED_1; t += z * 0x00000618fee9f800; // add y^09 * (20! / 09!)
z = z * y / FIXED_1; t += z * 0x0000009c197dcc00; // add y^10 * (20! / 10!)
z = z * y / FIXED_1; t += z * 0x0000000e30dce400; // add y^11 * (20! / 11!)
z = z * y / FIXED_1; t += z * 0x000000012ebd1300; // add y^12 * (20! / 12!)
z = z * y / FIXED_1; t += z * 0x0000000017499f00; // add y^13 * (20! / 13!)
z = z * y / FIXED_1; t += z * 0x0000000001a9d480; // add y^14 * (20! / 14!)
z = z * y / FIXED_1; t += z * 0x00000000001c6380; // add y^15 * (20! / 15!)
z = z * y / FIXED_1; t += z * 0x000000000001c638; // add y^16 * (20! / 16!)
z = z * y / FIXED_1; t += z * 0x0000000000001ab8; // add y^17 * (20! / 17!)
z = z * y / FIXED_1; t += z * 0x000000000000017c; // add y^18 * (20! / 18!)
z = z * y / FIXED_1; t += z * 0x0000000000000014; // add y^19 * (20! / 19!)
z = z * y / FIXED_1; t += z * 0x0000000000000001; // add y^20 * (20! / 20!)
t = t / 0x21c3677c82b40000 + y + FIXED_1; // divide by 20! and then add y^1 / 1! + y^0 / 0!
r = r * t / FIXED_1;
// q = x % 0.125
z = y = x % 0x0000000000000000000000000000000010000000000000000000000000000000;
z = z * y / FIXED_1; r += z * 0x10e1b3be415a0000; // add y^02 * (20! / 02!)
z = z * y / FIXED_1; r += z * 0x05a0913f6b1e0000; // add y^03 * (20! / 03!)
z = z * y / FIXED_1; r += z * 0x0168244fdac78000; // add y^04 * (20! / 04!)
z = z * y / FIXED_1; r += z * 0x004807432bc18000; // add y^05 * (20! / 05!)
z = z * y / FIXED_1; r += z * 0x000c0135dca04000; // add y^06 * (20! / 06!)
z = z * y / FIXED_1; r += z * 0x0001b707b1cdc000; // add y^07 * (20! / 07!)
z = z * y / FIXED_1; r += z * 0x000036e0f639b800; // add y^08 * (20! / 08!)
z = z * y / FIXED_1; r += z * 0x00000618fee9f800; // add y^09 * (20! / 09!)
z = z * y / FIXED_1; r += z * 0x0000009c197dcc00; // add y^10 * (20! / 10!)
z = z * y / FIXED_1; r += z * 0x0000000e30dce400; // add y^11 * (20! / 11!)
z = z * y / FIXED_1; r += z * 0x000000012ebd1300; // add y^12 * (20! / 12!)
z = z * y / FIXED_1; r += z * 0x0000000017499f00; // add y^13 * (20! / 13!)
z = z * y / FIXED_1; r += z * 0x0000000001a9d480; // add y^14 * (20! / 14!)
z = z * y / FIXED_1; r += z * 0x00000000001c6380; // add y^15 * (20! / 15!)
z = z * y / FIXED_1; r += z * 0x000000000001c638; // add y^16 * (20! / 16!)
z = z * y / FIXED_1; r += z * 0x0000000000001ab8; // add y^17 * (20! / 17!)
z = z * y / FIXED_1; r += z * 0x000000000000017c; // add y^18 * (20! / 18!)
z = z * y / FIXED_1; r += z * 0x0000000000000014; // add y^19 * (20! / 19!)
z = z * y / FIXED_1; r += z * 0x0000000000000001; // add y^20 * (20! / 20!)
r = r / 0x21c3677c82b40000 + y + FIXED_1; // divide by 20! and then add y^1 / 1! + y^0 / 0!
// Multiply with the non-residual terms.
x = -x;
// e ^ -32
if ((x & int256(0x0000000000000000000000000000001000000000000000000000000000000000)) != 0) {
r = r * int256(0x00000000000000000000000000000000000000f1aaddd7742e56d32fb9f99744)
/ int256(0x0000000000000000000000000043cbaf42a000812488fc5c220ad7b97bf6e99e); // * e ^ -32
}
// e ^ -16
if ((x & int256(0x0000000000000000000000000000000800000000000000000000000000000000)) != 0) {
r = r * int256(0x00000000000000000000000000000000000afe10820813d65dfe6a33c07f738f)
/ int256(0x000000000000000000000000000005d27a9f51c31b7c2f8038212a0574779991); // * e ^ -16
}
// e ^ -8
if ((x & int256(0x0000000000000000000000000000000400000000000000000000000000000000)) != 0) {
r = r * int256(0x0000000000000000000000000000000002582ab704279e8efd15e0265855c47a)
/ int256(0x0000000000000000000000000000001b4c902e273a58678d6d3bfdb93db96d02); // * e ^ -8
}
// e ^ -4
if ((x & int256(0x0000000000000000000000000000000200000000000000000000000000000000)) != 0) {
r = r * int256(0x000000000000000000000000000000001152aaa3bf81cb9fdb76eae12d029571)
/ int256(0x00000000000000000000000000000003b1cc971a9bb5b9867477440d6d157750); // * e ^ -4
}
// e ^ -2
if ((x & int256(0x0000000000000000000000000000000100000000000000000000000000000000)) != 0) {
r = r * int256(0x000000000000000000000000000000002f16ac6c59de6f8d5d6f63c1482a7c86)
/ int256(0x000000000000000000000000000000015bf0a8b1457695355fb8ac404e7a79e3); // * e ^ -2
}
// e ^ -1
if ((x & int256(0x0000000000000000000000000000000080000000000000000000000000000000)) != 0) {
r = r * int256(0x000000000000000000000000000000004da2cbf1be5827f9eb3ad1aa9866ebb3)
/ int256(0x00000000000000000000000000000000d3094c70f034de4b96ff7d5b6f99fcd8); // * e ^ -1
}
// e ^ -0.5
if ((x & int256(0x0000000000000000000000000000000040000000000000000000000000000000)) != 0) {
r = r * int256(0x0000000000000000000000000000000063afbe7ab2082ba1a0ae5e4eb1b479dc)
/ int256(0x00000000000000000000000000000000a45af1e1f40c333b3de1db4dd55f29a7); // * e ^ -0.5
}
// e ^ -0.25
if ((x & int256(0x0000000000000000000000000000000020000000000000000000000000000000)) != 0) {
r = r * int256(0x0000000000000000000000000000000070f5a893b608861e1f58934f97aea57d)
/ int256(0x00000000000000000000000000000000910b022db7ae67ce76b441c27035c6a1); // * e ^ -0.25
}
// e ^ -0.125
if ((x & int256(0x0000000000000000000000000000000010000000000000000000000000000000)) != 0) {
r = r * int256(0x00000000000000000000000000000000783eafef1c0a8f3978c7f81824d62ebf)
/ int256(0x0000000000000000000000000000000088415abbe9a76bead8d00cf112e4d4a8); // * e ^ -0.125
}
}
/// @dev Returns the multiplication two numbers, reverting on overflow.

View File

@@ -25,6 +25,10 @@ contract TestLibFixedMath {
function one() external pure returns (int256) {
return LibFixedMath._one();
}
function mulDiv(int256 a, int256 n, int256 d) external pure returns (int256) {
return LibFixedMath._mulDiv(a, n, d);
}
function mul(int256 a, int256 b) external pure returns (int256) {
return LibFixedMath._mul(a, b);
@@ -50,6 +54,10 @@ contract TestLibFixedMath {
return LibFixedMath._abs(a);
}
function invert(int256 a) external pure returns (int256) {
return LibFixedMath._invert(a);
}
function toFixedSigned(int256 n, int256 d) external pure returns (int256) {
return LibFixedMath._toFixed(n, d);
}

View File

@@ -19,7 +19,7 @@
"test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
"test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
"test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
"run_mocha": "UNLIMITED_CONTRACT_SIZE=true mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
"compile": "sol-compiler",
"watch": "sol-compiler -w",
"clean": "shx rm -rf lib generated-artifacts generated-wrappers",

View File

@@ -1,8 +1,271 @@
import { blockchainTests, expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { blockchainTests, expect, hexRandom } from '@0x/contracts-test-utils';
import { AnyRevertError, BigNumber, FixedMathRevertErrors } from '@0x/utils';
import { Decimal } from 'decimal.js';
import * as _ from 'lodash';
import { artifacts, TestCobbDouglasContract } from '../src/';
Decimal.set({ precision: 128 });
type Numberish = BigNumber | string | number;
// tslint:disable:no-unnecessary-type-assertion
blockchainTests('Cobb-Douglas', env => {
const FUZZ_COUNT = 1024;
let testContract: TestCobbDouglasContract;
before(async () => {
testContract = await TestCobbDouglasContract.deployFrom0xArtifactAsync(
artifacts.TestCobbDouglas,
env.provider,
env.txDefaults,
artifacts,
);
});
function toPrecision(n: Numberish, precision: number): BigNumber {
const _n = new BigNumber(n);
const integerDigits = _n.integerValue().sd(true);
const base = 10 ** (precision - integerDigits);
return _n.times(base).integerValue(BigNumber.ROUND_FLOOR).dividedBy(base);
}
function toDecimal(x: Numberish): Decimal {
if (BigNumber.isBigNumber(x)) {
return new Decimal(x.toString(10));
}
return new Decimal(x);
}
function assertRoughlyEquals(
actual: Numberish,
expected: Numberish,
precision: number = 14,
): void {
// SD is not what we want.
expect(toPrecision(actual, precision)).to.bignumber.eq(toPrecision(expected, precision));
}
function getRandomAmount(min: Numberish, max: Numberish): BigNumber {
const range = new BigNumber(max).minus(min);
const random = new BigNumber(hexRandom().substr(2), 16);
return random.mod(range).plus(min);
}
function getRandomPortion(total: Numberish): BigNumber {
return new BigNumber(total).times(Math.random()).integerValue();
}
describe('cobbDouglas()', () => {
interface CobbDouglasParams {
totalRewards: Numberish;
ownerFees: Numberish;
totalFees: Numberish;
ownerStake: Numberish;
totalStake: Numberish;
alphaNumerator: Numberish;
alphaDenominator: Numberish;
gas?: number;
}
const MAX_COBB_DOUGLAS_GAS = 15e3;
const TX_GAS_FEE = 21e3;
const DEFAULT_COBB_DOUGLAS_PARAMS: CobbDouglasParams = {
totalRewards: 100e18,
ownerFees: 10e18,
totalFees: 500e18,
ownerStake: 1.1e21,
totalStake: 3e27,
alphaNumerator: 1,
alphaDenominator: 3,
gas: MAX_COBB_DOUGLAS_GAS,
};
async function callCobbDouglasAsync(params?: Partial<CobbDouglasParams>): Promise<BigNumber> {
const _params = {
...DEFAULT_COBB_DOUGLAS_PARAMS,
...params,
};
return testContract.cobbDouglas.callAsync(
new BigNumber(_params.totalRewards),
new BigNumber(_params.ownerFees),
new BigNumber(_params.totalFees),
new BigNumber(_params.ownerStake),
new BigNumber(_params.totalStake),
new BigNumber(_params.alphaNumerator),
new BigNumber(_params.alphaDenominator),
{
gas: TX_GAS_FEE + (_params.gas === undefined ? MAX_COBB_DOUGLAS_GAS : _params.gas),
},
);
}
function cobbDouglas(params?: Partial<CobbDouglasParams>): BigNumber {
const {
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaNumerator,
alphaDenominator,
} = {
...DEFAULT_COBB_DOUGLAS_PARAMS,
...params,
};
const feeRatio = toDecimal(ownerFees).dividedBy(toDecimal(totalFees));
const stakeRatio = toDecimal(ownerStake).dividedBy(toDecimal(totalStake));
const alpha = toDecimal(alphaNumerator).dividedBy(toDecimal(alphaDenominator));
// totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)
return new BigNumber(
feeRatio.pow(alpha).times(
stakeRatio.pow(new Decimal(1).minus(alpha)),
).times(toDecimal(totalRewards)).toFixed(0, BigNumber.ROUND_FLOOR),
);
}
function getRandomParams(overrides?: Partial<CobbDouglasParams>): CobbDouglasParams {
const totalRewards = _.get(overrides, 'totalRewards', getRandomAmount(0, 1e27)) as Numberish;
const totalFees = _.get(overrides, 'totalFees', getRandomAmount(1, 1e27)) as Numberish;
const ownerFees = _.get(overrides, 'ownerFees', getRandomPortion(totalFees)) as Numberish;
const totalStake = _.get(overrides, 'totalStake', getRandomAmount(1, 1e27)) as Numberish;
const ownerStake = _.get(overrides, 'ownerStake', getRandomPortion(totalStake)) as Numberish;
const alphaDenominator = _.get(overrides, 'alphaDenominator', getRandomAmount(1, 1e6)) as Numberish;
const alphaNumerator = _.get(overrides, 'alphaNumerator', getRandomPortion(alphaDenominator)) as Numberish;
return {
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaNumerator,
alphaDenominator,
};
}
it('throws if `alphaNumerator` > `alphaDenominator`', async () => {
return expect(callCobbDouglasAsync({
alphaNumerator: 11,
alphaDenominator: 10,
})).to.revertWith(new AnyRevertError()); // Just an assertion failure.
});
it('throws if `ownerFees` > `totalFees`', async () => {
const expectedError = new FixedMathRevertErrors.FixedMathSignedValueError(
FixedMathRevertErrors.ValueErrorCodes.TooLarge,
);
return expect(callCobbDouglasAsync({
ownerFees: 11,
totalFees: 10,
})).to.revertWith(expectedError);
});
it('throws if `ownerStake` > `totalStake`', async () => {
const expectedError = new FixedMathRevertErrors.FixedMathSignedValueError(
FixedMathRevertErrors.ValueErrorCodes.TooLarge,
);
return expect(callCobbDouglasAsync({
ownerStake: 11,
totalStake: 10,
})).to.revertWith(expectedError);
});
it('computes the correct reward', async () => {
const expected = cobbDouglas();
const r = await callCobbDouglasAsync();
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with zero stake ratio', async () => {
const ownerStake = 0;
const expected = cobbDouglas({ ownerStake });
const r = await callCobbDouglasAsync({ ownerStake });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with full stake ratio', async () => {
const ownerStake = DEFAULT_COBB_DOUGLAS_PARAMS.totalStake;
const expected = cobbDouglas({ ownerStake });
const r = await callCobbDouglasAsync({ ownerStake });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with a very low stake ratio', async () => {
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake).times(1e-18);
const expected = cobbDouglas({ ownerStake });
const r = await callCobbDouglasAsync({ ownerStake });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with a very high stake ratio', async () => {
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake).times(1 - 1e-18);
const expected = cobbDouglas({ ownerStake });
const r = await callCobbDouglasAsync({ ownerStake });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with zero fee ratio', async () => {
const ownerFees = 0;
const expected = cobbDouglas({ ownerFees });
const r = await callCobbDouglasAsync({ ownerFees });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with full fee ratio', async () => {
const ownerFees = DEFAULT_COBB_DOUGLAS_PARAMS.totalFees;
const expected = cobbDouglas({ ownerFees });
const r = await callCobbDouglasAsync({ ownerFees });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with a very low fee ratio', async () => {
const ownerFees = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalFees).times(1e-18);
const expected = cobbDouglas({ ownerFees });
const r = await callCobbDouglasAsync({ ownerFees });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with a very high fee ratio', async () => {
const ownerFees = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalFees).times(1 - 1e-18);
const expected = cobbDouglas({ ownerFees });
const r = await callCobbDouglasAsync({ ownerFees });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with equal fee and stake ratios', async () => {
const ownerFees = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalFees).times(0.5);
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake).times(0.5);
const expected = cobbDouglas({ ownerFees, ownerStake });
const r = await callCobbDouglasAsync({ ownerFees, ownerStake });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with full fee and stake ratios', async () => {
const ownerFees = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalFees);
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake);
const expected = cobbDouglas({ ownerFees, ownerStake });
const r = await callCobbDouglasAsync({ ownerFees, ownerStake });
assertRoughlyEquals(r, expected);
});
it('computes the correct reward with zero fee and stake ratios', async () => {
const ownerFees = 0;
const ownerStake = 0;
const expected = cobbDouglas({ ownerFees, ownerStake });
const r = await callCobbDouglasAsync({ ownerFees, ownerStake });
assertRoughlyEquals(r, expected);
});
blockchainTests.optional('fuzzing', () => {
const inputs = _.times(FUZZ_COUNT, () => getRandomParams());
for (const params of inputs) {
it(`cobbDouglas(${JSON.stringify(params)})`, async () => {
const expected = cobbDouglas(params);
const r = await callCobbDouglasAsync(params);
assertRoughlyEquals(r, expected, 12);
});
}
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -9,11 +9,25 @@ Decimal.set({ precision: 128 });
// tslint:disable:no-unnecessary-type-assertion
blockchainTests('LibFixedMath', env => {
let testContract: TestLibFixedMathContract;
before(async () => {
testContract = await TestLibFixedMathContract.deployFrom0xArtifactAsync(
artifacts.TestLibFixedMath,
env.provider,
env.txDefaults,
artifacts,
);
});
const BITS_OF_PRECISION = 127;
const FIXED_POINT_DIVISOR = new BigNumber(2).pow(BITS_OF_PRECISION);
const MAX_FIXED_VALUE = new BigNumber(2).pow(255).minus(1);
const MIN_FIXED_VALUE = new BigNumber(2).pow(255).times(-1);
const MIN_EXP_NUMBER = new BigNumber('-69.07755278982137043720332303294494434957608235919531845909733017243007692132225692604324818191230406');
const MIN_EXP_NUMBER = new BigNumber('-63.875');
const MAX_EXP_NUMBER = new BigNumber(0);
// e ^ MIN_EXP_NUMBER
const MIN_LN_NUMBER = new BigNumber(new Decimal(MIN_EXP_NUMBER.toFixed(128)).exp().toFixed(128));
const FUZZ_COUNT = 1024;
type Numberish = BigNumber | string | number;
@@ -75,33 +89,22 @@ blockchainTests('LibFixedMath', env => {
}
function assertFixedEquals(
actual: BigNumber | string | number,
expected: BigNumber | string | number,
actual: Numberish,
expected: Numberish,
): void {
expect(fromFixed(actual)).to.bignumber.eq(numberToFixedToNumber(expected));
}
function assertFixedRoughlyEquals(
actual: BigNumber | string | number,
expected: BigNumber | string | number,
precision: number = 13,
actual: Numberish,
expected: Numberish,
precision: number = 18,
): void {
// SD is not what we want.
expect(toPrecision(fromFixed(actual), precision))
.to.bignumber.eq(toPrecision(numberToFixedToNumber(expected), precision));
}
let testContract: TestLibFixedMathContract;
before(async () => {
testContract = await TestLibFixedMathContract.deployFrom0xArtifactAsync(
artifacts.TestLibFixedMath,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('one()', () => {
it('equals 1', async () => {
const r = await testContract.one.callAsync();
@@ -129,6 +132,75 @@ blockchainTests('LibFixedMath', env => {
});
});
describe('invert()', () => {
it('invert(1) == 1', async () => {
const n = 1;
const r = await testContract.invert.callAsync(toFixed(n));
assertFixedEquals(r, n);
});
it('invert(n) == 1 / n', async () => {
const n = 1337.5912;
const r = await testContract.invert.callAsync(toFixed(n));
assertFixedRoughlyEquals(r, 1 / n);
});
it('invert(-n) == -1 / n', async () => {
const n = -1337.5912;
const r = await testContract.invert.callAsync(toFixed(n));
assertFixedRoughlyEquals(r, 1 / n);
});
it('invert(0) throws', async () => {
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
);
const tx = testContract.invert.callAsync(toFixed(0));
return expect(tx).to.revertWith(expectedError);
});
});
describe('mulDiv()', () => {
it('mulDiv(0, 0, 1) == 0', async () => {
const [ a, n, d ] = [ 0, 0, 1 ];
const r = await testContract.mulDiv.callAsync(toFixed(a), new BigNumber(n), new BigNumber(d));
assertFixedEquals(r, 0);
});
it('mulDiv(0, x, y) == 0', async () => {
const [ a, n, d ] = [ 0, 13, 300 ];
const r = await testContract.mulDiv.callAsync(toFixed(a), new BigNumber(n), new BigNumber(d));
assertFixedEquals(r, 0);
});
it('mulDiv(x, y, y) == x', async () => {
const [ a, n, d ] = [ 1.2345, 149, 149 ];
const r = await testContract.mulDiv.callAsync(toFixed(a), new BigNumber(n), new BigNumber(d));
assertFixedEquals(r, a);
});
it('mulDiv(x, -y, y) == -x', async () => {
const [ a, n, d ] = [ 1.2345, -149, 149 ];
const r = await testContract.mulDiv.callAsync(toFixed(a), new BigNumber(n), new BigNumber(d));
assertFixedEquals(r, -a);
});
it('mulDiv(-x, -y, y) == x', async () => {
const [ a, n, d ] = [ -1.2345, -149, 149 ];
const r = await testContract.mulDiv.callAsync(toFixed(a), new BigNumber(n), new BigNumber(d));
assertFixedEquals(r, -a);
});
it('mulDiv(x, y, 0) throws', async () => {
const [ a, n, d ] = [ 1.2345, 149, 0 ];
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
);
const tx = testContract.mulDiv.callAsync(toFixed(a), new BigNumber(n), new BigNumber(d));
return expect(tx).to.revertWith(expectedError);
});
});
describe('add()', () => {
it('0 + 0 == 0', async () => {
const [ a, b ] = [ 0, 0 ];
@@ -508,6 +580,8 @@ blockchainTests('LibFixedMath', env => {
});
describe('ln()', () => {
const LN_PRECISION = 13;
it('ln(x = 0) throws', async () => {
const x = toFixed(0);
const expectedError = new FixedMathRevertErrors.FixedMathSignedValueError(
@@ -544,22 +618,28 @@ blockchainTests('LibFixedMath', env => {
assertFixedEquals(r, 0);
});
it('ln(x < LN_MIN_VAL) == EXP_MIN_VAL', async () => {
const x = toFixed(MIN_LN_NUMBER).minus(1);
const r = await testContract.ln.callAsync(x);
assertFixedEquals(r, MIN_EXP_NUMBER);
});
it('ln(x), where x is close to 0', async () => {
const x = new BigNumber('1e-27');
const r = await testContract.ln.callAsync(toFixed(x));
assertFixedRoughlyEquals(r, ln(x));
assertFixedRoughlyEquals(r, ln(x), LN_PRECISION);
});
it('ln(x), where x is close to 1', async () => {
const x = new BigNumber(1).minus('1e-27');
const r = await testContract.ln.callAsync(toFixed(x));
assertFixedRoughlyEquals(r, ln(x));
assertFixedRoughlyEquals(r, ln(x), LN_PRECISION);
});
it('ln(x = 0.5)', async () => {
const x = 0.5;
it('ln(x = 0.85)', async () => {
const x = 0.85;
const r = await testContract.ln.callAsync(toFixed(x));
assertFixedRoughlyEquals(r, ln(x));
assertFixedRoughlyEquals(r, ln(x), LN_PRECISION);
});
blockchainTests.optional('fuzzing', () => {
@@ -567,21 +647,23 @@ blockchainTests('LibFixedMath', env => {
for (const x of inputs) {
it(`ln(${x.toString(10)})`, async () => {
const r = await testContract.ln.callAsync(toFixed(x));
assertFixedRoughlyEquals(r, ln(x), 11);
assertFixedRoughlyEquals(r, ln(x), LN_PRECISION);
});
}
});
});
describe('exp()', () => {
const EXP_PRECISION = 18;
it('exp(x = 0) == 1', async () => {
const x = toFixed(0);
const r = await testContract.exp.callAsync(x);
assertFixedEquals(r, 1);
});
it('exp(x > 0) throws', async () => {
const x = toFixed(1e-18);
it('exp(x > EXP_MAX_VAL) throws', async () => {
const x = toFixed(MAX_EXP_NUMBER).plus(1);
const expectedError = new FixedMathRevertErrors.FixedMathSignedValueError(
FixedMathRevertErrors.ValueErrorCodes.TooLarge,
x,
@@ -596,30 +678,30 @@ blockchainTests('LibFixedMath', env => {
assertFixedEquals(r, 0);
});
it('exp(x), where x is close to 0', async () => {
const x = new BigNumber('-1e-27');
it('exp(x < 0), where x is close to 0', async () => {
const x = new BigNumber('-1e-18');
const r = await testContract.exp.callAsync(toFixed(x));
assertFixedRoughlyEquals(r, exp(x));
assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION);
});
it('exp(x), where x is close to EXP_MIN_VAL', async () => {
const x = MIN_EXP_NUMBER.plus('1e-27');
const x = MIN_EXP_NUMBER.plus('1e-18');
const r = await testContract.exp.callAsync(toFixed(x));
assertFixedRoughlyEquals(r, exp(x));
assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION);
});
it('exp(x = -0.5)', async () => {
const x = -0.5;
it('exp(x = -0.85)', async () => {
const x = -0.85;
const r = await testContract.exp.callAsync(toFixed(x));
assertFixedRoughlyEquals(r, exp(x));
assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION);
});
blockchainTests.optional('fuzzing', () => {
const inputs = _.times(FUZZ_COUNT, () => getRandomNumber(MIN_EXP_NUMBER, 0));
const inputs = _.times(FUZZ_COUNT, () => getRandomNumber(MIN_EXP_NUMBER, MAX_EXP_NUMBER));
for (const x of inputs) {
it(`exp(${x.toString(10)})`, async () => {
const r = await testContract.exp.callAsync(toFixed(x));
assertFixedRoughlyEquals(r, exp(x), 11);
assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION);
});
}
});