Add function assertions required for staking rewards fuzzing: withdrawDelegatorRewards, finalizePool, and endEpoch. Also adds payProtocolFee-related assertions to fillOrder
This commit is contained in:
parent
fff3c1eb36
commit
4663eec950
@ -3,6 +3,9 @@ export {
|
|||||||
DummyMultipleReturnERC20TokenContract,
|
DummyMultipleReturnERC20TokenContract,
|
||||||
DummyNoReturnERC20TokenContract,
|
DummyNoReturnERC20TokenContract,
|
||||||
WETH9Contract,
|
WETH9Contract,
|
||||||
|
WETH9Events,
|
||||||
|
WETH9DepositEventArgs,
|
||||||
|
WETH9TransferEventArgs,
|
||||||
ZRXTokenContract,
|
ZRXTokenContract,
|
||||||
DummyERC20TokenTransferEventArgs,
|
DummyERC20TokenTransferEventArgs,
|
||||||
ERC20TokenEventArgs,
|
ERC20TokenEventArgs,
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -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 });
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
93
contracts/integrations/test/framework/assertions/endEpoch.ts
Normal file
93
contracts/integrations/test/framework/assertions/endEpoch.ts
Normal 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 */
|
@ -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 */
|
||||||
|
267
contracts/integrations/test/framework/assertions/finalizePool.ts
Normal file
267
contracts/integrations/test/framework/assertions/finalizePool.ts
Normal 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 */
|
@ -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 */
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 => {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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",
|
||||||
|
@ -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';
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user