Merge pull request #2431 from 0xProject/feature/fuzz/revert-assertions
`@0x/contracts-integrations`: Negative assertions for fuzzing
This commit is contained in:
@@ -9,6 +9,10 @@
|
||||
{
|
||||
"note": "Fuzz tests for `matchOrders` and `matchOrdersWithMaximalFill`.",
|
||||
"pr": 2437
|
||||
},
|
||||
{
|
||||
"note": "Add various negative assertions for fuzz tests",
|
||||
"pr": 2431
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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*/
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[]];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user