diff --git a/contracts/integrations/test/framework/actors/maker.ts b/contracts/integrations/test/framework/actors/maker.ts index 94858ab7bb..3158a6fbe1 100644 --- a/contracts/integrations/test/framework/actors/maker.ts +++ b/contracts/integrations/test/framework/actors/maker.ts @@ -111,7 +111,7 @@ export function MakerMixin(Base: TBase): TBase & Cons await (owner as Actor).configureERC20TokenAsync(token as DummyERC20TokenContract); balance = balanceStore.balances.erc20[owner.address][token.address] = constants.INITIAL_ERC20_BALANCE; - return Pseudorandom.integer(balance.dividedToIntegerBy(2)); + return Pseudorandom.integer(0, balance.dividedToIntegerBy(2)); }), ); // Encode asset data diff --git a/contracts/integrations/test/framework/actors/pool_operator.ts b/contracts/integrations/test/framework/actors/pool_operator.ts index 6352a8a863..f07e09f0f1 100644 --- a/contracts/integrations/test/framework/actors/pool_operator.ts +++ b/contracts/integrations/test/framework/actors/pool_operator.ts @@ -6,7 +6,7 @@ import * as _ from 'lodash'; import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool'; import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare'; import { AssertionResult } from '../assertions/function_assertion'; -import { Pseudorandom } from '../utils/pseudorandom'; +import { Distributions, Pseudorandom } from '../utils/pseudorandom'; import { Actor, Constructor } from './base'; @@ -83,7 +83,11 @@ export function PoolOperatorMixin(Base: TBase): TBase private async *_validCreateStakingPool(): AsyncIterableIterator { const assertion = validCreateStakingPoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!); while (true) { - const operatorShare = Pseudorandom.integer(constants.PPM).toNumber(); + const operatorShare = Pseudorandom.integer( + 0, + constants.PPM, + Distributions.Kumaraswamy(0.2, 0.2), + ).toNumber(); yield assertion.executeAsync([operatorShare, false], { from: this.actor.address }); } } @@ -96,7 +100,11 @@ export function PoolOperatorMixin(Base: TBase): TBase if (poolId === undefined) { yield undefined; } else { - const operatorShare = Pseudorandom.integer(stakingPools[poolId].operatorShare).toNumber(); + const operatorShare = Pseudorandom.integer( + 0, + stakingPools[poolId].operatorShare, + Distributions.Kumaraswamy(0.2, 0.2), + ).toNumber(); yield assertion.executeAsync([poolId, operatorShare], { from: this.actor.address }); } } diff --git a/contracts/integrations/test/framework/actors/staker.ts b/contracts/integrations/test/framework/actors/staker.ts index 48ef2694a9..11eee24622 100644 --- a/contracts/integrations/test/framework/actors/staker.ts +++ b/contracts/integrations/test/framework/actors/staker.ts @@ -79,7 +79,7 @@ export function StakerMixin(Base: TBase): TBase & Con while (true) { await balanceStore.updateErc20BalancesAsync(); const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address]; - const amount = Pseudorandom.integer(zrxBalance); + const amount = Pseudorandom.integer(0, zrxBalance); yield assertion.executeAsync([amount], { from: this.actor.address }); } } @@ -98,7 +98,7 @@ export function StakerMixin(Base: TBase): TBase & Con undelegatedStake.currentEpochBalance, undelegatedStake.nextEpochBalance, ); - const amount = Pseudorandom.integer(withdrawableStake); + const amount = Pseudorandom.integer(0, withdrawableStake); yield assertion.executeAsync([amount], { from: this.actor.address }); } } @@ -136,7 +136,7 @@ export function StakerMixin(Base: TBase): TBase & Con from.status === StakeStatus.Undelegated ? this.stake[StakeStatus.Undelegated].nextEpochBalance : this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance; - const amount = Pseudorandom.integer(moveableStake); + const amount = Pseudorandom.integer(0, moveableStake); yield assertion.executeAsync([from, to, amount], { from: this.actor.address }); } diff --git a/contracts/integrations/test/framework/actors/taker.ts b/contracts/integrations/test/framework/actors/taker.ts index 9da1fca91b..27e994232d 100644 --- a/contracts/integrations/test/framework/actors/taker.ts +++ b/contracts/integrations/test/framework/actors/taker.ts @@ -75,12 +75,12 @@ export function TakerMixin(Base: TBase): TBase & Cons // Maker creates and signs a fillable order const order = await maker.createFillableOrderAsync(this.actor); // Taker fills the order by a random amount (up to the order's takerAssetAmount) - const fillAmount = Pseudorandom.integer(order.takerAssetAmount); + const fillAmount = Pseudorandom.integer(0, order.takerAssetAmount); // Taker executes the fill with a random msg.value, so that sometimes the // protocol fee is paid in ETH and other times it's paid in WETH. yield assertion.executeAsync([order, fillAmount, order.signature], { from: this.actor.address, - value: Pseudorandom.integer(DeploymentManager.protocolFee.times(2)), + value: Pseudorandom.integer(0, DeploymentManager.protocolFee.times(2)), }); } } diff --git a/contracts/integrations/test/framework/assertions/decreaseStakingPoolOperatorShare.ts b/contracts/integrations/test/framework/assertions/decreaseStakingPoolOperatorShare.ts index 86899164ab..71ac27a02d 100644 --- a/contracts/integrations/test/framework/assertions/decreaseStakingPoolOperatorShare.ts +++ b/contracts/integrations/test/framework/assertions/decreaseStakingPoolOperatorShare.ts @@ -13,11 +13,11 @@ import { FunctionAssertion, FunctionResult } from './function_assertion'; export function validDecreaseStakingPoolOperatorShareAssertion( deployment: DeploymentManager, pools: StakingPoolById, -): FunctionAssertion<[string, number], {}, void> { +): FunctionAssertion<[string, number], void, void> { const { stakingWrapper } = deployment.staking; - return new FunctionAssertion<[string, number], {}, void>(stakingWrapper, 'decreaseStakingPoolOperatorShare', { - after: async (_beforeInfo, result: FunctionResult, args: [string, number], _txData: Partial) => { + return new FunctionAssertion<[string, number], void, void>(stakingWrapper, 'decreaseStakingPoolOperatorShare', { + after: async (_beforeInfo: void, result: FunctionResult, args: [string, number], _txData: Partial) => { // Ensure that the tx succeeded. expect(result.success, `Error: ${result.data}`).to.be.true(); diff --git a/contracts/integrations/test/framework/utils/pseudorandom.ts b/contracts/integrations/test/framework/utils/pseudorandom.ts index 6f4132650f..612b1f3892 100644 --- a/contracts/integrations/test/framework/utils/pseudorandom.ts +++ b/contracts/integrations/test/framework/utils/pseudorandom.ts @@ -4,7 +4,7 @@ import * as seedrandom from 'seedrandom'; class PRNGWrapper { public readonly seed = process.env.SEED || Math.random().toString(); - private readonly _rng = seedrandom(this.seed); + public readonly rng = seedrandom(this.seed); /* * Pseudorandom version of _.sample. Picks an element of the given array with uniform probability. @@ -14,7 +14,7 @@ class PRNGWrapper { if (arr.length === 0) { return undefined; } - const index = Math.abs(this._rng.int32()) % arr.length; + const index = Math.abs(this.rng.int32()) % arr.length; return arr[index]; } @@ -33,22 +33,39 @@ class PRNGWrapper { return samples; } - // tslint:disable:unified-signatures /* - * Pseudorandom version of getRandomPortion/getRandomInteger. If two arguments are provided, - * treats those arguments as the min and max (inclusive) of the desired range. If only one - * argument is provided, picks an integer between 0 and the argument. + * Pseudorandom version of getRandomPortion/getRandomInteger. If no distribution is provided, + * samples an integer between the min and max uniformly at random. If a distribution is + * provided, samples an integer from the given distribution (assumed to be defined on the + * interval [0, 1]) scaled to [min, max]. */ - public integer(max: Numberish): BigNumber; - public integer(min: Numberish, max: Numberish): BigNumber; - public integer(a: Numberish, b?: Numberish): BigNumber { - if (b === undefined) { - return new BigNumber(this._rng()).times(a).integerValue(BigNumber.ROUND_HALF_UP); - } else { - const range = new BigNumber(b).minus(a); - return this.integer(range).plus(a); - } + public integer(min: Numberish, max: Numberish, distribution: () => Numberish = this.rng): BigNumber { + const range = new BigNumber(max).minus(min); + return new BigNumber(distribution()) + .times(range) + .integerValue(BigNumber.ROUND_HALF_UP) + .plus(min); + } + + /* + * Returns a function that produces samples from the Kumaraswamy distribution parameterized by + * the given alpha and beta. The Kumaraswamy distribution is like the beta distribution, but + * with a nice closed form. + * https://en.wikipedia.org/wiki/Kumaraswamy_distribution + * https://www.johndcook.com/blog/2009/11/24/kumaraswamy-distribution/ + */ + public kumaraswamy(this: PRNGWrapper, alpha: Numberish, beta: Numberish): () => BigNumber { + const ONE = new BigNumber(1); + return () => { + const u = new BigNumber(this.rng()).modulo(ONE); // u ~ Uniform(0, 1) + // Evaluate the inverse CDF at `u` to obtain a sample from Kumaraswamy(alpha, beta) + return ONE.minus(ONE.minus(u).exponentiatedBy(ONE.dividedBy(beta))).exponentiatedBy(ONE.dividedBy(alpha)); + }; } } export const Pseudorandom = new PRNGWrapper(); +export const Distributions = { + Uniform: Pseudorandom.rng, + Kumaraswamy: Pseudorandom.kumaraswamy.bind(Pseudorandom), +};