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, DummyMultipleReturnERC20TokenContract,
DummyNoReturnERC20TokenContract, DummyNoReturnERC20TokenContract,
WETH9Contract, WETH9Contract,
WETH9Events,
WETH9DepositEventArgs,
WETH9TransferEventArgs,
ZRXTokenContract, ZRXTokenContract,
DummyERC20TokenTransferEventArgs, DummyERC20TokenTransferEventArgs,
ERC20TokenEventArgs, ERC20TokenEventArgs,

View File

@ -113,3 +113,31 @@ export function calculateFillResults(
protocolFeePaid: safeMul(protocolFeeMultiplier, gasPrice), 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> { private async *_validCreateStakingPool(): AsyncIterableIterator<AssertionResult> {
const { stakingPools } = this.actor.simulationEnvironment!; const assertion = validCreateStakingPoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
while (true) { while (true) {
const operatorShare = Pseudorandom.integer(constants.PPM).toNumber(); const operatorShare = Pseudorandom.integer(constants.PPM).toNumber();
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address }); 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> { private async *_validFillOrder(): AsyncIterableIterator<AssertionResult | void> {
const { actors, balanceStore } = this.actor.simulationEnvironment!; const { actors, balanceStore } = this.actor.simulationEnvironment!;
const assertion = validFillOrderAssertion(this.actor.deployment); const assertion = validFillOrderAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
while (true) { while (true) {
const maker = Pseudorandom.sample(filterActorsByRole(actors, Maker)); const maker = Pseudorandom.sample(filterActorsByRole(actors, Maker));
if (maker === undefined) { 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 { expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types'; import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager'; import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion'; import { FunctionAssertion, FunctionResult } from './function_assertion';
@ -16,7 +17,7 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
/* tslint:disable:no-non-null-assertion */ /* tslint:disable:no-non-null-assertion */
export function validCreateStakingPoolAssertion( export function validCreateStakingPoolAssertion(
deployment: DeploymentManager, deployment: DeploymentManager,
pools: StakingPoolById, simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[number, boolean], string, string> { ): FunctionAssertion<[number, boolean], string, string> {
const { stakingWrapper } = deployment.staking; const { stakingWrapper } = deployment.staking;
@ -44,7 +45,12 @@ export function validCreateStakingPoolAssertion(
expect(actualPoolId).to.equal(expectedPoolId); expect(actualPoolId).to.equal(expectedPoolId);
// Adds the new pool to local state // 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 { ERC20TokenEvents, ERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
import { ExchangeEvents, ExchangeFillEventArgs } from '@0x/contracts-exchange'; import { ExchangeEvents, ExchangeFillEventArgs } from '@0x/contracts-exchange';
import { ReferenceFunctions } from '@0x/contracts-exchange-libs'; 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 { expect, orderHashUtils, verifyEvents } from '@0x/contracts-test-utils';
import { FillResults, Order } from '@0x/types'; import { FillResults, Order } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types'; import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { Maker } from '../actors/maker';
import { filterActorsByRole } from '../actors/utils';
import { DeploymentManager } from '../deployment_manager'; import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion'; import { FunctionAssertion, FunctionResult } from './function_assertion';
function verifyFillEvents( function verifyFillEvents(
takerAddress: string, txData: Partial<TxData>,
order: Order, order: Order,
receipt: TransactionReceiptWithDecodedLogs, receipt: TransactionReceiptWithDecodedLogs,
deployment: DeploymentManager, deployment: DeploymentManager,
@ -24,6 +28,8 @@ function verifyFillEvents(
DeploymentManager.protocolFeeMultiplier, DeploymentManager.protocolFeeMultiplier,
DeploymentManager.gasPrice, DeploymentManager.gasPrice,
); );
const takerAddress = txData.from!;
const value = new BigNumber(txData.value || 0);
// Ensure that the fill event was correct. // Ensure that the fill event was correct.
verifyEvents<ExchangeFillEventArgs>( verifyEvents<ExchangeFillEventArgs>(
receipt, receipt,
@ -44,38 +50,48 @@ function verifyFillEvents(
ExchangeEvents.Fill, ExchangeEvents.Fill,
); );
const expectedTransferEvents = [
{
_from: takerAddress,
_to: order.makerAddress,
_value: fillResults.takerAssetFilledAmount,
},
{
_from: order.makerAddress,
_to: takerAddress,
_value: fillResults.makerAssetFilledAmount,
},
{
_from: takerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.takerFeePaid,
},
{
_from: order.makerAddress,
_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,
});
}
// Ensure that the transfer events were correctly emitted. // Ensure that the transfer events were correctly emitted.
verifyEvents<ERC20TokenTransferEventArgs>( verifyEvents<ERC20TokenTransferEventArgs>(receipt, expectedTransferEvents, ERC20TokenEvents.Transfer);
receipt, }
[
{ interface FillOrderBeforeInfo {
_from: takerAddress, poolStats: PoolStats;
_to: order.makerAddress, aggregatedStats: AggregatedStats;
_value: fillResults.takerAssetFilledAmount, poolStake: BigNumber;
}, operatorStake: BigNumber;
{ poolId: string;
_from: order.makerAddress,
_to: takerAddress,
_value: fillResults.makerAssetFilledAmount,
},
{
_from: takerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.takerFeePaid,
},
{
_from: order.makerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.makerFeePaid,
},
{
_from: takerAddress,
_to: deployment.staking.stakingProxy.address,
_value: DeploymentManager.protocolFee,
},
],
ERC20TokenEvents.Transfer,
);
} }
/** /**
@ -85,27 +101,96 @@ function verifyFillEvents(
/* tslint:disable:no-non-null-assertion */ /* tslint:disable:no-non-null-assertion */
export function validFillOrderAssertion( export function validFillOrderAssertion(
deployment: DeploymentManager, deployment: DeploymentManager,
): FunctionAssertion<[Order, BigNumber, string], {}, FillResults> { simulationEnvironment: SimulationEnvironment,
const exchange = deployment.exchange; ): 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>(
after: async ( deployment.exchange,
_beforeInfo, 'fillOrder',
result: FunctionResult, {
args: [Order, BigNumber, string], before: async (args: [Order, BigNumber, string]) => {
txData: Partial<TxData>, const [order] = args;
) => { const maker = filterActorsByRole(actors, Maker).find(maker => maker.address === order.makerAddress);
const [order, fillAmount] = args;
// Ensure that the tx succeeded. const poolId = maker!.makerPoolId;
expect(result.success, `Error: ${result.data}`).to.be.true(); 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: FillOrderBeforeInfo | void,
result: FunctionResult,
args: [Order, BigNumber, string],
txData: Partial<TxData>,
) => {
const [order, fillAmount] = args;
// Ensure that the correct events were emitted. // Ensure that the tx succeeded.
verifyFillEvents(txData.from!, order, result.receipt!, deployment, fillAmount); expect(result.success, `Error: ${result.data}`).to.be.true();
// TODO: Add validation for on-chain state (like balances) // Ensure that the correct events were emitted.
verifyFillEvents(txData, order, result.receipt!, deployment, fillAmount);
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-non-null-assertion */
/* tslint:enable:no-unnecessary-type-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, globalStake: this.globalStake,
stakingPools: this.stakingPools, stakingPools: this.stakingPools,
balanceStore: this.balanceStore.toReadable(), balanceStore: this.balanceStore.toReadable(),
currentEpoch: this.currentEpoch,
}; };
} }
} }

View File

@ -102,15 +102,6 @@ contract TestMixinCumulativeRewards is
_cumulativeRewardsByPoolLastStored[poolId] = epoch; _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. /// @dev Returns the raw cumulative reward for a given pool in an epoch.
/// This is considered "raw" because the internal implementation /// This is considered "raw" because the internal implementation
/// (_getCumulativeRewardAtEpochRaw) will query other state variables /// (_getCumulativeRewardAtEpochRaw) will query other state variables
@ -122,4 +113,3 @@ contract TestMixinCumulativeRewards is
return _cumulativeRewardsByPool[poolId][epoch]; 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-asset-proxy/contracts/src/interfaces/IAssetData.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol";
import "@0x/contracts-utils/contracts/src/LibFractions.sol";
import "../src/Staking.sol"; import "../src/Staking.sol";
import "../src/interfaces/IStructs.sol";
contract TestStaking is contract TestStaking is
@ -56,6 +58,78 @@ contract TestStaking is
testZrxVaultAddress = zrxVaultAddress; 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; /// @dev Overridden to use testWethAddress;
function getWethContract() function getWethContract()
public public

View File

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

View File

@ -147,45 +147,49 @@ export interface OwnerStakeByStatus {
}; };
} }
interface Fraction { export interface StakingPool {
numerator: BigNumber; operator: string;
denominator: BigNumber; operatorShare: number;
} delegatedStake: StoredBalance;
lastFinalized: BigNumber; // Epoch during which the pool was most recently finalized
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 StakingPoolById { export interface StakingPoolById {
[poolId: string]: StakingPool; [poolId: string]: StakingPool;
} }
export interface PoolStats { export class PoolStats {
feesCollected: BigNumber; public feesCollected: BigNumber = constants.ZERO_AMOUNT;
weightedStake: BigNumber; public weightedStake: BigNumber = constants.ZERO_AMOUNT;
membersStake: BigNumber; 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 { export class AggregatedStats {
rewardsAvailable: BigNumber; public rewardsAvailable: BigNumber = constants.ZERO_AMOUNT;
numPoolsToFinalize: BigNumber; public numPoolsToFinalize: BigNumber = constants.ZERO_AMOUNT;
totalFeesCollected: BigNumber; public totalFeesCollected: BigNumber = constants.ZERO_AMOUNT;
totalWeightedStake: BigNumber; public totalWeightedStake: BigNumber = constants.ZERO_AMOUNT;
totalRewardsFinalized: BigNumber; 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 { 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 { BigNumber, StakingRevertErrors } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -9,7 +9,6 @@ import { FinalizerActor } from './actors/finalizer_actor';
import { PoolOperatorActor } from './actors/pool_operator_actor'; import { PoolOperatorActor } from './actors/pool_operator_actor';
import { StakerActor } from './actors/staker_actor'; import { StakerActor } from './actors/staker_actor';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper'; import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { toBaseUnitAmount } from './utils/number_utils';
// tslint:disable:no-unnecessary-type-assertion // tslint:disable:no-unnecessary-type-assertion
// tslint:disable:max-file-line-count // tslint:disable:max-file-line-count

View File

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

View File

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

View File

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

View File

@ -1,10 +1,16 @@
import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils'; import {
import { BigNumber, FixedMathRevertErrors, hexUtils } from '@0x/utils'; assertRoughlyEquals,
blockchainTests,
expect,
fromFixed,
Numberish,
toDecimal,
toFixed,
} from '@0x/contracts-test-utils';
import { BigNumber, FixedMathRevertErrors } from '@0x/utils';
import { Decimal } from 'decimal.js'; import { Decimal } from 'decimal.js';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { assertRoughlyEquals, fromFixed, toDecimal, toFixed } from '../utils/number_utils';
import { artifacts } from '../artifacts'; import { artifacts } from '../artifacts';
import { TestLibFixedMathContract } from '../wrappers'; 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 { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { constants as stakingConstants } from '../../src/constants'; import { constants as stakingConstants } from '../../src/constants';
import { toBaseUnitAmount } from '../utils/number_utils';
import { artifacts } from '../artifacts'; import { artifacts } from '../artifacts';
import { TestMixinCumulativeRewardsContract } from '../wrappers'; import { TestMixinCumulativeRewardsContract } from '../wrappers';
@ -74,7 +73,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
await testContract await testContract
.addCumulativeReward(testPoolId, testRewards[0].numerator, testRewards[0].denominator) .addCumulativeReward(testPoolId, testRewards[0].numerator, testRewards[0].denominator)
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync(); const [mostRecentCumulativeReward] = await testContract
.getMostRecentCumulativeReward(testPoolId)
.callAsync();
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]); expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
}); });
@ -86,7 +87,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
await testContract await testContract
.addCumulativeReward(testPoolId, testRewards[1].numerator, testRewards[1].denominator) .addCumulativeReward(testPoolId, testRewards[1].numerator, testRewards[1].denominator)
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync(); const [mostRecentCumulativeReward] = await testContract
.getMostRecentCumulativeReward(testPoolId)
.callAsync();
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]); expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
}); });
@ -98,7 +101,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
await testContract await testContract
.addCumulativeReward(testPoolId, testRewards[1].numerator, testRewards[1].denominator) .addCumulativeReward(testPoolId, testRewards[1].numerator, testRewards[1].denominator)
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync(); const [mostRecentCumulativeReward] = await testContract
.getMostRecentCumulativeReward(testPoolId)
.callAsync();
expect(mostRecentCumulativeReward).to.deep.equal(sumOfTestRewardsNormalized); expect(mostRecentCumulativeReward).to.deep.equal(sumOfTestRewardsNormalized);
}); });
}); });

View File

@ -3,6 +3,7 @@ import {
constants, constants,
expect, expect,
filterLogsToArguments, filterLogsToArguments,
getRandomInteger,
Numberish, Numberish,
randomAddress, randomAddress,
} from '@0x/contracts-test-utils'; } from '@0x/contracts-test-utils';
@ -19,8 +20,6 @@ import {
TestProtocolFeesEvents, TestProtocolFeesEvents,
} from '../wrappers'; } from '../wrappers';
import { getRandomInteger } from '../utils/number_utils';
blockchainTests('Protocol Fees unit tests', env => { blockchainTests('Protocol Fees unit tests', env => {
let ownerAddress: string; let ownerAddress: string;
let exchangeAddress: 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 { BigNumber } from '@0x/utils';
import { DecodedLogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { DecodedLogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -8,7 +8,6 @@ import { artifacts } from '../artifacts';
import { TestCumulativeRewardTrackingContract, TestCumulativeRewardTrackingEvents } from '../wrappers'; import { TestCumulativeRewardTrackingContract, TestCumulativeRewardTrackingEvents } from '../wrappers';
import { StakingApiWrapper } from './api_wrapper'; import { StakingApiWrapper } from './api_wrapper';
import { toBaseUnitAmount } from './number_utils';
export enum TestAction { export enum TestAction {
Finalize, 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": { "devDependencies": {
"@0x/sol-compiler": "^4.0.1", "@0x/sol-compiler": "^4.0.1",
"@0x/tslint-config": "^4.0.0", "@0x/tslint-config": "^4.0.0",
"decimal.js": "^10.2.0",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"shx": "^0.2.2", "shx": "^0.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",

View File

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

View File

@ -5,6 +5,19 @@ import * as crypto from 'crypto';
import { expect } from './chai_setup'; import { expect } from './chai_setup';
import { constants } from './constants'; import { constants } from './constants';
import { Numberish } from './types'; 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. * Generate a random integer between `min` and `max`, inclusive.
@ -33,6 +46,22 @@ export function getRandomFloat(min: Numberish, max: Numberish): BigNumber {
.plus(min); .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 * Converts two decimal numbers to integers with `precision` digits, then returns
* the absolute difference. * the absolute difference.