Add function assertions required for staking rewards fuzzing: withdrawDelegatorRewards, finalizePool, and endEpoch. Also adds payProtocolFee-related assertions to fillOrder

This commit is contained in:
Michael Zhu 2019-12-04 14:44:19 -08:00
parent fff3c1eb36
commit 4663eec950
27 changed files with 817 additions and 239 deletions

View File

@ -3,6 +3,9 @@ export {
DummyMultipleReturnERC20TokenContract,
DummyNoReturnERC20TokenContract,
WETH9Contract,
WETH9Events,
WETH9DepositEventArgs,
WETH9TransferEventArgs,
ZRXTokenContract,
DummyERC20TokenTransferEventArgs,
ERC20TokenEventArgs,

View File

@ -113,3 +113,31 @@ export function calculateFillResults(
protocolFeePaid: safeMul(protocolFeeMultiplier, gasPrice),
};
}
export const LibFractions = {
add: (n1: BigNumber, d1: BigNumber, n2: BigNumber, d2: BigNumber): [BigNumber, BigNumber] => {
if (n1.isZero()) {
return [n2, d2];
}
if (n2.isZero()) {
return [n1, d1];
}
const numerator = safeAdd(safeMul(n1, d2), safeMul(n2, d1));
const denominator = safeMul(d1, d2);
return [numerator, denominator];
},
normalize: (
numerator: BigNumber,
denominator: BigNumber,
maxValue: BigNumber = new BigNumber(2 ** 127),
): [BigNumber, BigNumber] => {
if (numerator.isGreaterThan(maxValue) || denominator.isGreaterThan(maxValue)) {
const rescaleBase = numerator.isGreaterThanOrEqualTo(denominator)
? safeDiv(numerator, maxValue)
: safeDiv(denominator, maxValue);
return [safeDiv(numerator, rescaleBase), safeDiv(denominator, rescaleBase)];
} else {
return [numerator, denominator];
}
},
};

View File

@ -80,8 +80,7 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
}
private async *_validCreateStakingPool(): AsyncIterableIterator<AssertionResult> {
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
while (true) {
const operatorShare = Pseudorandom.integer(constants.PPM).toNumber();
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });

View File

@ -66,7 +66,7 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
private async *_validFillOrder(): AsyncIterableIterator<AssertionResult | void> {
const { actors, balanceStore } = this.actor.simulationEnvironment!;
const assertion = validFillOrderAssertion(this.actor.deployment);
const assertion = validFillOrderAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
while (true) {
const maker = Pseudorandom.sample(filterActorsByRole(actors, Maker));
if (maker === undefined) {

View File

@ -1,9 +1,10 @@
import { StakingPool, StakingPoolById } from '@0x/contracts-staking';
import { StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
@ -16,7 +17,7 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
/* tslint:disable:no-non-null-assertion */
export function validCreateStakingPoolAssertion(
deployment: DeploymentManager,
pools: StakingPoolById,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[number, boolean], string, string> {
const { stakingWrapper } = deployment.staking;
@ -44,7 +45,12 @@ export function validCreateStakingPoolAssertion(
expect(actualPoolId).to.equal(expectedPoolId);
// Adds the new pool to local state
pools[actualPoolId] = new StakingPool(txData.from!, operatorShare);
simulationEnvironment.stakingPools[actualPoolId] = {
operator: txData.from!,
operatorShare,
delegatedStake: new StoredBalance(),
lastFinalized: simulationEnvironment.currentEpoch,
};
},
});
}

View File

@ -0,0 +1,93 @@
import { WETH9DepositEventArgs, WETH9Events } from '@0x/contracts-erc20';
import { AggregatedStats, StakingEvents, StakingEpochEndedEventArgs } from '@0x/contracts-staking';
import { expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
interface EndEpochBeforeInfo {
wethReservedForPoolRewards: BigNumber;
aggregatedStatsBefore: AggregatedStats;
}
/**
* Returns a FunctionAssertion for `stake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX decrease and increase,
* respectively, by the input amount.
*/
/* tslint:disable:no-unnecessary-type-assertion */
export function validEndEpochAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[], EndEpochBeforeInfo, void> {
const { stakingWrapper } = deployment.staking;
const { balanceStore, currentEpoch } = simulationEnvironment;
return new FunctionAssertion(stakingWrapper, 'endEpoch', {
before: async () => {
await balanceStore.updateEthBalancesAsync();
const aggregatedStatsBefore = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
const wethReservedForPoolRewards = await stakingWrapper.wethReservedForPoolRewards().callAsync();
return { wethReservedForPoolRewards, aggregatedStatsBefore };
},
after: async (beforeInfo: EndEpochBeforeInfo, result: FunctionResult, _args: [], _txData: Partial<TxData>) => {
// Check WETH deposit event
const previousEthBalance = balanceStore.balances.eth[stakingWrapper.address];
if (previousEthBalance.isGreaterThan(0)) {
verifyEventsFromLogs<WETH9DepositEventArgs>(
result.receipt!.logs,
[
{
_owner: deployment.staking.stakingProxy.address,
_value: previousEthBalance,
},
],
WETH9Events.Deposit,
);
}
await balanceStore.updateErc20BalancesAsync();
const { wethReservedForPoolRewards, aggregatedStatsBefore } = beforeInfo;
const expectedAggregatedStats = {
...aggregatedStatsBefore,
rewardsAvailable: balanceStore.balances.erc20[stakingWrapper.address][
deployment.tokens.weth.address
].minus(wethReservedForPoolRewards),
};
const aggregatedStatsAfter = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
expect(aggregatedStatsAfter).to.deep.equal(expectedAggregatedStats);
const expectedEpochEndedEvents = aggregatedStatsAfter.numPoolsToFinalize.isZero()
? [
{
epoch: currentEpoch,
numPoolsToFinalize: aggregatedStatsAfter.numPoolsToFinalize,
rewardsAvailable: aggregatedStatsAfter.rewardsAvailable,
totalFeesCollected: aggregatedStatsAfter.totalFeesCollected,
totalWeightedStake: aggregatedStatsAfter.totalWeightedStake,
},
]
: [];
verifyEventsFromLogs<StakingEpochEndedEventArgs>(
result.receipt!.logs,
expectedEpochEndedEvents,
StakingEvents.EpochEnded,
);
expect(result.data, 'endEpoch should return the number of unfinalized pools').to.bignumber.equal(
aggregatedStatsAfter.numPoolsToFinalize,
);
simulationEnvironment.currentEpoch = currentEpoch.plus(1);
},
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@ -1,18 +1,22 @@
import { ERC20TokenEvents, ERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
import { ExchangeEvents, ExchangeFillEventArgs } from '@0x/contracts-exchange';
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import { AggregatedStats, constants as stakingConstants, PoolStats } from '@0x/contracts-staking';
import { expect, orderHashUtils, verifyEvents } from '@0x/contracts-test-utils';
import { FillResults, Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { Maker } from '../actors/maker';
import { filterActorsByRole } from '../actors/utils';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
function verifyFillEvents(
takerAddress: string,
txData: Partial<TxData>,
order: Order,
receipt: TransactionReceiptWithDecodedLogs,
deployment: DeploymentManager,
@ -24,6 +28,8 @@ function verifyFillEvents(
DeploymentManager.protocolFeeMultiplier,
DeploymentManager.gasPrice,
);
const takerAddress = txData.from!;
const value = new BigNumber(txData.value || 0);
// Ensure that the fill event was correct.
verifyEvents<ExchangeFillEventArgs>(
receipt,
@ -44,10 +50,7 @@ function verifyFillEvents(
ExchangeEvents.Fill,
);
// Ensure that the transfer events were correctly emitted.
verifyEvents<ERC20TokenTransferEventArgs>(
receipt,
[
const expectedTransferEvents = [
{
_from: takerAddress,
_to: order.makerAddress,
@ -68,14 +71,27 @@ function verifyFillEvents(
_to: order.feeRecipientAddress,
_value: fillResults.makerFeePaid,
},
{
];
// If not enough wei is sent to cover the protocol fee, there will be an additional WETH transfer event
if (value.isLessThan(DeploymentManager.protocolFee)) {
expectedTransferEvents.push({
_from: takerAddress,
_to: deployment.staking.stakingProxy.address,
_value: DeploymentManager.protocolFee,
},
],
ERC20TokenEvents.Transfer,
);
});
}
// Ensure that the transfer events were correctly emitted.
verifyEvents<ERC20TokenTransferEventArgs>(receipt, expectedTransferEvents, ERC20TokenEvents.Transfer);
}
interface FillOrderBeforeInfo {
poolStats: PoolStats;
aggregatedStats: AggregatedStats;
poolStake: BigNumber;
operatorStake: BigNumber;
poolId: string;
}
/**
@ -85,12 +101,40 @@ function verifyFillEvents(
/* tslint:disable:no-non-null-assertion */
export function validFillOrderAssertion(
deployment: DeploymentManager,
): FunctionAssertion<[Order, BigNumber, string], {}, FillResults> {
const exchange = deployment.exchange;
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults> {
const { stakingWrapper } = deployment.staking;
const { actors, currentEpoch } = simulationEnvironment;
return new FunctionAssertion<[Order, BigNumber, string], {}, FillResults>(exchange, 'fillOrder', {
return new FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults>(
deployment.exchange,
'fillOrder',
{
before: async (args: [Order, BigNumber, string]) => {
const [order] = args;
const maker = filterActorsByRole(actors, Maker).find(maker => maker.address === order.makerAddress);
const poolId = maker!.makerPoolId;
if (poolId === undefined) {
return;
} else {
const poolStats = PoolStats.fromArray(
await stakingWrapper.poolStatsByEpoch(poolId, currentEpoch).callAsync(),
);
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
const { currentEpochBalance: poolStake } = await stakingWrapper
.getTotalStakeDelegatedToPool(poolId)
.callAsync();
const { currentEpochBalance: operatorStake } = await stakingWrapper
.getStakeDelegatedToPoolByOwner(simulationEnvironment.stakingPools[poolId].operator, poolId)
.callAsync();
return { poolStats, aggregatedStats, poolStake, poolId, operatorStake };
}
},
after: async (
_beforeInfo,
beforeInfo: FillOrderBeforeInfo | void,
result: FunctionResult,
args: [Order, BigNumber, string],
txData: Partial<TxData>,
@ -101,11 +145,52 @@ export function validFillOrderAssertion(
expect(result.success, `Error: ${result.data}`).to.be.true();
// Ensure that the correct events were emitted.
verifyFillEvents(txData.from!, order, result.receipt!, deployment, fillAmount);
verifyFillEvents(txData, order, result.receipt!, deployment, fillAmount);
// TODO: Add validation for on-chain state (like balances)
if (beforeInfo !== undefined) {
const expectedPoolStats = { ...beforeInfo.poolStats };
const expectedAggregatedStats = { ...beforeInfo.aggregatedStats };
if (beforeInfo.poolStake.isGreaterThanOrEqualTo(stakingConstants.DEFAULT_PARAMS.minimumPoolStake)) {
if (beforeInfo.poolStats.feesCollected.isZero()) {
const membersStakeInPool = beforeInfo.poolStake.minus(beforeInfo.operatorStake);
const weightedStakeInPool = beforeInfo.operatorStake.plus(
ReferenceFunctions.getPartialAmountFloor(
stakingConstants.DEFAULT_PARAMS.rewardDelegatedStakeWeight,
new BigNumber(stakingConstants.PPM),
membersStakeInPool,
),
);
expectedPoolStats.membersStake = membersStakeInPool;
expectedPoolStats.weightedStake = weightedStakeInPool;
expectedAggregatedStats.totalWeightedStake = beforeInfo.aggregatedStats.totalWeightedStake.plus(
weightedStakeInPool,
);
expectedAggregatedStats.numPoolsToFinalize = beforeInfo.aggregatedStats.numPoolsToFinalize.plus(
1,
);
// TODO: Check event
}
expectedPoolStats.feesCollected = beforeInfo.poolStats.feesCollected.plus(
DeploymentManager.protocolFee,
);
expectedAggregatedStats.totalFeesCollected = beforeInfo.aggregatedStats.totalFeesCollected.plus(
DeploymentManager.protocolFee,
);
}
const poolStats = PoolStats.fromArray(
await stakingWrapper.poolStatsByEpoch(beforeInfo.poolId, currentEpoch).callAsync(),
);
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
expect(poolStats).to.deep.equal(expectedPoolStats);
expect(aggregatedStats).to.deep.equal(expectedAggregatedStats);
}
},
});
},
);
}
/* tslint:enable:no-non-null-assertion */
/* tslint:enable:no-unnecessary-type-assertion */

View File

@ -0,0 +1,267 @@
import { WETH9Events, WETH9TransferEventArgs } from '@0x/contracts-erc20';
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import {
AggregatedStats,
constants as stakingConstants,
PoolStats,
StakingEvents,
StakingEpochFinalizedEventArgs,
StakingRewardsPaidEventArgs,
} from '@0x/contracts-staking';
import {
assertRoughlyEquals,
constants,
expect,
filterLogsToArguments,
toDecimal,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
const COBB_DOUGLAS_PRECISION = 15;
const ALPHA_NUMERATOR = 1;
const ALPHA_DENOMINATOR = 3;
const COBB_DOUGLAS_ALPHA = toDecimal(ALPHA_NUMERATOR).dividedBy(toDecimal(ALPHA_DENOMINATOR));
function cobbDouglas(poolStats: PoolStats, aggregatedStats: AggregatedStats): BigNumber {
const { feesCollected, weightedStake } = poolStats;
const { rewardsAvailable, totalFeesCollected, totalWeightedStake } = aggregatedStats;
const feeRatio = toDecimal(feesCollected).dividedBy(toDecimal(totalFeesCollected));
const stakeRatio = toDecimal(weightedStake).dividedBy(toDecimal(totalWeightedStake));
// totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)
return new BigNumber(
feeRatio
.pow(COBB_DOUGLAS_ALPHA)
.times(stakeRatio.pow(toDecimal(1).minus(COBB_DOUGLAS_ALPHA)))
.times(toDecimal(rewardsAvailable))
.toFixed(0, BigNumber.ROUND_FLOOR),
);
}
interface FinalizePoolBeforeInfo {
poolStats: PoolStats;
aggregatedStats: AggregatedStats;
poolRewards: BigNumber;
cumulativeRewardsLastStored: BigNumber;
mostRecentCumulativeRewards: {
numerator: BigNumber;
denominator: BigNumber;
};
}
/**
* Returns a FunctionAssertion for `moveStake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker's
*/
/* tslint:disable:no-unnecessary-type-assertion */
export function validFinalizePoolAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[string], FinalizePoolBeforeInfo, void> {
const { stakingWrapper } = deployment.staking;
const { currentEpoch } = simulationEnvironment;
const prevEpoch = currentEpoch.minus(1);
return new FunctionAssertion<[string], FinalizePoolBeforeInfo, void>(stakingWrapper, 'finalizePool', {
before: async (args: [string]) => {
const [poolId] = args;
const poolStats = PoolStats.fromArray(await stakingWrapper.poolStatsByEpoch(poolId, prevEpoch).callAsync());
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(prevEpoch).callAsync(),
);
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
const [
mostRecentCumulativeRewards,
cumulativeRewardsLastStored,
] = await stakingWrapper.getMostRecentCumulativeReward(poolId).callAsync();
return {
poolStats,
aggregatedStats,
poolRewards,
cumulativeRewardsLastStored,
mostRecentCumulativeRewards,
};
},
after: async (beforeInfo: FinalizePoolBeforeInfo, result: FunctionResult, args: [string]) => {
// // Compute relevant epochs
// uint256 currentEpoch_ = currentEpoch;
// uint256 prevEpoch = currentEpoch_.safeSub(1);
const { stakingPools } = simulationEnvironment;
const [poolId] = args;
const pool = stakingPools[poolId];
// // Load the aggregated stats into memory; noop if no pools to finalize.
// IStructs.AggregatedStats memory aggregatedStats = aggregatedStatsByEpoch[prevEpoch];
// if (aggregatedStats.numPoolsToFinalize == 0) {
// return;
// }
//
// // Noop if the pool did not earn rewards or already finalized (has no fees).
// IStructs.PoolStats memory poolStats = poolStatsByEpoch[poolId][prevEpoch];
// if (poolStats.feesCollected == 0) {
// return;
// }
if (beforeInfo.aggregatedStats.numPoolsToFinalize.isZero() || beforeInfo.poolStats.feesCollected.isZero()) {
expect(result.receipt!.logs.length, 'Expect no events to be emitted').to.equal(0);
return;
}
// // Clear the pool stats so we don't finalize it again, and to recoup
// // some gas.
// delete poolStatsByEpoch[poolId][prevEpoch];
const poolStats = PoolStats.fromArray(await stakingWrapper.poolStatsByEpoch(poolId, prevEpoch).callAsync());
expect(poolStats).to.deep.equal({
feesCollected: constants.ZERO_AMOUNT,
weightedStake: constants.ZERO_AMOUNT,
membersStake: constants.ZERO_AMOUNT,
});
// // Compute the rewards.
// uint256 rewards = _getUnfinalizedPoolRewardsFromPoolStats(poolStats, aggregatedStats);
const rewards = BigNumber.min(
cobbDouglas(beforeInfo.poolStats, beforeInfo.aggregatedStats),
beforeInfo.aggregatedStats.rewardsAvailable.minus(beforeInfo.aggregatedStats.totalRewardsFinalized),
);
// // Pay the operator and update rewards for the pool.
// // Note that we credit at the CURRENT epoch even though these rewards
// // were earned in the previous epoch.
// (uint256 operatorReward, uint256 membersReward) = _syncPoolRewards(
// poolId,
// rewards,
// poolStats.membersStake
// );
//
// // Emit an event.
// emit RewardsPaid(
// currentEpoch_,
// poolId,
// operatorReward,
// membersReward
// );
//
// uint256 totalReward = operatorReward.safeAdd(membersReward);
const events = filterLogsToArguments<StakingRewardsPaidEventArgs>(
result.receipt!.logs,
StakingEvents.RewardsPaid,
);
expect(events.length, 'Number of RewardsPaid events emitted').to.equal(1);
const [rewardsPaidEvent] = events;
expect(rewardsPaidEvent.currentEpoch_, 'RewardsPaid event: currentEpoch_').to.bignumber.equal(currentEpoch);
expect(rewardsPaidEvent.poolId, 'RewardsPaid event: poolId').to.bignumber.equal(poolId);
expect(rewardsPaidEvent.currentEpoch_, 'RewardsPaid event: currentEpoch_').to.bignumber.equal(currentEpoch);
const { operatorReward, membersReward } = rewardsPaidEvent;
const totalReward = operatorReward.plus(membersReward);
assertRoughlyEquals(totalReward, rewards, COBB_DOUGLAS_PRECISION);
// See _computePoolRewardsSplit
if (beforeInfo.poolStats.membersStake.isZero()) {
expect(
operatorReward,
"operatorReward should equal totalReward if pool's membersStake is 0",
).to.bignumber.equal(totalReward);
} else {
expect(operatorReward).to.bignumber.equal(
ReferenceFunctions.getPartialAmountCeil(
new BigNumber(pool.operatorShare),
new BigNumber(stakingConstants.PPM),
totalReward,
),
);
}
// See _syncPoolRewards
const expectedTransferEvents = operatorReward.isGreaterThan(0)
? [
{
_from: deployment.staking.stakingProxy.address,
_to: pool.operator,
_value: operatorReward,
},
]
: [];
// Check for WETH transfer event emitted when paying out operator's reward.
verifyEventsFromLogs<WETH9TransferEventArgs>(
result.receipt!.logs,
expectedTransferEvents,
WETH9Events.Transfer,
);
// Check that pool rewards have increased.
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
expect(poolRewards).to.bignumber.equal(beforeInfo.poolRewards.plus(membersReward));
// Check that cumulative rewards have increased.
const [
mostRecentCumulativeRewards,
cumulativeRewardsLastStored,
] = await stakingWrapper.getMostRecentCumulativeReward(poolId).callAsync();
expect(cumulativeRewardsLastStored).to.bignumber.equal(currentEpoch);
let [numerator, denominator] = ReferenceFunctions.LibFractions.add(
beforeInfo.mostRecentCumulativeRewards.numerator,
beforeInfo.mostRecentCumulativeRewards.denominator,
membersReward,
beforeInfo.poolStats.membersStake,
);
[numerator, denominator] = ReferenceFunctions.LibFractions.normalize(numerator, denominator);
expect(mostRecentCumulativeRewards).to.deep.equal({
numerator,
denominator,
});
// // Increase `totalRewardsFinalized`.
// aggregatedStatsByEpoch[prevEpoch].totalRewardsFinalized =
// aggregatedStats.totalRewardsFinalized =
// aggregatedStats.totalRewardsFinalized.safeAdd(totalReward);
//
// // Decrease the number of unfinalized pools left.
// aggregatedStatsByEpoch[prevEpoch].numPoolsToFinalize =
// aggregatedStats.numPoolsToFinalize =
// aggregatedStats.numPoolsToFinalize.safeSub(1);
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(prevEpoch).callAsync(),
);
expect(aggregatedStats).to.deep.equal({
...beforeInfo.aggregatedStats,
totalRewardsFinalized: beforeInfo.aggregatedStats.totalRewardsFinalized.plus(totalReward),
numPoolsToFinalize: beforeInfo.aggregatedStats.numPoolsToFinalize.minus(1),
});
// // If there are no more unfinalized pools remaining, the epoch is
// // finalized.
// if (aggregatedStats.numPoolsToFinalize == 0) {
// emit EpochFinalized(
// prevEpoch,
// aggregatedStats.totalRewardsFinalized,
// aggregatedStats.rewardsAvailable.safeSub(aggregatedStats.totalRewardsFinalized)
// );
// }
const expectedEpochFinalizedEvents = aggregatedStats.numPoolsToFinalize.isZero()
? [
{
epoch: currentEpoch.minus(1),
rewardsPaid: aggregatedStats.totalRewardsFinalized,
rewardsRemaining: aggregatedStats.rewardsAvailable.minus(
aggregatedStats.totalRewardsFinalized,
),
},
]
: [];
verifyEventsFromLogs<StakingEpochFinalizedEventArgs>(
result.receipt!.logs,
expectedEpochFinalizedEvents,
StakingEvents.EpochFinalized,
);
pool.lastFinalized = currentEpoch;
},
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@ -0,0 +1,97 @@
import { WETH9TransferEventArgs, WETH9Events } from '@0x/contracts-erc20';
import { StoredBalance } from '@0x/contracts-staking';
import { expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
interface WithdrawDelegatorRewardsBeforeInfo {
delegatorStake: StoredBalance;
poolRewards: BigNumber;
wethReservedForPoolRewards: BigNumber;
delegatorReward: BigNumber;
}
/**
* Returns a FunctionAssertion for `stake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX decrease and increase,
* respectively, by the input amount.
*/
/* tslint:disable:no-unnecessary-type-assertion */
export function validWithdrawDelegatorRewardsAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[string], WithdrawDelegatorRewardsBeforeInfo, void> {
const { stakingWrapper } = deployment.staking;
const { currentEpoch } = simulationEnvironment;
return new FunctionAssertion(stakingWrapper, 'withdrawDelegatorRewards', {
before: async (args: [string], txData: Partial<TxData>) => {
const [poolId] = args;
const delegatorStake = await stakingWrapper
.getStakeDelegatedToPoolByOwner(txData.from!, poolId)
.callAsync();
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
const wethReservedForPoolRewards = await stakingWrapper.wethReservedForPoolRewards().callAsync();
const delegatorReward = BigNumber.sum(
await stakingWrapper
.computeMemberRewardOverInterval(
poolId,
delegatorStake.currentEpochBalance,
delegatorStake.currentEpoch,
delegatorStake.currentEpoch.plus(1),
)
.callAsync(),
await stakingWrapper
.computeMemberRewardOverInterval(
poolId,
delegatorStake.nextEpochBalance,
delegatorStake.currentEpoch.plus(1),
currentEpoch,
)
.callAsync(),
); // TODO: Test the reward computation more robustly
return { delegatorStake, poolRewards, wethReservedForPoolRewards, delegatorReward };
},
after: async (
beforeInfo: WithdrawDelegatorRewardsBeforeInfo,
result: FunctionResult,
args: [string],
txData: Partial<TxData>,
) => {
const [poolId] = args;
const expectedDelegatorStake = {
...beforeInfo.delegatorStake,
currentEpoch: currentEpoch,
currentEpochBalance: currentEpoch.isGreaterThan(beforeInfo.delegatorStake.currentEpoch)
? beforeInfo.delegatorStake.nextEpochBalance
: beforeInfo.delegatorStake.currentEpochBalance,
};
const delegatorStake = await stakingWrapper
.getStakeDelegatedToPoolByOwner(txData.from!, poolId)
.callAsync();
expect(delegatorStake).to.deep.equal(expectedDelegatorStake);
const expectedPoolRewards = beforeInfo.poolRewards.minus(beforeInfo.delegatorReward);
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
expect(poolRewards).to.bignumber.equal(expectedPoolRewards);
const expectedTransferEvents = beforeInfo.delegatorReward.isZero()
? []
: [{ _from: stakingWrapper.address, _to: txData.from!, _value: beforeInfo.delegatorReward }];
verifyEventsFromLogs<WETH9TransferEventArgs>(
result.receipt!.logs,
expectedTransferEvents,
WETH9Events.Transfer,
);
// TODO: Check CR
},
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@ -34,6 +34,7 @@ export class SimulationEnvironment {
globalStake: this.globalStake,
stakingPools: this.stakingPools,
balanceStore: this.balanceStore.toReadable(),
currentEpoch: this.currentEpoch,
};
}
}

View File

@ -102,15 +102,6 @@ contract TestMixinCumulativeRewards is
_cumulativeRewardsByPoolLastStored[poolId] = epoch;
}
/// @dev Returns the most recent cumulative reward for a given pool.
function getMostRecentCumulativeReward(bytes32 poolId)
public
returns (IStructs.Fraction memory)
{
uint256 mostRecentEpoch = _cumulativeRewardsByPoolLastStored[poolId];
return _cumulativeRewardsByPool[poolId][mostRecentEpoch];
}
/// @dev Returns the raw cumulative reward for a given pool in an epoch.
/// This is considered "raw" because the internal implementation
/// (_getCumulativeRewardAtEpochRaw) will query other state variables
@ -122,4 +113,3 @@ contract TestMixinCumulativeRewards is
return _cumulativeRewardsByPool[poolId][epoch];
}
}

View File

@ -21,7 +21,9 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol";
import "@0x/contracts-utils/contracts/src/LibFractions.sol";
import "../src/Staking.sol";
import "../src/interfaces/IStructs.sol";
contract TestStaking is
@ -56,6 +58,78 @@ contract TestStaking is
testZrxVaultAddress = zrxVaultAddress;
}
function getMostRecentCumulativeReward(bytes32 poolId)
external
view
returns (IStructs.Fraction memory cumulativeRewards, uint256 lastStoredEpoch)
{
lastStoredEpoch = _cumulativeRewardsByPoolLastStored[poolId];
cumulativeRewards = _cumulativeRewardsByPool[poolId][lastStoredEpoch];
}
function computeMemberRewardOverInterval(
bytes32 poolId,
uint256 memberStakeOverInterval,
uint256 beginEpoch,
uint256 endEpoch
)
external
view
returns (uint256 reward)
{
// Sanity check if we can skip computation, as it will result in zero.
if (memberStakeOverInterval == 0 || beginEpoch == endEpoch) {
return 0;
}
// Sanity check interval
require(beginEpoch < endEpoch, "CR_INTERVAL_INVALID");
// Sanity check begin reward
IStructs.Fraction memory beginReward = getCumulativeRewardAtEpoch(poolId, beginEpoch);
IStructs.Fraction memory endReward = getCumulativeRewardAtEpoch(poolId, endEpoch);
// Compute reward
reward = LibFractions.scaleDifference(
endReward.numerator,
endReward.denominator,
beginReward.numerator,
beginReward.denominator,
memberStakeOverInterval
);
}
function getCumulativeRewardAtEpoch(bytes32 poolId, uint256 epoch)
public
view
returns (IStructs.Fraction memory cumulativeReward)
{
// Return CR at `epoch`, given it's set.
cumulativeReward = _cumulativeRewardsByPool[poolId][epoch];
if (_isCumulativeRewardSet(cumulativeReward)) {
return cumulativeReward;
}
// Return CR at `epoch-1`, given it's set.
uint256 lastEpoch = epoch.safeSub(1);
cumulativeReward = _cumulativeRewardsByPool[poolId][lastEpoch];
if (_isCumulativeRewardSet(cumulativeReward)) {
return cumulativeReward;
}
// Return the most recent CR, given it's less than `epoch`.
uint256 mostRecentEpoch = _cumulativeRewardsByPoolLastStored[poolId];
if (mostRecentEpoch < epoch) {
cumulativeReward = _cumulativeRewardsByPool[poolId][mostRecentEpoch];
if (_isCumulativeRewardSet(cumulativeReward)) {
return cumulativeReward;
}
}
// Otherwise return an empty CR.
return IStructs.Fraction(0, 1);
}
/// @dev Overridden to use testWethAddress;
function getWethContract()
public

View File

@ -45,6 +45,7 @@ export { StakingRevertErrors, FixedMathRevertErrors } from '@0x/utils';
export { constants } from './constants';
export {
AggregatedStats,
AggregatedStatsByEpoch,
StakeInfo,
StakeStatus,
StoredBalance,
@ -52,6 +53,7 @@ export {
OwnerStakeByStatus,
GlobalStakeByStatus,
StakingPool,
PoolStats,
} from './types';
export {
ContractArtifact,

View File

@ -147,45 +147,49 @@ export interface OwnerStakeByStatus {
};
}
interface Fraction {
numerator: BigNumber;
denominator: BigNumber;
}
export class StakingPool {
public delegatedStake: StoredBalance = new StoredBalance();
public rewards: BigNumber = constants.ZERO_AMOUNT;
public cumulativeRewards: {
[epoch: string]: Fraction;
} = {};
public cumulativeRewardsLastStored: string = stakingConstants.INITIAL_EPOCH.toString();
public stats: {
[epoch: string]: PoolStats;
} = {};
constructor(public readonly operator: string, public operatorShare: number) {}
public finalize(): void {}
public creditProtocolFee(): void {}
public withdrawDelegatorRewards(delegator: string): void {}
public delegateStake(delegator: string, amount: BigNumber): void {}
public undelegateStake(delegator: string, amount: BigNumber): void {}
export interface StakingPool {
operator: string;
operatorShare: number;
delegatedStake: StoredBalance;
lastFinalized: BigNumber; // Epoch during which the pool was most recently finalized
}
export interface StakingPoolById {
[poolId: string]: StakingPool;
}
export interface PoolStats {
feesCollected: BigNumber;
weightedStake: BigNumber;
membersStake: BigNumber;
export class PoolStats {
public feesCollected: BigNumber = constants.ZERO_AMOUNT;
public weightedStake: BigNumber = constants.ZERO_AMOUNT;
public membersStake: BigNumber = constants.ZERO_AMOUNT;
public static fromArray(arr: [BigNumber, BigNumber, BigNumber]): PoolStats {
const poolStats = new PoolStats();
[poolStats.feesCollected, poolStats.weightedStake, poolStats.membersStake] = arr;
return poolStats;
}
}
export interface AggregatedStats {
rewardsAvailable: BigNumber;
numPoolsToFinalize: BigNumber;
totalFeesCollected: BigNumber;
totalWeightedStake: BigNumber;
totalRewardsFinalized: BigNumber;
export class AggregatedStats {
public rewardsAvailable: BigNumber = constants.ZERO_AMOUNT;
public numPoolsToFinalize: BigNumber = constants.ZERO_AMOUNT;
public totalFeesCollected: BigNumber = constants.ZERO_AMOUNT;
public totalWeightedStake: BigNumber = constants.ZERO_AMOUNT;
public totalRewardsFinalized: BigNumber = constants.ZERO_AMOUNT;
public static fromArray(arr: [BigNumber, BigNumber, BigNumber, BigNumber, BigNumber]): AggregatedStats {
const aggregatedStats = new AggregatedStats();
[
aggregatedStats.rewardsAvailable,
aggregatedStats.numPoolsToFinalize,
aggregatedStats.totalFeesCollected,
aggregatedStats.totalWeightedStake,
aggregatedStats.totalRewardsFinalized,
] = arr;
return aggregatedStats;
}
}
export interface AggregatedStatsByEpoch {
[epoch: string]: AggregatedStats;
}

View File

@ -1,5 +1,5 @@
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { blockchainTests, constants, describe, expect, shortZip } from '@0x/contracts-test-utils';
import { blockchainTests, constants, describe, expect, shortZip, toBaseUnitAmount } from '@0x/contracts-test-utils';
import { BigNumber, StakingRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
@ -9,7 +9,6 @@ import { FinalizerActor } from './actors/finalizer_actor';
import { PoolOperatorActor } from './actors/pool_operator_actor';
import { StakerActor } from './actors/staker_actor';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { toBaseUnitAmount } from './utils/number_utils';
// tslint:disable:no-unnecessary-type-assertion
// tslint:disable:max-file-line-count

View File

@ -1,5 +1,5 @@
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { blockchainTests, describe } from '@0x/contracts-test-utils';
import { blockchainTests, describe, toBaseUnitAmount } from '@0x/contracts-test-utils';
import { BigNumber, StakingRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
@ -7,7 +7,6 @@ import { StakeInfo, StakeStatus } from '../src/types';
import { StakerActor } from './actors/staker_actor';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { toBaseUnitAmount } from './utils/number_utils';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests.resets('Stake Statuses', env => {

View File

@ -1,20 +1,18 @@
import {
assertIntegerRoughlyEquals as assertRoughlyEquals,
blockchainTests,
constants,
expect,
filterLogsToArguments,
getRandomInteger,
Numberish,
randomAddress,
toBaseUnitAmount,
} from '@0x/contracts-test-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { LogEntry } from 'ethereum-types';
import { artifacts } from '../artifacts';
import {
assertIntegerRoughlyEquals as assertRoughlyEquals,
getRandomInteger,
toBaseUnitAmount,
} from '../utils/number_utils';
import {
TestDelegatorRewardsContract,

View File

@ -1,10 +1,13 @@
import {
assertIntegerRoughlyEquals,
blockchainTests,
constants,
expect,
filterLogsToArguments,
getRandomInteger,
Numberish,
shortZip,
toBaseUnitAmount,
} from '@0x/contracts-test-utils';
import { BigNumber, hexUtils, StakingRevertErrors } from '@0x/utils';
import { LogEntry } from 'ethereum-types';
@ -13,7 +16,6 @@ import * as _ from 'lodash';
import { constants as stakingConstants } from '../../src/constants';
import { artifacts } from '../artifacts';
import { assertIntegerRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
import {
IStakingEventsEpochEndedEventArgs,

View File

@ -1,9 +1,14 @@
import { blockchainTests, Numberish } from '@0x/contracts-test-utils';
import {
assertRoughlyEquals,
blockchainTests,
getRandomInteger,
getRandomPortion,
Numberish,
toDecimal,
} from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { assertRoughlyEquals, getRandomInteger, getRandomPortion, toDecimal } from '../utils/number_utils';
import { artifacts } from '../artifacts';
import { TestCobbDouglasContract } from '../wrappers';

View File

@ -1,10 +1,16 @@
import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils';
import { BigNumber, FixedMathRevertErrors, hexUtils } from '@0x/utils';
import {
assertRoughlyEquals,
blockchainTests,
expect,
fromFixed,
Numberish,
toDecimal,
toFixed,
} from '@0x/contracts-test-utils';
import { BigNumber, FixedMathRevertErrors } from '@0x/utils';
import { Decimal } from 'decimal.js';
import * as _ from 'lodash';
import { assertRoughlyEquals, fromFixed, toDecimal, toFixed } from '../utils/number_utils';
import { artifacts } from '../artifacts';
import { TestLibFixedMathContract } from '../wrappers';

View File

@ -1,9 +1,8 @@
import { blockchainTests, expect } from '@0x/contracts-test-utils';
import { blockchainTests, expect, toBaseUnitAmount } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants as stakingConstants } from '../../src/constants';
import { toBaseUnitAmount } from '../utils/number_utils';
import { artifacts } from '../artifacts';
import { TestMixinCumulativeRewardsContract } from '../wrappers';
@ -74,7 +73,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
await testContract
.addCumulativeReward(testPoolId, testRewards[0].numerator, testRewards[0].denominator)
.awaitTransactionSuccessAsync();
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync();
const [mostRecentCumulativeReward] = await testContract
.getMostRecentCumulativeReward(testPoolId)
.callAsync();
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
});
@ -86,7 +87,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
await testContract
.addCumulativeReward(testPoolId, testRewards[1].numerator, testRewards[1].denominator)
.awaitTransactionSuccessAsync();
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync();
const [mostRecentCumulativeReward] = await testContract
.getMostRecentCumulativeReward(testPoolId)
.callAsync();
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
});
@ -98,7 +101,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
await testContract
.addCumulativeReward(testPoolId, testRewards[1].numerator, testRewards[1].denominator)
.awaitTransactionSuccessAsync();
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync();
const [mostRecentCumulativeReward] = await testContract
.getMostRecentCumulativeReward(testPoolId)
.callAsync();
expect(mostRecentCumulativeReward).to.deep.equal(sumOfTestRewardsNormalized);
});
});

View File

@ -3,6 +3,7 @@ import {
constants,
expect,
filterLogsToArguments,
getRandomInteger,
Numberish,
randomAddress,
} from '@0x/contracts-test-utils';
@ -19,8 +20,6 @@ import {
TestProtocolFeesEvents,
} from '../wrappers';
import { getRandomInteger } from '../utils/number_utils';
blockchainTests('Protocol Fees unit tests', env => {
let ownerAddress: string;
let exchangeAddress: string;

View File

@ -1,4 +1,4 @@
import { BlockchainTestsEnvironment, expect, txDefaults } from '@0x/contracts-test-utils';
import { BlockchainTestsEnvironment, expect, toBaseUnitAmount, txDefaults } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { DecodedLogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
@ -8,7 +8,6 @@ import { artifacts } from '../artifacts';
import { TestCumulativeRewardTrackingContract, TestCumulativeRewardTrackingEvents } from '../wrappers';
import { StakingApiWrapper } from './api_wrapper';
import { toBaseUnitAmount } from './number_utils';
export enum TestAction {
Finalize,

View File

@ -1,116 +0,0 @@
import { expect, Numberish } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as crypto from 'crypto';
import { Decimal } from 'decimal.js';
Decimal.set({ precision: 80 });
/**
* Convert `x` to a `Decimal` type.
*/
export function toDecimal(x: Numberish): Decimal {
if (BigNumber.isBigNumber(x)) {
return new Decimal(x.toString(10));
}
return new Decimal(x);
}
/**
* Generate a random integer between `min` and `max`, inclusive.
*/
export function getRandomInteger(min: Numberish, max: Numberish): BigNumber {
const range = new BigNumber(max).minus(min);
return getRandomPortion(range).plus(min);
}
/**
* Generate a random integer between `0` and `total`, inclusive.
*/
export function getRandomPortion(total: Numberish): BigNumber {
return new BigNumber(total).times(getRandomFloat(0, 1)).integerValue(BigNumber.ROUND_HALF_UP);
}
/**
* Generate a random, high-precision decimal between `min` and `max`, inclusive.
*/
export function getRandomFloat(min: Numberish, max: Numberish): BigNumber {
// Generate a really high precision number between [0, 1]
const r = new BigNumber(crypto.randomBytes(32).toString('hex'), 16).dividedBy(new BigNumber(2).pow(256).minus(1));
return new BigNumber(max)
.minus(min)
.times(r)
.plus(min);
}
export const FIXED_POINT_BASE = new BigNumber(2).pow(127);
/**
* Convert `n` to fixed-point integer represenatation.
*/
export function toFixed(n: Numberish): BigNumber {
return new BigNumber(n).times(FIXED_POINT_BASE).integerValue();
}
/**
* Convert `n` from fixed-point integer represenatation.
*/
export function fromFixed(n: Numberish): BigNumber {
return new BigNumber(n).dividedBy(FIXED_POINT_BASE);
}
/**
* Converts two decimal numbers to integers with `precision` digits, then returns
* the absolute difference.
*/
export function getNumericalDivergence(a: Numberish, b: Numberish, precision: number = 18): number {
const _a = new BigNumber(a);
const _b = new BigNumber(b);
const maxIntegerDigits = Math.max(
_a.integerValue(BigNumber.ROUND_DOWN).sd(true),
_b.integerValue(BigNumber.ROUND_DOWN).sd(true),
);
const _toInteger = (n: BigNumber) => {
const base = 10 ** (precision - maxIntegerDigits);
return n.times(base).integerValue(BigNumber.ROUND_DOWN);
};
return _toInteger(_a)
.minus(_toInteger(_b))
.abs()
.toNumber();
}
/**
* Asserts that two numbers are equal up to `precision` digits.
*/
export function assertRoughlyEquals(actual: Numberish, expected: Numberish, precision: number = 18): void {
if (getNumericalDivergence(actual, expected, precision) <= 1) {
return;
}
expect(actual).to.bignumber.eq(expected);
}
/**
* Asserts that two numbers are equal with up to `maxError` difference between them.
*/
export function assertIntegerRoughlyEquals(actual: Numberish, expected: Numberish, maxError: number = 1): void {
const diff = new BigNumber(actual)
.minus(expected)
.abs()
.toNumber();
if (diff <= maxError) {
return;
}
expect(actual).to.bignumber.eq(expected);
}
/**
* Converts `amount` into a base unit amount with a specified number of digits. If
* no digits are provided, this defaults to 18 digits.
*/
export function toBaseUnitAmount(amount: Numberish, decimals?: number): BigNumber {
const amountAsBigNumber = new BigNumber(amount);
const baseDecimals = decimals !== undefined ? decimals : 18;
const baseUnitAmount = Web3Wrapper.toBaseUnitAmount(amountAsBigNumber, baseDecimals);
return baseUnitAmount;
}

View File

@ -36,6 +36,7 @@
"devDependencies": {
"@0x/sol-compiler": "^4.0.1",
"@0x/tslint-config": "^4.0.0",
"decimal.js": "^10.2.0",
"npm-run-all": "^4.1.2",
"shx": "^0.2.2",
"tslint": "5.11.0",

View File

@ -54,12 +54,15 @@ export { replaceKeysDeep, shortZip } from './lang_utils';
export {
assertIntegerRoughlyEquals,
assertRoughlyEquals,
fromFixed,
getRandomFloat,
getRandomInteger,
getRandomPortion,
getNumericalDivergence,
getPercentageOfValue,
toBaseUnitAmount,
toDecimal,
toFixed,
} from './number_utils';
export { orderHashUtils } from './order_hash';
export { transactionHashUtils } from './transaction_hash';

View File

@ -5,6 +5,19 @@ import * as crypto from 'crypto';
import { expect } from './chai_setup';
import { constants } from './constants';
import { Numberish } from './types';
import { Decimal } from 'decimal.js';
Decimal.set({ precision: 80 });
/**
* Convert `x` to a `Decimal` type.
*/
export function toDecimal(x: Numberish): Decimal {
if (BigNumber.isBigNumber(x)) {
return new Decimal(x.toString(10));
}
return new Decimal(x);
}
/**
* Generate a random integer between `min` and `max`, inclusive.
@ -33,6 +46,22 @@ export function getRandomFloat(min: Numberish, max: Numberish): BigNumber {
.plus(min);
}
export const FIXED_POINT_BASE = new BigNumber(2).pow(127);
/**
* Convert `n` to fixed-point integer represenatation.
*/
export function toFixed(n: Numberish): BigNumber {
return new BigNumber(n).times(FIXED_POINT_BASE).integerValue();
}
/**
* Convert `n` from fixed-point integer represenatation.
*/
export function fromFixed(n: Numberish): BigNumber {
return new BigNumber(n).dividedBy(FIXED_POINT_BASE);
}
/**
* Converts two decimal numbers to integers with `precision` digits, then returns
* the absolute difference.