Merge pull request #2392 from 0xProject/feature/fuzz/better-input-gen

`@0x/contracts-integrations`: Better input generation for fuzzing
This commit is contained in:
mzhu25
2019-12-18 12:03:23 -08:00
committed by GitHub
13 changed files with 145 additions and 73 deletions

View File

@@ -111,7 +111,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
await (owner as Actor).configureERC20TokenAsync(token as DummyERC20TokenContract); await (owner as Actor).configureERC20TokenAsync(token as DummyERC20TokenContract);
balance = balanceStore.balances.erc20[owner.address][token.address] = balance = balanceStore.balances.erc20[owner.address][token.address] =
constants.INITIAL_ERC20_BALANCE; constants.INITIAL_ERC20_BALANCE;
return Pseudorandom.integer(balance.dividedToIntegerBy(2)); return Pseudorandom.integer(0, balance.dividedToIntegerBy(2));
}), }),
); );
// Encode asset data // Encode asset data

View File

@@ -6,7 +6,7 @@ import * as _ from 'lodash';
import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool'; import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool';
import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare'; import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare';
import { AssertionResult } from '../assertions/function_assertion'; import { AssertionResult } from '../assertions/function_assertion';
import { Pseudorandom } from '../utils/pseudorandom'; import { Distributions, Pseudorandom } from '../utils/pseudorandom';
import { Actor, Constructor } from './base'; import { Actor, Constructor } from './base';
@@ -83,7 +83,11 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
private async *_validCreateStakingPool(): AsyncIterableIterator<AssertionResult> { private async *_validCreateStakingPool(): AsyncIterableIterator<AssertionResult> {
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!); const assertion = validCreateStakingPoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
while (true) { 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 }); yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });
} }
} }
@@ -96,7 +100,11 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
if (poolId === undefined) { if (poolId === undefined) {
yield undefined; yield undefined;
} else { } 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 }); yield assertion.executeAsync([poolId, operatorShare], { from: this.actor.address });
} }
} }

View File

@@ -79,7 +79,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
while (true) { while (true) {
await balanceStore.updateErc20BalancesAsync(); await balanceStore.updateErc20BalancesAsync();
const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address]; 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 }); yield assertion.executeAsync([amount], { from: this.actor.address });
} }
} }
@@ -98,7 +98,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
undelegatedStake.currentEpochBalance, undelegatedStake.currentEpochBalance,
undelegatedStake.nextEpochBalance, undelegatedStake.nextEpochBalance,
); );
const amount = Pseudorandom.integer(withdrawableStake); const amount = Pseudorandom.integer(0, withdrawableStake);
yield assertion.executeAsync([amount], { from: this.actor.address }); yield assertion.executeAsync([amount], { from: this.actor.address });
} }
} }
@@ -118,7 +118,10 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
const fromStatus = const fromStatus =
fromPoolId === undefined || stakingPools[fromPoolId].lastFinalized.isLessThan(currentEpoch.minus(1)) fromPoolId === undefined || stakingPools[fromPoolId].lastFinalized.isLessThan(currentEpoch.minus(1))
? StakeStatus.Undelegated ? StakeStatus.Undelegated
: (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus); : (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 from = new StakeInfo(fromStatus, fromPoolId);
// Pick a random pool to move the stake to // Pick a random pool to move the stake to
@@ -128,7 +131,10 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
const toStatus = const toStatus =
toPoolId === undefined || stakingPools[toPoolId].lastFinalized.isLessThan(currentEpoch.minus(1)) toPoolId === undefined || stakingPools[toPoolId].lastFinalized.isLessThan(currentEpoch.minus(1))
? StakeStatus.Undelegated ? StakeStatus.Undelegated
: (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus); : (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); const to = new StakeInfo(toStatus, toPoolId);
// The next epoch balance of the `from` stake is the amount that can be moved // The next epoch balance of the `from` stake is the amount that can be moved
@@ -136,7 +142,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
from.status === StakeStatus.Undelegated from.status === StakeStatus.Undelegated
? this.stake[StakeStatus.Undelegated].nextEpochBalance ? this.stake[StakeStatus.Undelegated].nextEpochBalance
: this.stake[StakeStatus.Delegated][from.poolId].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 }); yield assertion.executeAsync([from, to, amount], { from: this.actor.address });
} }

View File

@@ -75,12 +75,12 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
// Maker creates and signs a fillable order // Maker creates and signs a fillable order
const order = await maker.createFillableOrderAsync(this.actor); const order = await maker.createFillableOrderAsync(this.actor);
// Taker fills the order by a random amount (up to the order's takerAssetAmount) // 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 // 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. // protocol fee is paid in ETH and other times it's paid in WETH.
yield assertion.executeAsync([order, fillAmount, order.signature], { yield assertion.executeAsync([order, fillAmount, order.signature], {
from: this.actor.address, from: this.actor.address,
value: Pseudorandom.integer(DeploymentManager.protocolFee.times(2)), value: Pseudorandom.integer(0, DeploymentManager.protocolFee.times(2)),
}); });
} }
} }

View File

@@ -2,6 +2,7 @@
"extends": ["@0x/tslint-config"], "extends": ["@0x/tslint-config"],
"rules": { "rules": {
"max-classes-per-file": false, "max-classes-per-file": false,
"no-non-null-assertion": false "no-non-null-assertion": false,
"custom-no-magic-numbers": false
} }
} }

View File

@@ -13,11 +13,11 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
export function validDecreaseStakingPoolOperatorShareAssertion( export function validDecreaseStakingPoolOperatorShareAssertion(
deployment: DeploymentManager, deployment: DeploymentManager,
pools: StakingPoolById, pools: StakingPoolById,
): FunctionAssertion<[string, number], {}, void> { ): FunctionAssertion<[string, number], void, void> {
const { stakingWrapper } = deployment.staking; const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<[string, number], {}, void>(stakingWrapper, 'decreaseStakingPoolOperatorShare', { return new FunctionAssertion<[string, number], void, void>(stakingWrapper, 'decreaseStakingPoolOperatorShare', {
after: async (_beforeInfo, result: FunctionResult, args: [string, number], _txData: Partial<TxData>) => { after: async (_beforeInfo: void, result: FunctionResult, args: [string, number], _txData: Partial<TxData>) => {
// Ensure that the tx succeeded. // Ensure that the tx succeeded.
expect(result.success, `Error: ${result.data}`).to.be.true(); expect(result.success, `Error: ${result.data}`).to.be.true();

View File

@@ -1,54 +1,87 @@
import { Numberish } from '@0x/contracts-test-utils'; import { Numberish } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import * as seedrandom from 'seedrandom'; import * as seedrandom from 'seedrandom';
class PRNGWrapper { class PRNGWrapper {
public readonly seed = process.env.SEED || Math.random().toString(); 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. * Pseudorandom version of _.sample. Picks an element of the given array. If an array of weights
* Return undefined if the array is empty. * is provided, elements of `arr` are weighted according to the value in the corresponding index
* of `weights`. Otherwise, the samples are chosen uniformly at random. Return undefined if the
* array is empty.
*/ */
public sample<T>(arr: T[]): T | undefined { public sample<T>(arr: T[], weights?: number[]): T | undefined {
if (arr.length === 0) { if (arr.length === 0) {
return undefined; return undefined;
} }
const index = Math.abs(this._rng.int32()) % arr.length;
let index: number;
if (weights !== undefined) {
const cdf = weights.map((_weight, i) => _.sum(weights.slice(0, i + 1)) / _.sum(weights));
const x = this.rng();
index = cdf.findIndex(value => value > x);
} else {
index = Math.abs(this.rng.int32()) % arr.length;
}
return arr[index]; return arr[index];
} }
/* /*
* Pseudorandom version of _.sampleSize. Returns an array of `n` samples from the given array * Pseudorandom version of _.sampleSize. Returns an array of `n` samples from the given array
* (with replacement), chosen with uniform probability. Return undefined if the array is empty. * (with replacement). If an array of weights is provided, elements of `arr` are weighted
* according to the value in the corresponding index of `weights`. Otherwise, the samples are
* chosen uniformly at random. Return undefined if the array is empty.
*/ */
public sampleSize<T>(arr: T[], n: number): T[] | undefined { public sampleSize<T>(arr: T[], n: number, weights?: number[]): T[] | undefined {
if (arr.length === 0) { if (arr.length === 0) {
return undefined; return undefined;
} }
const samples = []; const samples = [];
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
samples.push(this.sample(arr) as T); samples.push(this.sample(arr, weights) as T);
} }
return samples; return samples;
} }
// tslint:disable:unified-signatures
/* /*
* Pseudorandom version of getRandomPortion/getRandomInteger. If two arguments are provided, * Pseudorandom version of getRandomPortion/getRandomInteger. If no distribution is provided,
* treats those arguments as the min and max (inclusive) of the desired range. If only one * samples an integer between the min and max uniformly at random. If a distribution is
* argument is provided, picks an integer between 0 and the argument. * 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, distribution: () => Numberish = this.rng): BigNumber {
public integer(min: Numberish, max: Numberish): BigNumber; const range = new BigNumber(max).minus(min);
public integer(a: Numberish, b?: Numberish): BigNumber { return new BigNumber(distribution())
if (b === undefined) { .times(range)
return new BigNumber(this._rng()).times(a).integerValue(BigNumber.ROUND_HALF_UP); .integerValue(BigNumber.ROUND_HALF_UP)
} else { .plus(min);
const range = new BigNumber(b).minus(a); }
return this.integer(range).plus(a);
} /*
* 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. More info:
* https://en.wikipedia.org/wiki/Kumaraswamy_distribution
* https://www.johndcook.com/blog/2009/11/24/kumaraswamy-distribution/
* Alpha and beta default to 0.2, so that the distribution favors the extremes of the domain.
* The PDF for alpha=0.2, beta=0.2:
* https://www.wolframalpha.com/input/?i=0.2*0.2*x%5E%280.2-1%29*%281-x%5E0.2%29%5E%280.2-1%29+from+0+to+1
*/
public kumaraswamy(this: PRNGWrapper, alpha: Numberish = 0.2, beta: Numberish = 0.2): () => 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 Pseudorandom = new PRNGWrapper();
export const Distributions = {
Uniform: Pseudorandom.rng,
Kumaraswamy: Pseudorandom.kumaraswamy.bind(Pseudorandom),
};

View File

@@ -1,4 +1,5 @@
import { blockchainTests } from '@0x/contracts-test-utils'; import { blockchainTests } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { Actor } from '../framework/actors/base'; import { Actor } from '../framework/actors/base';
import { PoolOperator } from '../framework/actors/pool_operator'; import { PoolOperator } from '../framework/actors/pool_operator';
@@ -14,12 +15,14 @@ export class PoolManagementSimulation extends Simulation {
const { actors } = this.environment; const { actors } = this.environment;
const operators = filterActorsByRole(actors, PoolOperator); const operators = filterActorsByRole(actors, PoolOperator);
const actions = [ const [actions, weights] = _.unzip([
...operators.map(operator => operator.simulationActions.validCreateStakingPool), // 40% chance of executing validCreateStakingPool assertion for a random operator
...operators.map(operator => operator.simulationActions.validDecreaseStakingPoolOperatorShare), ...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]),
]) as [Array<AsyncIterableIterator<AssertionResult | void>>, number[]];
while (true) { while (true) {
const action = Pseudorandom.sample(actions); const action = Pseudorandom.sample(actions, weights);
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
} }
} }

View File

@@ -1,4 +1,5 @@
import { blockchainTests } from '@0x/contracts-test-utils'; import { blockchainTests } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { Actor } from '../framework/actors/base'; import { Actor } from '../framework/actors/base';
import { MakerTaker } from '../framework/actors/hybrids'; import { MakerTaker } from '../framework/actors/hybrids';
@@ -22,14 +23,17 @@ export class PoolMembershipSimulation extends Simulation {
const poolManagement = new PoolManagementSimulation(this.environment); const poolManagement = new PoolManagementSimulation(this.environment);
const actions = [ const [actions, weights] = _.unzip([
...makers.map(maker => maker.simulationActions.validJoinStakingPool), // 20% chance of executing validJoinStakingPool for a random maker
...takers.map(taker => taker.simulationActions.validFillOrder), ...makers.map(maker => [maker.simulationActions.validJoinStakingPool, 0.2 / makers.length]),
poolManagement.generator, // 60% chance of executing validFillOrder for a random taker
]; ...takers.map(taker => [taker.simulationActions.validFillOrder, 0.6 / takers.length]),
// 20% chance of executing an assertion generated from the pool management simulation
[poolManagement.generator, 0.2],
]) as [Array<AsyncIterableIterator<AssertionResult | void>>, number[]];
while (true) { while (true) {
const action = Pseudorandom.sample(actions); const action = Pseudorandom.sample(actions, weights);
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
} }
} }

View File

@@ -1,4 +1,5 @@
import { blockchainTests } from '@0x/contracts-test-utils'; import { blockchainTests } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { Actor } from '../framework/actors/base'; import { Actor } from '../framework/actors/base';
import { StakerOperator } from '../framework/actors/hybrids'; import { StakerOperator } from '../framework/actors/hybrids';
@@ -20,14 +21,19 @@ export class StakeManagementSimulation extends Simulation {
const poolManagement = new PoolManagementSimulation(this.environment); const poolManagement = new PoolManagementSimulation(this.environment);
const actions = [ const [actions, weights] = _.unzip([
...stakers.map(staker => staker.simulationActions.validStake), // 30% chance of executing validStake for a random staker
...stakers.map(staker => staker.simulationActions.validUnstake), ...stakers.map(staker => [staker.simulationActions.validStake, 0.3 / stakers.length]),
...stakers.map(staker => staker.simulationActions.validMoveStake), // 20% chance of executing validUnstake for a random staker
poolManagement.generator, ...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]),
// 20% chance of executing an assertion generated from the pool management simulation
[poolManagement.generator, 0.2],
]) as [Array<AsyncIterableIterator<AssertionResult | void>>, number[]];
while (true) { while (true) {
const action = Pseudorandom.sample(actions); const action = Pseudorandom.sample(actions, weights);
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
} }
} }

View File

@@ -1,4 +1,5 @@
import { blockchainTests } from '@0x/contracts-test-utils'; import { blockchainTests } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { Actor } from '../framework/actors/base'; import { Actor } from '../framework/actors/base';
import { import {
@@ -32,15 +33,20 @@ export class StakingRewardsSimulation extends Simulation {
const poolMembership = new PoolMembershipSimulation(this.environment); const poolMembership = new PoolMembershipSimulation(this.environment);
const stakeManagement = new StakeManagementSimulation(this.environment); const stakeManagement = new StakeManagementSimulation(this.environment);
const actions = [ const [actions, weights] = _.unzip([
...stakers.map(staker => staker.simulationActions.validWithdrawDelegatorRewards), // 10% chance of executing validWithdrawDelegatorRewards for a random staker
...keepers.map(keeper => keeper.simulationActions.validFinalizePool), ...stakers.map(staker => [staker.simulationActions.validWithdrawDelegatorRewards, 0.1 / stakers.length]),
...keepers.map(keeper => keeper.simulationActions.validEndEpoch), // 10% chance of executing validFinalizePool for a random keeper
poolMembership.generator, ...keepers.map(keeper => [keeper.simulationActions.validFinalizePool, 0.1 / keepers.length]),
stakeManagement.generator, // 10% chance of executing validEndEpoch for a random keeper
]; ...keepers.map(keeper => [keeper.simulationActions.validEndEpoch, 0.1 / 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
[stakeManagement.generator, 0.2],
]) as [Array<AsyncIterableIterator<AssertionResult | void>>, number[]];
while (true) { while (true) {
const action = Pseudorandom.sample(actions); const action = Pseudorandom.sample(actions, weights);
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
} }
} }

View File

@@ -1,6 +1,7 @@
{ {
"extends": ["@0x/tslint-config"], "extends": ["@0x/tslint-config"],
"rules": { "rules": {
"no-invalid-this": false "no-invalid-this": false,
"custom-no-magic-numbers": false
} }
} }

View File

@@ -83,9 +83,10 @@ export function loadCurrentBalance(balance: StoredBalance, epoch: BigNumber): St
* Simulates _increaseNextBalance * Simulates _increaseNextBalance
*/ */
export function increaseNextBalance(balance: StoredBalance, amount: Numberish, epoch: BigNumber): StoredBalance { export function increaseNextBalance(balance: StoredBalance, amount: Numberish, epoch: BigNumber): StoredBalance {
const newBalance = loadCurrentBalance(balance, epoch);
return { return {
...loadCurrentBalance(balance, epoch), ...newBalance,
nextEpochBalance: balance.nextEpochBalance.plus(amount), nextEpochBalance: newBalance.nextEpochBalance.plus(amount),
}; };
} }
@@ -93,9 +94,10 @@ export function increaseNextBalance(balance: StoredBalance, amount: Numberish, e
* Simulates _decreaseNextBalance * Simulates _decreaseNextBalance
*/ */
export function decreaseNextBalance(balance: StoredBalance, amount: Numberish, epoch: BigNumber): StoredBalance { export function decreaseNextBalance(balance: StoredBalance, amount: Numberish, epoch: BigNumber): StoredBalance {
const newBalance = loadCurrentBalance(balance, epoch);
return { return {
...loadCurrentBalance(balance, epoch), ...newBalance,
nextEpochBalance: balance.nextEpochBalance.minus(amount), nextEpochBalance: newBalance.nextEpochBalance.minus(amount),
}; };
} }
@@ -107,10 +109,11 @@ export function increaseCurrentAndNextBalance(
amount: Numberish, amount: Numberish,
epoch: BigNumber, epoch: BigNumber,
): StoredBalance { ): StoredBalance {
const newBalance = loadCurrentBalance(balance, epoch);
return { return {
...loadCurrentBalance(balance, epoch), ...newBalance,
currentEpochBalance: balance.currentEpochBalance.plus(amount), currentEpochBalance: newBalance.currentEpochBalance.plus(amount),
nextEpochBalance: balance.nextEpochBalance.plus(amount), nextEpochBalance: newBalance.nextEpochBalance.plus(amount),
}; };
} }
@@ -122,10 +125,11 @@ export function decreaseCurrentAndNextBalance(
amount: Numberish, amount: Numberish,
epoch: BigNumber, epoch: BigNumber,
): StoredBalance { ): StoredBalance {
const newBalance = loadCurrentBalance(balance, epoch);
return { return {
...loadCurrentBalance(balance, epoch), ...newBalance,
currentEpochBalance: balance.currentEpochBalance.minus(amount), currentEpochBalance: newBalance.currentEpochBalance.minus(amount),
nextEpochBalance: balance.nextEpochBalance.minus(amount), nextEpochBalance: newBalance.nextEpochBalance.minus(amount),
}; };
} }