@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:
Lawrence Forman
2019-08-30 01:37:35 -04:00
committed by Lawrence Forman
parent cb1dc92594
commit 8d5e28f099
21 changed files with 415 additions and 301 deletions

View File

@@ -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
}
]
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)

View File

@@ -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
);

View File

@@ -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)
{

View File

@@ -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(

View File

@@ -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
);

View File

@@ -46,4 +46,13 @@ contract TestCobbDouglas is
alphaDenominator
);
}
function getCobbDouglasAlpha()
external
view
returns (uint256 numerator, uint256 denominator)
{
numerator = cobbDouglasAlphaNumerator;
denominator = cobbDouglasAlphaDenomintor;
}
}

View File

@@ -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);
});
}
});

View File

@@ -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));

View File

@@ -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];

View File

@@ -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);

View File

@@ -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`);
}
}
}

View 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);
}

View File

@@ -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;
}