@0x/contracts-staking: Change the way operator stake is computed.
`@0x/contracts-staking`: Denominate pool operator shares in parts-per-million. `@0x/contracts-staking`: Update tests for new stake computation and higher precision math. `@0x/contracts-staking`: Add `setCobbDouglasAlpha()` function.
This commit is contained in:
committed by
Lawrence Forman
parent
cb1dc92594
commit
8d5e28f099
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -170,7 +170,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,
|
||||
@@ -210,8 +210,9 @@ contract MixinExchangeFees is
|
||||
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_ONE)
|
||||
);
|
||||
|
||||
// store pool stats
|
||||
|
||||
@@ -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_ONE = 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -78,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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -20,9 +20,10 @@ 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)
|
||||
|
||||
@@ -110,9 +110,9 @@ 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 =
|
||||
@@ -420,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
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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_ONE) {
|
||||
LibRichErrors.rrevert(LibStakingRichErrors.InvalidPoolOperatorShareError(
|
||||
poolId,
|
||||
poolOperatorShare
|
||||
));
|
||||
@@ -229,7 +229,7 @@ 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,
|
||||
PPM_ONE,
|
||||
amount
|
||||
);
|
||||
|
||||
|
||||
@@ -46,4 +46,13 @@ contract TestCobbDouglas is
|
||||
alphaDenominator
|
||||
);
|
||||
}
|
||||
|
||||
function getCobbDouglasAlpha()
|
||||
external
|
||||
view
|
||||
returns (uint256 numerator, uint256 denominator)
|
||||
{
|
||||
numerator = cobbDouglasAlphaNumerator;
|
||||
denominator = cobbDouglasAlphaDenomintor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { blockchainTests, expect, hexRandom } from '@0x/contracts-test-utils';
|
||||
import { AnyRevertError, BigNumber, FixedMathRevertErrors } from '@0x/utils';
|
||||
import { blockchainTests, constants, expect, filterLogsToArguments, hexRandom } from '@0x/contracts-test-utils';
|
||||
import { StakingRevertErrors } from '@0x/order-utils';
|
||||
import { AnyRevertError, BigNumber, FixedMathRevertErrors, OwnableRevertErrors } from '@0x/utils';
|
||||
import { Decimal } from 'decimal.js';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { artifacts, TestCobbDouglasContract } from '../src/';
|
||||
import {
|
||||
artifacts,
|
||||
TestCobbDouglasCobbDouglasAlphaChangedEventArgs,
|
||||
TestCobbDouglasContract,
|
||||
TestCobbDouglasEvents,
|
||||
} from '../src/';
|
||||
|
||||
Decimal.set({ precision: 128 });
|
||||
type Numberish = BigNumber | string | number;
|
||||
import { assertRoughlyEquals, Numberish } 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,
|
||||
@@ -22,13 +32,6 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
);
|
||||
});
|
||||
|
||||
function toPrecision(n: Numberish, precision: number): BigNumber {
|
||||
const _n = new BigNumber(n);
|
||||
const integerDigits = _n.integerValue().sd(true);
|
||||
const base = 10 ** (precision - integerDigits);
|
||||
return _n.times(base).integerValue(BigNumber.ROUND_FLOOR).dividedBy(base);
|
||||
}
|
||||
|
||||
function toDecimal(x: Numberish): Decimal {
|
||||
if (BigNumber.isBigNumber(x)) {
|
||||
return new Decimal(x.toString(10));
|
||||
@@ -36,16 +39,7 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
return new Decimal(x);
|
||||
}
|
||||
|
||||
function assertRoughlyEquals(
|
||||
actual: Numberish,
|
||||
expected: Numberish,
|
||||
precision: number = 14,
|
||||
): void {
|
||||
// SD is not what we want.
|
||||
expect(toPrecision(actual, precision)).to.bignumber.eq(toPrecision(expected, precision));
|
||||
}
|
||||
|
||||
function getRandomAmount(min: Numberish, max: Numberish): BigNumber {
|
||||
function getRandomInteger(min: Numberish, max: Numberish): BigNumber {
|
||||
const range = new BigNumber(max).minus(min);
|
||||
const random = new BigNumber(hexRandom().substr(2), 16);
|
||||
return random.mod(range).plus(min);
|
||||
@@ -55,6 +49,72 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
return new BigNumber(total).times(Math.random()).integerValue();
|
||||
}
|
||||
|
||||
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<void> {
|
||||
const [_n, _d] = [new BigNumber(n), new BigNumber(d)];
|
||||
const receipt = await testContract.setCobbDouglasAlpha.awaitTransactionSuccessAsync(_n, _d);
|
||||
const logs = filterLogsToArguments<TestCobbDouglasCobbDouglasAlphaChangedEventArgs>(
|
||||
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;
|
||||
@@ -100,15 +160,7 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
}
|
||||
|
||||
function cobbDouglas(params?: Partial<CobbDouglasParams>): BigNumber {
|
||||
const {
|
||||
totalRewards,
|
||||
ownerFees,
|
||||
totalFees,
|
||||
ownerStake,
|
||||
totalStake,
|
||||
alphaNumerator,
|
||||
alphaDenominator,
|
||||
} = {
|
||||
const { totalRewards, ownerFees, totalFees, ownerStake, totalStake, alphaNumerator, alphaDenominator } = {
|
||||
...DEFAULT_COBB_DOUGLAS_PARAMS,
|
||||
...params,
|
||||
};
|
||||
@@ -117,19 +169,21 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
const alpha = toDecimal(alphaNumerator).dividedBy(toDecimal(alphaDenominator));
|
||||
// totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)
|
||||
return new BigNumber(
|
||||
feeRatio.pow(alpha).times(
|
||||
stakeRatio.pow(new Decimal(1).minus(alpha)),
|
||||
).times(toDecimal(totalRewards)).toFixed(0, BigNumber.ROUND_FLOOR),
|
||||
feeRatio
|
||||
.pow(alpha)
|
||||
.times(stakeRatio.pow(new Decimal(1).minus(alpha)))
|
||||
.times(toDecimal(totalRewards))
|
||||
.toFixed(0, BigNumber.ROUND_FLOOR),
|
||||
);
|
||||
}
|
||||
|
||||
function getRandomParams(overrides?: Partial<CobbDouglasParams>): CobbDouglasParams {
|
||||
const totalRewards = _.get(overrides, 'totalRewards', getRandomAmount(0, 1e27)) as Numberish;
|
||||
const totalFees = _.get(overrides, 'totalFees', getRandomAmount(1, 1e27)) as Numberish;
|
||||
const 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', getRandomAmount(1, 1e27)) 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', getRandomAmount(1, 1e6)) as Numberish;
|
||||
const alphaDenominator = _.get(overrides, 'alphaDenominator', getRandomInteger(1, 1e6)) as Numberish;
|
||||
const alphaNumerator = _.get(overrides, 'alphaNumerator', getRandomPortion(alphaDenominator)) as Numberish;
|
||||
return {
|
||||
totalRewards,
|
||||
@@ -143,92 +197,98 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
}
|
||||
|
||||
it('throws if `alphaNumerator` > `alphaDenominator`', async () => {
|
||||
return expect(callCobbDouglasAsync({
|
||||
alphaNumerator: 11,
|
||||
alphaDenominator: 10,
|
||||
})).to.revertWith(new AnyRevertError()); // Just an assertion failure.
|
||||
return expect(
|
||||
callCobbDouglasAsync({
|
||||
alphaNumerator: 11,
|
||||
alphaDenominator: 10,
|
||||
}),
|
||||
).to.revertWith(new AnyRevertError()); // Just an assertion failure.
|
||||
});
|
||||
|
||||
it('throws if `ownerFees` > `totalFees`', async () => {
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathSignedValueError(
|
||||
FixedMathRevertErrors.ValueErrorCodes.TooLarge,
|
||||
);
|
||||
return expect(callCobbDouglasAsync({
|
||||
ownerFees: 11,
|
||||
totalFees: 10,
|
||||
})).to.revertWith(expectedError);
|
||||
return expect(
|
||||
callCobbDouglasAsync({
|
||||
ownerFees: 11,
|
||||
totalFees: 10,
|
||||
}),
|
||||
).to.revertWith(expectedError);
|
||||
});
|
||||
|
||||
it('throws if `ownerStake` > `totalStake`', async () => {
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathSignedValueError(
|
||||
FixedMathRevertErrors.ValueErrorCodes.TooLarge,
|
||||
);
|
||||
return expect(callCobbDouglasAsync({
|
||||
ownerStake: 11,
|
||||
totalStake: 10,
|
||||
})).to.revertWith(expectedError);
|
||||
return expect(
|
||||
callCobbDouglasAsync({
|
||||
ownerStake: 11,
|
||||
totalStake: 10,
|
||||
}),
|
||||
).to.revertWith(expectedError);
|
||||
});
|
||||
|
||||
it('computes the correct reward', async () => {
|
||||
const expected = cobbDouglas();
|
||||
const r = await callCobbDouglasAsync();
|
||||
assertRoughlyEquals(r, expected);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
assertRoughlyEquals(r, expected, PRECISION);
|
||||
});
|
||||
|
||||
it('computes the correct reward with equal fee and stake ratios', async () => {
|
||||
@@ -236,7 +296,7 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
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);
|
||||
assertRoughlyEquals(r, expected, PRECISION);
|
||||
});
|
||||
|
||||
it('computes the correct reward with full fee and stake ratios', async () => {
|
||||
@@ -244,7 +304,7 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake);
|
||||
const expected = cobbDouglas({ ownerFees, ownerStake });
|
||||
const r = await callCobbDouglasAsync({ ownerFees, ownerStake });
|
||||
assertRoughlyEquals(r, expected);
|
||||
assertRoughlyEquals(r, expected, PRECISION);
|
||||
});
|
||||
|
||||
it('computes the correct reward with zero fee and stake ratios', async () => {
|
||||
@@ -252,7 +312,7 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
const ownerStake = 0;
|
||||
const expected = cobbDouglas({ ownerFees, ownerStake });
|
||||
const r = await callCobbDouglasAsync({ ownerFees, ownerStake });
|
||||
assertRoughlyEquals(r, expected);
|
||||
assertRoughlyEquals(r, expected, PRECISION);
|
||||
});
|
||||
|
||||
blockchainTests.optional('fuzzing', () => {
|
||||
@@ -261,7 +321,7 @@ blockchainTests('Cobb-Douglas', env => {
|
||||
it(`cobbDouglas(${JSON.stringify(params)})`, async () => {
|
||||
const expected = cobbDouglas(params);
|
||||
const r = await callCobbDouglasAsync(params);
|
||||
assertRoughlyEquals(r, expected, 12);
|
||||
assertRoughlyEquals(r, expected, PRECISION);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as _ from 'lodash';
|
||||
|
||||
import { artifacts, TestLibFixedMathContract } from '../src/';
|
||||
|
||||
Decimal.set({ precision: 128 });
|
||||
import { assertRoughlyEquals, Numberish } from './utils/number_utils';
|
||||
|
||||
blockchainTests('LibFixedMath', env => {
|
||||
let testContract: TestLibFixedMathContract;
|
||||
@@ -29,8 +29,6 @@ blockchainTests('LibFixedMath', env => {
|
||||
const MIN_LN_NUMBER = new BigNumber(new Decimal(MIN_EXP_NUMBER.toFixed(128)).exp().toFixed(128));
|
||||
const FUZZ_COUNT = 1024;
|
||||
|
||||
type Numberish = BigNumber | string | number;
|
||||
|
||||
function fromFixed(n: Numberish): BigNumber {
|
||||
return new BigNumber(n).dividedBy(FIXED_POINT_DIVISOR);
|
||||
}
|
||||
@@ -43,30 +41,6 @@ blockchainTests('LibFixedMath', env => {
|
||||
return fromFixed(toFixed(n));
|
||||
}
|
||||
|
||||
function add(a: Numberish, b: Numberish): BigNumber {
|
||||
return fromFixed(toFixed(a).plus(toFixed(b)));
|
||||
}
|
||||
|
||||
function sub(a: Numberish, b: Numberish): BigNumber {
|
||||
return fromFixed(toFixed(a).minus(toFixed(b)));
|
||||
}
|
||||
|
||||
function mul(a: Numberish, b: Numberish): BigNumber {
|
||||
return fromFixed(toFixed(a).times(toFixed(b)).dividedToIntegerBy(FIXED_POINT_DIVISOR));
|
||||
}
|
||||
|
||||
function div(a: Numberish, b: Numberish): BigNumber {
|
||||
return fromFixed(toFixed(a).times(FIXED_POINT_DIVISOR).dividedBy(toFixed(b)));
|
||||
}
|
||||
|
||||
function ln(x: Numberish): BigNumber {
|
||||
return new BigNumber(toDecimal(x).ln().toFixed(128));
|
||||
}
|
||||
|
||||
function exp(x: Numberish): BigNumber {
|
||||
return new BigNumber(toDecimal(x).exp().toFixed(128));
|
||||
}
|
||||
|
||||
function toDecimal(x: Numberish): Decimal {
|
||||
if (BigNumber.isBigNumber(x)) {
|
||||
return new Decimal(x.toString(10));
|
||||
@@ -74,34 +48,12 @@ blockchainTests('LibFixedMath', env => {
|
||||
return new Decimal(x);
|
||||
}
|
||||
|
||||
function getRandomNumber(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);
|
||||
function assertFixedEquals(actualFixed: Numberish, expected: Numberish): void {
|
||||
expect(fromFixed(actualFixed)).to.bignumber.eq(numberToFixedToNumber(expected));
|
||||
}
|
||||
|
||||
function toPrecision(n: Numberish, precision: number = 13): BigNumber {
|
||||
const _n = new BigNumber(n);
|
||||
const integerDigits = _n.integerValue().sd(true);
|
||||
const base = 10 ** (precision - integerDigits);
|
||||
return _n.times(base).integerValue(BigNumber.ROUND_HALF_FLOOR).dividedBy(base);
|
||||
}
|
||||
|
||||
function assertFixedEquals(
|
||||
actual: Numberish,
|
||||
expected: Numberish,
|
||||
): void {
|
||||
expect(fromFixed(actual)).to.bignumber.eq(numberToFixedToNumber(expected));
|
||||
}
|
||||
|
||||
function assertFixedRoughlyEquals(
|
||||
actual: Numberish,
|
||||
expected: Numberish,
|
||||
precision: number = 18,
|
||||
): void {
|
||||
// SD is not what we want.
|
||||
expect(toPrecision(fromFixed(actual), precision))
|
||||
.to.bignumber.eq(toPrecision(numberToFixedToNumber(expected), precision));
|
||||
function assertFixedRoughlyEquals(actualFixed: Numberish, expected: Numberish, precision: number = 18): void {
|
||||
assertRoughlyEquals(fromFixed(actualFixed), expected, precision);
|
||||
}
|
||||
|
||||
describe('one()', () => {
|
||||
@@ -161,37 +113,37 @@ blockchainTests('LibFixedMath', env => {
|
||||
|
||||
describe('mulDiv()', () => {
|
||||
it('mulDiv(0, 0, 1) == 0', async () => {
|
||||
const [ a, n, d ] = [ 0, 0, 1 ];
|
||||
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 [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 [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 [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 [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 [a, n, d] = [1.2345, 149, 0];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
|
||||
);
|
||||
@@ -201,26 +153,30 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
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 [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 [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 [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 [a, b] = [MAX_FIXED_VALUE, new BigNumber(1)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
|
||||
a,
|
||||
@@ -231,7 +187,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('throws on underflow', async () => {
|
||||
const [ a, b ] = [ MIN_FIXED_VALUE, new BigNumber(-1) ];
|
||||
const [a, b] = [MIN_FIXED_VALUE, new BigNumber(-1)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow,
|
||||
a,
|
||||
@@ -243,26 +199,30 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
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 [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 [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 [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 [a, b] = [MIN_FIXED_VALUE, new BigNumber(1)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow,
|
||||
a,
|
||||
@@ -273,7 +233,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('throws on overflow', async () => {
|
||||
const [ a, b ] = [ MAX_FIXED_VALUE, new BigNumber(-1) ];
|
||||
const [a, b] = [MAX_FIXED_VALUE, new BigNumber(-1)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
|
||||
a,
|
||||
@@ -285,38 +245,46 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
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 [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 [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 [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 [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 [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 [a, b] = [MIN_FIXED_VALUE, new BigNumber(2)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
|
||||
a,
|
||||
@@ -327,7 +295,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('throws on overflow', async () => {
|
||||
const [ a, b ] = [ MAX_FIXED_VALUE, new BigNumber(2) ];
|
||||
const [a, b] = [MAX_FIXED_VALUE, new BigNumber(2)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
|
||||
a,
|
||||
@@ -339,8 +307,16 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
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 [a, b] = [1, 0];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
|
||||
toFixed(a).times(FIXED_POINT_DIVISOR),
|
||||
@@ -351,25 +327,25 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('x / 1 == x', async () => {
|
||||
const [ a, b ] = [ 1.41214552, 1 ];
|
||||
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 [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 [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 [a, b] = ['1.25394912112', '-0.03413318948193'];
|
||||
const r = await testContract.div.callAsync(toFixed(a), toFixed(b));
|
||||
assertFixedEquals(r, div(a, b));
|
||||
});
|
||||
@@ -377,37 +353,37 @@ blockchainTests('LibFixedMath', env => {
|
||||
|
||||
describe('uintMul()', () => {
|
||||
it('0 * x == 0', async () => {
|
||||
const [ a, b ] = [ 0, 1234 ];
|
||||
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 [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 [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 [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 [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 [a, b] = [toFixed(1), MAX_FIXED_VALUE.plus(1)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathUnsignedValueError(
|
||||
FixedMathRevertErrors.ValueErrorCodes.TooLarge,
|
||||
b,
|
||||
@@ -417,7 +393,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('throws if lhs is too large', async () => {
|
||||
const [ a, b ] = [ MAX_FIXED_VALUE, new BigNumber(2) ];
|
||||
const [a, b] = [MAX_FIXED_VALUE, new BigNumber(2)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
|
||||
a,
|
||||
@@ -475,31 +451,31 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('converts a fraction with a positive numerator and denominator', async () => {
|
||||
const [ n, d ] = [ 1337, 1000 ];
|
||||
const [n, d] = [1337, 1000];
|
||||
const r = await testContract.toFixedSigned2.callAsync(new BigNumber(n), new BigNumber(d));
|
||||
assertFixedEquals(r, div(n, d));
|
||||
assertFixedEquals(r, n / d);
|
||||
});
|
||||
|
||||
it('converts a fraction with a negative numerator and positive denominator', async () => {
|
||||
const [ n, d ] = [ -1337, 1000 ];
|
||||
const [n, d] = [-1337, 1000];
|
||||
const r = await testContract.toFixedSigned2.callAsync(new BigNumber(n), new BigNumber(d));
|
||||
assertFixedEquals(r, div(n, d));
|
||||
assertFixedEquals(r, n / d);
|
||||
});
|
||||
|
||||
it('converts a fraction with a negative numerator and denominator', async () => {
|
||||
const [ n, d ] = [ -1337, -1000 ];
|
||||
const [n, d] = [-1337, -1000];
|
||||
const r = await testContract.toFixedSigned2.callAsync(new BigNumber(n), new BigNumber(d));
|
||||
assertFixedEquals(r, div(n, d));
|
||||
assertFixedEquals(r, n / d);
|
||||
});
|
||||
|
||||
it('converts a fraction with a negative numerator and negative denominator', async () => {
|
||||
const [ n, d ] = [ -1337, -1000 ];
|
||||
const [n, d] = [-1337, -1000];
|
||||
const r = await testContract.toFixedSigned2.callAsync(new BigNumber(n), new BigNumber(d));
|
||||
assertFixedEquals(r, div(n, 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 [n, d] = [MAX_FIXED_VALUE.dividedToIntegerBy(FIXED_POINT_DIVISOR).plus(1), new BigNumber(1000)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
|
||||
n,
|
||||
@@ -510,7 +486,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('throws if the denominator is zero', async () => {
|
||||
const [ n, d ] = [ new BigNumber(1), new BigNumber(0) ];
|
||||
const [n, d] = [new BigNumber(1), new BigNumber(0)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
|
||||
n.times(FIXED_POINT_DIVISOR),
|
||||
@@ -529,13 +505,13 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('converts a fraction', async () => {
|
||||
const [ n, d ] = [ 1337, 1000 ];
|
||||
const [n, d] = [1337, 1000];
|
||||
const r = await testContract.toFixedUnsigned2.callAsync(new BigNumber(n), new BigNumber(d));
|
||||
assertFixedEquals(r, div(n, 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 [n, d] = [MAX_FIXED_VALUE.plus(1), new BigNumber(1000)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathUnsignedValueError(
|
||||
FixedMathRevertErrors.ValueErrorCodes.TooLarge,
|
||||
n,
|
||||
@@ -545,7 +521,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('throws if the denominator is too large', async () => {
|
||||
const [ n, d ] = [ new BigNumber(1000), MAX_FIXED_VALUE.plus(1) ];
|
||||
const [n, d] = [new BigNumber(1000), MAX_FIXED_VALUE.plus(1)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathUnsignedValueError(
|
||||
FixedMathRevertErrors.ValueErrorCodes.TooLarge,
|
||||
d,
|
||||
@@ -555,7 +531,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
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 [n, d] = [MAX_FIXED_VALUE.dividedToIntegerBy(FIXED_POINT_DIVISOR).plus(1), new BigNumber(1000)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
|
||||
n,
|
||||
@@ -566,7 +542,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
it('throws if the denominator is zero', async () => {
|
||||
const [ n, d ] = [ new BigNumber(1), new BigNumber(0) ];
|
||||
const [n, d] = [new BigNumber(1), new BigNumber(0)];
|
||||
const expectedError = new FixedMathRevertErrors.FixedMathBinOpError(
|
||||
FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
|
||||
n.times(FIXED_POINT_DIVISOR),
|
||||
@@ -578,8 +554,22 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
});
|
||||
|
||||
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 = 13;
|
||||
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);
|
||||
@@ -626,7 +616,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
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), LN_PRECISION);
|
||||
assertFixedRoughlyEquals(r, ln(x), 12);
|
||||
});
|
||||
|
||||
it('ln(x), where x is close to 1', async () => {
|
||||
@@ -642,7 +632,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
blockchainTests.optional('fuzzing', () => {
|
||||
const inputs = _.times(FUZZ_COUNT, () => getRandomNumber(0, 1));
|
||||
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));
|
||||
@@ -655,6 +645,14 @@ blockchainTests('LibFixedMath', env => {
|
||||
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);
|
||||
@@ -696,7 +694,7 @@ blockchainTests('LibFixedMath', env => {
|
||||
});
|
||||
|
||||
blockchainTests.optional('fuzzing', () => {
|
||||
const inputs = _.times(FUZZ_COUNT, () => getRandomNumber(MIN_EXP_NUMBER, MAX_EXP_NUMBER));
|
||||
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));
|
||||
|
||||
@@ -15,6 +15,7 @@ import { StakingWrapper } from './utils/staking_wrapper';
|
||||
blockchainTests('Staking Pool Management', env => {
|
||||
// constants
|
||||
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
|
||||
const PPM_ONE = 1e6;
|
||||
// tokens & addresses
|
||||
let accounts: string[];
|
||||
let owner: string;
|
||||
@@ -44,7 +45,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_ONE;
|
||||
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
|
||||
// create pool
|
||||
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
|
||||
@@ -54,6 +55,17 @@ blockchainTests('Staking Pool Management', env => {
|
||||
const nextPoolId = await stakingWrapper.getNextStakingPoolIdAsync();
|
||||
expect(nextPoolId).to.be.equal(expectedNextPoolId);
|
||||
});
|
||||
it('Should throw if poolOperatorShare is > PPM_ONE', async () => {
|
||||
// test parameters
|
||||
const operatorAddress = users[0];
|
||||
const operatorShare = PPM_ONE + 1;
|
||||
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
|
||||
// create pool
|
||||
const tx = poolOperator.createStakingPoolAsync(operatorShare);
|
||||
const expectedPoolId = '0x0000000000000000000000000000000100000000000000000000000000000000';
|
||||
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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void> {
|
||||
// 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<void> {
|
||||
// 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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
contracts/staking/test/utils/number_utils.ts
Normal file
34
contracts/staking/test/utils/number_utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { Decimal } from 'decimal.js';
|
||||
|
||||
Decimal.set({ precision: 80 });
|
||||
|
||||
export type Numberish = BigNumber | string | number;
|
||||
|
||||
/**
|
||||
* 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 _toInteger = (n: Numberish) => {
|
||||
const _n = new BigNumber(n);
|
||||
const integerDigits = _n.integerValue().sd(true);
|
||||
const base = 10 ** (precision - integerDigits);
|
||||
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);
|
||||
}
|
||||
@@ -420,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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user