Merge pull request #2431 from 0xProject/feature/fuzz/revert-assertions

`@0x/contracts-integrations`: Negative assertions for fuzzing
This commit is contained in:
mzhu25
2020-01-17 13:10:00 -08:00
committed by GitHub
16 changed files with 469 additions and 81 deletions

View File

@@ -9,6 +9,10 @@
{
"note": "Fuzz tests for `matchOrders` and `matchOrdersWithMaximalFill`.",
"pr": 2437
},
{
"note": "Add various negative assertions for fuzz tests",
"pr": 2431
}
]
},

View File

@@ -7,7 +7,11 @@ import { filterLogsToArguments, web3Wrapper } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { BlockParamLiteral, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { validEndEpochAssertion } from '../assertions/endEpoch';
import {
endEpochTooEarlyAssertion,
endEpochUnfinalizedPoolsAssertion,
validEndEpochAssertion,
} from '../assertions/endEpoch';
import { validFinalizePoolAssertion } from '../assertions/finalizePool';
import { AssertionResult } from '../assertions/function_assertion';
import { Pseudorandom } from '../utils/pseudorandom';
@@ -43,6 +47,7 @@ export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Con
...this.actor.simulationActions,
validFinalizePool: this._validFinalizePool(),
validEndEpoch: this._validEndEpoch(),
invalidEndEpoch: this._invalidEndEpoch(),
};
}
@@ -118,6 +123,27 @@ export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Con
}
}
private async *_invalidEndEpoch(): AsyncIterableIterator<AssertionResult | void> {
const { stakingWrapper } = this.actor.deployment.staking;
while (true) {
const { simulationEnvironment } = this.actor;
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper
.aggregatedStatsByEpoch(simulationEnvironment!.currentEpoch.minus(1))
.callAsync(),
);
const assertion = aggregatedStats.numPoolsToFinalize.isGreaterThan(0)
? endEpochUnfinalizedPoolsAssertion(
this.actor.deployment,
simulationEnvironment!,
aggregatedStats.numPoolsToFinalize,
)
: endEpochTooEarlyAssertion(this.actor.deployment);
yield assertion.executeAsync([], { from: this.actor.address });
}
}
private async _fastForwardToNextEpochAsync(): Promise<void> {
const { stakingWrapper } = this.actor.deployment.staking;

View File

@@ -1,10 +1,14 @@
import { constants, StakingPoolById } from '@0x/contracts-staking';
import { constants as stakingConstants, StakingPoolById } from '@0x/contracts-staking';
import { constants } from '@0x/contracts-test-utils';
import '@azure/core-asynciterator-polyfill';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool';
import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare';
import { invalidCreateStakingPoolAssertion, validCreateStakingPoolAssertion } from '../assertions/createStakingPool';
import {
invalidDecreaseStakingPoolOperatorShareAssertion,
validDecreaseStakingPoolOperatorShareAssertion,
} from '../assertions/decreaseStakingPoolOperatorShare';
import { AssertionResult } from '../assertions/function_assertion';
import { Distributions, Pseudorandom } from '../utils/pseudorandom';
@@ -41,7 +45,9 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
this.actor.simulationActions = {
...this.actor.simulationActions,
validCreateStakingPool: this._validCreateStakingPool(),
invalidCreateStakingPool: this._invalidCreateStakingPool(),
validDecreaseStakingPoolOperatorShare: this._validDecreaseStakingPoolOperatorShare(),
invalidDecreaseStakingPoolOperatorShare: this._invalidDecreaseStakingPoolOperatorShare(),
};
}
@@ -85,8 +91,20 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
while (true) {
const operatorShare = Pseudorandom.integer(
0,
constants.PPM,
Distributions.Kumaraswamy(0.2, 0.2),
stakingConstants.PPM,
Distributions.Kumaraswamy(),
).toNumber();
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });
}
}
private async *_invalidCreateStakingPool(): AsyncIterableIterator<AssertionResult> {
const assertion = invalidCreateStakingPoolAssertion(this.actor.deployment);
while (true) {
const operatorShare = Pseudorandom.integer(
(stakingConstants.PPM as number) + 1,
constants.MAX_UINT32,
Distributions.Kumaraswamy(),
).toNumber();
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });
}
@@ -103,7 +121,25 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
const operatorShare = Pseudorandom.integer(
0,
stakingPools[poolId].operatorShare,
Distributions.Kumaraswamy(0.2, 0.2),
Distributions.Kumaraswamy(),
).toNumber();
yield assertion.executeAsync([poolId, operatorShare], { from: this.actor.address });
}
}
}
private async *_invalidDecreaseStakingPoolOperatorShare(): AsyncIterableIterator<AssertionResult | void> {
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = invalidDecreaseStakingPoolOperatorShareAssertion(this.actor.deployment);
while (true) {
const poolId = Pseudorandom.sample(this._getOperatorPoolIds(stakingPools));
if (poolId === undefined) {
yield undefined;
} else {
const operatorShare = Pseudorandom.integer(
(stakingPools[poolId].operatorShare as number) + 1,
constants.MAX_UINT32,
Distributions.Kumaraswamy(),
).toNumber();
yield assertion.executeAsync([poolId, operatorShare], { from: this.actor.address });
}

View File

@@ -1,13 +1,18 @@
import { OwnerStakeByStatus, StakeInfo, StakeStatus, StoredBalance } from '@0x/contracts-staking';
import { constants } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import '@azure/core-asynciterator-polyfill';
import * as _ from 'lodash';
import { AssertionResult } from '../assertions/function_assertion';
import { validMoveStakeAssertion } from '../assertions/moveStake';
import { assetProxyTransferFailedAssertion } from '../assertions/generic_assertions';
import { moveStakeNonexistentPoolAssertion, validMoveStakeAssertion } from '../assertions/moveStake';
import { validStakeAssertion } from '../assertions/stake';
import { validUnstakeAssertion } from '../assertions/unstake';
import { validWithdrawDelegatorRewardsAssertion } from '../assertions/withdrawDelegatorRewards';
import { invalidUnstakeAssertion, validUnstakeAssertion } from '../assertions/unstake';
import {
invalidWithdrawDelegatorRewardsAssertion,
validWithdrawDelegatorRewardsAssertion,
} from '../assertions/withdrawDelegatorRewards';
import { Pseudorandom } from '../utils/pseudorandom';
import { Actor, Constructor } from './base';
@@ -45,9 +50,13 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
this.actor.simulationActions = {
...this.actor.simulationActions,
validStake: this._validStake(),
invalidStake: this._invalidStake(),
validUnstake: this._validUnstake(),
invalidUnstake: this._invalidUnstake(),
validMoveStake: this._validMoveStake(),
moveStakeNonexistentPool: this._moveStakeNonexistentPool(),
validWithdrawDelegatorRewards: this._validWithdrawDelegatorRewards(),
invalidWithdrawDelegatorRewards: this._invalidWithdrawDelegatorRewards(),
};
}
@@ -84,6 +93,19 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
}
}
private async *_invalidStake(): AsyncIterableIterator<AssertionResult> {
const { zrx } = this.actor.deployment.tokens;
const { deployment, balanceStore } = this.actor.simulationEnvironment!;
const assertion = assetProxyTransferFailedAssertion(deployment.staking.stakingWrapper, 'stake');
while (true) {
await balanceStore.updateErc20BalancesAsync();
const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address];
const amount = Pseudorandom.integer(zrxBalance.plus(1), constants.MAX_UINT256);
yield assertion.executeAsync([amount], { from: this.actor.address });
}
}
private async *_validUnstake(): AsyncIterableIterator<AssertionResult> {
const { stakingWrapper } = this.actor.deployment.staking;
const { deployment, balanceStore } = this.actor.simulationEnvironment!;
@@ -103,51 +125,108 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
}
}
private async *_validMoveStake(): AsyncIterableIterator<AssertionResult> {
const { deployment, stakingPools } = this.actor.simulationEnvironment!;
const assertion = validMoveStakeAssertion(deployment, this.actor.simulationEnvironment!, this.stake);
private async *_invalidUnstake(): AsyncIterableIterator<AssertionResult> {
const { stakingWrapper } = this.actor.deployment.staking;
const { deployment, balanceStore } = this.actor.simulationEnvironment!;
while (true) {
const { currentEpoch } = this.actor.simulationEnvironment!;
// Pick a random pool that this staker has delegated to (undefined if no such pools exist)
const fromPoolId = Pseudorandom.sample(
Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])),
await balanceStore.updateErc20BalancesAsync();
const undelegatedStake = await stakingWrapper
.getOwnerStakeByStatus(this.actor.address, StakeStatus.Undelegated)
.callAsync();
const withdrawableStake = BigNumber.min(
undelegatedStake.currentEpochBalance,
undelegatedStake.nextEpochBalance,
);
// The `from` status must be Undelegated if the staker isn't delegated to any pools
// at the moment, or if the chosen pool is unfinalized
const fromStatus =
fromPoolId === undefined || stakingPools[fromPoolId].lastFinalized.isLessThan(currentEpoch.minus(1))
? StakeStatus.Undelegated
: (Pseudorandom.sample(
[StakeStatus.Undelegated, StakeStatus.Delegated],
[0.2, 0.8], // 20% chance of `Undelegated`, 80% chance of `Delegated`
) as StakeStatus);
const from = new StakeInfo(fromStatus, fromPoolId);
const assertion = invalidUnstakeAssertion(deployment, withdrawableStake);
const amount = Pseudorandom.integer(withdrawableStake.plus(1), constants.MAX_UINT256);
yield assertion.executeAsync([amount], { from: this.actor.address });
}
}
// Pick a random pool to move the stake to
const toPoolId = Pseudorandom.sample(Object.keys(stakingPools));
// The `from` status must be Undelegated if no pools exist in the simulation yet,
// or if the chosen pool is unfinalized
const toStatus =
toPoolId === undefined || stakingPools[toPoolId].lastFinalized.isLessThan(currentEpoch.minus(1))
? StakeStatus.Undelegated
: (Pseudorandom.sample(
[StakeStatus.Undelegated, StakeStatus.Delegated],
[0.2, 0.8], // 20% chance of `Undelegated`, 80% chance of `Delegated`
) as StakeStatus);
const to = new StakeInfo(toStatus, toPoolId);
private _validMoveParams(): [StakeInfo, StakeInfo, BigNumber] {
const { stakingPools, currentEpoch } = this.actor.simulationEnvironment!;
// Pick a random pool that this staker has delegated to (undefined if no such pools exist)
const fromPoolId = Pseudorandom.sample(Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])));
// The `from` status must be Undelegated if the staker isn't delegated to any pools
// at the moment, or if the chosen pool is unfinalized
const fromStatus =
fromPoolId === undefined || stakingPools[fromPoolId].lastFinalized.isLessThan(currentEpoch.minus(1))
? StakeStatus.Undelegated
: (Pseudorandom.sample(
[StakeStatus.Undelegated, StakeStatus.Delegated],
[0.2, 0.8], // 20% chance of `Undelegated`, 80% chance of `Delegated`
) as StakeStatus);
const from = new StakeInfo(fromStatus, fromPoolId);
// The next epoch balance of the `from` stake is the amount that can be moved
const moveableStake =
from.status === StakeStatus.Undelegated
? this.stake[StakeStatus.Undelegated].nextEpochBalance
: this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance;
const amount = Pseudorandom.integer(0, moveableStake);
// Pick a random pool to move the stake to
const toPoolId = Pseudorandom.sample(Object.keys(stakingPools));
// The `from` status must be Undelegated if no pools exist in the simulation yet,
// or if the chosen pool is unfinalized
const toStatus =
toPoolId === undefined || stakingPools[toPoolId].lastFinalized.isLessThan(currentEpoch.minus(1))
? StakeStatus.Undelegated
: (Pseudorandom.sample(
[StakeStatus.Undelegated, StakeStatus.Delegated],
[0.2, 0.8], // 20% chance of `Undelegated`, 80% chance of `Delegated`
) as StakeStatus);
const to = new StakeInfo(toStatus, toPoolId);
// The next epoch balance of the `from` stake is the amount that can be moved
const moveableStake =
from.status === StakeStatus.Undelegated
? this.stake[StakeStatus.Undelegated].nextEpochBalance
: this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance;
const amount = Pseudorandom.integer(0, moveableStake);
return [from, to, amount];
}
private async *_validMoveStake(): AsyncIterableIterator<AssertionResult> {
const assertion = validMoveStakeAssertion(
this.actor.deployment,
this.actor.simulationEnvironment!,
this.stake,
);
while (true) {
const [from, to, amount] = this._validMoveParams();
yield assertion.executeAsync([from, to, amount], { from: this.actor.address });
}
}
private async *_moveStakeNonexistentPool(): AsyncIterableIterator<AssertionResult> {
while (true) {
const [from, to, amount] = this._validMoveParams();
// If there is 0 moveable stake for the sampled `to` pool, we need to mutate the
// `from` info, otherwise `moveStake` will just noop
if (amount.isZero()) {
from.poolId = Pseudorandom.hex();
// Status must be delegated and amount must be nonzero to trigger _assertStakingPoolExists
from.status = StakeStatus.Delegated;
const randomAmount = Pseudorandom.integer(1, constants.MAX_UINT256);
const assertion = moveStakeNonexistentPoolAssertion(this.actor.deployment, from.poolId);
yield assertion.executeAsync([from, to, randomAmount], { from: this.actor.address });
} else {
// One or both of the `from` and `to` poolId are invalid
const infoToMutate = Pseudorandom.sample([[from], [to], [from, to]]);
let nonExistentPoolId;
for (const info of infoToMutate!) {
info.poolId = Pseudorandom.hex();
nonExistentPoolId = nonExistentPoolId || info.poolId;
// Status must be delegated and amount must be nonzero to trigger _assertStakingPoolExists
info.status = StakeStatus.Delegated;
}
const assertion = moveStakeNonexistentPoolAssertion(
this.actor.deployment,
nonExistentPoolId as string,
);
yield assertion.executeAsync([from, to, amount], { from: this.actor.address });
}
}
}
private async *_validWithdrawDelegatorRewards(): AsyncIterableIterator<AssertionResult | void> {
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validWithdrawDelegatorRewardsAssertion(
@@ -169,6 +248,26 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
}
}
}
private async *_invalidWithdrawDelegatorRewards(): AsyncIterableIterator<AssertionResult | void> {
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = invalidWithdrawDelegatorRewardsAssertion(
this.actor.deployment,
this.actor.simulationEnvironment!,
);
while (true) {
const prevEpoch = this.actor.simulationEnvironment!.currentEpoch.minus(1);
// Pick an unfinalized pool
const poolId = Pseudorandom.sample(
Object.keys(stakingPools).filter(id => stakingPools[id].lastFinalized.isLessThan(prevEpoch)),
);
if (poolId === undefined) {
yield;
} else {
yield assertion.executeAsync([poolId], { from: this.actor.address });
}
}
}
};
}

View File

@@ -1,6 +1,6 @@
import { StoredBalance } from '@0x/contracts-staking';
import { StakingRevertErrors, StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
@@ -22,14 +22,11 @@ export function validCreateStakingPoolAssertion(
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<[number, boolean], string, string>(stakingWrapper, 'createStakingPool', {
// Returns the expected ID of th created pool
// Returns the expected ID of the created pool
before: async () => {
const lastPoolId = await stakingWrapper.lastPoolId().callAsync();
// Effectively the last poolId + 1, but as a bytestring
return `0x${new BigNumber(lastPoolId)
.plus(1)
.toString(16)
.padStart(64, '0')}`;
return hexUtils.leftPad(new BigNumber(lastPoolId).plus(1));
},
after: async (
expectedPoolId: string,
@@ -57,4 +54,38 @@ export function validCreateStakingPoolAssertion(
},
});
}
/**
* Returns a FunctionAssertion for `createStakingPool` which assumes an invalid operator share (i.e.
* greater than 1,000,000) is provided. The FunctionAssertion checks that the transaction reverts
* with the expected OperatorShareError.
*/
export function invalidCreateStakingPoolAssertion(
deployment: DeploymentManager,
): FunctionAssertion<[number, boolean], string, string> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<[number, boolean], string, string>(stakingWrapper, 'createStakingPool', {
// Returns the poolId we are expecting to revert with
before: async () => {
const lastPoolId = await stakingWrapper.lastPoolId().callAsync();
// Effectively the last poolId + 1, but as a bytestring
return hexUtils.leftPad(new BigNumber(lastPoolId).plus(1));
},
after: async (expectedPoolId: string, result: FunctionResult, args: [number, boolean]) => {
// Ensure that the tx reverted.
expect(result.success).to.be.false();
// Check revert error
const [operatorShare] = args;
expect(result.data).to.equal(
new StakingRevertErrors.OperatorShareError(
StakingRevertErrors.OperatorShareErrorCodes.OperatorShareTooLarge,
expectedPoolId,
operatorShare,
),
);
},
});
}
/* tslint:enable:no-non-null-assertion*/

View File

@@ -1,4 +1,4 @@
import { StakingPoolById } from '@0x/contracts-staking';
import { constants, StakingPoolById, StakingRevertErrors } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { TxData } from 'ethereum-types';
@@ -32,3 +32,37 @@ export function validDecreaseStakingPoolOperatorShareAssertion(
},
});
}
/**
* Returns a FunctionAssertion for `decreaseStakingPoolOperatorShare` which assumes the given
* operator share is larger than the current operator share for the pool. The FunctionAssertion
* checks that the transaction reverts with the correct error in this scenario.
*/
export function invalidDecreaseStakingPoolOperatorShareAssertion(
deployment: DeploymentManager,
): FunctionAssertion<[string, number], void, void> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<[string, number], void, void>(stakingWrapper, 'decreaseStakingPoolOperatorShare', {
after: async (_beforeInfo: void, result: FunctionResult, args: [string, number], _txData: Partial<TxData>) => {
// Ensure that the tx reverted.
expect(result.success).to.be.false();
// Check revert error
const [poolId, operatorShare] = args;
const expectedError =
operatorShare > constants.PPM
? new StakingRevertErrors.OperatorShareError(
StakingRevertErrors.OperatorShareErrorCodes.OperatorShareTooLarge,
poolId,
operatorShare,
)
: new StakingRevertErrors.OperatorShareError(
StakingRevertErrors.OperatorShareErrorCodes.CanOnlyDecreaseOperatorShare,
poolId,
operatorShare,
);
expect(result.data).to.equal(expectedError);
},
});
}

View File

@@ -4,6 +4,7 @@ import {
StakingEpochEndedEventArgs,
StakingEpochFinalizedEventArgs,
StakingEvents,
StakingRevertErrors,
} from '@0x/contracts-staking';
import { constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
@@ -117,3 +118,49 @@ export function validEndEpochAssertion(
},
});
}
/**
* Returns a FunctionAssertion for `endEpoch` which assumes it has been called while the previous
* epoch hasn't been fully finalized. Checks that the transaction reverts with PreviousEpochNotFinalizedError.
*/
export function endEpochUnfinalizedPoolsAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
numPoolsToFinalizeFromPrevEpoch: BigNumber,
): FunctionAssertion<[], void, void> {
return new FunctionAssertion(deployment.staking.stakingWrapper, 'endEpoch', {
after: async (_beforeInfo: void, result: FunctionResult) => {
// Ensure that the tx reverted.
expect(result.success).to.be.false();
// Check revert error
expect(result.data).to.equal(
new StakingRevertErrors.PreviousEpochNotFinalizedError(
simulationEnvironment.currentEpoch.minus(1),
numPoolsToFinalizeFromPrevEpoch,
),
);
},
});
}
/**
* Returns a FunctionAssertion for `endEpoch` which assumes it has been called before the full epoch
* duration has elapsed. Checks that the transaction reverts with BlockTimestampTooLowError.
*/
export function endEpochTooEarlyAssertion(deployment: DeploymentManager): FunctionAssertion<[], void, void> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion(stakingWrapper, 'endEpoch', {
after: async (_beforeInfo: void, result: FunctionResult) => {
// Ensure that the tx reverted.
expect(result.success).to.be.false();
// Check revert error
const epochEndTime = await stakingWrapper.getCurrentEpochEarliestEndTimeInSeconds().callAsync();
const lastBlockTime = await deployment.web3Wrapper.getBlockTimestampAsync('latest');
expect(result.data).to.equal(
new StakingRevertErrors.BlockTimestampTooLowError(epochEndTime, lastBlockTime),
);
},
});
}

View File

@@ -166,20 +166,24 @@ export function validFinalizePoolAssertion(
// 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 });
if (membersReward.isGreaterThan(0)) {
// 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 });
}
// Check that aggregated stats have been updated
const aggregatedStats = AggregatedStats.fromArray(

View File

@@ -0,0 +1,24 @@
import { BaseContract } from '@0x/base-contract';
import { expect } from '@0x/contracts-test-utils';
import { RevertReason } from '@0x/types';
import { StringRevertError } from '@0x/utils';
import { FunctionAssertion, FunctionResult } from './function_assertion';
/**
* Returns a generic FunctionAssertion for the given contract function, asserting that the
* function call reverts in an asset proxy contract with TRANSFER_FAILED.
*/
export function assetProxyTransferFailedAssertion<TArgs extends any[]>(
contract: BaseContract,
functionName: string,
): FunctionAssertion<TArgs, void, void> {
return new FunctionAssertion(contract, functionName, {
after: async (_beforeInfo: void, result: FunctionResult) => {
// Ensure that the tx reverted.
expect(result.success).to.be.false();
// Check revert error
expect(result.data).to.equal(new StringRevertError(RevertReason.TransferFailed));
},
});
}

View File

@@ -5,6 +5,7 @@ import {
OwnerStakeByStatus,
StakeInfo,
StakeStatus,
StakingRevertErrors,
StoredBalance,
} from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
@@ -196,4 +197,27 @@ export function validMoveStakeAssertion(
},
});
}
/**
* Returns a FunctionAssertion for `moveStake` which asserts that the transaction reverts with a
* PoolExistenceError.
*/
export function moveStakeNonexistentPoolAssertion(
deployment: DeploymentManager,
nonExistentPoolId: string,
): FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], void, void> {
return new FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], void, void>(
deployment.staking.stakingWrapper,
'moveStake',
{
after: async (_beforeInfo: void, result: FunctionResult) => {
// Ensure that the tx reverted.
expect(result.success).to.be.false();
// Check revert error
expect(result.data).to.equal(new StakingRevertErrors.PoolExistenceError(nonExistentPoolId, false));
},
},
);
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -1,4 +1,9 @@
import { decreaseCurrentAndNextBalance, OwnerStakeByStatus, StakeStatus } from '@0x/contracts-staking';
import {
decreaseCurrentAndNextBalance,
OwnerStakeByStatus,
StakeStatus,
StakingRevertErrors,
} from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
@@ -81,4 +86,24 @@ export function validUnstakeAssertion(
},
});
}
/**
* Returns a FunctionAssertion for `unstake` which assumes that the input exceeds the amount that
* can be unstaked. Checks that the call reverts with an InsufficientBalanceError. Note that we
* close over `withdrawableStake` to avoid duplicating work done in the assertion generator.
*/
export function invalidUnstakeAssertion(
deployment: DeploymentManager,
withdrawableStake: BigNumber,
): FunctionAssertion<[BigNumber], void, void> {
return new FunctionAssertion<[BigNumber], void, void>(deployment.staking.stakingWrapper, 'unstake', {
after: async (_beforeInfo: void, result: FunctionResult, args: [BigNumber]) => {
// Ensure that the tx reverted.
expect(result.success).to.be.false();
const [amount] = args;
expect(result.data).to.equal(new StakingRevertErrors.InsufficientBalanceError(amount, withdrawableStake));
},
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -1,5 +1,5 @@
import { WETH9Events, WETH9TransferEventArgs } from '@0x/contracts-erc20';
import { loadCurrentBalance, StoredBalance } from '@0x/contracts-staking';
import { loadCurrentBalance, StakingRevertErrors, StoredBalance } from '@0x/contracts-staking';
import { expect, filterLogsToArguments } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
@@ -73,4 +73,25 @@ export function validWithdrawDelegatorRewardsAssertion(
},
});
}
/**
* Returns a FunctionAssertion for `withdrawDelegatorRewards` which assumes the given pool hasn't
* been finalized for the previous epoch. It checks that the call reverts with a PoolNotFinalizedError.
*/
export function invalidWithdrawDelegatorRewardsAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[string], void, void> {
return new FunctionAssertion(deployment.staking.stakingWrapper, 'withdrawDelegatorRewards', {
after: async (_beforeInfo: void, result: FunctionResult, args: [string]) => {
// Ensure that the tx reverted.
expect(result.success).to.be.false();
// Check revert error
const [poolId] = args;
const { currentEpoch } = simulationEnvironment;
expect(result.data).to.equal(new StakingRevertErrors.PoolNotFinalizedError(poolId, currentEpoch.minus(1)));
},
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -16,10 +16,14 @@ export class PoolManagementSimulation extends Simulation {
const operators = filterActorsByRole(actors, PoolOperator);
const [actions, weights] = _.unzip([
// 40% chance of executing validCreateStakingPool assertion for a random operator
...operators.map(operator => [operator.simulationActions.validCreateStakingPool, 0.4]),
// 60% chance of executing validDecreaseStakingPoolOperatorShare for a random operator
...operators.map(operator => [operator.simulationActions.validDecreaseStakingPoolOperatorShare, 0.6]),
// 38% chance of executing validCreateStakingPool assertion for a random operator
...operators.map(operator => [operator.simulationActions.validCreateStakingPool, 0.38]),
// 2% chance of executing invalidCreateStakingPool assertion for a random operator
...operators.map(operator => [operator.simulationActions.invalidCreateStakingPool, 0.02]),
// 58% chance of executing validDecreaseStakingPoolOperatorShare for a random operator
...operators.map(operator => [operator.simulationActions.validDecreaseStakingPoolOperatorShare, 0.58]),
// 2% chance of executing invalidDecreaseStakingPoolOperatorShare for a random operator
...operators.map(operator => [operator.simulationActions.invalidDecreaseStakingPoolOperatorShare, 0.02]),
]) as [Array<AsyncIterableIterator<AssertionResult | void>>, number[]];
while (true) {
const action = Pseudorandom.sample(actions, weights);

View File

@@ -22,12 +22,18 @@ export class StakeManagementSimulation extends Simulation {
const poolManagement = new PoolManagementSimulation(this.environment);
const [actions, weights] = _.unzip([
// 30% chance of executing validStake for a random staker
...stakers.map(staker => [staker.simulationActions.validStake, 0.3 / stakers.length]),
// 20% chance of executing validUnstake for a random staker
...stakers.map(staker => [staker.simulationActions.validUnstake, 0.2 / stakers.length]),
// 30% chance of executing validMoveStake for a random staker
...stakers.map(staker => [staker.simulationActions.validMoveStake, 0.3 / stakers.length]),
// 28% chance of executing validStake for a random staker
...stakers.map(staker => [staker.simulationActions.validStake, 0.28 / stakers.length]),
// 2% chance of executing invalidUnstake for a random staker
...stakers.map(staker => [staker.simulationActions.invalidStake, 0.02 / stakers.length]),
// 28% chance of executing validUnstake for a random staker
...stakers.map(staker => [staker.simulationActions.validStake, 0.28 / stakers.length]),
// 2% chance of executing invalidUnstake for a random staker
...stakers.map(staker => [staker.simulationActions.validUnstake, 0.02 / stakers.length]),
// 28% chance of executing validMoveStake for a random staker
...stakers.map(staker => [staker.simulationActions.validMoveStake, 0.28 / stakers.length]),
// 2% chance of executing moveStakeNonexistentPool for a random staker
...stakers.map(staker => [staker.simulationActions.moveStakeNonexistentPool, 0.02 / stakers.length]),
// 20% chance of executing an assertion generated from the pool management simulation
[poolManagement.generator, 0.2],
]) as [Array<AsyncIterableIterator<AssertionResult | void>>, number[]];

View File

@@ -38,8 +38,10 @@ export class StakingRewardsSimulation extends Simulation {
...stakers.map(staker => [staker.simulationActions.validWithdrawDelegatorRewards, 0.1 / stakers.length]),
// 10% chance of executing validFinalizePool for a random keeper
...keepers.map(keeper => [keeper.simulationActions.validFinalizePool, 0.1 / keepers.length]),
// 10% chance of executing validEndEpoch for a random keeper
...keepers.map(keeper => [keeper.simulationActions.validEndEpoch, 0.1 / keepers.length]),
// 7% chance of executing validEndEpoch for a random keeper
...keepers.map(keeper => [keeper.simulationActions.validEndEpoch, 0.07 / keepers.length]),
// 3% chance of executing invalidEndEpoch for a random keeper
...keepers.map(keeper => [keeper.simulationActions.invalidEndEpoch, 0.03 / keepers.length]),
// 50% chance of executing an assertion generated from the pool membership simulation
[poolMembership.generator, 0.5],
// 20% chance of executing an assertion generated from the stake management simulation

View File

@@ -60,6 +60,7 @@ export const constants = {
NULL_BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000',
UNLIMITED_ALLOWANCE_IN_BASE_UNITS: MAX_UINT256,
MAX_UINT256,
MAX_UINT32: new BigNumber(2).pow(32).minus(1),
TESTRPC_PRIVATE_KEYS: _.map(TESTRPC_PRIVATE_KEYS_STRINGS, privateKeyString => ethUtil.toBuffer(privateKeyString)),
INITIAL_ERC20_BALANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18),
INITIAL_ERC20_ALLOWANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18),