diff --git a/contracts/staking/CHANGELOG.json b/contracts/staking/CHANGELOG.json index a9328b1616..3626869dc6 100644 --- a/contracts/staking/CHANGELOG.json +++ b/contracts/staking/CHANGELOG.json @@ -9,6 +9,22 @@ { "note": "First implementation", "pr": 1910 + }, + { + "note": "Replace `LibFeeMath` with `LibFixedMath`.", + "pr": 2109 + }, + { + "note": "Use a more precise cobb-douglas implementation.", + "pr": 2109 + }, + { + "note": "Change the way operator stake is computed.", + "pr": 2109 + }, + { + "note": "Denominate pool operator shares in parts-per-million.", + "pr": 2109 } ] } diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index f0ddd560c0..71b682a4db 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -1,6 +1,6 @@ /* - Copyright 2018 ZeroEx Intl. + Copyright 2019 ZeroEx Intl. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,8 +20,8 @@ pragma solidity ^0.5.9; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; -import "../libs/LibFeeMath.sol"; import "../libs/LibStakingRichErrors.sol"; +import "../libs/LibFixedMath.sol"; import "../immutable/MixinStorage.sol"; import "../immutable/MixinConstants.sol"; import "../interfaces/IStakingEvents.sol"; @@ -56,13 +56,37 @@ contract MixinExchangeFees is { using LibSafeMath for uint256; + /// @dev Set the cobb douglas alpha value used when calculating rewards. + /// Valid inputs: 0 <= `numerator` / `denominator` <= 1.0 + /// @param numerator The alpha numerator. + /// @param denominator The alpha denominator. + function setCobbDouglasAlpha( + uint256 numerator, + uint256 denominator + ) + external + onlyOwner + { + if (int256(numerator) < 0 || int256(denominator) <= 0 || numerator > denominator) { + LibRichErrors.rrevert(LibStakingRichErrors.InvalidCobbDouglasAlphaError( + numerator, + denominator + )); + } + cobbDouglasAlphaNumerator = numerator; + cobbDouglasAlphaDenomintor = denominator; + emit CobbDouglasAlphaChanged(numerator, denominator); + } + /// TODO(jalextowle): Add WETH to protocol fees. Should this be unwrapped? /// @dev Pays a protocol fee in ETH. /// Only a known 0x exchange can call this method. See (MixinExchangeManager). /// @param makerAddress The address of the order's maker. function payProtocolFee( address makerAddress, + // solhint-disable-next-line address payerAddress, + // solhint-disable-next-line uint256 protocolFeePaid ) external @@ -71,10 +95,16 @@ contract MixinExchangeFees is { uint256 amount = msg.value; bytes32 poolId = getStakingPoolIdOfMaker(makerAddress); - uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId]; - protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(amount); - if (_feesCollectedThisEpoch == 0) { - activePoolsThisEpoch.push(poolId); + if (poolId != NIL_MAKER_ID) { + // There is a pool associated with `makerAddress`. + // TODO(dorothy-zbornak): When we have epoch locks on delegating, we could + // preclude pools that have no delegated stake, since they will never have + // stake in this epoch and are therefore not entitled to rewards. + uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId]; + protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(amount); + if (_feesCollectedThisEpoch == 0) { + activePoolsThisEpoch.push(poolId); + } } } @@ -125,7 +155,7 @@ contract MixinExchangeFees is } /// @dev Pays rewards to market making pools that were active this epoch. - /// Each pool receives a portion of the fees generated this epoch (see LibFeeMath) that is + /// Each pool receives a portion of the fees generated this epoch (see _cobbDouglas) that is /// proportional to (i) the fee volume attributed to their pool over the epoch, and /// (ii) the amount of stake provided by the maker and their delegators. Rebates are paid /// into the Reward Vault (see MixinStakingPoolRewardVault) where they can be withdraw by makers and @@ -139,7 +169,7 @@ contract MixinExchangeFees is /// @return initialContractBalance Balance of this contract before paying rewards. /// @return finalContractBalance Balance of this contract after paying rewards. function _distributeFeesAmongMakerPools() - private + internal returns ( uint256 totalActivePools, uint256 totalFeesCollected, @@ -176,11 +206,12 @@ contract MixinExchangeFees is // compute weighted stake uint256 totalStakeDelegatedToPool = getTotalStakeDelegatedToPool(poolId); - uint256 stakeHeldByPoolOperator = getActivatedAndUndelegatedStake(getStakingPoolOperator(poolId)); + uint256 stakeHeldByPoolOperator = getStakeDelegatedToPoolByOwner(getStakingPoolOperator(poolId), poolId); uint256 weightedStake = stakeHeldByPoolOperator.safeAdd( totalStakeDelegatedToPool - .safeMul(REWARD_PAYOUT_DELEGATED_STAKE_PERCENT_VALUE) - .safeDiv(PERCENTAGE_DENOMINATOR) + .safeSub(stakeHeldByPoolOperator) + .safeMul(REWARD_DELEGATED_STAKE_WEIGHT) + .safeDiv(PPM_DENOMINATOR) ); // store pool stats @@ -209,12 +240,14 @@ contract MixinExchangeFees is // step 2/3 - record reward for each pool for (uint256 i = 0; i != totalActivePools; i++) { // compute reward using cobb-douglas formula - uint256 reward = LibFeeMath._cobbDouglasSuperSimplified( + uint256 reward = _cobbDouglas( initialContractBalance, activePools[i].feesCollected, totalFeesCollected, activePools[i].weightedStake, - totalWeightedStake + totalWeightedStake, + cobbDouglasAlphaNumerator, + cobbDouglasAlphaDenomintor ); // record reward in vault @@ -250,4 +283,69 @@ contract MixinExchangeFees is finalContractBalance ); } + + /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. + /// Note that in this function there is no limitation on alpha; we tend to get better rounding + /// on the simplified versions below. + /// @param totalRewards collected over an epoch. + /// @param ownerFees Fees attributed to the owner of the staking pool. + /// @param totalFees collected across all active staking pools in the epoch. + /// @param ownerStake Stake attributed to the owner of the staking pool. + /// @param totalStake collected across all active staking pools in the epoch. + /// @param alphaNumerator Numerator of `alpha` in the cobb-dougles function. + /// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function. + /// @return ownerRewards Rewards for the owner. + function _cobbDouglas( + uint256 totalRewards, + uint256 ownerFees, + uint256 totalFees, + uint256 ownerStake, + uint256 totalStake, + uint256 alphaNumerator, + uint256 alphaDenominator + ) + internal + pure + returns (uint256 ownerRewards) + { + int256 feeRatio = LibFixedMath._toFixed(ownerFees, totalFees); + int256 stakeRatio = LibFixedMath._toFixed(ownerStake, totalStake); + if (feeRatio == 0 || stakeRatio == 0) { + return ownerRewards = 0; + } + + // The cobb-doublas function has the form: + // `totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)` + // This is equivalent to: + // `totalRewards * stakeRatio * e^(alpha * (ln(feeRatio / stakeRatio)))` + // However, because `ln(x)` has the domain of `0 < x < 1` + // and `exp(x)` has the domain of `x < 0`, + // and fixed-point math easily overflows with multiplication, + // we will choose the following if `stakeRatio > feeRatio`: + // `totalRewards * stakeRatio / e^(alpha * (ln(stakeRatio / feeRatio)))` + + // Compute + // `e^(alpha * (ln(feeRatio/stakeRatio)))` if feeRatio <= stakeRatio + // or + // `e^(ln(stakeRatio/feeRatio))` if feeRatio > stakeRatio + int256 n = feeRatio <= stakeRatio ? + LibFixedMath._div(feeRatio, stakeRatio) : + LibFixedMath._div(stakeRatio, feeRatio); + n = LibFixedMath._exp( + LibFixedMath._mulDiv( + LibFixedMath._ln(n), + int256(alphaNumerator), + int256(alphaDenominator) + ) + ); + // Compute + // `totalRewards * n` if feeRatio <= stakeRatio + // or + // `totalRewards / n` if stakeRatio > feeRatio + n = feeRatio <= stakeRatio ? + LibFixedMath._mul(stakeRatio, n) : + LibFixedMath._div(stakeRatio, n); + // Multiply the above with totalRewards. + ownerRewards = LibFixedMath._uintMul(n, totalRewards); + } } diff --git a/contracts/staking/contracts/src/immutable/MixinConstants.sol b/contracts/staking/contracts/src/immutable/MixinConstants.sol index 0d1daa93be..faa96e9b13 100644 --- a/contracts/staking/contracts/src/immutable/MixinConstants.sol +++ b/contracts/staking/contracts/src/immutable/MixinConstants.sol @@ -24,8 +24,7 @@ import "./MixinDeploymentConstants.sol"; contract MixinConstants is MixinDeploymentConstants { - // TODO: Reevaluate this variable - uint8 constant internal PERCENTAGE_DENOMINATOR = 100; + uint32 constant internal PPM_DENOMINATOR = 1000000; // The upper 16 bytes represent the pool id, so this would be pool id 1. See MixinStakinPool for more information. bytes32 constant internal INITIAL_POOL_ID = 0x0000000000000000000000000000000100000000000000000000000000000000; diff --git a/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol b/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol index 4885a544f1..33ce6debcd 100644 --- a/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol +++ b/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol @@ -27,9 +27,8 @@ contract MixinDeploymentConstants { uint256 constant internal TIMELOCK_DURATION_IN_EPOCHS = 3; - uint256 constant internal COBB_DOUGLAS_ALPHA_DENOMINATOR = 6; - - uint256 constant internal REWARD_PAYOUT_DELEGATED_STAKE_PERCENT_VALUE = 90; + // How much delegated stake is weighted vs operator stake, in ppm. + uint32 constant internal REWARD_DELEGATED_STAKE_WEIGHT = 900000; // 90% uint256 constant internal CHAIN_ID = 1; } diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index 4debe20ba9..5f3677100b 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -25,7 +25,7 @@ import "../interfaces/IStakingPoolRewardVault.sol"; import "../interfaces/IStructs.sol"; -// solhint-disable max-states-count +// solhint-disable max-states-count, no-empty-blocks contract MixinStorage is MixinDeploymentConstants, MixinConstants, @@ -106,4 +106,10 @@ contract MixinStorage is // Rebate Vault IStakingPoolRewardVault internal rewardVault; + + // Numerator for cobb douglas alpha factor. + uint256 internal cobbDouglasAlphaNumerator = 1; + + // Denominator for cobb douglas alpha factor. + uint256 internal cobbDouglasAlphaDenomintor = 6; } diff --git a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol index d54a458b08..7e3a42b4c1 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol @@ -41,6 +41,14 @@ interface IStakingEvents { uint256 earliestEndTimeInSeconds ); + /// @dev Emitted by MixinExchangeFees when the cobb douglas alpha is updated. + /// @param numerator The alpha numerator. + /// @param denominator The alpha denominator. + event CobbDouglasAlphaChanged( + uint256 numerator, + uint256 denominator + ); + /// @dev Emitted by MixinScheduler when the timeLock period is changed. /// @param timeLockPeriod The timeLock period we changed to. /// @param startEpoch The epoch this period started. @@ -70,11 +78,11 @@ interface IStakingEvents { /// @dev Emitted by MixinStakingPool when a new pool is created. /// @param poolId Unique id generated for pool. /// @param operatorAddress Address of creator/operator of pool. - /// @param operatorShare The share of rewards given to the operator. + /// @param operatorShare The share of rewards given to the operator, in ppm. event StakingPoolCreated( bytes32 poolId, address operatorAddress, - uint8 operatorShare + uint32 operatorShare ); /// @dev Emitted by MixinStakingPool when a new maker is added to a pool. diff --git a/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol b/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol index 4ae3f46998..5da3b766f8 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingPoolRewardVault.sol @@ -29,12 +29,12 @@ interface IStakingPoolRewardVault { /// @dev Holds the balance for a staking pool. /// @param initialzed True iff the balance struct is initialized. - /// @param operatorShare Percentage of the total balance owned by the operator. + /// @param operatorShare Fraction of the total balance owned by the operator, in ppm. /// @param operatorBalance Balance in ETH of the operator. /// @param membersBalance Balance in ETH co-owned by the pool members. struct Balance { bool initialized; - uint8 operatorShare; + uint32 operatorShare; uint96 operatorBalance; uint96 membersBalance; } @@ -69,10 +69,10 @@ interface IStakingPoolRewardVault { /// @dev Emitted when a staking pool is registered. /// @param poolId Unique Id of pool that was registered. - /// @param operatorShare Share of rewards owned by operator. + /// @param operatorShare Share of rewards owned by operator. in ppm. event StakingPoolRegistered( bytes32 poolId, - uint8 operatorShare + uint32 operatorShare ); /// @dev Default constructor. @@ -119,8 +119,8 @@ interface IStakingPoolRewardVault { /// Note that this is only callable by the staking contract, and when /// not in catastrophic failure mode. /// @param poolId Unique Id of pool. - /// @param poolOperatorShare Percentage of rewards given to the pool operator. - function registerStakingPool(bytes32 poolId, uint8 poolOperatorShare) + /// @param poolOperatorShare Share of rewards given to the pool operator, in ppm. + function registerStakingPool(bytes32 poolId, uint32 poolOperatorShare) external; /// @dev Returns the total balance of a pool. diff --git a/contracts/staking/contracts/src/interfaces/IStructs.sol b/contracts/staking/contracts/src/interfaces/IStructs.sol index e854662f86..5bc1b069bf 100644 --- a/contracts/staking/contracts/src/interfaces/IStructs.sol +++ b/contracts/staking/contracts/src/interfaces/IStructs.sol @@ -41,10 +41,10 @@ interface IStructs { /// @dev State for Staking Pools (see MixinStakingPool). /// @param operatorAddress Address of pool operator. - /// @param operatorShare Portion of pool rewards owned by operator. + /// @param operatorShare Portion of pool rewards owned by operator, in ppm. struct Pool { address payable operatorAddress; - uint8 operatorShare; + uint32 operatorShare; } /// @dev State for a pool that actively traded during the current epoch. diff --git a/contracts/staking/contracts/src/libs/LibFeeMath.sol b/contracts/staking/contracts/src/libs/LibFeeMath.sol deleted file mode 100644 index f15d72c1a0..0000000000 --- a/contracts/staking/contracts/src/libs/LibFeeMath.sol +++ /dev/null @@ -1,315 +0,0 @@ - - -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.5.9; - - -/// @dev This library implements math helpers for fee computation. -/// *** READ MixinExchangeFees BEFORE CONTINUING *** -/// To do - Optimization / Precision / SafeMath. -/// To do - Once better nth root - choose a value that is not a divisor of 18, like 7. -/// To do - Update these values for deployment. -/// There may be better, more efficient ways of implementing these functions. -/// This works well enough to test the end-to-system, but it would be really -/// good to get some math experts in here to check out this code. We should also -/// look at existing projects, in case similar functionality exists and has been -/// audited by a third-party. -library LibFeeMath { - - // Denominator of alpha in cobb-douglas function - uint256 constant internal COBB_DOUGLAS_ALPHA_DENOMINATOR = 6; - - // Reflects number of decimal places in a token amount - uint256 constant internal TOKEN_MULTIPLIER = 1000000000000000000; - - // The divisor for finding the nth root of token amounts. - uint256 constant internal NTH_ROOT_OF_TOKEN_MULTIPLIER = 1000; - - /// @dev Computes the nth root of a number. - /// @param base to compute the root. - /// @param n nth root. - /// @return nth root of base. - function _nthRoot(uint256 base, uint256 n) - internal - pure - returns (uint256 root) - { - assembly { - ///// Implements Newton's Approximation, derived from Newton's nth Root Algorithm ///// - ///// See https://en.wikipedia.org/wiki/Nth_root#nth_root_algorithm - - // 1. Find greatest power-of-2 <= `value` - let nearestPowerOf2 := 0x100000000000000000000000000000000 - let m := 128 - for {let p := 64} - gt(p, 0) - { p := div(p, 2) } - { - - switch gt(nearestPowerOf2, base) - case 1 { - nearestPowerOf2 := shr(p, nearestPowerOf2) - m := sub(m, p) - } - case 0 { - switch lt(nearestPowerOf2, base) - case 1 { - nearestPowerOf2 := shl(p, nearestPowerOf2) - m := add(m, p) - } - case 0 { - p := 0 - } - } - } - if gt(nearestPowerOf2, base) { - nearestPowerOf2 := shr(1, nearestPowerOf2) - m := sub(m, 1) - } - - - // 2. Find greatest power-of-2 that, when raised to the power of `n`, - // is <= `value` - let x := exp(2, div(m, n)) - - // 3. Find y such that `x` + `y` = `base` - let y := sub(base, exp2(x, n)) - - // 4. Run Newton's Approximation to approximate the root - let denominator := mul(n, exp2(x, sub(n, 1))) - root := add(x, div(y, denominator)) - - // 5. Run Newton's nth Root Algorithm - let delta := 1 // run at least once - // solhint-disable no-empty-blocks - for {} - gt(delta, 0) - {} - { - // compute lhs - let lhsDenominator := exp2(root, sub(n, 1)) - let lhs := div(base, lhsDenominator) - - // check for overlow - switch lt(lhs, root) - case 0 { - // underestimate - delta := div(sub(lhs, root), n) - root := add(root, delta) - } - case 1 { - // overestimate - delta := div(sub(root, lhs), n) - root := sub(root, delta) - } - } - - // @HACK(hysz) - ganache core workaround (issue #430) - function exp2(b, p) -> z { - z := b - for {p := sub(p, 1)} - gt(p, 0) - {p := sub(p, 1)} - { - z := mul(z, b) - } - } - } - } - - /// @dev Computes the nth root of a fixed point value. - /// @param base to compute the root. - /// @param n nth root. - /// @return nth root of base. - function _nthRootFixedPoint( - uint256 base, - uint256 n - ) - internal - pure - returns (uint256 root) - { - uint256 scalar = 10**18; - uint256 numerator = _nthRoot(base, n); - uint256 denominator = _nthRoot(scalar, n); - root = (scalar * numerator) / denominator; - } - - /// @dev Computes the nth root of a fixed point value, where the - /// number of decimal places is known before hand (hardcoded above). - /// @param base to compute the root. - /// @return nth root of base. - function _nthRootFixedPointFixedN( - uint256 base - ) - internal - pure - returns (uint256 root) - { - uint256 numerator = _nthRoot(base, COBB_DOUGLAS_ALPHA_DENOMINATOR); - root = (TOKEN_MULTIPLIER * numerator) / NTH_ROOT_OF_TOKEN_MULTIPLIER; - return root; - } - - /// @dev Computes an exponent of a value in the form (ab)/c, minimizing loss of precision. - /// @param numerator of fraction - /// @param scalar to be multiplied by the numerator - /// @param denominator of fraction - /// @param power to raise value to - /// @return Exponent of input value. - function _exp(uint256 numerator, uint256 scalar, uint256 denominator, uint256 power) - internal - pure - returns (uint256 result) - { - result = (numerator * scalar) / denominator; - for (power = power - 1; power > 0; power -= 1) { - result = (result * numerator) / denominator; - } - return result; - } - - /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. - /// Note that in this function there is no limitation on alpha; we tend to get better rounding - /// on the simplified versions below. - /// @param totalRewards collected over an epoch. - /// @param ownerFees Fees attributed to the owner of the staking pool. - /// @param totalFees collected across all active staking pools in the epoch. - /// @param ownerStake Stake attributed to the owner of the staking pool. - /// @param totalStake collected across all active staking pools in the epoch. - /// @param alphaNumerator Numerator of `alpha` in the cobb-dougles function. - /// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function. - /// @return Result of computing the cobb-douglas function. - function _cobbDouglas( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake, - uint8 alphaNumerator, - uint8 alphaDenominator - ) - internal - pure - returns (uint256) - { - return _exp(_nthRootFixedPoint(ownerFees * totalStake, alphaDenominator), - ((totalRewards * ownerStake) / totalStake), - _nthRootFixedPoint(totalFees * ownerStake, alphaDenominator), - alphaNumerator - ); - } - - /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. - /// Note - we assume that alpha = 1/x - /// @param totalRewards collected over an epoch. - /// @param ownerFees Fees attributed to the owner of the staking pool. - /// @param totalFees collected across all active staking pools in the epoch. - /// @param ownerStake Stake attributed to the owner of the staking pool. - /// @param totalStake collected across all active staking pools in the epoch. - /// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function. - /// @return Result of computing the cobb-douglas function. - function _cobbDouglasSimplified( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake, - uint8 alphaDenominator - ) - internal - pure - returns (uint256) - { - return (_nthRootFixedPoint(ownerFees * totalStake, alphaDenominator) * totalRewards * ownerStake) / - (_nthRootFixedPoint(totalFees * ownerStake, alphaDenominator) * totalStake); - } - - /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. - /// Note - we assume that (1 - alpha) = 1/x - /// @param totalRewards collected over an epoch. - /// @param ownerFees Fees attributed to the owner of the staking pool. - /// @param totalFees collected across all active staking pools in the epoch. - /// @param ownerStake Stake attributed to the owner of the staking pool. - /// @param totalStake collected across all active staking pools in the epoch. - /// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function. - /// @return Result of computing the cobb-douglas function. - function _cobbDouglasSimplifiedInverse( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake, - uint8 alphaDenominator - ) - internal - pure - returns (uint256) - { - return (_nthRootFixedPoint(ownerStake * totalFees, alphaDenominator) * totalRewards * ownerFees) / - (_nthRootFixedPoint(totalStake * ownerFees, alphaDenominator) * totalFees); - } - - /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. - /// Note - we assume that alpha = 1/x, where x is defined by `COBB_DOUGLAS_ALPHA_DENOMINATOR` - /// @param totalRewards collected over an epoch. - /// @param ownerFees Fees attributed to the owner of the staking pool. - /// @param totalFees collected across all active staking pools in the epoch. - /// @param ownerStake Stake attributed to the owner of the staking pool. - /// @param totalStake collected across all active staking pools in the epoch. - /// @return Result of computing the cobb-douglas function. - function _cobbDouglasSuperSimplified( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake - ) - internal - pure - returns (uint256) - { - return (_nthRootFixedPointFixedN(ownerFees * totalStake) * totalRewards * ownerStake) / - (_nthRootFixedPointFixedN(totalFees * ownerStake) * totalStake); - } - - /// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch. - /// Note - we assume that (1 - alpha) = 1/x, where x is defined by `COBB_DOUGLAS_ALPHA_DENOMINATOR` - /// @param totalRewards collected over an epoch. - /// @param ownerFees Fees attributed to the owner of the staking pool. - /// @param totalFees collected across all active staking pools in the epoch. - /// @param ownerStake Stake attributed to the owner of the staking pool. - /// @param totalStake collected across all active staking pools in the epoch. - /// @return Result of computing the cobb-douglas function. - function _cobbDouglasSuperSimplifiedInverse( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake - ) - internal - pure - returns (uint256) - { - return (_nthRootFixedPointFixedN(ownerStake * totalFees) * totalRewards * ownerFees) / - (_nthRootFixedPointFixedN(totalStake * ownerFees) * totalFees); - } -} diff --git a/contracts/staking/contracts/src/libs/LibFixedMath.sol b/contracts/staking/contracts/src/libs/LibFixedMath.sol new file mode 100644 index 0000000000..cfe58695a5 --- /dev/null +++ b/contracts/staking/contracts/src/libs/LibFixedMath.sol @@ -0,0 +1,376 @@ +/* + + Copyright 2017 Bprotocol Foundation, 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; + +import "./LibFixedMathRichErrors.sol"; + + +// solhint-disable indent +/// @dev Signed, fixed-point, 127-bit precision math library. +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; + // 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) { + f = FIXED_1; + } + + /// @dev Returns the addition of two fixed point numbers, reverting on overflow. + function _add(int256 a, int256 b) internal pure returns (int256 c) { + c = __add(a, b); + } + + /// @dev Returns the addition of two fixed point numbers, reverting on overflow. + function _sub(int256 a, int256 b) internal pure returns (int256 c) { + c = __add(a, -b); + } + + /// @dev Returns the multiplication of two fixed point numbers, reverting on overflow. + function _mul(int256 a, int256 b) internal pure returns (int256 c) { + c = __mul(a, b) / FIXED_1; + } + + /// @dev Returns the division of two fixed point numbers. + function _div(int256 a, int256 b) internal pure returns (int256 c) { + 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. + function _uintMul(int256 f, uint256 u) internal pure returns (uint256) { + if (int256(u) < int256(0)) { + LibRichErrors.rrevert(LibFixedMathRichErrors.UnsignedValueError( + LibFixedMathRichErrors.ValueErrorCodes.TOO_LARGE, + u + )); + } + int256 c = __mul(f, int256(u)); + if (c <= 0) { + return 0; + } + return uint256(uint256(c) >> 127); + } + + /// @dev Returns the absolute value of a fixed point number. + function _abs(int256 f) internal pure returns (int256 c) { + if (f >= 0) { + c = f; + } else { + 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); + } + + /// @dev Convert signed `n` / `d` to a fixed-point number. + function _toFixed(int256 n, int256 d) internal pure returns (int256 f) { + f = __div(__mul(n, FIXED_1), d); + } + + /// @dev Convert unsigned `n` / 1 to a fixed-point number. + /// Reverts if `n` is too large to fit in a fixed-point number. + function _toFixed(uint256 n) internal pure returns (int256 f) { + if (int256(n) < int256(0)) { + LibRichErrors.rrevert(LibFixedMathRichErrors.UnsignedValueError( + LibFixedMathRichErrors.ValueErrorCodes.TOO_LARGE, + n + )); + } + f = __mul(int256(n), FIXED_1); + } + + /// @dev Convert unsigned `n` / `d` to a fixed-point number. + /// Reverts if `n` / `d` is too large to fit in a fixed-point number. + function _toFixed(uint256 n, uint256 d) internal pure returns (int256 f) { + if (int256(n) < int256(0)) { + LibRichErrors.rrevert(LibFixedMathRichErrors.UnsignedValueError( + LibFixedMathRichErrors.ValueErrorCodes.TOO_LARGE, + n + )); + } + if (int256(d) < int256(0)) { + LibRichErrors.rrevert(LibFixedMathRichErrors.UnsignedValueError( + LibFixedMathRichErrors.ValueErrorCodes.TOO_LARGE, + d + )); + } + f = __div(__mul(int256(n), FIXED_1), int256(d)); + } + + /// @dev Convert a fixed-point number to an integer. + function _toInteger(int256 f) internal pure returns (int256 n) { + return f / FIXED_1; + } + + /// @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 > LN_MAX_VAL) { + LibRichErrors.rrevert(LibFixedMathRichErrors.SignedValueError( + LibFixedMathRichErrors.ValueErrorCodes.TOO_LARGE, + x + )); + } + if (x <= 0) { + LibRichErrors.rrevert(LibFixedMathRichErrors.SignedValueError( + LibFixedMathRichErrors.ValueErrorCodes.TOO_SMALL, + 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 ^ -32 + if (x <= int256(0x00000000000000000000000000000000000000000001c8464f76164760000000)) { + r -= int256(0x0000000000000000000000000000001000000000000000000000000000000000); // - 32 + x = x * FIXED_1 / int256(0x00000000000000000000000000000000000000000001c8464f76164760000000); // / e ^ -32 + } + // e ^ -16 + if (x <= int256(0x00000000000000000000000000000000000000f1aaddd7742e90000000000000)) { + r -= int256(0x0000000000000000000000000000000800000000000000000000000000000000); // - 16 + x = x * FIXED_1 / int256(0x00000000000000000000000000000000000000f1aaddd7742e90000000000000); // / e ^ -16 + } + // e ^ -8 + if (x <= int256(0x00000000000000000000000000000000000afe10820813d78000000000000000)) { + r -= int256(0x0000000000000000000000000000000400000000000000000000000000000000); // - 8 + x = x * FIXED_1 / int256(0x00000000000000000000000000000000000afe10820813d78000000000000000); // / e ^ -8 + } + // e ^ -4 + if (x <= int256(0x0000000000000000000000000000000002582ab704279ec00000000000000000)) { + r -= int256(0x0000000000000000000000000000000200000000000000000000000000000000); // - 4 + x = x * FIXED_1 / int256(0x0000000000000000000000000000000002582ab704279ec00000000000000000); // / e ^ -4 + } + // e ^ -2 + if (x <= int256(0x000000000000000000000000000000001152aaa3bf81cc000000000000000000)) { + r -= int256(0x0000000000000000000000000000000100000000000000000000000000000000); // - 2 + x = x * FIXED_1 / int256(0x000000000000000000000000000000001152aaa3bf81cc000000000000000000); // / e ^ -2 + } + // e ^ -1 + if (x <= int256(0x000000000000000000000000000000002f16ac6c59de70000000000000000000)) { + r -= int256(0x0000000000000000000000000000000080000000000000000000000000000000); // - 1 + x = x * FIXED_1 / int256(0x000000000000000000000000000000002f16ac6c59de70000000000000000000); // / e ^ -1 + } + // e ^ -0.5 + if (x <= int256(0x000000000000000000000000000000004da2cbf1be5828000000000000000000)) { + r -= int256(0x0000000000000000000000000000000040000000000000000000000000000000); // - 0.5 + x = x * FIXED_1 / int256(0x000000000000000000000000000000004da2cbf1be5828000000000000000000); // / e ^ -0.5 + } + // e ^ -0.25 + if (x <= int256(0x0000000000000000000000000000000063afbe7ab2082c000000000000000000)) { + 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 + z = y = x - FIXED_1; + w = y * y / FIXED_1; + r += z * (0x100000000000000000000000000000000 - y) / 0x100000000000000000000000000000000; z = z * w / FIXED_1; // add y^01 / 01 - y^02 / 02 + r += z * (0x0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - y) / 0x200000000000000000000000000000000; z = z * w / FIXED_1; // add y^03 / 03 - y^04 / 04 + r += z * (0x099999999999999999999999999999999 - y) / 0x300000000000000000000000000000000; z = z * w / FIXED_1; // add y^05 / 05 - y^06 / 06 + r += z * (0x092492492492492492492492492492492 - y) / 0x400000000000000000000000000000000; z = z * w / FIXED_1; // add y^07 / 07 - y^08 / 08 + r += z * (0x08e38e38e38e38e38e38e38e38e38e38e - y) / 0x500000000000000000000000000000000; z = z * w / FIXED_1; // add y^09 / 09 - y^10 / 10 + r += z * (0x08ba2e8ba2e8ba2e8ba2e8ba2e8ba2e8b - y) / 0x600000000000000000000000000000000; z = z * w / FIXED_1; // add y^11 / 11 - y^12 / 12 + r += z * (0x089d89d89d89d89d89d89d89d89d89d89 - y) / 0x700000000000000000000000000000000; z = z * w / FIXED_1; // add y^13 / 13 - y^14 / 14 + r += z * (0x088888888888888888888888888888888 - y) / 0x800000000000000000000000000000000; // add y^15 / 15 - y^16 / 16 + } + + /// @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) { + // Saturate to zero below EXP_MIN_VAL. + return 0; + } + if (x == 0) { + return FIXED_1; + } + if (x > EXP_MAX_VAL) { + LibRichErrors.rrevert(LibFixedMathRichErrors.SignedValueError( + LibFixedMathRichErrors.ValueErrorCodes.TOO_LARGE, + x + )); + } + + // 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.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; + // q = x % 0.125 (the residual) + 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. + function __mul(int256 a, int256 b) private pure returns (int256 c) { + if (a == 0) { + return 0; + } + c = a * b; + if (c / a != b) { + LibRichErrors.rrevert(LibFixedMathRichErrors.BinOpError( + LibFixedMathRichErrors.BinOpErrorCodes.MULTIPLICATION_OVERFLOW, + a, + b + )); + } + } + + /// @dev Returns the division of two numbers, reverting on division by zero. + function __div(int256 a, int256 b) private pure returns (int256 c) { + if (b == 0) { + LibRichErrors.rrevert(LibFixedMathRichErrors.BinOpError( + LibFixedMathRichErrors.BinOpErrorCodes.DIVISION_BY_ZERO, + a, + b + )); + } + c = a / b; + } + + /// @dev Adds two numbers, reverting on overflow. + function __add(int256 a, int256 b) private pure returns (int256 c) { + c = a + b; + if (c > 0 && a < 0 && b < 0) { + LibRichErrors.rrevert(LibFixedMathRichErrors.BinOpError( + LibFixedMathRichErrors.BinOpErrorCodes.SUBTRACTION_OVERFLOW, + a, + b + )); + } + if (c < 0 && a > 0 && b > 0) { + LibRichErrors.rrevert(LibFixedMathRichErrors.BinOpError( + LibFixedMathRichErrors.BinOpErrorCodes.ADDITION_OVERFLOW, + a, + b + )); + } + } +} diff --git a/contracts/staking/contracts/src/libs/LibFixedMathRichErrors.sol b/contracts/staking/contracts/src/libs/LibFixedMathRichErrors.sol new file mode 100644 index 0000000000..8ea570bb87 --- /dev/null +++ b/contracts/staking/contracts/src/libs/LibFixedMathRichErrors.sol @@ -0,0 +1,97 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; + +import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; + + +library LibFixedMathRichErrors { + + enum ValueErrorCodes { + TOO_SMALL, + TOO_LARGE + } + + enum BinOpErrorCodes { + ADDITION_OVERFLOW, + SUBTRACTION_OVERFLOW, + MULTIPLICATION_OVERFLOW, + DIVISION_BY_ZERO + } + + // bytes4(keccak256("SignedValueError(uint8,int256)")) + bytes4 internal constant SIGNED_VALUE_ERROR_SELECTOR = + 0xed2f26a1; + + // bytes4(keccak256("UnsignedValueError(uint8,uint256)")) + bytes4 internal constant UNSIGNED_VALUE_ERROR_SELECTOR = + 0xbd79545f; + + // bytes4(keccak256("BinOpError(uint8,int256,int256)")) + bytes4 internal constant BIN_OP_ERROR_SELECTOR = + 0x8c12dfe7; + + // solhint-disable func-name-mixedcase + function SignedValueError( + ValueErrorCodes error, + int256 n + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + SIGNED_VALUE_ERROR_SELECTOR, + uint8(error), + n + ); + } + + function UnsignedValueError( + ValueErrorCodes error, + uint256 n + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + UNSIGNED_VALUE_ERROR_SELECTOR, + uint8(error), + n + ); + } + + function BinOpError( + BinOpErrorCodes error, + int256 a, + int256 b + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + BIN_OP_ERROR_SELECTOR, + uint8(error), + a, + b + ); + } +} diff --git a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol index d8cd017121..78090942ef 100644 --- a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol +++ b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol @@ -110,14 +110,18 @@ library LibStakingRichErrors { bytes4 internal constant AMOUNT_EXCEEDS_BALANCE_OF_POOL_ERROR_SELECTOR = 0x4c5c09dd; - // bytes4(keccak256("OperatorShareMustBeBetween0And100Error(bytes32,uint8)")) - bytes4 internal constant OPERATOR_SHARE_MUST_BE_BETWEEN_0_AND_100_ERROR_SELECTOR = - 0xde447684; + // bytes4(keccak256("InvalidPoolOperatorShareError(bytes32,uint32)")) + bytes4 internal constant INVALID_POOL_OPERATOR_SHARE_ERROR_SELECTOR = + 0x70f55b5a; // bytes4(keccak256("PoolAlreadyExistsError(bytes32)")) bytes4 internal constant POOL_ALREADY_EXISTS_ERROR_SELECTOR = 0x2a5e4dcf; + // bytes4(keccak256("InvalidCobbDouglasAlphaError(uint256,uint256)")) + bytes4 internal constant INVALID_COBB_DOUGLAS_ALPHA_ERROR_SELECTOR = + 0x8f8e73de; + // solhint-disable func-name-mixedcase function MiscalculatedRewardsError( uint256 totalRewardsPaid, @@ -416,16 +420,16 @@ library LibStakingRichErrors { ); } - function OperatorShareMustBeBetween0And100Error( + function InvalidPoolOperatorShareError( bytes32 poolId, - uint8 poolOperatorShare + uint32 poolOperatorShare ) internal pure returns (bytes memory) { return abi.encodeWithSelector( - OPERATOR_SHARE_MUST_BE_BETWEEN_0_AND_100_ERROR_SELECTOR, + INVALID_POOL_OPERATOR_SHARE_ERROR_SELECTOR, poolId, poolOperatorShare ); @@ -443,4 +447,19 @@ library LibStakingRichErrors { poolId ); } + + function InvalidCobbDouglasAlphaError( + uint256 numerator, + uint256 denominator + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + INVALID_COBB_DOUGLAS_ALPHA_ERROR_SELECTOR, + numerator, + denominator + ); + } } diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol index 22d4327989..bae923b351 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol @@ -97,9 +97,9 @@ contract MixinStakingPool is /// @dev Create a new staking pool. The sender will be the operator of this pool. /// Note that an operator must be payable. - /// @param operatorShare The percentage of any rewards owned by the operator. + /// @param operatorShare Portion of rewards owned by the operator, in ppm. /// @return poolId The unique pool id generated for this pool. - function createStakingPool(uint8 operatorShare) + function createStakingPool(uint32 operatorShare) external returns (bytes32 poolId) { diff --git a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewardVault.sol b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewardVault.sol index 175a339ffc..5d01558a0d 100644 --- a/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewardVault.sol @@ -88,8 +88,8 @@ contract MixinStakingPoolRewardVault is /// @dev Registers a staking pool in the reward vault. /// @param poolId Unique id of pool. - /// @param operatorShare The percentage of the rewards owned by the operator. - function _registerStakingPoolInRewardVault(bytes32 poolId, uint8 operatorShare) + /// @param operatorShare Portion of rewards owned by the operator, in ppm. + function _registerStakingPoolInRewardVault(bytes32 poolId, uint32 operatorShare) internal { rewardVault.registerStakingPool( diff --git a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol index edd8668d79..d6d4ef7e0e 100644 --- a/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol +++ b/contracts/staking/contracts/src/vaults/StakingPoolRewardVault.sol @@ -153,15 +153,15 @@ contract StakingPoolRewardVault is /// Note that this is only callable by the staking contract, and when /// not in catastrophic failure mode. /// @param poolId Unique Id of pool. - /// @param poolOperatorShare Percentage of rewards given to the pool operator. - function registerStakingPool(bytes32 poolId, uint8 poolOperatorShare) + /// @param poolOperatorShare Fraction of rewards given to the pool operator, in ppm. + function registerStakingPool(bytes32 poolId, uint32 poolOperatorShare) external onlyStakingContract onlyNotInCatastrophicFailure { - // operator share must be a valid percentage - if (poolOperatorShare > PERCENTAGE_DENOMINATOR) { - LibRichErrors.rrevert(LibStakingRichErrors.OperatorShareMustBeBetween0And100Error( + // operator share must be a valid fraction + if (poolOperatorShare > PPM_DENOMINATOR) { + LibRichErrors.rrevert(LibStakingRichErrors.InvalidPoolOperatorShareError( poolId, poolOperatorShare )); @@ -228,8 +228,8 @@ contract StakingPoolRewardVault is { // compute portions. One of the two must round down: the operator always receives the leftover from rounding. uint256 operatorPortion = LibMath.getPartialAmountCeil( - uint256(balance.operatorShare), // Operator share out of 100 - PERCENTAGE_DENOMINATOR, + uint256(balance.operatorShare), // Operator share out of 1e6 + PPM_DENOMINATOR, amount ); diff --git a/contracts/staking/contracts/test/LibFeeMathTest.sol b/contracts/staking/contracts/test/LibFeeMathTest.sol deleted file mode 100644 index c58e297d50..0000000000 --- a/contracts/staking/contracts/test/LibFeeMathTest.sol +++ /dev/null @@ -1,112 +0,0 @@ - - -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.5.5; - -import "../src/libs/LibFeeMath.sol"; - - -contract LibFeeMathTest { - - function nthRoot(uint256 base, uint256 n) public pure returns (uint256 root) { - return LibFeeMath._nthRoot(base, n); - } - - function nthRootFixedPoint( - uint256 base, - uint256 n - ) - public - pure - returns (uint256 root) - { - return LibFeeMath._nthRootFixedPoint(base, n); - } - - function cobbDouglas( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake, - uint8 alphaNumerator, - uint8 alphaDenominator - ) - public - pure - returns (uint256) - { - return LibFeeMath._cobbDouglas( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaNumerator, - alphaDenominator - ); - } - - function cobbDouglasSimplified( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake, - uint8 alphaDenominator - ) - public - pure - returns (uint256) - { - return LibFeeMath._cobbDouglasSimplified( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaDenominator - ); - } - - function cobbDouglasSimplifiedInverse( - uint256 totalRewards, - uint256 ownerFees, - uint256 totalFees, - uint256 ownerStake, - uint256 totalStake, - uint8 alphaDenominator - ) - public - // pure - returns (uint256) - { - return LibFeeMath._cobbDouglasSimplifiedInverse( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaDenominator - ); - } -} - - diff --git a/contracts/staking/contracts/test/TestCobbDouglas.sol b/contracts/staking/contracts/test/TestCobbDouglas.sol new file mode 100644 index 0000000000..28c598ca72 --- /dev/null +++ b/contracts/staking/contracts/test/TestCobbDouglas.sol @@ -0,0 +1,58 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ +pragma solidity ^0.5.9; + +import "../src/Staking.sol"; + + +contract TestCobbDouglas is + Staking +{ + function cobbDouglas( + uint256 totalRewards, + uint256 ownerFees, + uint256 totalFees, + uint256 ownerStake, + uint256 totalStake, + uint256 alphaNumerator, + uint256 alphaDenominator + ) + external + pure + returns (uint256 ownerRewards) + { + ownerRewards = _cobbDouglas( + totalRewards, + ownerFees, + totalFees, + ownerStake, + totalStake, + alphaNumerator, + alphaDenominator + ); + } + + function getCobbDouglasAlpha() + external + view + returns (uint256 numerator, uint256 denominator) + { + numerator = cobbDouglasAlphaNumerator; + denominator = cobbDouglasAlphaDenomintor; + } +} diff --git a/contracts/staking/contracts/test/TestLibFixedMath.sol b/contracts/staking/contracts/test/TestLibFixedMath.sol new file mode 100644 index 0000000000..f53760aad1 --- /dev/null +++ b/contracts/staking/contracts/test/TestLibFixedMath.sol @@ -0,0 +1,88 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ +pragma solidity ^0.5.9; + +import "../src/libs/LibFixedMath.sol"; + + +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); + } + + function div(int256 a, int256 b) external pure returns (int256) { + return LibFixedMath._div(a, b); + } + + function add(int256 a, int256 b) external pure returns (int256) { + return LibFixedMath._add(a, b); + } + + function sub(int256 a, int256 b) external pure returns (int256) { + return LibFixedMath._sub(a, b); + } + + function uintMul(int256 f, uint256 u) external pure returns (uint256) { + return LibFixedMath._uintMul(f, u); + } + + function abs(int256 a) external pure returns (int256) { + 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); + } + + function toFixedSigned(int256 n) external pure returns (int256) { + return LibFixedMath._toFixed(n); + } + + function toFixedUnsigned(uint256 n, uint256 d) external pure returns (int256) { + return LibFixedMath._toFixed(n, d); + } + + function toFixedUnsigned(uint256 n) external pure returns (int256) { + return LibFixedMath._toFixed(n); + } + + function toInteger(int256 f) external pure returns (int256) { + return LibFixedMath._toInteger(f); + } + + function ln(int256 x) external pure returns (int256 r) { + return LibFixedMath._ln(x); + } + + function exp(int256 x) external pure returns (int256 r) { + return LibFixedMath._exp(x); + } +} diff --git a/contracts/staking/package.json b/contracts/staking/package.json index 8546250ee4..b9a7ecda32 100644 --- a/contracts/staking/package.json +++ b/contracts/staking/package.json @@ -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", @@ -36,7 +36,7 @@ "compile:truffle": "truffle compile" }, "config": { - "abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStructs|IVaultCore|IWallet|IZrxVault|LibEIP712Hash|LibFeeMath|LibFeeMathTest|LibRewardMath|LibSafeDowncast|LibSignatureValidator|LibStakingRichErrors|MixinConstants|MixinDelegatedStake|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinTimeLockedStake|MixinVaultCore|MixinZrxVault|Staking|StakingPoolRewardVault|StakingProxy|TestStorageLayout|ZrxVault).json", + "abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStructs|IVaultCore|IWallet|IZrxVault|LibEIP712Hash|LibFixedMath|LibFixedMathRichErrors|LibRewardMath|LibSafeDowncast|LibSignatureValidator|LibStakingRichErrors|MixinConstants|MixinDelegatedStake|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinTimeLockedStake|MixinVaultCore|MixinZrxVault|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestLibFixedMath|TestStorageLayout|ZrxVault).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { @@ -55,12 +55,13 @@ "@0x/dev-utils": "^2.2.1", "@0x/sol-compiler": "^3.1.6", "@0x/tslint-config": "^3.0.1", + "@0x/utils": "^4.3.1", "@types/lodash": "4.14.104", "@types/node": "*", - "@0x/utils": "^4.3.1", "chai": "^4.0.1", "chai-as-promised": "^7.1.0", "chai-bignumber": "^3.0.0", + "decimal.js": "^10.2.0", "dirty-chai": "^2.0.1", "make-promises-safe": "^1.1.0", "mocha": "^4.1.0", @@ -73,9 +74,9 @@ }, "dependencies": { "@0x/base-contract": "^5.1.0", - "@0x/contracts-utils": "^3.2.1", "@0x/contracts-asset-proxy": "^2.2.5", "@0x/contracts-erc20": "^2.2.0", + "@0x/contracts-utils": "^3.2.1", "@0x/order-utils": "^8.1.0", "@0x/types": "^2.2.2", "@0x/typescript-typings": "^4.2.2", diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index c183d1b48f..45e8b99e7e 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -14,8 +14,8 @@ import * as IVaultCore from '../generated-artifacts/IVaultCore.json'; import * as IWallet from '../generated-artifacts/IWallet.json'; import * as IZrxVault from '../generated-artifacts/IZrxVault.json'; import * as LibEIP712Hash from '../generated-artifacts/LibEIP712Hash.json'; -import * as LibFeeMath from '../generated-artifacts/LibFeeMath.json'; -import * as LibFeeMathTest from '../generated-artifacts/LibFeeMathTest.json'; +import * as LibFixedMath from '../generated-artifacts/LibFixedMath.json'; +import * as LibFixedMathRichErrors from '../generated-artifacts/LibFixedMathRichErrors.json'; import * as LibRewardMath from '../generated-artifacts/LibRewardMath.json'; import * as LibSafeDowncast from '../generated-artifacts/LibSafeDowncast.json'; import * as LibSignatureValidator from '../generated-artifacts/LibSignatureValidator.json'; @@ -38,6 +38,8 @@ import * as MixinZrxVault from '../generated-artifacts/MixinZrxVault.json'; import * as Staking from '../generated-artifacts/Staking.json'; import * as StakingPoolRewardVault from '../generated-artifacts/StakingPoolRewardVault.json'; import * as StakingProxy from '../generated-artifacts/StakingProxy.json'; +import * as TestCobbDouglas from '../generated-artifacts/TestCobbDouglas.json'; +import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json'; import * as TestStorageLayout from '../generated-artifacts/TestStorageLayout.json'; import * as ZrxVault from '../generated-artifacts/ZrxVault.json'; export const artifacts = { @@ -57,7 +59,8 @@ export const artifacts = { IWallet: IWallet as ContractArtifact, IZrxVault: IZrxVault as ContractArtifact, LibEIP712Hash: LibEIP712Hash as ContractArtifact, - LibFeeMath: LibFeeMath as ContractArtifact, + LibFixedMath: LibFixedMath as ContractArtifact, + LibFixedMathRichErrors: LibFixedMathRichErrors as ContractArtifact, LibRewardMath: LibRewardMath as ContractArtifact, LibSafeDowncast: LibSafeDowncast as ContractArtifact, LibSignatureValidator: LibSignatureValidator as ContractArtifact, @@ -74,6 +77,7 @@ export const artifacts = { MixinVaultCore: MixinVaultCore as ContractArtifact, StakingPoolRewardVault: StakingPoolRewardVault as ContractArtifact, ZrxVault: ZrxVault as ContractArtifact, - LibFeeMathTest: LibFeeMathTest as ContractArtifact, + TestCobbDouglas: TestCobbDouglas as ContractArtifact, + TestLibFixedMath: TestLibFixedMath as ContractArtifact, TestStorageLayout: TestStorageLayout as ContractArtifact, }; diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index 2e54518d3d..27eaa411b2 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -12,8 +12,8 @@ export * from '../generated-wrappers/i_vault_core'; export * from '../generated-wrappers/i_wallet'; export * from '../generated-wrappers/i_zrx_vault'; export * from '../generated-wrappers/lib_e_i_p712_hash'; -export * from '../generated-wrappers/lib_fee_math'; -export * from '../generated-wrappers/lib_fee_math_test'; +export * from '../generated-wrappers/lib_fixed_math'; +export * from '../generated-wrappers/lib_fixed_math_rich_errors'; export * from '../generated-wrappers/lib_reward_math'; export * from '../generated-wrappers/lib_safe_downcast'; export * from '../generated-wrappers/lib_signature_validator'; @@ -36,5 +36,7 @@ export * from '../generated-wrappers/mixin_zrx_vault'; export * from '../generated-wrappers/staking'; export * from '../generated-wrappers/staking_pool_reward_vault'; export * from '../generated-wrappers/staking_proxy'; +export * from '../generated-wrappers/test_cobb_douglas'; +export * from '../generated-wrappers/test_lib_fixed_math'; export * from '../generated-wrappers/test_storage_layout'; export * from '../generated-wrappers/zrx_vault'; diff --git a/contracts/staking/test/cobb_douglas.ts b/contracts/staking/test/cobb_douglas.ts new file mode 100644 index 0000000000..3d1d3c09c3 --- /dev/null +++ b/contracts/staking/test/cobb_douglas.ts @@ -0,0 +1,279 @@ +import { blockchainTests, constants, expect, filterLogsToArguments } from '@0x/contracts-test-utils'; +import { StakingRevertErrors } from '@0x/order-utils'; +import { BigNumber, OwnableRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { + artifacts, + TestCobbDouglasCobbDouglasAlphaChangedEventArgs, + TestCobbDouglasContract, + TestCobbDouglasEvents, +} from '../src/'; + +import { assertRoughlyEquals, getRandomInteger, getRandomPortion, Numberish, toDecimal } from './utils/number_utils'; + +// tslint:disable: no-unnecessary-type-assertion +blockchainTests('Cobb-Douglas', env => { + const FUZZ_COUNT = 1024; + const PRECISION = 15; + + let testContract: TestCobbDouglasContract; + let ownerAddress: string; + let notOwnerAddress: string; + + before(async () => { + [ownerAddress, notOwnerAddress] = await env.getAccountAddressesAsync(); + testContract = await TestCobbDouglasContract.deployFrom0xArtifactAsync( + artifacts.TestCobbDouglas, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + blockchainTests.resets('setCobbDouglasAlpha()', () => { + const NEGATIVE_ONE = constants.MAX_UINT256.minus(1); + + it('throws if not called by owner', async () => { + const [n, d] = [new BigNumber(1), new BigNumber(2)]; + const tx = testContract.setCobbDouglasAlpha.awaitTransactionSuccessAsync(n, d, { from: notOwnerAddress }); + const expectedError = new OwnableRevertErrors.OnlyOwnerError(notOwnerAddress, ownerAddress); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws with int256(numerator) < 0', async () => { + const [n, d] = [NEGATIVE_ONE, NEGATIVE_ONE]; + const tx = testContract.setCobbDouglasAlpha.awaitTransactionSuccessAsync(n, d); + const expectedError = new StakingRevertErrors.InvalidCobbDouglasAlphaError(n, d); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws with int256(denominator) < 0', async () => { + const [n, d] = [new BigNumber(1), NEGATIVE_ONE]; + const tx = testContract.setCobbDouglasAlpha.awaitTransactionSuccessAsync(n, d); + const expectedError = new StakingRevertErrors.InvalidCobbDouglasAlphaError(n, d); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws with denominator == 0', async () => { + const [n, d] = [new BigNumber(0), new BigNumber(0)]; + const tx = testContract.setCobbDouglasAlpha.awaitTransactionSuccessAsync(n, d); + const expectedError = new StakingRevertErrors.InvalidCobbDouglasAlphaError(n, d); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws with numerator > denominator', async () => { + const [n, d] = [new BigNumber(2), new BigNumber(1)]; + const tx = testContract.setCobbDouglasAlpha.awaitTransactionSuccessAsync(n, d); + const expectedError = new StakingRevertErrors.InvalidCobbDouglasAlphaError(n, d); + return expect(tx).to.revertWith(expectedError); + }); + + async function setCobbDouglasAlphaAndAssertEffectsAsync(n: Numberish, d: Numberish): Promise { + const [_n, _d] = [new BigNumber(n), new BigNumber(d)]; + const receipt = await testContract.setCobbDouglasAlpha.awaitTransactionSuccessAsync(_n, _d); + const logs = filterLogsToArguments( + receipt.logs, + TestCobbDouglasEvents.CobbDouglasAlphaChanged, + ); + expect(logs.length).to.eq(1); + expect(logs[0].numerator).to.bignumber.eq(_n); + expect(logs[0].denominator).to.bignumber.eq(_d); + const [actualNumerator, actualDenominator] = await testContract.getCobbDouglasAlpha.callAsync(); + expect(actualNumerator).to.bignumber.eq(_n); + expect(actualDenominator).to.bignumber.eq(_d); + } + + it('accepts numerator == denominator', async () => { + return setCobbDouglasAlphaAndAssertEffectsAsync(1, 1); + }); + + it('accepts numerator < denominator', async () => { + return setCobbDouglasAlphaAndAssertEffectsAsync(1, 2); + }); + + it('accepts numerator == 0', async () => { + return setCobbDouglasAlphaAndAssertEffectsAsync(0, 1); + }); + }); + + 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 = 11e3; + 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): Promise { + 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): 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(toDecimal(1).minus(alpha))) + .times(toDecimal(totalRewards)) + .toFixed(0, BigNumber.ROUND_FLOOR), + ); + } + + function getRandomParams(overrides?: Partial): CobbDouglasParams { + const totalRewards = _.get(overrides, 'totalRewards', getRandomInteger(0, 1e27)) as Numberish; + const totalFees = _.get(overrides, 'totalFees', getRandomInteger(1, 1e27)) as Numberish; + const ownerFees = _.get(overrides, 'ownerFees', getRandomPortion(totalFees)) as Numberish; + const totalStake = _.get(overrides, 'totalStake', getRandomInteger(1, 1e27)) as Numberish; + const ownerStake = _.get(overrides, 'ownerStake', getRandomPortion(totalStake)) as Numberish; + const alphaDenominator = _.get(overrides, 'alphaDenominator', getRandomInteger(1, 1e6)) as Numberish; + const alphaNumerator = _.get(overrides, 'alphaNumerator', getRandomPortion(alphaDenominator)) as Numberish; + return { + totalRewards, + ownerFees, + totalFees, + ownerStake, + totalStake, + alphaNumerator, + alphaDenominator, + }; + } + + it('computes the correct reward', async () => { + const expected = cobbDouglas(); + const r = await callCobbDouglasAsync(); + assertRoughlyEquals(r, expected, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + + 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, PRECISION); + }); + } + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion diff --git a/contracts/staking/test/lib_fixed_math.ts b/contracts/staking/test/lib_fixed_math.ts new file mode 100644 index 0000000000..811be944ac --- /dev/null +++ b/contracts/staking/test/lib_fixed_math.ts @@ -0,0 +1,688 @@ +import { blockchainTests, expect, hexRandom } from '@0x/contracts-test-utils'; +import { BigNumber, FixedMathRevertErrors } from '@0x/utils'; +import { Decimal } from 'decimal.js'; +import * as _ from 'lodash'; + +import { artifacts, TestLibFixedMathContract } from '../src/'; + +import { assertRoughlyEquals, fromFixed, Numberish, toDecimal, toFixed } from './utils/number_utils'; + +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('-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; + + function assertFixedEquals(actualFixed: Numberish, expected: Numberish): void { + expect(fromFixed(actualFixed)).to.bignumber.eq(fromFixed(toFixed(expected))); + } + + function assertFixedRoughlyEquals(actualFixed: Numberish, expected: Numberish, precision: number = 18): void { + assertRoughlyEquals(fromFixed(actualFixed), expected, precision); + } + + describe('one()', () => { + it('equals 1', async () => { + const r = await testContract.one.callAsync(); + assertFixedEquals(r, 1); + }); + }); + + describe('abs()', () => { + it('abs(n) == n', async () => { + const n = 1337.5912; + const r = await testContract.abs.callAsync(toFixed(n)); + assertFixedEquals(r, n); + }); + + it('abs(-n) == n', async () => { + const n = -1337.5912; + const r = await testContract.abs.callAsync(toFixed(n)); + assertFixedEquals(r, -n); + }); + + it('abs(0) == 0', async () => { + const n = 0; + const r = await testContract.abs.callAsync(toFixed(n)); + assertFixedEquals(r, n); + }); + }); + + 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.BinOpError( + 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.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero, + ); + const tx = testContract.mulDiv.callAsync(toFixed(a), new BigNumber(n), new BigNumber(d)); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('add()', () => { + function add(a: Numberish, b: Numberish): BigNumber { + return fromFixed(toFixed(a).plus(toFixed(b))); + } + + it('0 + 0 == 0', async () => { + const [a, b] = [0, 0]; + const r = await testContract.add.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, 0); + }); + + it('adds two positive decimals', async () => { + const [a, b] = ['9310841.31841', '491021921.318948193']; + const r = await testContract.add.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, add(a, b)); + }); + + it('adds two mixed decimals', async () => { + const [a, b] = ['9310841.31841', '-491021921.318948193']; + const r = await testContract.add.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, add(a, b)); + }); + + it('throws on overflow', async () => { + const [a, b] = [MAX_FIXED_VALUE, new BigNumber(1)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow, + a, + b, + ); + const tx = testContract.add.callAsync(a, b); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws on underflow', async () => { + const [a, b] = [MIN_FIXED_VALUE, new BigNumber(-1)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow, + a, + b, + ); + const tx = testContract.add.callAsync(a, b); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('sub()', () => { + function sub(a: Numberish, b: Numberish): BigNumber { + return fromFixed(toFixed(a).minus(toFixed(b))); + } + + it('0 - 0 == 0', async () => { + const [a, b] = [0, 0]; + const r = await testContract.sub.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, 0); + }); + + it('subtracts two positive decimals', async () => { + const [a, b] = ['9310841.31841', '491021921.318948193']; + const r = await testContract.sub.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, sub(a, b)); + }); + + it('subtracts two mixed decimals', async () => { + const [a, b] = ['9310841.31841', '-491021921.318948193']; + const r = await testContract.sub.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, sub(a, b)); + }); + + it('throws on underflow', async () => { + const [a, b] = [MIN_FIXED_VALUE, new BigNumber(1)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow, + a, + b.negated(), + ); + const tx = testContract.sub.callAsync(a, b); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws on overflow', async () => { + const [a, b] = [MAX_FIXED_VALUE, new BigNumber(-1)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow, + a, + b.negated(), + ); + const tx = testContract.sub.callAsync(a, b); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('mul()', () => { + function mul(a: Numberish, b: Numberish): BigNumber { + return fromFixed( + toFixed(a) + .times(toFixed(b)) + .dividedToIntegerBy(FIXED_POINT_DIVISOR), + ); + } + + it('x * 0 == 0', async () => { + const [a, b] = [1337, 0]; + const r = await testContract.mul.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, b); + }); + + it('x * 1 == x', async () => { + const [a, b] = [0.5, 1]; + const r = await testContract.mul.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, a); + }); + + it('x * -1 == -x', async () => { + const [a, b] = [0.5, -1]; + const r = await testContract.mul.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, -a); + }); + + it('multiplies two positive decimals', async () => { + const [a, b] = ['1.25394912112', '0.03413318948193']; + const r = await testContract.mul.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, mul(a, b)); + }); + + it('multiplies two mixed decimals', async () => { + const [a, b] = ['1.25394912112', '-0.03413318948193']; + const r = await testContract.mul.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, mul(a, b)); + }); + + it('throws on underflow', async () => { + const [a, b] = [MIN_FIXED_VALUE, new BigNumber(2)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow, + a, + b, + ); + const tx = testContract.mul.callAsync(a, b); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws on overflow', async () => { + const [a, b] = [MAX_FIXED_VALUE, new BigNumber(2)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow, + a, + b, + ); + const tx = testContract.mul.callAsync(a, b); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('div()', () => { + function div(a: Numberish, b: Numberish): BigNumber { + return fromFixed( + toFixed(a) + .times(FIXED_POINT_DIVISOR) + .dividedBy(toFixed(b)), + ); + } + + it('x / 0 throws', async () => { + const [a, b] = [1, 0]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero, + toFixed(a).times(FIXED_POINT_DIVISOR), + toFixed(b), + ); + const tx = testContract.div.callAsync(toFixed(a), toFixed(b)); + return expect(tx).to.revertWith(expectedError); + }); + + it('x / 1 == x', async () => { + const [a, b] = [1.41214552, 1]; + const r = await testContract.div.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, a); + }); + + it('x / -1 == -x', async () => { + const [a, b] = [1.109312, -1]; + const r = await testContract.div.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, -a); + }); + + it('divides two positive decimals', async () => { + const [a, b] = ['1.25394912112', '0.03413318948193']; + const r = await testContract.div.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, div(a, b)); + }); + + it('divides two mixed decimals', async () => { + const [a, b] = ['1.25394912112', '-0.03413318948193']; + const r = await testContract.div.callAsync(toFixed(a), toFixed(b)); + assertFixedEquals(r, div(a, b)); + }); + }); + + describe('uintMul()', () => { + it('0 * x == 0', async () => { + const [a, b] = [0, 1234]; + const r = await testContract.uintMul.callAsync(toFixed(a), new BigNumber(b)); + expect(r).to.bignumber.eq(0); + }); + + it('1 * x == int(x)', async () => { + const [a, b] = [1, 1234]; + const r = await testContract.uintMul.callAsync(toFixed(a), new BigNumber(b)); + expect(r).to.bignumber.eq(Math.trunc(b)); + }); + + it('-1 * x == 0', async () => { + const [a, b] = [-1, 1234]; + const r = await testContract.uintMul.callAsync(toFixed(a), new BigNumber(b)); + expect(r).to.bignumber.eq(0); + }); + + it('0.5 * x == x/2', async () => { + const [a, b] = [0.5, 1234]; + const r = await testContract.uintMul.callAsync(toFixed(a), new BigNumber(b)); + expect(r).to.bignumber.eq(b / 2); + }); + + it('0.5 * x == 0 if x = 1', async () => { + const [a, b] = [0.5, 1]; + const r = await testContract.uintMul.callAsync(toFixed(a), new BigNumber(b)); + expect(r).to.bignumber.eq(0); + }); + + it('throws if rhs is too large', async () => { + const [a, b] = [toFixed(1), MAX_FIXED_VALUE.plus(1)]; + const expectedError = new FixedMathRevertErrors.UnsignedValueError( + FixedMathRevertErrors.ValueErrorCodes.TooLarge, + b, + ); + const tx = testContract.uintMul.callAsync(a, b); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws if lhs is too large', async () => { + const [a, b] = [MAX_FIXED_VALUE, new BigNumber(2)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow, + a, + b, + ); + const tx = testContract.uintMul.callAsync(a, b); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('toInteger()', () => { + it('toInteger(n) == int(n)', async () => { + const n = 1337.5912; + const r = await testContract.toInteger.callAsync(toFixed(n)); + expect(r).to.bignumber.eq(Math.trunc(n)); + }); + + it('toInteger(-n) == -int(n)', async () => { + const n = -1337.5912; + const r = await testContract.toInteger.callAsync(toFixed(n)); + expect(r).to.bignumber.eq(Math.trunc(n)); + }); + + it('toInteger(n) == 0, when 0 < n < 1', async () => { + const n = 0.9995; + const r = await testContract.toInteger.callAsync(toFixed(n)); + expect(r).to.bignumber.eq(0); + }); + + it('toInteger(-n) == 0, when -1 < n < 0', async () => { + const n = -0.9995; + const r = await testContract.toInteger.callAsync(toFixed(n)); + expect(r).to.bignumber.eq(0); + }); + + it('toInteger(0) == 0', async () => { + const n = 0; + const r = await testContract.toInteger.callAsync(toFixed(n)); + expect(r).to.bignumber.eq(0); + }); + }); + + describe('toFixed()', () => { + describe('signed', () => { + it('converts a positive integer', async () => { + const n = 1337; + const r = await testContract.toFixedSigned1.callAsync(new BigNumber(n)); + assertFixedEquals(r, n); + }); + + it('converts a negative integer', async () => { + const n = -1337; + const r = await testContract.toFixedSigned1.callAsync(new BigNumber(n)); + assertFixedEquals(r, n); + }); + + it('converts a fraction with a positive numerator and denominator', async () => { + const [n, d] = [1337, 1000]; + const r = await testContract.toFixedSigned2.callAsync(new BigNumber(n), new BigNumber(d)); + assertFixedEquals(r, n / d); + }); + + it('converts a fraction with a negative numerator and positive denominator', async () => { + const [n, d] = [-1337, 1000]; + const r = await testContract.toFixedSigned2.callAsync(new BigNumber(n), new BigNumber(d)); + assertFixedEquals(r, n / d); + }); + + it('converts a fraction with a negative numerator and denominator', async () => { + const [n, d] = [-1337, -1000]; + const r = await testContract.toFixedSigned2.callAsync(new BigNumber(n), new BigNumber(d)); + assertFixedEquals(r, n / d); + }); + + it('converts a fraction with a negative numerator and negative denominator', async () => { + const [n, d] = [-1337, -1000]; + const r = await testContract.toFixedSigned2.callAsync(new BigNumber(n), new BigNumber(d)); + assertFixedEquals(r, n / d); + }); + + it('throws if the numerator is too large to convert', async () => { + const [n, d] = [MAX_FIXED_VALUE.dividedToIntegerBy(FIXED_POINT_DIVISOR).plus(1), new BigNumber(1000)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow, + n, + FIXED_POINT_DIVISOR, + ); + const tx = testContract.toFixedSigned2.callAsync(n, d); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws if the denominator is zero', async () => { + const [n, d] = [new BigNumber(1), new BigNumber(0)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero, + n.times(FIXED_POINT_DIVISOR), + d, + ); + const tx = testContract.toFixedSigned2.callAsync(n, d); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('unsigned', () => { + it('converts an integer', async () => { + const n = 1337; + const r = await testContract.toFixedUnsigned1.callAsync(new BigNumber(n)); + assertFixedEquals(r, n); + }); + + it('converts a fraction', async () => { + const [n, d] = [1337, 1000]; + const r = await testContract.toFixedUnsigned2.callAsync(new BigNumber(n), new BigNumber(d)); + assertFixedEquals(r, n / d); + }); + + it('throws if the numerator is too large', async () => { + const [n, d] = [MAX_FIXED_VALUE.plus(1), new BigNumber(1000)]; + const expectedError = new FixedMathRevertErrors.UnsignedValueError( + FixedMathRevertErrors.ValueErrorCodes.TooLarge, + n, + ); + const tx = testContract.toFixedUnsigned2.callAsync(n, d); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws if the denominator is too large', async () => { + const [n, d] = [new BigNumber(1000), MAX_FIXED_VALUE.plus(1)]; + const expectedError = new FixedMathRevertErrors.UnsignedValueError( + FixedMathRevertErrors.ValueErrorCodes.TooLarge, + d, + ); + const tx = testContract.toFixedUnsigned2.callAsync(n, d); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws if the numerator is too large to convert', async () => { + const [n, d] = [MAX_FIXED_VALUE.dividedToIntegerBy(FIXED_POINT_DIVISOR).plus(1), new BigNumber(1000)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow, + n, + FIXED_POINT_DIVISOR, + ); + const tx = testContract.toFixedUnsigned2.callAsync(n, d); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws if the denominator is zero', async () => { + const [n, d] = [new BigNumber(1), new BigNumber(0)]; + const expectedError = new FixedMathRevertErrors.BinOpError( + FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero, + n.times(FIXED_POINT_DIVISOR), + d, + ); + const tx = testContract.toFixedUnsigned2.callAsync(n, d); + return expect(tx).to.revertWith(expectedError); + }); + }); + }); + + function getRandomDecimal(min: Numberish, max: Numberish): BigNumber { + const range = new BigNumber(max).minus(min); + const random = fromFixed(new BigNumber(hexRandom().substr(2), 16)); + return random.mod(range).plus(min); + } + + describe('ln()', () => { + const LN_PRECISION = 16; + + function ln(x: Numberish): BigNumber { + return new BigNumber( + toDecimal(x) + .ln() + .toFixed(128), + ); + } + + it('ln(x = 0) throws', async () => { + const x = toFixed(0); + const expectedError = new FixedMathRevertErrors.SignedValueError( + FixedMathRevertErrors.ValueErrorCodes.TooSmall, + x, + ); + const tx = testContract.ln.callAsync(x); + return expect(tx).to.revertWith(expectedError); + }); + + it('ln(x > 1) throws', async () => { + const x = toFixed(1.000001); + const expectedError = new FixedMathRevertErrors.SignedValueError( + FixedMathRevertErrors.ValueErrorCodes.TooLarge, + x, + ); + const tx = testContract.ln.callAsync(x); + return expect(tx).to.revertWith(expectedError); + }); + + it('ln(x < 0) throws', async () => { + const x = toFixed(-0.000001); + const expectedError = new FixedMathRevertErrors.SignedValueError( + FixedMathRevertErrors.ValueErrorCodes.TooSmall, + x, + ); + const tx = testContract.ln.callAsync(x); + return expect(tx).to.revertWith(expectedError); + }); + + it('ln(x = 1) == 0', async () => { + const x = toFixed(1); + const r = await testContract.ln.callAsync(x); + 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), 12); + }); + + 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), LN_PRECISION); + }); + + it('ln(x = 0.85)', async () => { + const x = 0.85; + const r = await testContract.ln.callAsync(toFixed(x)); + assertFixedRoughlyEquals(r, ln(x), LN_PRECISION); + }); + + blockchainTests.optional('fuzzing', () => { + const inputs = _.times(FUZZ_COUNT, () => getRandomDecimal(0, 1)); + for (const x of inputs) { + it(`ln(${x.toString(10)})`, async () => { + const r = await testContract.ln.callAsync(toFixed(x)); + assertFixedRoughlyEquals(r, ln(x), LN_PRECISION); + }); + } + }); + }); + + describe('exp()', () => { + const EXP_PRECISION = 18; + + function exp(x: Numberish): BigNumber { + return new BigNumber( + toDecimal(x) + .exp() + .toFixed(128), + ); + } + + it('exp(x = 0) == 1', async () => { + const x = toFixed(0); + const r = await testContract.exp.callAsync(x); + assertFixedEquals(r, 1); + }); + + it('exp(x > EXP_MAX_VAL) throws', async () => { + const x = toFixed(MAX_EXP_NUMBER).plus(1); + const expectedError = new FixedMathRevertErrors.SignedValueError( + FixedMathRevertErrors.ValueErrorCodes.TooLarge, + x, + ); + const tx = testContract.exp.callAsync(x); + return expect(tx).to.revertWith(expectedError); + }); + + it('exp(x < EXP_MIN_VAL) == 0', async () => { + const x = toFixed(MIN_EXP_NUMBER).minus(1); + const r = await testContract.exp.callAsync(x); + assertFixedEquals(r, 0); + }); + + 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), EXP_PRECISION); + }); + + it('exp(x), where x is close to EXP_MIN_VAL', async () => { + const x = MIN_EXP_NUMBER.plus('1e-18'); + const r = await testContract.exp.callAsync(toFixed(x)); + assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION); + }); + + it('exp(x = -0.85)', async () => { + const x = -0.85; + const r = await testContract.exp.callAsync(toFixed(x)); + assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION); + }); + + blockchainTests.optional('fuzzing', () => { + const inputs = _.times(FUZZ_COUNT, () => getRandomDecimal(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), EXP_PRECISION); + }); + } + }); + }); +}); +// tslint:disable-next-line: max-file-line-count diff --git a/contracts/staking/test/math_test.ts b/contracts/staking/test/math_test.ts deleted file mode 100644 index c0bd1c73b4..0000000000 --- a/contracts/staking/test/math_test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy'; -import { DummyERC20TokenContract } from '@0x/contracts-erc20'; -import { chaiSetup, provider, web3Wrapper } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import * as _ from 'lodash'; - -import { StakingWrapper } from './utils/staking_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -// tslint:disable:no-unnecessary-type-assertion -describe('Math Libraries', () => { - // constants - const ZRX_TOKEN_DECIMALS = new BigNumber(18); - // tokens & addresses - let accounts: string[]; - let owner: string; - let zrxTokenContract: DummyERC20TokenContract; - let erc20ProxyContract: ERC20ProxyContract; - // wrappers - let stakingWrapper: StakingWrapper; - let erc20Wrapper: ERC20Wrapper; - // tests - before(async () => { - await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - before(async () => { - // create accounts - accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = accounts[0]; - // deploy erc20 proxy - erc20Wrapper = new ERC20Wrapper(provider, accounts, owner); - erc20ProxyContract = await erc20Wrapper.deployProxyAsync(); - // deploy zrx token - [zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS); - await erc20Wrapper.setBalancesAndAllowancesAsync(); - // deploy staking contracts - stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts); - await stakingWrapper.deployAndConfigureContractsAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('LibFeesMath', () => { - it('nth root', async () => { - const base = new BigNumber(1419857); - const n = new BigNumber(5); - const root = await stakingWrapper.nthRootAsync(base, n); - expect(root).to.be.bignumber.equal(17); - }); - - it('nth root #2', async () => { - const base = new BigNumber(3375); - const n = new BigNumber(3); - const root = await stakingWrapper.nthRootAsync(base, n); - expect(root).to.be.bignumber.equal(15); - }); - - it('nth root #3 with fixed point', async () => { - const decimals = 18; - const base = StakingWrapper.toFixedPoint(4.234, decimals); - const n = new BigNumber(2); - const root = await stakingWrapper.nthRootFixedPointAsync(base, n); - const rootAsFloatingPoint = StakingWrapper.toFloatingPoint(root, decimals); - const expectedResult = new BigNumber(2.057668584); - expect(rootAsFloatingPoint).to.be.bignumber.equal(expectedResult); - }); - - it('nth root #3 with fixed point (integer nth root would fail here)', async () => { - const decimals = 18; - const base = StakingWrapper.toFixedPoint(5429503678976, decimals); - const n = new BigNumber(9); - const root = await stakingWrapper.nthRootFixedPointAsync(base, n); - const rootAsFloatingPoint = StakingWrapper.toFloatingPoint(root, decimals); - const expectedResult = new BigNumber(26); - expect(rootAsFloatingPoint).to.be.bignumber.equal(expectedResult); - }); - - it.skip('nth root #4 with fixed point (integer nth root would fail here) (max number of decimals - currently does not retain)', async () => { - // @TODO This is the gold standard for nth root. Retain all these decimals :) - const decimals = 18; - const base = StakingWrapper.toFixedPoint(new BigNumber('5429503678976.295036789761543678', 10), decimals); - const n = new BigNumber(9); - const root = await stakingWrapper.nthRootFixedPointAsync(base, n); - const rootAsFloatingPoint = StakingWrapper.toFloatingPoint(root, decimals); - const expectedResult = new BigNumber(26); - expect(rootAsFloatingPoint).to.be.bignumber.equal(expectedResult); - }); - - it('cobb douglas - approximate', async () => { - const totalRewards = StakingWrapper.toBaseUnitAmount(57.154398); - const ownerFees = StakingWrapper.toBaseUnitAmount(5.64375); - const totalFees = StakingWrapper.toBaseUnitAmount(29.00679); - const ownerStake = StakingWrapper.toBaseUnitAmount(56); - const totalStake = StakingWrapper.toBaseUnitAmount(10906); - const alphaNumerator = new BigNumber(3); - const alphaDenominator = new BigNumber(7); - // create expected output - // https://www.wolframalpha.com/input/?i=57.154398+*+(5.64375%2F29.00679)+%5E+(3%2F7)+*+(56+%2F+10906)+%5E+(1+-+3%2F7) - const expectedOwnerReward = new BigNumber(1.3934); - // run computation - const ownerReward = await stakingWrapper.cobbDouglasAsync( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaNumerator, - alphaDenominator, - ); - const ownerRewardFloatingPoint = StakingWrapper.trimFloat( - StakingWrapper.toFloatingPoint(ownerReward, 18), - 4, - ); - // validation - expect(ownerRewardFloatingPoint).to.be.bignumber.equal(expectedOwnerReward); - }); - - it('cobb douglas - simplified (alpha = 1/x)', async () => { - // setup test parameters - const totalRewards = StakingWrapper.toBaseUnitAmount(57.154398); - const ownerFees = StakingWrapper.toBaseUnitAmount(5.64375); - const totalFees = StakingWrapper.toBaseUnitAmount(29.00679); - const ownerStake = StakingWrapper.toBaseUnitAmount(56); - const totalStake = StakingWrapper.toBaseUnitAmount(10906); - const alphaDenominator = new BigNumber(3); - // create expected output - // https://www.wolframalpha.com/input/?i=57.154398+*+(5.64375%2F29.00679)+%5E+(1%2F3)+*+(56+%2F+10906)+%5E+(1+-+1%2F3) - const expectedOwnerReward = new BigNumber(0.98572107681878); - // run computation - const ownerReward = await stakingWrapper.cobbDouglasSimplifiedAsync( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaDenominator, - ); - const ownerRewardFloatingPoint = StakingWrapper.trimFloat( - StakingWrapper.toFloatingPoint(ownerReward, 18), - 14, - ); - // validation - expect(ownerRewardFloatingPoint).to.be.bignumber.equal(expectedOwnerReward); - }); - - it('cobb douglas - simplified inverse (1 - alpha = 1/x)', async () => { - const totalRewards = StakingWrapper.toBaseUnitAmount(57.154398); - const ownerFees = StakingWrapper.toBaseUnitAmount(5.64375); - const totalFees = StakingWrapper.toBaseUnitAmount(29.00679); - const ownerStake = StakingWrapper.toBaseUnitAmount(56); - const totalStake = StakingWrapper.toBaseUnitAmount(10906); - const inverseAlphaDenominator = new BigNumber(3); - // create expected output - // https://www.wolframalpha.com/input/?i=57.154398+*+(5.64375%2F29.00679)+%5E+(2%2F3)+*+(56+%2F+10906)+%5E+(1+-+2%2F3) - const expectedOwnerReward = new BigNumber(3.310822494188); - // run computation - const ownerReward = await stakingWrapper.cobbDouglasSimplifiedInverseAsync( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - inverseAlphaDenominator, - ); - const ownerRewardFloatingPoint = StakingWrapper.trimFloat( - StakingWrapper.toFloatingPoint(ownerReward, 18), - 12, - ); - // validation - expect(ownerRewardFloatingPoint).to.be.bignumber.equal(expectedOwnerReward); - }); - }); -}); -// tslint:enable:no-unnecessary-type-assertion diff --git a/contracts/staking/test/pools_test.ts b/contracts/staking/test/pools_test.ts index 0489925a37..e77ba44b72 100644 --- a/contracts/staking/test/pools_test.ts +++ b/contracts/staking/test/pools_test.ts @@ -1,8 +1,7 @@ import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy'; import { DummyERC20TokenContract } from '@0x/contracts-erc20'; -import { blockchainTests, expect } from '@0x/contracts-test-utils'; +import { blockchainTests, constants, expect } from '@0x/contracts-test-utils'; import { StakingRevertErrors } from '@0x/order-utils'; -import { BigNumber } from '@0x/utils'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; @@ -14,7 +13,7 @@ import { StakingWrapper } from './utils/staking_wrapper'; // tslint:disable:no-unnecessary-type-assertion blockchainTests('Staking Pool Management', env => { // constants - const ZRX_TOKEN_DECIMALS = new BigNumber(18); + const { DUMMY_TOKEN_DECIMALS, PPM_100_PERCENT, PPM_DENOMINATOR } = constants; // tokens & addresses let accounts: string[]; let owner: string; @@ -34,7 +33,7 @@ blockchainTests('Staking Pool Management', env => { erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner); erc20ProxyContract = await erc20Wrapper.deployProxyAsync(); // deploy zrx token - [zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS); + [zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, DUMMY_TOKEN_DECIMALS); await erc20Wrapper.setBalancesAndAllowancesAsync(); // deploy staking contracts stakingWrapper = new StakingWrapper(env.provider, owner, erc20ProxyContract, zrxTokenContract, accounts); @@ -44,7 +43,7 @@ blockchainTests('Staking Pool Management', env => { it('Should successfully create a pool', async () => { // test parameters const operatorAddress = users[0]; - const operatorShare = 39; + const operatorShare = (39 / 100) * PPM_DENOMINATOR; const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper); // create pool const poolId = await poolOperator.createStakingPoolAsync(operatorShare); @@ -54,6 +53,18 @@ blockchainTests('Staking Pool Management', env => { const nextPoolId = await stakingWrapper.getNextStakingPoolIdAsync(); expect(nextPoolId).to.be.equal(expectedNextPoolId); }); + it('Should throw if poolOperatorShare is > PPM_DENOMINATOR', async () => { + // test parameters + const operatorAddress = users[0]; + // tslint:disable-next-line + const operatorShare = PPM_100_PERCENT + 1; + const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper); + // create pool + const tx = poolOperator.createStakingPoolAsync(operatorShare); + const expectedPoolId = stakingConstants.INITIAL_POOL_ID; + const expectedError = new StakingRevertErrors.InvalidPoolOperatorShareError(expectedPoolId, operatorShare); + return expect(tx).to.revertWith(expectedError); + }); it('Should successfully add/remove a maker to a pool', async () => { // test parameters const operatorAddress = users[0]; diff --git a/contracts/staking/test/simulations_test.ts b/contracts/staking/test/simulations_test.ts index d4fd0b129a..1e328c2e6c 100644 --- a/contracts/staking/test/simulations_test.ts +++ b/contracts/staking/test/simulations_test.ts @@ -11,6 +11,7 @@ import { StakingWrapper } from './utils/staking_wrapper'; blockchainTests('End-To-End Simulations', env => { // constants const ZRX_TOKEN_DECIMALS = new BigNumber(18); + const PPM_ONE = 1e6; // tokens & addresses let accounts: string[]; let owner: string; @@ -28,7 +29,7 @@ blockchainTests('End-To-End Simulations', env => { owner = accounts[0]; exchange = accounts[1]; users = accounts.slice(2); - users = [...users, ...users]; // @TODO figure out how to get more addresses from `web3Wrapper` + users = [...users]; // deploy erc20 proxy erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner); @@ -46,7 +47,7 @@ blockchainTests('End-To-End Simulations', env => { const simulationParams = { users, numberOfPools: 3, - poolOperatorShares: [100, 100, 100], + poolOperatorShares: [100, 100, 100].map(v => (v / 100) * PPM_ONE), stakeByPoolOperator: [ StakingWrapper.toBaseUnitAmount(42), StakingWrapper.toBaseUnitAmount(84), @@ -76,14 +77,14 @@ blockchainTests('End-To-End Simulations', env => { StakingWrapper.toBaseUnitAmount(28.12222236), ], expectedPayoutByPool: [ - new BigNumber('4.75677'), // 4.756772362932728793619590327361600155564384201215274334070 - new BigNumber('16.28130'), // 16.28130500394935316563988584956596823402223838026190634525 - new BigNumber('20.31028'), // 20.31028447343014834523983759032242063760612769662934308289 + new BigNumber('4.7567723629327287936195903273616'), + new BigNumber('16.281305003949353165639885849565'), + new BigNumber('20.310284473430148345239837590322'), ], expectedPayoutByPoolOperator: [ - new BigNumber('4.75677'), // 4.756772362932728793619590327361600155564384201215274334070 - new BigNumber('16.28130'), // 16.28130500394935316563988584956596823402223838026190634525 - new BigNumber('20.31028'), // 20.31028447343014834523983759032242063760612769662934308289 + new BigNumber('4.7567723629327287936195903273616'), + new BigNumber('16.281305003949353165639885849565'), + new BigNumber('20.310284473430148345239837590322'), ], expectedMembersPayoutByPool: [new BigNumber('0'), new BigNumber('0'), new BigNumber('0')], expectedPayoutByDelegator: [], @@ -93,7 +94,7 @@ blockchainTests('End-To-End Simulations', env => { await simulator.runAsync(); }); - it('Should successfully simulate (delegators withdraw by undeleating / no shadow balances)', async () => { + it('Should successfully simulate (delegators withdraw by undelegating / no shadow balances)', async () => { // @TODO - get computations more accurate /* \ // the expected payouts were computed by hand @@ -110,7 +111,7 @@ blockchainTests('End-To-End Simulations', env => { const simulationParams = { users, numberOfPools: 3, - poolOperatorShares: [39, 59, 43], + poolOperatorShares: [39, 59, 43].map(v => (v / 100) * PPM_ONE), stakeByPoolOperator: [ StakingWrapper.toBaseUnitAmount(42), StakingWrapper.toBaseUnitAmount(84), @@ -144,27 +145,27 @@ blockchainTests('End-To-End Simulations', env => { StakingWrapper.toBaseUnitAmount(28.12222236), ], expectedPayoutByPool: [ - new BigNumber('3.00603'), // 3.006037310109530277237724562632303034914024715508955780682 - new BigNumber('10.28895'), // 10.28895363598396754741643198605226143579652264694121578135 - new BigNumber('29.26472'), // 29.26473180250053106672049765968527817034954761113582833460 + new BigNumber('3.0060373101095302067028699237670'), + new BigNumber('10.288953635983966866289393130525'), + new BigNumber('29.264731802500529663161540874979'), ], expectedPayoutByPoolOperator: [ - new BigNumber('1.17235'), // 0.39 * 3.00603 - new BigNumber('6.07048'), // 0.59 * 10.28895 - new BigNumber('12.58383'), // 0.43 * 29.26472 + new BigNumber('1.1723545509427168206625812850596'), + new BigNumber('6.0704826452305401312658116198463'), + new BigNumber('12.583834675075227560217188236544'), ], expectedMembersPayoutByPool: [ - new BigNumber('1.83368'), // (1 - 0.39) * 3.00603 - new BigNumber('4.21847'), // (1 - 0.59) * 10.28895 - new BigNumber('16.68089'), // (1 - 0.43) * 29.26472 + new BigNumber('1.8336827591668133860402886387074'), + new BigNumber('4.2184709907534267350235815106787'), + new BigNumber('16.680897127425302102944352638435'), ], expectedPayoutByDelegator: [ // note that the on-chain values may be slightly different due to rounding down on each entry // there is a carry over between calls, which we account for here. the result is that delegators // who withdraw later on will scoop up any rounding spillover from those who have already withdrawn. - new BigNumber('1.55810'), // (17 / 182) * 16.6809 - new BigNumber('6.87399'), // (75 / 182) * 16.6809 - new BigNumber('8.24879'), // (90 / 182) * 16.6809 + new BigNumber('1.0163987496997496894870114443624'), + new BigNumber('4.4841121310283074536191681368932'), + new BigNumber('5.3809345572339689443430017642717'), ], exchangeAddress: exchange, }; @@ -192,7 +193,7 @@ blockchainTests('End-To-End Simulations', env => { const simulationParams = { users, numberOfPools: 3, - poolOperatorShares: [39, 59, 43], + poolOperatorShares: [39, 59, 43].map(v => (v / 100) * PPM_ONE), stakeByPoolOperator: [ StakingWrapper.toBaseUnitAmount(42), StakingWrapper.toBaseUnitAmount(84), @@ -226,25 +227,21 @@ blockchainTests('End-To-End Simulations', env => { StakingWrapper.toBaseUnitAmount(28.12222236), ], expectedPayoutByPool: [ - new BigNumber('4.75677'), // 4.756772362932728793619590327361600155564384201215274334070 - new BigNumber('16.28130'), // 16.28130500394935316563988584956596823402223838026190634525 - new BigNumber('20.31028'), // 20.31028447343014834523983759032242063760612769662934308289 + new BigNumber('4.7567723629327287476569912989141'), + new BigNumber('16.281305003949352312532097047985'), + new BigNumber('20.310284473430147203349271380151'), ], expectedPayoutByPoolOperator: [ - new BigNumber('1.85514'), // 0.39 * 4.75677 - new BigNumber('9.60597'), // 0.59 * 16.28130 - new BigNumber('8.73342'), // 0.43 * 20.31028 + new BigNumber('1.8551412215437642749591650093188'), + new BigNumber('9.6059699523301173582693060410895'), + new BigNumber('8.7334223235749631621465139389311'), ], expectedMembersPayoutByPool: [ - new BigNumber('2.90163'), // (1 - 0.39) * 4.75677 - new BigNumber('6.67533'), // (1 - 0.59) * 16.28130 - new BigNumber('11.57686'), // (1 - 0.43) * 20.31028 - ], - expectedPayoutByDelegator: [ - new BigNumber('11.57686'), // (1 - 0.43) * 20.31028 - new BigNumber(0), - new BigNumber(0), + new BigNumber('2.9016311413889644726978262895953'), + new BigNumber('6.6753350516192349542627910068955'), + new BigNumber('11.576862149855184041202757441220'), ], + expectedPayoutByDelegator: [new BigNumber(0), new BigNumber(0), new BigNumber(0)], exchangeAddress: exchange, }; const simulator = new Simulation(stakingWrapper, simulationParams); @@ -271,7 +268,7 @@ blockchainTests('End-To-End Simulations', env => { const simulationParams = { users, numberOfPools: 3, - poolOperatorShares: [39, 59, 43], + poolOperatorShares: [39, 59, 43].map(v => (v / 100) * PPM_ONE), stakeByPoolOperator: [ StakingWrapper.toBaseUnitAmount(42), StakingWrapper.toBaseUnitAmount(84), @@ -305,25 +302,21 @@ blockchainTests('End-To-End Simulations', env => { StakingWrapper.toBaseUnitAmount(28.12222236), ], expectedPayoutByPool: [ - new BigNumber('4.75677'), // 4.756772362932728793619590327361600155564384201215274334070 - new BigNumber('16.28130'), // 16.28130500394935316563988584956596823402223838026190634525 - new BigNumber('20.31028'), // 20.31028447343014834523983759032242063760612769662934308289 + new BigNumber('4.7567723629327287476569912989141'), + new BigNumber('16.281305003949352312532097047985'), + new BigNumber('20.310284473430147203349271380151'), ], expectedPayoutByPoolOperator: [ - new BigNumber('1.85514'), // 0.39 * 4.75677 - new BigNumber('9.60597'), // 0.59 * 16.28130 - new BigNumber('8.73342'), // 0.43 * 20.31028 + new BigNumber('1.8551412215437642749591650093188'), + new BigNumber('9.6059699523301173582693060410895'), + new BigNumber('8.7334223235749631621465139389311'), ], expectedMembersPayoutByPool: [ - new BigNumber('2.90163'), // (1 - 0.39) * 4.75677 - new BigNumber('6.67533'), // (1 - 0.59) * 16.28130 - new BigNumber('11.57686'), // (1 - 0.43) * 20.31028 - ], - expectedPayoutByDelegator: [ - new BigNumber('11.57686'), // (1 - 0.43) * 20.31028 - new BigNumber(0), - new BigNumber(0), + new BigNumber('2.9016311413889644726978262895953'), + new BigNumber('6.6753350516192349542627910068955'), + new BigNumber('11.576862149855184041202757441220'), ], + expectedPayoutByDelegator: [new BigNumber(0), new BigNumber(0), new BigNumber(0)], exchangeAddress: exchange, }; const simulator = new Simulation(stakingWrapper, simulationParams); diff --git a/contracts/staking/test/utils/Simulation.ts b/contracts/staking/test/utils/Simulation.ts index 1db33021ef..df9161c20e 100644 --- a/contracts/staking/test/utils/Simulation.ts +++ b/contracts/staking/test/utils/Simulation.ts @@ -1,6 +1,5 @@ -import { chaiSetup } from '@0x/contracts-test-utils'; +import { expect } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; import * as _ from 'lodash'; import { DelegatorActor } from '../actors/delegator_actor'; @@ -11,8 +10,7 @@ import { Queue } from './queue'; import { StakingWrapper } from './staking_wrapper'; import { SimulationParams } from './types'; -chaiSetup.configure(); -const expect = chai.expect; +const REWARD_PRECISION = 12; export class Simulation { private readonly _stakingWrapper: StakingWrapper; @@ -24,6 +22,13 @@ export class Simulation { private readonly _makers: MakerActor[]; private readonly _delegators: DelegatorActor[]; + private static _assertRewardsEqual(actual: BigNumber, expected: BigNumber, message?: string): void { + expect( + StakingWrapper.trimFloat(StakingWrapper.toFloatingPoint(actual, 18), REWARD_PRECISION), + message, + ).to.be.bignumber.equal(StakingWrapper.trimFloat(expected, REWARD_PRECISION)); + } + constructor(stakingWrapper: StakingWrapper, simulationParams: SimulationParams) { this._stakingWrapper = stakingWrapper; this._p = simulationParams; @@ -75,12 +80,12 @@ export class Simulation { await delegator.deactivateAndTimeLockDelegatedStakeAsync(poolId, amountOfStakeDelegated); const finalEthBalance = await this._stakingWrapper.getEthBalanceAsync(delegatorAddress); const reward = finalEthBalance.minus(initEthBalance); - const rewardTrimmed = StakingWrapper.trimFloat(StakingWrapper.toFloatingPoint(reward, 18), 5); const expectedReward = p.expectedPayoutByDelegator[delegatorIdx]; - expect( - rewardTrimmed, + Simulation._assertRewardsEqual( + reward, + expectedReward, `reward withdrawn from pool ${poolId} for delegator ${delegatorAddress}`, - ).to.be.bignumber.equal(expectedReward); + ); delegatorIdx += 1; } poolIdx += 1; @@ -100,12 +105,12 @@ export class Simulation { await this._stakingWrapper.withdrawTotalRewardForStakingPoolMemberAsync(poolId, delegatorAddress); const finalEthBalance = await this._stakingWrapper.getEthBalanceAsync(delegatorAddress); const reward = finalEthBalance.minus(initEthBalance); - const rewardTrimmed = StakingWrapper.trimFloat(StakingWrapper.toFloatingPoint(reward, 18), 5); const expectedReward = p.expectedPayoutByDelegator[delegatorIdx]; - expect( - rewardTrimmed, + Simulation._assertRewardsEqual( + reward, + expectedReward, `reward withdrawn from pool ${poolId} for delegator ${delegatorAddress}`, - ).to.be.bignumber.equal(expectedReward); + ); delegatorIdx += 1; } poolIdx += 1; @@ -127,7 +132,7 @@ export class Simulation { this._poolOperatorsAsDelegators.push(poolOperatorAsDelegator); // add stake to the operator's pool const amountOfStake = p.stakeByPoolOperator[i]; - await poolOperatorAsDelegator.depositZrxAndMintActivatedStakeAsync(amountOfStake); + await poolOperatorAsDelegator.depositZrxAndDelegateToStakingPoolAsync(poolId, amountOfStake); } } @@ -217,44 +222,35 @@ export class Simulation { private async _assertVaultBalancesAsync(p: SimulationParams): Promise { // tslint:disable-next-line no-unused-variable for (const i of _.range(p.numberOfPools)) { - // @TODO - we trim balances in here because payouts are accurate only to 5 decimal places. + // @TODO - we trim balances in here because payouts are accurate only to REWARD_PRECISION decimal places. // update once more accurate. // check pool balance in vault const poolId = this._poolIds[i]; const rewardVaultBalance = await this._stakingWrapper.rewardVaultBalanceOfAsync(poolId); - const rewardVaultBalanceTrimmed = StakingWrapper.trimFloat( - StakingWrapper.toFloatingPoint(rewardVaultBalance, 18), - 5, - ); const expectedRewardBalance = p.expectedPayoutByPool[i]; - expect( - rewardVaultBalanceTrimmed, + Simulation._assertRewardsEqual( + rewardVaultBalance, + expectedRewardBalance, `expected balance in vault for pool with id ${poolId}`, - ).to.be.bignumber.equal(expectedRewardBalance); + ); // check operator's balance const poolOperatorVaultBalance = await this._stakingWrapper.getRewardBalanceOfStakingPoolOperatorAsync( poolId, ); - const poolOperatorVaultBalanceTrimmed = StakingWrapper.trimFloat( - StakingWrapper.toFloatingPoint(poolOperatorVaultBalance, 18), - 5, - ); const expectedPoolOperatorVaultBalance = p.expectedPayoutByPoolOperator[i]; - expect( - poolOperatorVaultBalanceTrimmed, + Simulation._assertRewardsEqual( + poolOperatorVaultBalance, + expectedPoolOperatorVaultBalance, `operator balance in vault for pool with id ${poolId}`, - ).to.be.bignumber.equal(expectedPoolOperatorVaultBalance); + ); // check balance of pool members const membersVaultBalance = await this._stakingWrapper.getRewardBalanceOfStakingPoolMembersAsync(poolId); - const membersVaultBalanceTrimmed = StakingWrapper.trimFloat( - StakingWrapper.toFloatingPoint(membersVaultBalance, 18), - 5, - ); const expectedMembersVaultBalance = p.expectedMembersPayoutByPool[i]; - expect( - membersVaultBalanceTrimmed, + Simulation._assertRewardsEqual( + membersVaultBalance, + expectedMembersVaultBalance, `members balance in vault for pool with id ${poolId}`, - ).to.be.bignumber.equal(expectedMembersVaultBalance); + ); // @TODO compute balance of each member } } @@ -262,7 +258,7 @@ export class Simulation { private async _withdrawRewardForStakingPoolMemberForOperatorsAsync(p: SimulationParams): Promise { // tslint:disable-next-line no-unused-variable for (const i of _.range(p.numberOfPools)) { - // @TODO - we trim balances in here because payouts are accurate only to 5 decimal places. + // @TODO - we trim balances in here because payouts are accurate only to REWARD_PRECISION decimal places. // update once more accurate. // check pool balance in vault const poolId = this._poolIds[i]; @@ -272,11 +268,8 @@ export class Simulation { await this._stakingWrapper.withdrawTotalRewardForStakingPoolOperatorAsync(poolId, poolOperatorAddress); const finalEthBalance = await this._stakingWrapper.getEthBalanceAsync(poolOperatorAddress); const reward = finalEthBalance.minus(initEthBalance); - const rewardTrimmed = StakingWrapper.trimFloat(StakingWrapper.toFloatingPoint(reward, 18), 5); const expectedReward = p.expectedPayoutByPoolOperator[i]; - expect(rewardTrimmed, `reward withdrawn from pool ${poolId} for operator`).to.be.bignumber.equal( - expectedReward, - ); + Simulation._assertRewardsEqual(reward, expectedReward, `reward withdrawn from pool ${poolId} for operator`); } } } diff --git a/contracts/staking/test/utils/number_utils.ts b/contracts/staking/test/utils/number_utils.ts new file mode 100644 index 0000000000..c62dad3c9f --- /dev/null +++ b/contracts/staking/test/utils/number_utils.ts @@ -0,0 +1,92 @@ +import { expect } from '@0x/contracts-test-utils'; +import { BigNumber } from '@0x/utils'; +import * as crypto from 'crypto'; +import { Decimal } from 'decimal.js'; + +Decimal.set({ precision: 80 }); + +export type Numberish = BigNumber | string | number; + +/** + * Convert `x` to a `Decimal` type. + */ +export function toDecimal(x: Numberish): Decimal { + if (BigNumber.isBigNumber(x)) { + return new Decimal(x.toString(10)); + } + return new Decimal(x); +} + +/** + * Generate a random integer between `min` and `max`, inclusive. + */ +export function getRandomInteger(min: Numberish, max: Numberish): BigNumber { + const range = new BigNumber(max).minus(min); + return getRandomPortion(range).plus(min); +} + +/** + * Generate a random integer between `0` and `total`, inclusive. + */ +export function getRandomPortion(total: Numberish): BigNumber { + return new BigNumber(total).times(getRandomFloat(0, 1)).integerValue(BigNumber.ROUND_HALF_UP); +} + +/** + * Generate a random, high-precision decimal between `min` and `max`, inclusive. + */ +export function getRandomFloat(min: Numberish, max: Numberish): BigNumber { + // Generate a really high precision number between [0, 1] + const r = new BigNumber(crypto.randomBytes(32).toString('hex'), 16).dividedBy(new BigNumber(2).pow(256).minus(1)); + return new BigNumber(max) + .minus(min) + .times(r) + .plus(min); +} + +export const FIXED_POINT_BASE = new BigNumber(2).pow(127); + +/** + * Convert `n` to fixed-point integer represenatation. + */ +export function toFixed(n: Numberish): BigNumber { + return new BigNumber(n).times(FIXED_POINT_BASE).integerValue(); +} + +/** + * Convert `n` from fixed-point integer represenatation. + */ +export function fromFixed(n: Numberish): BigNumber { + return new BigNumber(n).dividedBy(FIXED_POINT_BASE); +} + +/** + * Converts two decimal numbers to integers with `precision` digits, then returns + * the absolute difference. + */ +export function getNumericalDivergence(a: Numberish, b: Numberish, precision: number = 18): number { + const _a = new BigNumber(a); + const _b = new BigNumber(b); + const maxIntegerDigits = Math.max( + _a.integerValue(BigNumber.ROUND_DOWN).sd(true), + _b.integerValue(BigNumber.ROUND_DOWN).sd(true), + ); + const _toInteger = (n: BigNumber) => { + const base = 10 ** (precision - maxIntegerDigits); + return n.times(base).integerValue(BigNumber.ROUND_DOWN); + }; + return _toInteger(_a) + .minus(_toInteger(_b)) + .abs() + .toNumber(); +} + +/** + * Asserts that two numbers are equal up to `precision` digits. + */ +export function assertRoughlyEquals(actual: Numberish, expected: Numberish, precision: number = 18): void { + if (getNumericalDivergence(actual, expected, precision) <= 1) { + return; + } + expect(actual).to.bignumber.eq(expected); +} diff --git a/contracts/staking/test/utils/staking_wrapper.ts b/contracts/staking/test/utils/staking_wrapper.ts index 848f3a7d74..4affb01132 100644 --- a/contracts/staking/test/utils/staking_wrapper.ts +++ b/contracts/staking/test/utils/staking_wrapper.ts @@ -9,7 +9,6 @@ import * as _ from 'lodash'; import { artifacts, - LibFeeMathTestContract, StakingContract, StakingPoolRewardVaultContract, StakingProxyContract, @@ -32,7 +31,6 @@ export class StakingWrapper { private _stakingProxyContractIfExists?: StakingProxyContract; private _zrxVaultContractIfExists?: ZrxVaultContract; private _rewardVaultContractIfExists?: StakingPoolRewardVaultContract; - private _LibFeeMathTestContractIfExists?: LibFeeMathTestContract; public static toBaseUnitAmount(amount: BigNumber | number): BigNumber { const decimals = 18; const amountAsBigNumber = typeof amount === 'number' ? new BigNumber(amount) : amount; @@ -93,10 +91,6 @@ export class StakingWrapper { this._validateDeployedOrThrow(); return this._rewardVaultContractIfExists as StakingPoolRewardVaultContract; } - public getLibFeeMathTestContract(): LibFeeMathTestContract { - this._validateDeployedOrThrow(); - return this._LibFeeMathTestContractIfExists as LibFeeMathTestContract; - } public async deployAndConfigureContractsAsync(): Promise { // deploy zrx vault this._zrxVaultContractIfExists = await ZrxVaultContract.deployFrom0xArtifactAsync( @@ -165,13 +159,6 @@ export class StakingWrapper { await this._web3Wrapper.awaitTransactionSuccessAsync( await this._web3Wrapper.sendTransactionAsync(setStakingPoolRewardVaultTxData), ); - // deploy libmath test - this._LibFeeMathTestContractIfExists = await LibFeeMathTestContract.deployFrom0xArtifactAsync( - artifacts.LibFeeMathTest, - this._provider, - txDefaults, - artifacts, - ); } public async getEthBalanceAsync(owner: string): Promise { const balance = this._web3Wrapper.getBalanceInWeiAsync(owner); @@ -433,7 +420,6 @@ export class StakingWrapper { await this._web3Wrapper.mineBlockAsync(); // increment epoch in contracts const txReceipt = await this.goToNextEpochAsync(); - // mine next block await this._web3Wrapper.mineBlockAsync(); return txReceipt; } @@ -704,88 +690,6 @@ export class StakingWrapper { const balance = await this._zrxTokenContract.balanceOf.callAsync(this.getZrxVaultContract().address); return balance; } - ///// MATH ///// - public async nthRootAsync(value: BigNumber, n: BigNumber): Promise { - // const txReceipt = await this.getLibFeeMathTestContract().nthRoot.await(value, n); - const output = await this.getLibFeeMathTestContract().nthRoot.callAsync(value, n); - return output; - } - public async nthRootFixedPointAsync(value: BigNumber, n: BigNumber): Promise { - const output = await this.getLibFeeMathTestContract().nthRootFixedPoint.callAsync(value, n); - return output; - } - public async cobbDouglasAsync( - totalRewards: BigNumber, - ownerFees: BigNumber, - totalFees: BigNumber, - ownerStake: BigNumber, - totalStake: BigNumber, - alphaNumerator: BigNumber, - alphaDenominator: BigNumber, - ): Promise { - const output = await this.getLibFeeMathTestContract().cobbDouglas.callAsync( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaNumerator, - alphaDenominator, - ); - return output; - } - public async cobbDouglasSimplifiedAsync( - totalRewards: BigNumber, - ownerFees: BigNumber, - totalFees: BigNumber, - ownerStake: BigNumber, - totalStake: BigNumber, - alphaDenominator: BigNumber, - ): Promise { - await this.getLibFeeMathTestContract().cobbDouglasSimplifiedInverse.awaitTransactionSuccessAsync( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaDenominator, - ); - const output = await this.getLibFeeMathTestContract().cobbDouglasSimplified.callAsync( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaDenominator, - ); - return output; - } - public async cobbDouglasSimplifiedInverseAsync( - totalRewards: BigNumber, - ownerFees: BigNumber, - totalFees: BigNumber, - ownerStake: BigNumber, - totalStake: BigNumber, - alphaDenominator: BigNumber, - ): Promise { - await this.getLibFeeMathTestContract().cobbDouglasSimplifiedInverse.awaitTransactionSuccessAsync( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaDenominator, - ); - const output = await this.getLibFeeMathTestContract().cobbDouglasSimplifiedInverse.callAsync( - totalRewards, - ownerFees, - totalFees, - ownerStake, - totalStake, - alphaDenominator, - ); - return output; - } private async _executeTransactionAsync( calldata: string, from?: string, diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index de249e25d1..8a83093a77 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -12,8 +12,8 @@ "generated-artifacts/IWallet.json", "generated-artifacts/IZrxVault.json", "generated-artifacts/LibEIP712Hash.json", - "generated-artifacts/LibFeeMath.json", - "generated-artifacts/LibFeeMathTest.json", + "generated-artifacts/LibFixedMath.json", + "generated-artifacts/LibFixedMathRichErrors.json", "generated-artifacts/LibRewardMath.json", "generated-artifacts/LibSafeDowncast.json", "generated-artifacts/LibSignatureValidator.json", @@ -36,6 +36,8 @@ "generated-artifacts/Staking.json", "generated-artifacts/StakingPoolRewardVault.json", "generated-artifacts/StakingProxy.json", + "generated-artifacts/TestCobbDouglas.json", + "generated-artifacts/TestLibFixedMath.json", "generated-artifacts/TestStorageLayout.json", "generated-artifacts/ZrxVault.json" ], diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index 315c518d8d..65324ea5c2 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -65,6 +65,18 @@ { "note": "`web3Wrapper` is created with `shouldAllowUnlimitedContractSize` if `UNLIMITED_CONTRACT_SIZE` environment variable is set.", "pr": 2075 + }, + { + "note": "Add `toHex()`, `hexLeftPad()`, `hexRightPad()`, and 'hexInvert()' hex utils", + "pr": 2109 + }, + { + "note": "Add `PPM_DENOMINATOR` and `PPM_100_PERCENT` constants.", + "pr": 2109 + }, + { + "note": "Increase the number of ganache accounts to 20", + "pr": 2109 } ] }, diff --git a/contracts/test-utils/src/constants.ts b/contracts/test-utils/src/constants.ts index 3c10572ef1..8a6fa01209 100644 --- a/contracts/test-utils/src/constants.ts +++ b/contracts/test-utils/src/constants.ts @@ -14,6 +14,16 @@ const TESTRPC_PRIVATE_KEYS_STRINGS = [ '0xbb2d3f7c9583780a7d3904a2f55d792707c345f21de1bacb2d389934d82796b2', '0xb2fd4d29c1390b71b8795ae81196bfd60293adf99f9d32a0aff06288fcdac55f', '0x23cb7121166b9a2f93ae0b7c05bde02eae50d64449b2cbb42bc84e9d38d6cc89', + '0x5ad34d7f8704ed33ab9e8dc30a76a8c48060649204c1f7b21b973235bba8092f', + '0xf18b03c1ae8e3876d76f20c7a5127a169dd6108c55fe9ce78bc7a91aca67dee3', + '0x4ccc4e7d7843e0701295e8fd671332a0e2f1e92d0dab16e8792e91cb0b719c9d', + '0xd7638ae813450e710e6f1b09921cc1593181073ce2099fb418fc03a933c7f41f', + '0xbc7bbca8ca15eb567be60df82e4452b13072dcb60db89747e3c85df63d8270ca', + '0x55131517839bf782e6e573bc3ac8f262efd2b6cb0ac86e8f147db26fcbdb15a5', + '0x6c2b5a16e327e0c4e7fafca5ae35616141de81f77da66ee0857bc3101d446e68', + '0xfd79b71625eec963e6ec42e9b5b10602c938dfec29cbbc7d17a492dd4f403859', + '0x3003eace3d4997c52ba69c2ca97a6b5d0d1216d894035a97071590ee284c1023', + '0x84a8bb71450a1b82be2b1cdd25d079cbf23dc8054e94c47ad14510aa967f45de', ]; const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); @@ -69,4 +79,7 @@ export const constants = { EIP712_DOMAIN_NAME: '0x Protocol', EIP712_DOMAIN_VERSION: '3.0.0', DEFAULT_GAS_PRICE: 1, + NUM_TEST_ACCOUNTS: 20, + PPM_DENOMINATOR: 1e6, + PPM_100_PERCENT: 1e6, }; diff --git a/contracts/test-utils/src/hex_utils.ts b/contracts/test-utils/src/hex_utils.ts index d1a70442c2..cdf873f83e 100644 --- a/contracts/test-utils/src/hex_utils.ts +++ b/contracts/test-utils/src/hex_utils.ts @@ -1,15 +1,62 @@ +import { BigNumber } from '@0x/utils'; import * as crypto from 'crypto'; import * as ethUtil from 'ethereumjs-util'; +import { constants } from './constants'; + +const { WORD_LENGTH } = constants; +const WORD_CEIL = new BigNumber(2).pow(WORD_LENGTH * 8); + /** * Concatenate all arguments as a hex string. */ export function hexConcat(...args: Array): string { return ethUtil.bufferToHex(Buffer.concat(args.map(h => ethUtil.toBuffer(h)))); } + /** * Generate a random hex string. */ -export function hexRandom(size: number = 32): string { +export function hexRandom(size: number = WORD_LENGTH): string { return ethUtil.bufferToHex(crypto.randomBytes(size)); } + +/** + * Left-pad a hex number to a number of bytes. + */ +export function hexLeftPad(n: string | BigNumber | number, size: number = WORD_LENGTH): string { + return ethUtil.bufferToHex(ethUtil.setLengthLeft(toHex(n), size)); +} + +/** + * Right-pad a hex number to a number of bytes. + */ +export function hexRightPad(n: string | BigNumber | number, size: number = WORD_LENGTH): string { + return ethUtil.bufferToHex(ethUtil.setLengthRight(toHex(n), size)); +} + +/** + * Inverts a hex word. + */ +export function hexInvert(n: string | BigNumber | number, size: number = WORD_LENGTH): string { + const buf = ethUtil.setLengthLeft(toHex(n), size); + // tslint:disable-next-line: no-bitwise + return ethUtil.bufferToHex(Buffer.from(buf.map(b => ~b))); +} + +/** + * Convert a string, a number, or a BigNumber into a hex string. + * Works with negative numbers, as well. + */ +export function toHex(n: string | BigNumber | number, size: number = WORD_LENGTH): string { + if (typeof n === 'string' && /^0x[0-9a-f]+$/i.test(n)) { + // Already a hex. + return n; + } + let _n = new BigNumber(n); + if (_n.isNegative()) { + // Perform two's-complement. + _n = new BigNumber(hexInvert(toHex(_n.abs()), size).substr(2), 16).plus(1).mod(WORD_CEIL); + } + return `0x${_n.toString(16)}`; +} diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 06fa5cec92..e3dea662f0 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -28,7 +28,7 @@ export { bytes32Values, testCombinatoriallyWithReferenceFunc, uint256Values } fr export { TransactionFactory } from './transaction_factory'; export { MutatorContractFunction, TransactionHelper } from './transaction_helper'; export { testWithReferenceFuncAsync } from './test_with_reference'; -export { hexConcat, hexRandom } from './hex_utils'; +export { hexConcat, hexLeftPad, hexInvert, hexRandom, hexRightPad, toHex } from './hex_utils'; export { BatchMatchOrder, ContractName, diff --git a/contracts/test-utils/src/web3_wrapper.ts b/contracts/test-utils/src/web3_wrapper.ts index a5f9c5dd5e..ab5b3940f5 100644 --- a/contracts/test-utils/src/web3_wrapper.ts +++ b/contracts/test-utils/src/web3_wrapper.ts @@ -45,6 +45,7 @@ const gethConfigs = { shouldUseFakeGasEstimate: false, }; const ganacheConfigs = { + total_accounts: constants.NUM_TEST_ACCOUNTS, shouldUseInProcessGanache: true, shouldAllowUnlimitedContractSize: true, }; diff --git a/contracts/utils/test/authorizable.ts b/contracts/utils/test/authorizable.ts index 8e4bf800d8..df70f84e19 100644 --- a/contracts/utils/test/authorizable.ts +++ b/contracts/utils/test/authorizable.ts @@ -130,7 +130,7 @@ describe('Authorizable', () => { constants.AWAIT_TRANSACTION_MINED_MS, ); const index = new BigNumber(1); - const expectedError = new AuthorizableRevertErrors.IndexOutOfBoundsError(index, constants.ZERO_AMOUNT); + const expectedError = new AuthorizableRevertErrors.IndexOutOfBoundsError(index, index); const tx = authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { from: owner, }); diff --git a/contracts/utils/test/lib_rich_errors.ts b/contracts/utils/test/lib_rich_errors.ts index 7d074badb6..217e0abf30 100644 --- a/contracts/utils/test/lib_rich_errors.ts +++ b/contracts/utils/test/lib_rich_errors.ts @@ -1,36 +1,31 @@ -import { chaiSetup, constants, provider, txDefaults, web3Wrapper } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { StringRevertError } from '@0x/utils'; -import * as chai from 'chai'; -import * as _ from 'lodash'; +import { blockchainTests, expect, hexRandom } from '@0x/contracts-test-utils'; +import { coerceThrownErrorAsRevertError, StringRevertError } from '@0x/utils'; import { artifacts, TestLibRichErrorsContract } from '../src'; -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('LibRichErrors', () => { +blockchainTests('LibRichErrors', env => { let lib: TestLibRichErrorsContract; before(async () => { - await blockchainLifecycle.startAsync(); // Deploy SafeMath lib = await TestLibRichErrorsContract.deployFrom0xArtifactAsync( artifacts.TestLibRichErrors, - provider, - txDefaults, + env.provider, + env.txDefaults, {}, ); }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('_rrevert', () => { it('should correctly revert the extra bytes', async () => { - return expect(lib.externalRRevert.callAsync(constants.NULL_BYTES)).to.revertWith(constants.NULL_BYTES); + const extraBytes = hexRandom(100); + try { + await lib.externalRRevert.callAsync(extraBytes); + } catch (err) { + const revertError = coerceThrownErrorAsRevertError(err); + return expect(revertError.encode()).to.eq(extraBytes); + } + return expect.fail('Expected call to revert'); }); it('should correctly revert a StringRevertError', async () => { diff --git a/packages/dev-utils/CHANGELOG.json b/packages/dev-utils/CHANGELOG.json index c9aa9a2ac1..06d40fd0c3 100644 --- a/packages/dev-utils/CHANGELOG.json +++ b/packages/dev-utils/CHANGELOG.json @@ -13,6 +13,10 @@ { "note": "Add `UNLIMITED_CONTRACT_SIZE` to `EnvVars`.", "pr": 2075 + }, + { + "note": "Add `total_accounts` option to `Web3Config`.", + "pr": 2109 } ] }, diff --git a/packages/dev-utils/src/web3_factory.ts b/packages/dev-utils/src/web3_factory.ts index 943f20d3f3..9fc25c73b8 100644 --- a/packages/dev-utils/src/web3_factory.ts +++ b/packages/dev-utils/src/web3_factory.ts @@ -13,6 +13,7 @@ import { constants } from './constants'; import { env, EnvVars } from './env'; export interface Web3Config { + total_accounts?: number; // default: 10 hasAddresses?: boolean; // default: true shouldUseInProcessGanache?: boolean; // default: false shouldThrowErrorsOnGanacheRPCResponse?: boolean; // default: true @@ -25,16 +26,18 @@ export interface Web3Config { export const web3Factory = { getRpcProvider(config: Web3Config = {}): Web3ProviderEngine { const provider = new Web3ProviderEngine(); + const hasAddresses = config.hasAddresses === undefined || config.hasAddresses; - config.shouldUseFakeGasEstimate = + const shouldUseFakeGasEstimate = config.shouldUseFakeGasEstimate === undefined || config.shouldUseFakeGasEstimate; + if (!hasAddresses) { provider.addProvider(new EmptyWalletSubprovider()); } - - if (config.shouldUseFakeGasEstimate) { + if (shouldUseFakeGasEstimate) { provider.addProvider(new FakeGasEstimateSubprovider(constants.GAS_LIMIT)); } + const logger = { log: (arg: any) => { fs.appendFileSync('ganache.log', `${arg}\n`); @@ -43,11 +46,9 @@ export const web3Factory = { const shouldUseInProcessGanache = !!config.shouldUseInProcessGanache; if (shouldUseInProcessGanache) { if (config.rpcUrl !== undefined) { - throw new Error('Cannot use both GanacheSubrovider and RPCSubprovider'); + throw new Error('Cannot use both GanacheSubprovider and RPCSubprovider'); } - const shouldThrowErrorsOnGanacheRPCResponse = - config.shouldThrowErrorsOnGanacheRPCResponse === undefined || - config.shouldThrowErrorsOnGanacheRPCResponse; + if (config.ganacheDatabasePath !== undefined) { const doesDatabaseAlreadyExist = fs.existsSync(config.ganacheDatabasePath); if (!doesDatabaseAlreadyExist) { @@ -55,8 +56,14 @@ export const web3Factory = { fs.mkdirSync(config.ganacheDatabasePath); } } + + const shouldThrowErrorsOnGanacheRPCResponse = + config.shouldThrowErrorsOnGanacheRPCResponse === undefined || + config.shouldThrowErrorsOnGanacheRPCResponse; + provider.addProvider( new GanacheSubprovider({ + total_accounts: config.total_accounts, vmErrorsOnRPCResponse: shouldThrowErrorsOnGanacheRPCResponse, db_path: config.ganacheDatabasePath, allowUnlimitedContractSize: config.shouldAllowUnlimitedContractSize, diff --git a/packages/dev-utils/test/chai_test.ts b/packages/dev-utils/test/chai_test.ts index aa8f6cebf4..32762679e0 100644 --- a/packages/dev-utils/test/chai_test.ts +++ b/packages/dev-utils/test/chai_test.ts @@ -68,13 +68,6 @@ describe('Chai tests', () => { const revert = new StringRevertError('foo'); expect(error).is.equal(revert); }); - it('should equate an empty ganache transaction revert error to any RevertError', () => { - const error: any = new Error(`VM Exception while processing transaction: revert`); - error.hashes = ['0x1']; - error.results = { '0x1': { error: 'revert', program_counter: 1, return: '0x', reason: undefined } }; - const revert = new StringRevertError('foo'); - expect(error).is.equal(revert); - }); it('should not equate a ganache transaction revert error with reason to a StringRevertError with a different message', () => { const message = 'foo'; const error: any = new Error(`VM Exception while processing transaction: revert ${message}`); diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 231ccd4570..63a942e78f 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -69,6 +69,18 @@ { "note": "Add EIP712 types for Staking", "pr": 1910 + }, + { + "note": "Add `InvalidCobbDouglasAlphaError` `RevertError` type to `StakingRevertErrors`", + "pr": 2109 + }, + { + "note": "Rename `OperatorShareMustBeBetween0And100Error` `RevertError` type to `InvalidPoolOperatorShareError`.", + "pr": 2109 + }, + { + "note": "Add `TransactionGasPriceError` and `TransactionInvalidContextError` to error registry.", + "pr": 2109 } ] }, diff --git a/packages/order-utils/src/exchange_revert_errors.ts b/packages/order-utils/src/exchange_revert_errors.ts index 694f2cf968..6ae833d711 100644 --- a/packages/order-utils/src/exchange_revert_errors.ts +++ b/packages/order-utils/src/exchange_revert_errors.ts @@ -273,26 +273,26 @@ export class PayProtocolFeeError extends RevertError { } const types = [ + AssetProxyExistsError, + AssetProxyDispatchError, + AssetProxyTransferError, BatchMatchOrdersError, + EIP1271SignatureError, + ExchangeInvalidContextError, + FillError, + IncompleteFillError, + NegativeSpreadError, + OrderEpochError, OrderStatusError, + PayProtocolFeeError, SignatureError, SignatureValidatorNotApprovedError, SignatureWalletError, - EIP1271SignatureError, - FillError, - OrderEpochError, - AssetProxyExistsError, - AssetProxyDispatchError, - AssetProxyTransferError, - NegativeSpreadError, TransactionError, + TransactionExecutionError, TransactionGasPriceError, TransactionInvalidContextError, TransactionSignatureError, - TransactionExecutionError, - IncompleteFillError, - PayProtocolFeeError, - ExchangeInvalidContextError, ]; // Register the types we've defined. diff --git a/packages/order-utils/src/staking_revert_errors.ts b/packages/order-utils/src/staking_revert_errors.ts index 9a8ac68e51..cdf46ab69e 100644 --- a/packages/order-utils/src/staking_revert_errors.ts +++ b/packages/order-utils/src/staking_revert_errors.ts @@ -179,11 +179,11 @@ export class AmountExceedsBalanceOfPoolError extends RevertError { } } -export class OperatorShareMustBeBetween0And100Error extends RevertError { +export class InvalidPoolOperatorShareError extends RevertError { constructor(poolId?: string, poolOperatorShare?: BigNumber | number | string) { super( - 'OperatorShareMustBeBetween0And100Error', - 'OperatorShareMustBeBetween0And100Error(bytes32 poolId, uint8 poolOperatorShare)', + 'InvalidPoolOperatorShareError', + 'InvalidPoolOperatorShareError(bytes32 poolId, uint32 poolOperatorShare)', { poolId, poolOperatorShare }, ); } @@ -195,6 +195,15 @@ export class PoolAlreadyExistsError extends RevertError { } } +export class InvalidCobbDouglasAlphaError extends RevertError { + constructor(numerator: BigNumber | number | string, denominator: BigNumber | number | string) { + super('InvalidCobbDouglasAlphaError', 'InvalidCobbDouglasAlphaError(uint256 numerator, uint256 denominator)', { + numerator, + denominator, + }); + } +} + const types = [ MiscalculatedRewardsError, OnlyCallableByExchangeError, @@ -218,8 +227,9 @@ const types = [ OnlyCallableIfInCatastrophicFailureError, OnlyCallableIfNotInCatastrophicFailureError, AmountExceedsBalanceOfPoolError, - OperatorShareMustBeBetween0And100Error, + InvalidPoolOperatorShareError, PoolAlreadyExistsError, + InvalidCobbDouglasAlphaError, ]; // Register the types we've defined. diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index 71c83657dd..596d1cfdec 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -5,6 +5,26 @@ { "note": "Allow for array types in `RevertError`s.", "pr": 2075 + }, + { + "note": "Have Ganache `Error` -> `RevertError` coercion fail if it can't look up the selector.", + "pr": 2109 + }, + { + "note": "Add `LibFixedMath` `RevertError` types.", + "pr": 2109 + }, + { + "note": "Add `RawRevertError` `RevertError` type.", + "pr": 2109 + }, + { + "note": "Make `RevertError.decode()` optionally return a `RawRevertError` if the selector is unknown.", + "pr": 2109 + }, + { + "note": "Rename `length` field of `AuthorizableRevertErrors.IndexOutOfBoundsError` type to `len`.", + "pr": 2109 } ] }, diff --git a/packages/utils/src/authorizable_revert_errors.ts b/packages/utils/src/authorizable_revert_errors.ts index 01a1f547ab..c1b8a535d2 100644 --- a/packages/utils/src/authorizable_revert_errors.ts +++ b/packages/utils/src/authorizable_revert_errors.ts @@ -12,8 +12,8 @@ export class AuthorizedAddressMismatchError extends RevertError { } export class IndexOutOfBoundsError extends RevertError { - constructor(index?: BigNumber, length?: BigNumber) { - super('IndexOutOfBoundsError', 'IndexOutOfBoundsError(uint256 index, uint256 length)', { index, length }); + constructor(index?: BigNumber, len?: BigNumber) { + super('IndexOutOfBoundsError', 'IndexOutOfBoundsError(uint256 index, uint256 len)', { index, len }); } } diff --git a/packages/utils/src/fixed_math_revert_errors.ts b/packages/utils/src/fixed_math_revert_errors.ts new file mode 100644 index 0000000000..b405653e6d --- /dev/null +++ b/packages/utils/src/fixed_math_revert_errors.ts @@ -0,0 +1,51 @@ +import { BigNumber } from './configured_bignumber'; +import { RevertError } from './revert_error'; + +// tslint:disable:max-classes-per-file + +export enum ValueErrorCodes { + TooSmall, + TooLarge, +} + +export enum BinOpErrorCodes { + AdditionOverflow, + SubtractionUnderflow, + MultiplicationOverflow, + DivisionByZero, +} + +export class SignedValueError extends RevertError { + constructor(error?: ValueErrorCodes, n?: BigNumber | number | string) { + super('SignedValueError', 'SignedValueError(uint8 error, int256 n)', { + error, + n, + }); + } +} + +export class UnsignedValueError extends RevertError { + constructor(error?: ValueErrorCodes, n?: BigNumber | number | string) { + super('UnsignedValueError', 'UnsignedValueError(uint8 error, uint256 n)', { + error, + n, + }); + } +} + +export class BinOpError extends RevertError { + constructor(error?: BinOpErrorCodes, a?: BigNumber | number | string, b?: BigNumber | number | string) { + super('BinOpError', 'BinOpError(uint8 error, int256 a, int256 b)', { + error, + a, + b, + }); + } +} + +const types = [SignedValueError, UnsignedValueError, BinOpError]; + +// Register the types we've defined. +for (const type of types) { + RevertError.registerType(type); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 7ecb00b4f9..4433e4eac7 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,5 @@ import * as AuthorizableRevertErrors from './authorizable_revert_errors'; +import * as FixedMathRevertErrors from './fixed_math_revert_errors'; import * as LibAddressArrayRevertErrors from './lib_address_array_revert_errors'; import * as LibBytesRevertErrors from './lib_bytes_revert_errors'; import * as OwnableRevertErrors from './ownable_revert_errors'; @@ -26,6 +27,7 @@ export { decodeBytesAsRevertError, decodeThrownErrorAsRevertError, coerceThrownErrorAsRevertError, + RawRevertError, registerRevertErrorType, RevertError, StringRevertError, @@ -34,6 +36,7 @@ export { export { AuthorizableRevertErrors, + FixedMathRevertErrors, LibAddressArrayRevertErrors, LibBytesRevertErrors, OwnableRevertErrors, diff --git a/packages/utils/src/revert_error.ts b/packages/utils/src/revert_error.ts index 99586783e7..0df9302a59 100644 --- a/packages/utils/src/revert_error.ts +++ b/packages/utils/src/revert_error.ts @@ -35,23 +35,25 @@ export function registerRevertErrorType(revertClass: RevertErrorType): void { * Decode an ABI encoded revert error. * Throws if the data cannot be decoded as a known RevertError type. * @param bytes The ABI encoded revert error. Either a hex string or a Buffer. + * @param coerce Coerce unknown selectors into a `RawRevertError` type. * @return A RevertError object. */ -export function decodeBytesAsRevertError(bytes: string | Buffer): RevertError { - return RevertError.decode(bytes); +export function decodeBytesAsRevertError(bytes: string | Buffer, coerce: boolean = false): RevertError { + return RevertError.decode(bytes, coerce); } /** * Decode a thrown error. * Throws if the data cannot be decoded as a known RevertError type. * @param error Any thrown error. + * @param coerce Coerce unknown selectors into a `RawRevertError` type. * @return A RevertError object. */ -export function decodeThrownErrorAsRevertError(error: Error): RevertError { +export function decodeThrownErrorAsRevertError(error: Error, coerce: boolean = false): RevertError { if (error instanceof RevertError) { return error; } - return RevertError.decode(getThrownErrorRevertErrorBytes(error)); + return RevertError.decode(getThrownErrorRevertErrorBytes(error), coerce); } /** @@ -64,10 +66,10 @@ export function coerceThrownErrorAsRevertError(error: Error): RevertError { return error; } try { - return decodeThrownErrorAsRevertError(error); + return decodeThrownErrorAsRevertError(error, true); } catch (err) { if (isGanacheTransactionRevertError(error)) { - return new AnyRevertError(); + throw err; } // Handle geth transaction reverts. if (isGethTransactionRevertError(error)) { @@ -88,18 +90,26 @@ export abstract class RevertError extends Error { private static readonly _typeRegistry: ObjectMap = {}; public readonly abi?: RevertErrorAbi; public readonly values: ValueMap = {}; + protected readonly _raw?: string; /** * Decode an ABI encoded revert error. * Throws if the data cannot be decoded as a known RevertError type. * @param bytes The ABI encoded revert error. Either a hex string or a Buffer. + * @param coerce Whether to coerce unknown selectors into a `RawRevertError` type. * @return A RevertError object. */ - public static decode(bytes: string | Buffer): RevertError { + public static decode(bytes: string | Buffer, coerce: boolean = false): RevertError { const _bytes = bytes instanceof Buffer ? ethUtil.bufferToHex(bytes) : ethUtil.addHexPrefix(bytes); // tslint:disable-next-line: custom-no-magic-numbers const selector = _bytes.slice(2, 10); - const { decoder, type } = this._lookupType(selector); + if (!(selector in RevertError._typeRegistry)) { + if (coerce) { + return new RawRevertError(bytes); + } + throw new Error(`Unknown selector: ${selector}`); + } + const { type, decoder } = RevertError._typeRegistry[selector]; const instance = new type(); try { const values = decoder(_bytes); @@ -130,21 +140,16 @@ export abstract class RevertError extends Error { }; } - // Ge tthe registry info given a selector. - private static _lookupType(selector: string): RevertErrorRegistryItem { - if (selector in RevertError._typeRegistry) { - return RevertError._typeRegistry[selector]; - } - throw new Error(`Unknown revert error selector "${selector}"`); - } - /** * Create a RevertError instance with optional parameter values. * Parameters that are left undefined will not be tested in equality checks. * @param declaration Function-style declaration of the revert (e.g., Error(string message)) * @param values Optional mapping of parameters to values. + * @param raw Optional encoded form of the revert error. If supplied, this + * instance will be treated as a `RawRevertError`, meaning it can only + * match other `RawRevertError` types with the same encoded payload. */ - protected constructor(name: string, declaration?: string, values?: ValueMap) { + protected constructor(name: string, declaration?: string, values?: ValueMap, raw?: string) { super(createErrorMessage(name, values)); if (declaration !== undefined) { this.abi = declarationToAbi(declaration); @@ -152,6 +157,7 @@ export abstract class RevertError extends Error { _.assign(this.values, _.cloneDeep(values)); } } + this._raw = raw; // Extending Error is tricky; we need to explicitly set the prototype. Object.setPrototypeOf(this, new.target.prototype); } @@ -181,6 +187,10 @@ export abstract class RevertError extends Error { if (!_.isNil(this.abi)) { return toSelector(this.abi); } + if (this._isRawType) { + // tslint:disable-next-line: custom-no-magic-numbers + return (this._raw as string).slice(2, 10); + } return ''; } @@ -230,6 +240,10 @@ export abstract class RevertError extends Error { if (this._isAnyType || _other._isAnyType) { return true; } + // If either are raw types, they must match their raw data. + if (this._isRawType || _other._isRawType) { + return this._raw === _other._raw; + } // Must be of same type. if (this.constructor !== _other.constructor) { return false; @@ -252,6 +266,9 @@ export abstract class RevertError extends Error { } public encode(): string { + if (this._raw !== undefined) { + return this._raw; + } if (!this._hasAllArgumentValues) { throw new Error(`Instance of ${this.typeName} does not have all its parameter values set.`); } @@ -260,6 +277,9 @@ export abstract class RevertError extends Error { } public toString(): string { + if (this._isRawType) { + return `${this.constructor.name}(${this._raw})`; + } const values = _.omitBy(this.values, (v: any) => _.isNil(v)); const inner = _.isEmpty(values) ? '' : inspect(values); return `${this.constructor.name}(${inner})`; @@ -274,7 +294,11 @@ export abstract class RevertError extends Error { } private get _isAnyType(): boolean { - return _.isNil(this.abi); + return _.isNil(this.abi) && _.isNil(this._raw); + } + + private get _isRawType(): boolean { + return !_.isNil(this._raw); } private get _hasAllArgumentValues(): boolean { @@ -361,6 +385,20 @@ export class AnyRevertError extends RevertError { } } +/** + * Special RevertError type that is not decoded. + */ +export class RawRevertError extends RevertError { + constructor(encoded: string | Buffer) { + super( + 'RawRevertError', + undefined, + undefined, + typeof encoded === 'string' ? encoded : ethUtil.bufferToHex(encoded), + ); + } +} + /** * Create an error message for a RevertError. * @param name The name of the RevertError. @@ -488,3 +526,4 @@ function toSelector(abi: RevertErrorAbi): string { // Register StringRevertError RevertError.registerType(StringRevertError); +// tslint:disable-next-line max-file-line-count diff --git a/packages/utils/test/revert_error_test.ts b/packages/utils/test/revert_error_test.ts index d9dd9f001d..efb86453de 100644 --- a/packages/utils/test/revert_error_test.ts +++ b/packages/utils/test/revert_error_test.ts @@ -1,7 +1,7 @@ import * as chai from 'chai'; import * as _ from 'lodash'; -import { AnyRevertError, RevertError, StringRevertError } from '../src/revert_error'; +import { AnyRevertError, RawRevertError, RevertError, StringRevertError } from '../src/revert_error'; import { chaiSetup } from './utils/chai_setup'; @@ -107,6 +107,16 @@ describe('RevertError', () => { const revert2 = new DescendantRevertError(message); expect(revert1.equals(revert2)).to.be.false(); }); + it('should equate two `RawRevertError` types with the same raw data', () => { + const revert1 = new RawRevertError('0x0123456789'); + const revert2 = new RawRevertError(revert1.encode()); + expect(revert1.equals(revert2)).to.be.true(); + }); + it('should not equate two `RawRevertError` types with the different raw data', () => { + const revert1 = new RawRevertError('0x0123456789'); + const revert2 = new RawRevertError(`${revert1.encode()}00`); + expect(revert1.equals(revert2)).to.be.false(); + }); }); describe('registering', () => { it('should throw when registering an already registered signature', () => { @@ -133,15 +143,15 @@ describe('RevertError', () => { const decoded = RevertError.decode(encoded); expect(decoded.equals(expected)).to.be.true(); }); - it('should fail to decode an ABI encoded revert error with an unknown selector', () => { + it('should decode an unknown selector as a `RawRevertError`', () => { const _encoded = encoded.substr(0, 2) + '00' + encoded.substr(4); - const decode = () => RevertError.decode(_encoded); - expect(decode).to.be.throw(); + const decoded = RevertError.decode(_encoded, true); + expect(decoded).is.instanceof(RawRevertError); }); it('should fail to decode a malformed ABI encoded revert error', () => { - const _encoded = encoded.substr(0, encoded.substr.length - 1); + const _encoded = encoded.substr(0, encoded.length - 1); const decode = () => RevertError.decode(_encoded); - expect(decode).to.be.throw(); + expect(decode).to.throw(); }); }); describe('encoding', () => { diff --git a/yarn.lock b/yarn.lock index e52f0a1717..cfd343eb17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5905,6 +5905,11 @@ decamelize@^2.0.0: dependencies: xregexp "4.0.0" +decimal.js@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" + integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== + decko@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decko/-/decko-1.2.0.tgz#fd43c735e967b8013306884a56fbe665996b6817" @@ -8845,7 +8850,7 @@ got@^6.7.1: graceful-fs@4.1.15, graceful-fs@^3.0.0, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@~1.2.0: version "4.1.15" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== "graceful-readlink@>= 1.0.0":