Merge pull request #2387 from 0xProject/feature/fuzz/staking-rewards
`@0x/contracts-integrations`: Staking rewards fuzz test
This commit is contained in:
@@ -3,6 +3,9 @@ export {
|
||||
DummyMultipleReturnERC20TokenContract,
|
||||
DummyNoReturnERC20TokenContract,
|
||||
WETH9Contract,
|
||||
WETH9Events,
|
||||
WETH9DepositEventArgs,
|
||||
WETH9TransferEventArgs,
|
||||
ZRXTokenContract,
|
||||
DummyERC20TokenTransferEventArgs,
|
||||
ERC20TokenEventArgs,
|
||||
|
||||
@@ -103,7 +103,7 @@ export function calculateFillResults(
|
||||
order.takerAssetAmount,
|
||||
order.makerAssetAmount,
|
||||
);
|
||||
const makerFeePaid = safeGetPartialAmountFloor(makerAssetFilledAmount, order.makerAssetAmount, order.makerFee);
|
||||
const makerFeePaid = safeGetPartialAmountFloor(takerAssetFilledAmount, order.takerAssetAmount, order.makerFee);
|
||||
const takerFeePaid = safeGetPartialAmountFloor(takerAssetFilledAmount, order.takerAssetAmount, order.takerFee);
|
||||
return {
|
||||
makerAssetFilledAmount,
|
||||
@@ -113,3 +113,30 @@ export function calculateFillResults(
|
||||
protocolFeePaid: safeMul(protocolFeeMultiplier, gasPrice),
|
||||
};
|
||||
}
|
||||
|
||||
export const LibFractions = {
|
||||
add: (n1: BigNumber, d1: BigNumber, n2: BigNumber, d2: BigNumber): [BigNumber, BigNumber] => {
|
||||
if (n1.isZero()) {
|
||||
return [n2, d2];
|
||||
}
|
||||
if (n2.isZero()) {
|
||||
return [n1, d1];
|
||||
}
|
||||
const numerator = safeAdd(safeMul(n1, d2), safeMul(n2, d1));
|
||||
const denominator = safeMul(d1, d2);
|
||||
return [numerator, denominator];
|
||||
},
|
||||
normalize: (
|
||||
numerator: BigNumber,
|
||||
denominator: BigNumber,
|
||||
maxValue: BigNumber = new BigNumber(2).exponentiatedBy(127),
|
||||
): [BigNumber, BigNumber] => {
|
||||
if (numerator.isGreaterThan(maxValue) || denominator.isGreaterThan(maxValue)) {
|
||||
let rescaleBase = numerator.isGreaterThanOrEqualTo(denominator) ? numerator : denominator;
|
||||
rescaleBase = safeDiv(rescaleBase, maxValue);
|
||||
return [safeDiv(numerator, rescaleBase), safeDiv(denominator, rescaleBase)];
|
||||
} else {
|
||||
return [numerator, denominator];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -107,23 +107,6 @@ describe('Reference functions', () => {
|
||||
).to.throw(expectedError.message);
|
||||
});
|
||||
|
||||
it('reverts if `order.makerAssetAmount` is 0', () => {
|
||||
const order = makeOrder({
|
||||
makerAssetAmount: constants.ZERO_AMOUNT,
|
||||
takerAssetAmount: ONE_ETHER,
|
||||
});
|
||||
const takerAssetFilledAmount = ONE_ETHER;
|
||||
const expectedError = new LibMathRevertErrors.DivisionByZeroError();
|
||||
return expect(() =>
|
||||
LibReferenceFunctions.calculateFillResults(
|
||||
order,
|
||||
takerAssetFilledAmount,
|
||||
DEFAULT_PROTOCOL_FEE_MULTIPLIER,
|
||||
DEFAULT_GAS_PRICE,
|
||||
),
|
||||
).to.throw(expectedError.message);
|
||||
});
|
||||
|
||||
it('reverts if `order.takerAssetAmount` is 0', () => {
|
||||
const order = makeOrder({
|
||||
makerAssetAmount: ONE_ETHER,
|
||||
|
||||
@@ -15,7 +15,6 @@ export type Constructor<T = {}> = new (...args: any[]) => T;
|
||||
export interface ActorConfig {
|
||||
name?: string;
|
||||
deployment: DeploymentManager;
|
||||
simulationEnvironment?: SimulationEnvironment;
|
||||
[mixinProperty: string]: any;
|
||||
}
|
||||
|
||||
@@ -25,10 +24,11 @@ export class Actor {
|
||||
public readonly name: string;
|
||||
public readonly privateKey: Buffer;
|
||||
public readonly deployment: DeploymentManager;
|
||||
public readonly simulationEnvironment?: SimulationEnvironment;
|
||||
public simulationEnvironment?: SimulationEnvironment;
|
||||
public simulationActions: {
|
||||
[action: string]: AsyncIterableIterator<AssertionResult | void>;
|
||||
} = {};
|
||||
public mixins: string[] = [];
|
||||
protected readonly _transactionFactory: TransactionFactory;
|
||||
|
||||
public static reset(): void {
|
||||
@@ -47,7 +47,6 @@ export class Actor {
|
||||
this.name = config.name || this.address;
|
||||
this.deployment = config.deployment;
|
||||
this.privateKey = constants.TESTRPC_PRIVATE_KEYS[config.deployment.accounts.indexOf(this.address)];
|
||||
this.simulationEnvironment = config.simulationEnvironment;
|
||||
this._transactionFactory = new TransactionFactory(
|
||||
this.privateKey,
|
||||
config.deployment.exchange.address,
|
||||
@@ -123,7 +122,6 @@ export class Actor {
|
||||
if (logs.length !== 1) {
|
||||
throw new Error('Invalid number of `TransferSingle` logs');
|
||||
}
|
||||
|
||||
const { id } = logs[0];
|
||||
|
||||
// Mint the token
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface FeeRecipientInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with fee recipients within the 0x ecosystem.
|
||||
* This mixin encapsulates functionality associated with fee recipients within the 0x ecosystem.
|
||||
* As of writing, the only extra functionality provided is signing Coordinator approvals.
|
||||
*/
|
||||
export function FeeRecipientMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<FeeRecipientInterface> {
|
||||
@@ -35,6 +35,7 @@ export function FeeRecipientMixin<TBase extends Constructor>(Base: TBase): TBase
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
this.actor.mixins.push('FeeRecipient');
|
||||
|
||||
const { verifyingContract } = args[0] as FeeRecipientConfig;
|
||||
if (verifyingContract !== undefined) {
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs, TestStakingEvents } from '@0x/contracts-staking';
|
||||
import {
|
||||
AggregatedStats,
|
||||
IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs,
|
||||
TestStakingEvents,
|
||||
} from '@0x/contracts-staking';
|
||||
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 { validFinalizePoolAssertion } from '../assertions/finalizePool';
|
||||
import { AssertionResult } from '../assertions/function_assertion';
|
||||
import { Pseudorandom } from '../utils/pseudorandom';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
export interface KeeperInterface {
|
||||
@@ -11,7 +20,7 @@ export interface KeeperInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with keepers within the 0x ecosystem.
|
||||
* This mixin encapsulates functionality associated with keepers within the 0x ecosystem.
|
||||
* This includes ending epochs sand finalizing pools in the staking system.
|
||||
*/
|
||||
export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<KeeperInterface> {
|
||||
@@ -27,6 +36,14 @@ export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
this.actor.mixins.push('Keeper');
|
||||
|
||||
// Register this mixin's assertion generators
|
||||
this.actor.simulationActions = {
|
||||
...this.actor.simulationActions,
|
||||
validFinalizePool: this._validFinalizePool(),
|
||||
validEndEpoch: this._validEndEpoch(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,14 +52,7 @@ export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
public async endEpochAsync(shouldFastForward: boolean = true): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
if (shouldFastForward) {
|
||||
// increase timestamp of next block by how many seconds we need to
|
||||
// get to the next epoch.
|
||||
const epochEndTime = await stakingWrapper.getCurrentEpochEarliestEndTimeInSeconds().callAsync();
|
||||
const lastBlockTime = await web3Wrapper.getBlockTimestampAsync('latest');
|
||||
const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber());
|
||||
await web3Wrapper.increaseTimeAsync(dt);
|
||||
// mine next block
|
||||
await web3Wrapper.mineBlockAsync();
|
||||
await this._fastForwardToNextEpochAsync();
|
||||
}
|
||||
return stakingWrapper.endEpoch().awaitTransactionSuccessAsync({ from: this.actor.address });
|
||||
}
|
||||
@@ -75,6 +85,51 @@ export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async *_validFinalizePool(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validFinalizePoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
|
||||
while (true) {
|
||||
// Finalize a random pool, or do nothing if there are no pools in the simulation yet.
|
||||
const poolId = Pseudorandom.sample(Object.keys(stakingPools));
|
||||
if (poolId === undefined) {
|
||||
yield;
|
||||
} else {
|
||||
yield assertion.executeAsync([poolId], { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async *_validEndEpoch(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const assertion = validEndEpochAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
while (true) {
|
||||
const { currentEpoch } = this.actor.simulationEnvironment!;
|
||||
const aggregatedStats = AggregatedStats.fromArray(
|
||||
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch.minus(1)).callAsync(),
|
||||
);
|
||||
if (aggregatedStats.numPoolsToFinalize.isGreaterThan(0)) {
|
||||
// Can't end the epoch if the previous epoch is not fully finalized.
|
||||
yield;
|
||||
} else {
|
||||
await this._fastForwardToNextEpochAsync();
|
||||
yield assertion.executeAsync([], { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _fastForwardToNextEpochAsync(): Promise<void> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
|
||||
// increase timestamp of next block by how many seconds we need to
|
||||
// get to the next epoch.
|
||||
const epochEndTime = await stakingWrapper.getCurrentEpochEarliestEndTimeInSeconds().callAsync();
|
||||
const lastBlockTime = await web3Wrapper.getBlockTimestampAsync('latest');
|
||||
const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber());
|
||||
await web3Wrapper.increaseTimeAsync(dt);
|
||||
// mine next block
|
||||
await web3Wrapper.mineBlockAsync();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
|
||||
import { constants, OrderFactory } from '@0x/contracts-test-utils';
|
||||
import { Order, SignedOrder } from '@0x/types';
|
||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
@@ -18,10 +19,11 @@ export interface MakerInterface {
|
||||
signOrderAsync: (customOrderParams?: Partial<Order>) => Promise<SignedOrder>;
|
||||
cancelOrderAsync: (order: SignedOrder) => Promise<TransactionReceiptWithDecodedLogs>;
|
||||
joinStakingPoolAsync: (poolId: string) => Promise<TransactionReceiptWithDecodedLogs>;
|
||||
createFillableOrderAsync: (taker: Actor) => Promise<SignedOrder>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with makers within the 0x ecosystem.
|
||||
* This mixin encapsulates functionality associated with makers within the 0x ecosystem.
|
||||
* This includes signing and canceling orders, as well as joining a staking pool as a maker.
|
||||
*/
|
||||
export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<MakerInterface> {
|
||||
@@ -39,6 +41,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
this.actor.mixins.push('Maker');
|
||||
|
||||
const { orderConfig } = args[0] as MakerConfig;
|
||||
const defaultOrderParams = {
|
||||
@@ -84,13 +87,64 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
|
||||
});
|
||||
}
|
||||
|
||||
public async createFillableOrderAsync(taker: Actor): Promise<SignedOrder> {
|
||||
const { actors, balanceStore } = this.actor.simulationEnvironment!;
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
|
||||
// Choose the assets for the order
|
||||
const [makerToken, makerFeeToken, takerToken, takerFeeToken] = Pseudorandom.sampleSize(
|
||||
this.actor.deployment.tokens.erc20,
|
||||
4, // tslint:disable-line:custom-no-magic-numbers
|
||||
);
|
||||
|
||||
// Maker and taker set balances/allowances to guarantee that the fill succeeds.
|
||||
// Amounts are chosen to be within each actor's balance (divided by 2, in case
|
||||
// e.g. makerAsset = makerFeeAsset)
|
||||
const [makerAssetAmount, makerFee, takerAssetAmount, takerFee] = await Promise.all(
|
||||
[
|
||||
[this.actor, makerToken],
|
||||
[this.actor, makerFeeToken],
|
||||
[taker, takerToken],
|
||||
[taker, takerFeeToken],
|
||||
].map(async ([owner, token]) => {
|
||||
let balance = balanceStore.balances.erc20[owner.address][token.address];
|
||||
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));
|
||||
}),
|
||||
);
|
||||
// Encode asset data
|
||||
const [makerAssetData, makerFeeAssetData, takerAssetData, takerFeeAssetData] = [
|
||||
makerToken,
|
||||
makerFeeToken,
|
||||
takerToken,
|
||||
takerFeeToken,
|
||||
].map(token =>
|
||||
this.actor.deployment.assetDataEncoder.ERC20Token(token.address).getABIEncodedTransactionData(),
|
||||
);
|
||||
|
||||
// Maker signs the order
|
||||
return this.signOrderAsync({
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
makerFeeAssetData,
|
||||
takerFeeAssetData,
|
||||
makerAssetAmount,
|
||||
takerAssetAmount,
|
||||
makerFee,
|
||||
takerFee,
|
||||
feeRecipientAddress: Pseudorandom.sample(actors)!.address,
|
||||
});
|
||||
}
|
||||
|
||||
private async *_validJoinStakingPool(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validJoinStakingPoolAssertion(this.actor.deployment);
|
||||
while (true) {
|
||||
const poolId = Pseudorandom.sample(Object.keys(stakingPools));
|
||||
if (poolId === undefined) {
|
||||
yield undefined;
|
||||
yield;
|
||||
} else {
|
||||
yield assertion.executeAsync([poolId], { from: this.actor.address });
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface PoolOperatorInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with pool operators within the 0x ecosystem.
|
||||
* This mixin encapsulates functionality associated with pool operators within the 0x ecosystem.
|
||||
* This includes creating staking pools and decreasing the operator share of a pool.
|
||||
*/
|
||||
export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<PoolOperatorInterface> {
|
||||
@@ -35,6 +35,7 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
this.actor.mixins.push('PoolOperator');
|
||||
|
||||
// Register this mixin's assertion generators
|
||||
this.actor.simulationActions = {
|
||||
@@ -80,8 +81,7 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
|
||||
}
|
||||
|
||||
private async *_validCreateStakingPool(): AsyncIterableIterator<AssertionResult> {
|
||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
|
||||
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
|
||||
while (true) {
|
||||
const operatorShare = Pseudorandom.integer(constants.PPM).toNumber();
|
||||
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AssertionResult } from '../assertions/function_assertion';
|
||||
import { validMoveStakeAssertion } from '../assertions/moveStake';
|
||||
import { validStakeAssertion } from '../assertions/stake';
|
||||
import { validUnstakeAssertion } from '../assertions/unstake';
|
||||
import { validWithdrawDelegatorRewardsAssertion } from '../assertions/withdrawDelegatorRewards';
|
||||
import { Pseudorandom } from '../utils/pseudorandom';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
@@ -16,7 +17,7 @@ export interface StakerInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with stakers within the 0x ecosystem.
|
||||
* This mixin encapsulates functionality associated with stakers within the 0x ecosystem.
|
||||
* This includes staking ZRX (and optionally delegating it to a specific pool).
|
||||
*/
|
||||
export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<StakerInterface> {
|
||||
@@ -33,6 +34,8 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
this.actor.mixins.push('Staker');
|
||||
|
||||
this.stake = {
|
||||
[StakeStatus.Undelegated]: new StoredBalance(),
|
||||
[StakeStatus.Delegated]: { total: new StoredBalance() },
|
||||
@@ -44,6 +47,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
validStake: this._validStake(),
|
||||
validUnstake: this._validUnstake(),
|
||||
validMoveStake: this._validMoveStake(),
|
||||
validWithdrawDelegatorRewards: this._validWithdrawDelegatorRewards(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,8 +73,8 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
|
||||
private async *_validStake(): AsyncIterableIterator<AssertionResult> {
|
||||
const { zrx } = this.actor.deployment.tokens;
|
||||
const { deployment, balanceStore, globalStake } = this.actor.simulationEnvironment!;
|
||||
const assertion = validStakeAssertion(deployment, balanceStore, globalStake, this.stake);
|
||||
const { deployment, balanceStore } = this.actor.simulationEnvironment!;
|
||||
const assertion = validStakeAssertion(deployment, this.actor.simulationEnvironment!, this.stake);
|
||||
|
||||
while (true) {
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
@@ -82,8 +86,8 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
|
||||
private async *_validUnstake(): AsyncIterableIterator<AssertionResult> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
const { deployment, balanceStore, globalStake } = this.actor.simulationEnvironment!;
|
||||
const assertion = validUnstakeAssertion(deployment, balanceStore, globalStake, this.stake);
|
||||
const { deployment, balanceStore } = this.actor.simulationEnvironment!;
|
||||
const assertion = validUnstakeAssertion(deployment, this.actor.simulationEnvironment!, this.stake);
|
||||
|
||||
while (true) {
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
@@ -100,26 +104,34 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
}
|
||||
|
||||
private async *_validMoveStake(): AsyncIterableIterator<AssertionResult> {
|
||||
const { deployment, globalStake, stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validMoveStakeAssertion(deployment, globalStake, this.stake, stakingPools);
|
||||
const { deployment, stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validMoveStakeAssertion(deployment, this.actor.simulationEnvironment!, this.stake);
|
||||
|
||||
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'])),
|
||||
);
|
||||
// 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
|
||||
fromPoolId === undefined || stakingPools[fromPoolId].lastFinalized.isLessThan(currentEpoch.minus(1))
|
||||
? StakeStatus.Undelegated
|
||||
: (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
|
||||
const from = new StakeInfo(fromStatus, fromPoolId);
|
||||
|
||||
// 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
|
||||
toPoolId === undefined || stakingPools[toPoolId].lastFinalized.isLessThan(currentEpoch.minus(1))
|
||||
? StakeStatus.Undelegated
|
||||
: (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.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
|
||||
@@ -129,6 +141,28 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
|
||||
yield assertion.executeAsync([from, to, amount], { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
|
||||
private async *_validWithdrawDelegatorRewards(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validWithdrawDelegatorRewardsAssertion(
|
||||
this.actor.deployment,
|
||||
this.actor.simulationEnvironment!,
|
||||
);
|
||||
while (true) {
|
||||
const prevEpoch = this.actor.simulationEnvironment!.currentEpoch.minus(1);
|
||||
// Pick a finalized pool
|
||||
const poolId = Pseudorandom.sample(
|
||||
Object.keys(stakingPools).filter(id =>
|
||||
stakingPools[id].lastFinalized.isGreaterThanOrEqualTo(prevEpoch),
|
||||
),
|
||||
);
|
||||
if (poolId === undefined) {
|
||||
yield;
|
||||
} else {
|
||||
yield assertion.executeAsync([poolId], { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { constants } from '@0x/contracts-test-utils';
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
|
||||
|
||||
import { validFillOrderCompleteFillAssertion } from '../assertions/fillOrder';
|
||||
import { validFillOrderAssertion } from '../assertions/fillOrder';
|
||||
import { AssertionResult } from '../assertions/function_assertion';
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { Pseudorandom } from '../utils/pseudorandom';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
import { Maker } from './maker';
|
||||
import { filterActorsByRole } from './utils';
|
||||
|
||||
export interface TakerInterface {
|
||||
fillOrderAsync: (
|
||||
@@ -19,7 +20,7 @@ export interface TakerInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with takers within the 0x ecosystem.
|
||||
* This mixin encapsulates functionality associated with takers within the 0x ecosystem.
|
||||
* As of writing, the only extra functionality provided is a utility wrapper around `fillOrder`,
|
||||
*/
|
||||
export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<TakerInterface> {
|
||||
@@ -35,11 +36,12 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
this.actor.mixins.push('Taker');
|
||||
|
||||
// Register this mixin's assertion generators
|
||||
this.actor.simulationActions = {
|
||||
...this.actor.simulationActions,
|
||||
validFillOrderCompleteFill: this._validFillOrderCompleteFill(),
|
||||
validFillOrder: this._validFillOrder(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,32 +63,24 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
|
||||
});
|
||||
}
|
||||
|
||||
private async *_validFillOrderCompleteFill(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { marketMakers } = this.actor.simulationEnvironment!;
|
||||
const assertion = validFillOrderCompleteFillAssertion(this.actor.deployment);
|
||||
private async *_validFillOrder(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { actors } = this.actor.simulationEnvironment!;
|
||||
const assertion = validFillOrderAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
|
||||
while (true) {
|
||||
const maker = Pseudorandom.sample(marketMakers);
|
||||
// Choose a maker to be the other side of the order
|
||||
const maker = Pseudorandom.sample(filterActorsByRole(actors, Maker));
|
||||
if (maker === undefined) {
|
||||
yield undefined;
|
||||
yield;
|
||||
} else {
|
||||
// Configure the maker's token balances so that the order will definitely be fillable.
|
||||
await Promise.all([
|
||||
...this.actor.deployment.tokens.erc20.map(async token => maker.configureERC20TokenAsync(token)),
|
||||
...this.actor.deployment.tokens.erc20.map(async token =>
|
||||
this.actor.configureERC20TokenAsync(token),
|
||||
),
|
||||
this.actor.configureERC20TokenAsync(
|
||||
this.actor.deployment.tokens.weth,
|
||||
this.actor.deployment.staking.stakingProxy.address,
|
||||
),
|
||||
]);
|
||||
|
||||
const order = await maker.signOrderAsync({
|
||||
makerAssetAmount: Pseudorandom.integer(constants.INITIAL_ERC20_BALANCE),
|
||||
takerAssetAmount: Pseudorandom.integer(constants.INITIAL_ERC20_BALANCE),
|
||||
});
|
||||
yield assertion.executeAsync([order, order.takerAssetAmount, order.signature], {
|
||||
// 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);
|
||||
// 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)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ObjectMap } from '@0x/types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { Actor } from './base';
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
/**
|
||||
* Utility function to convert Actors into an object mapping readable names to addresses.
|
||||
@@ -10,3 +10,14 @@ import { Actor } from './base';
|
||||
export function actorAddressesByName(actors: Actor[]): ObjectMap<string> {
|
||||
return _.zipObject(actors.map(actor => actor.name), actors.map(actor => actor.address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the given actors by role, specified by the class exported by an actor mixin file,
|
||||
* e.g, 'Maker', 'Taker', etc.
|
||||
*/
|
||||
export function filterActorsByRole<TClass extends Constructor>(
|
||||
actors: Actor[],
|
||||
role: TClass,
|
||||
): Array<InstanceType<typeof role>> {
|
||||
return actors.filter(actor => actor.mixins.includes(role.name)) as InstanceType<typeof role>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { StakingPoolById, StoredBalance } from '@0x/contracts-staking';
|
||||
import { StoredBalance } from '@0x/contracts-staking';
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
@@ -16,7 +17,7 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
/* tslint:disable:no-non-null-assertion */
|
||||
export function validCreateStakingPoolAssertion(
|
||||
deployment: DeploymentManager,
|
||||
pools: StakingPoolById,
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
): FunctionAssertion<[number, boolean], string, string> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
@@ -36,6 +37,9 @@ export function validCreateStakingPoolAssertion(
|
||||
args: [number, boolean],
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const [operatorShare] = args;
|
||||
|
||||
// Checks the logs for the new poolId, verifies that it is as expected
|
||||
@@ -44,10 +48,11 @@ export function validCreateStakingPoolAssertion(
|
||||
expect(actualPoolId).to.equal(expectedPoolId);
|
||||
|
||||
// Adds the new pool to local state
|
||||
pools[actualPoolId] = {
|
||||
simulationEnvironment.stakingPools[actualPoolId] = {
|
||||
operator: txData.from!,
|
||||
operatorShare,
|
||||
delegatedStake: new StoredBalance(),
|
||||
lastFinalized: simulationEnvironment.currentEpoch,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,7 +17,10 @@ export function validDecreaseStakingPoolOperatorShareAssertion(
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion<[string, number], {}, void>(stakingWrapper, 'decreaseStakingPoolOperatorShare', {
|
||||
after: async (_beforeInfo, _result: FunctionResult, args: [string, number], _txData: Partial<TxData>) => {
|
||||
after: async (_beforeInfo, result: FunctionResult, args: [string, number], _txData: Partial<TxData>) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const [poolId, expectedOperatorShare] = args;
|
||||
|
||||
// Checks that the on-chain pool's operator share has been updated.
|
||||
|
||||
119
contracts/integrations/test/framework/assertions/endEpoch.ts
Normal file
119
contracts/integrations/test/framework/assertions/endEpoch.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { WETH9DepositEventArgs, WETH9Events } from '@0x/contracts-erc20';
|
||||
import {
|
||||
AggregatedStats,
|
||||
StakingEpochEndedEventArgs,
|
||||
StakingEpochFinalizedEventArgs,
|
||||
StakingEvents,
|
||||
} from '@0x/contracts-staking';
|
||||
import { constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
interface EndEpochBeforeInfo {
|
||||
wethReservedForPoolRewards: BigNumber;
|
||||
aggregatedStatsBefore: AggregatedStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `endEpoch` which assumes valid input is provided. It checks
|
||||
* that the staking proxy contract wrapped its ETH balance, aggregated stats were updated, and
|
||||
* EpochFinalized/EpochEnded events were emitted.
|
||||
*/
|
||||
export function validEndEpochAssertion(
|
||||
deployment: DeploymentManager,
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
): FunctionAssertion<[], EndEpochBeforeInfo, void> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
const { balanceStore } = simulationEnvironment;
|
||||
|
||||
return new FunctionAssertion(stakingWrapper, 'endEpoch', {
|
||||
before: async () => {
|
||||
await balanceStore.updateEthBalancesAsync();
|
||||
const aggregatedStatsBefore = AggregatedStats.fromArray(
|
||||
await stakingWrapper.aggregatedStatsByEpoch(simulationEnvironment.currentEpoch).callAsync(),
|
||||
);
|
||||
const wethReservedForPoolRewards = await stakingWrapper.wethReservedForPoolRewards().callAsync();
|
||||
return { wethReservedForPoolRewards, aggregatedStatsBefore };
|
||||
},
|
||||
after: async (beforeInfo: EndEpochBeforeInfo, result: FunctionResult, _args: [], _txData: Partial<TxData>) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const { currentEpoch } = simulationEnvironment;
|
||||
const logs = result.receipt!.logs; // tslint:disable-line
|
||||
|
||||
// Check WETH deposit event
|
||||
const previousEthBalance = balanceStore.balances.eth[stakingWrapper.address] || constants.ZERO_AMOUNT;
|
||||
const expectedDepositEvents = previousEthBalance.isGreaterThan(0)
|
||||
? [
|
||||
{
|
||||
_owner: deployment.staking.stakingProxy.address,
|
||||
_value: previousEthBalance,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
verifyEventsFromLogs<WETH9DepositEventArgs>(logs, expectedDepositEvents, WETH9Events.Deposit);
|
||||
|
||||
// Check that the aggregated stats were updated
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
const { wethReservedForPoolRewards, aggregatedStatsBefore } = beforeInfo;
|
||||
const expectedAggregatedStats = {
|
||||
...aggregatedStatsBefore,
|
||||
rewardsAvailable: _.get(
|
||||
balanceStore.balances,
|
||||
['erc20', stakingWrapper.address, deployment.tokens.weth.address],
|
||||
constants.ZERO_AMOUNT,
|
||||
).minus(wethReservedForPoolRewards),
|
||||
};
|
||||
const aggregatedStatsAfter = AggregatedStats.fromArray(
|
||||
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
|
||||
);
|
||||
expect(aggregatedStatsAfter).to.deep.equal(expectedAggregatedStats);
|
||||
|
||||
// Check that an EpochEnded event was emitted
|
||||
verifyEventsFromLogs<StakingEpochEndedEventArgs>(
|
||||
logs,
|
||||
[
|
||||
{
|
||||
epoch: currentEpoch,
|
||||
numPoolsToFinalize: aggregatedStatsAfter.numPoolsToFinalize,
|
||||
rewardsAvailable: aggregatedStatsAfter.rewardsAvailable,
|
||||
totalFeesCollected: aggregatedStatsAfter.totalFeesCollected,
|
||||
totalWeightedStake: aggregatedStatsAfter.totalWeightedStake,
|
||||
},
|
||||
],
|
||||
StakingEvents.EpochEnded,
|
||||
);
|
||||
|
||||
// If there are no more pools to finalize, an EpochFinalized event should've been emitted
|
||||
const expectedEpochFinalizedEvents = aggregatedStatsAfter.numPoolsToFinalize.isZero()
|
||||
? [
|
||||
{
|
||||
epoch: currentEpoch,
|
||||
rewardsPaid: constants.ZERO_AMOUNT,
|
||||
rewardsRemaining: aggregatedStatsAfter.rewardsAvailable,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
verifyEventsFromLogs<StakingEpochFinalizedEventArgs>(
|
||||
logs,
|
||||
expectedEpochFinalizedEvents,
|
||||
StakingEvents.EpochFinalized,
|
||||
);
|
||||
|
||||
// The function returns the remaining number of unfinalized pools for the epoch
|
||||
expect(result.data, 'endEpoch should return the number of unfinalized pools').to.bignumber.equal(
|
||||
aggregatedStatsAfter.numPoolsToFinalize,
|
||||
);
|
||||
|
||||
// Update currentEpoch locally
|
||||
simulationEnvironment.currentEpoch = currentEpoch.plus(1);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,21 +1,41 @@
|
||||
import { ERC20TokenEvents, ERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
|
||||
import { ExchangeEvents, ExchangeFillEventArgs } from '@0x/contracts-exchange';
|
||||
import { constants, expect, orderHashUtils, verifyEvents } from '@0x/contracts-test-utils';
|
||||
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
|
||||
import {
|
||||
AggregatedStats,
|
||||
constants as stakingConstants,
|
||||
PoolStats,
|
||||
StakingEvents,
|
||||
StakingStakingPoolEarnedRewardsInEpochEventArgs,
|
||||
} from '@0x/contracts-staking';
|
||||
import { expect, orderHashUtils, verifyEvents } from '@0x/contracts-test-utils';
|
||||
import { FillResults, Order } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { Maker } from '../actors/maker';
|
||||
import { filterActorsByRole } from '../actors/utils';
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
function verifyFillEvents(
|
||||
takerAddress: string,
|
||||
txData: Partial<TxData>,
|
||||
order: Order,
|
||||
receipt: TransactionReceiptWithDecodedLogs,
|
||||
deployment: DeploymentManager,
|
||||
takerAssetFillAmount: BigNumber,
|
||||
): void {
|
||||
const fillResults = ReferenceFunctions.calculateFillResults(
|
||||
order,
|
||||
takerAssetFillAmount,
|
||||
DeploymentManager.protocolFeeMultiplier,
|
||||
DeploymentManager.gasPrice,
|
||||
);
|
||||
const takerAddress = txData.from as string;
|
||||
const value = new BigNumber(txData.value || 0);
|
||||
// Ensure that the fill event was correct.
|
||||
verifyEvents<ExchangeFillEventArgs>(
|
||||
receipt,
|
||||
@@ -30,38 +50,54 @@ function verifyFillEvents(
|
||||
orderHash: orderHashUtils.getOrderHashHex(order),
|
||||
takerAddress,
|
||||
senderAddress: takerAddress,
|
||||
makerAssetFilledAmount: order.makerAssetAmount,
|
||||
takerAssetFilledAmount: order.takerAssetAmount,
|
||||
makerFeePaid: constants.ZERO_AMOUNT,
|
||||
takerFeePaid: constants.ZERO_AMOUNT,
|
||||
protocolFeePaid: DeploymentManager.protocolFee,
|
||||
...fillResults,
|
||||
},
|
||||
],
|
||||
ExchangeEvents.Fill,
|
||||
);
|
||||
|
||||
const expectedTransferEvents = [
|
||||
{
|
||||
_from: takerAddress,
|
||||
_to: order.makerAddress,
|
||||
_value: fillResults.takerAssetFilledAmount,
|
||||
},
|
||||
{
|
||||
_from: order.makerAddress,
|
||||
_to: takerAddress,
|
||||
_value: fillResults.makerAssetFilledAmount,
|
||||
},
|
||||
{
|
||||
_from: takerAddress,
|
||||
_to: order.feeRecipientAddress,
|
||||
_value: fillResults.takerFeePaid,
|
||||
},
|
||||
{
|
||||
_from: order.makerAddress,
|
||||
_to: order.feeRecipientAddress,
|
||||
_value: fillResults.makerFeePaid,
|
||||
},
|
||||
].filter(event => event._value.isGreaterThan(0));
|
||||
|
||||
// If not enough wei is sent to cover the protocol fee, there will be an additional WETH transfer event
|
||||
if (value.isLessThan(DeploymentManager.protocolFee)) {
|
||||
expectedTransferEvents.push({
|
||||
_from: takerAddress,
|
||||
_to: deployment.staking.stakingProxy.address,
|
||||
_value: DeploymentManager.protocolFee,
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure that the transfer events were correctly emitted.
|
||||
verifyEvents<ERC20TokenTransferEventArgs>(
|
||||
receipt,
|
||||
[
|
||||
{
|
||||
_from: takerAddress,
|
||||
_to: order.makerAddress,
|
||||
_value: order.takerAssetAmount,
|
||||
},
|
||||
{
|
||||
_from: order.makerAddress,
|
||||
_to: takerAddress,
|
||||
_value: order.makerAssetAmount,
|
||||
},
|
||||
{
|
||||
_from: takerAddress,
|
||||
_to: deployment.staking.stakingProxy.address,
|
||||
_value: DeploymentManager.protocolFee,
|
||||
},
|
||||
],
|
||||
ERC20TokenEvents.Transfer,
|
||||
);
|
||||
verifyEvents<ERC20TokenTransferEventArgs>(receipt, expectedTransferEvents, ERC20TokenEvents.Transfer);
|
||||
}
|
||||
|
||||
interface FillOrderBeforeInfo {
|
||||
poolStats: PoolStats;
|
||||
aggregatedStats: AggregatedStats;
|
||||
poolStake: BigNumber;
|
||||
operatorStake: BigNumber;
|
||||
poolId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,29 +105,117 @@ function verifyFillEvents(
|
||||
*/
|
||||
/* tslint:disable:no-unnecessary-type-assertion */
|
||||
/* tslint:disable:no-non-null-assertion */
|
||||
export function validFillOrderCompleteFillAssertion(
|
||||
export function validFillOrderAssertion(
|
||||
deployment: DeploymentManager,
|
||||
): FunctionAssertion<[Order, BigNumber, string], {}, FillResults> {
|
||||
const exchange = deployment.exchange;
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
): FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
const { actors } = simulationEnvironment;
|
||||
|
||||
return new FunctionAssertion<[Order, BigNumber, string], {}, FillResults>(exchange, 'fillOrder', {
|
||||
after: async (
|
||||
_beforeInfo,
|
||||
result: FunctionResult,
|
||||
args: [Order, BigNumber, string],
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
const [order] = args;
|
||||
return new FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults>(
|
||||
deployment.exchange,
|
||||
'fillOrder',
|
||||
{
|
||||
before: async (args: [Order, BigNumber, string]) => {
|
||||
const [order] = args;
|
||||
const { currentEpoch } = simulationEnvironment;
|
||||
const maker = filterActorsByRole(actors, Maker).find(actor => actor.address === order.makerAddress);
|
||||
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success).to.be.true();
|
||||
const poolId = maker!.makerPoolId;
|
||||
if (poolId === undefined) {
|
||||
return;
|
||||
} else {
|
||||
const poolStats = PoolStats.fromArray(
|
||||
await stakingWrapper.poolStatsByEpoch(poolId, currentEpoch).callAsync(),
|
||||
);
|
||||
const aggregatedStats = AggregatedStats.fromArray(
|
||||
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
|
||||
);
|
||||
const { currentEpochBalance: poolStake } = await stakingWrapper
|
||||
.getTotalStakeDelegatedToPool(poolId)
|
||||
.callAsync();
|
||||
const { currentEpochBalance: operatorStake } = await stakingWrapper
|
||||
.getStakeDelegatedToPoolByOwner(simulationEnvironment.stakingPools[poolId].operator, poolId)
|
||||
.callAsync();
|
||||
return { poolStats, aggregatedStats, poolStake, poolId, operatorStake };
|
||||
}
|
||||
},
|
||||
after: async (
|
||||
beforeInfo: FillOrderBeforeInfo | void,
|
||||
result: FunctionResult,
|
||||
args: [Order, BigNumber, string],
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
// Ensure that the correct events were emitted.
|
||||
verifyFillEvents(txData.from!, order, result.receipt!, deployment);
|
||||
const [order, fillAmount] = args;
|
||||
const { currentEpoch } = simulationEnvironment;
|
||||
|
||||
// TODO: Add validation for on-chain state (like balances)
|
||||
// Ensure that the correct events were emitted.
|
||||
verifyFillEvents(txData, order, result.receipt!, deployment, fillAmount);
|
||||
|
||||
// If the maker is not in a staking pool, there's nothing to check
|
||||
if (beforeInfo === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedPoolStats = { ...beforeInfo.poolStats };
|
||||
const expectedAggregatedStats = { ...beforeInfo.aggregatedStats };
|
||||
const expectedEvents = [];
|
||||
|
||||
// Refer to `payProtocolFee`
|
||||
if (beforeInfo.poolStake.isGreaterThanOrEqualTo(stakingConstants.DEFAULT_PARAMS.minimumPoolStake)) {
|
||||
if (beforeInfo.poolStats.feesCollected.isZero()) {
|
||||
const membersStakeInPool = beforeInfo.poolStake.minus(beforeInfo.operatorStake);
|
||||
const weightedStakeInPool = beforeInfo.operatorStake.plus(
|
||||
ReferenceFunctions.getPartialAmountFloor(
|
||||
stakingConstants.DEFAULT_PARAMS.rewardDelegatedStakeWeight,
|
||||
new BigNumber(stakingConstants.PPM),
|
||||
membersStakeInPool,
|
||||
),
|
||||
);
|
||||
expectedPoolStats.membersStake = membersStakeInPool;
|
||||
expectedPoolStats.weightedStake = weightedStakeInPool;
|
||||
expectedAggregatedStats.totalWeightedStake = beforeInfo.aggregatedStats.totalWeightedStake.plus(
|
||||
weightedStakeInPool,
|
||||
);
|
||||
expectedAggregatedStats.numPoolsToFinalize = beforeInfo.aggregatedStats.numPoolsToFinalize.plus(
|
||||
1,
|
||||
);
|
||||
// StakingPoolEarnedRewardsInEpoch event emitted
|
||||
expectedEvents.push({
|
||||
epoch: currentEpoch,
|
||||
poolId: beforeInfo.poolId,
|
||||
});
|
||||
}
|
||||
// Credit a protocol fee to the maker's staking pool
|
||||
expectedPoolStats.feesCollected = beforeInfo.poolStats.feesCollected.plus(
|
||||
DeploymentManager.protocolFee,
|
||||
);
|
||||
// Update aggregated stats
|
||||
expectedAggregatedStats.totalFeesCollected = beforeInfo.aggregatedStats.totalFeesCollected.plus(
|
||||
DeploymentManager.protocolFee,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for updated stats and event
|
||||
const poolStats = PoolStats.fromArray(
|
||||
await stakingWrapper.poolStatsByEpoch(beforeInfo.poolId, currentEpoch).callAsync(),
|
||||
);
|
||||
const aggregatedStats = AggregatedStats.fromArray(
|
||||
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
|
||||
);
|
||||
expect(poolStats).to.deep.equal(expectedPoolStats);
|
||||
expect(aggregatedStats).to.deep.equal(expectedAggregatedStats);
|
||||
verifyEvents<StakingStakingPoolEarnedRewardsInEpochEventArgs>(
|
||||
result.receipt!,
|
||||
expectedEvents,
|
||||
StakingEvents.StakingPoolEarnedRewardsInEpoch,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
/* tslint:enable:no-non-null-assertion */
|
||||
/* tslint:enable:no-unnecessary-type-assertion */
|
||||
|
||||
217
contracts/integrations/test/framework/assertions/finalizePool.ts
Normal file
217
contracts/integrations/test/framework/assertions/finalizePool.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { WETH9Events, WETH9TransferEventArgs } from '@0x/contracts-erc20';
|
||||
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
|
||||
import {
|
||||
AggregatedStats,
|
||||
constants as stakingConstants,
|
||||
PoolStats,
|
||||
StakingEpochFinalizedEventArgs,
|
||||
StakingEvents,
|
||||
StakingRewardsPaidEventArgs,
|
||||
} from '@0x/contracts-staking';
|
||||
import {
|
||||
assertRoughlyEquals,
|
||||
constants,
|
||||
expect,
|
||||
filterLogsToArguments,
|
||||
toDecimal,
|
||||
verifyEventsFromLogs,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
const PRECISION = 15;
|
||||
const COBB_DOUGLAS_ALPHA = toDecimal(stakingConstants.DEFAULT_PARAMS.cobbDouglasAlphaNumerator).dividedBy(
|
||||
toDecimal(stakingConstants.DEFAULT_PARAMS.cobbDouglasAlphaDenominator),
|
||||
);
|
||||
|
||||
// Reference function for Cobb-Douglas
|
||||
function cobbDouglas(poolStats: PoolStats, aggregatedStats: AggregatedStats): BigNumber {
|
||||
const { feesCollected, weightedStake } = poolStats;
|
||||
const { rewardsAvailable, totalFeesCollected, totalWeightedStake } = aggregatedStats;
|
||||
|
||||
const feeRatio = toDecimal(feesCollected).dividedBy(toDecimal(totalFeesCollected));
|
||||
const stakeRatio = toDecimal(weightedStake).dividedBy(toDecimal(totalWeightedStake));
|
||||
// totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)
|
||||
return new BigNumber(
|
||||
feeRatio
|
||||
.pow(COBB_DOUGLAS_ALPHA)
|
||||
.times(stakeRatio.pow(toDecimal(1).minus(COBB_DOUGLAS_ALPHA)))
|
||||
.times(toDecimal(rewardsAvailable))
|
||||
.toFixed(0, BigNumber.ROUND_FLOOR),
|
||||
);
|
||||
}
|
||||
|
||||
interface FinalizePoolBeforeInfo {
|
||||
poolStats: PoolStats;
|
||||
aggregatedStats: AggregatedStats;
|
||||
poolRewards: BigNumber;
|
||||
cumulativeRewardsLastStored: BigNumber;
|
||||
mostRecentCumulativeRewards: {
|
||||
numerator: BigNumber;
|
||||
denominator: BigNumber;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `finalizePool` which assumes valid input is provided. The `after`
|
||||
* callback below is annotated with the solidity source of `finalizePool`.
|
||||
*/
|
||||
/* tslint:disable:no-unnecessary-type-assertion */
|
||||
export function validFinalizePoolAssertion(
|
||||
deployment: DeploymentManager,
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
): FunctionAssertion<[string], FinalizePoolBeforeInfo, void> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion<[string], FinalizePoolBeforeInfo, void>(stakingWrapper, 'finalizePool', {
|
||||
before: async (args: [string]) => {
|
||||
const [poolId] = args;
|
||||
const { currentEpoch } = simulationEnvironment;
|
||||
const prevEpoch = currentEpoch.minus(1);
|
||||
|
||||
const poolStats = PoolStats.fromArray(await stakingWrapper.poolStatsByEpoch(poolId, prevEpoch).callAsync());
|
||||
const aggregatedStats = AggregatedStats.fromArray(
|
||||
await stakingWrapper.aggregatedStatsByEpoch(prevEpoch).callAsync(),
|
||||
);
|
||||
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
|
||||
const [
|
||||
mostRecentCumulativeRewards,
|
||||
cumulativeRewardsLastStored,
|
||||
] = await stakingWrapper.getMostRecentCumulativeReward(poolId).callAsync();
|
||||
return {
|
||||
poolStats,
|
||||
aggregatedStats,
|
||||
poolRewards,
|
||||
cumulativeRewardsLastStored,
|
||||
mostRecentCumulativeRewards,
|
||||
};
|
||||
},
|
||||
after: async (beforeInfo: FinalizePoolBeforeInfo, result: FunctionResult, args: [string]) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const logs = result.receipt!.logs; // tslint:disable-line:no-non-null-assertion
|
||||
const { stakingPools, currentEpoch } = simulationEnvironment;
|
||||
const prevEpoch = currentEpoch.minus(1);
|
||||
const [poolId] = args;
|
||||
const pool = stakingPools[poolId];
|
||||
|
||||
// finalizePool noops if there are no pools to finalize or
|
||||
// the pool did not earn rewards or already finalized (has no fees).
|
||||
if (beforeInfo.aggregatedStats.numPoolsToFinalize.isZero() || beforeInfo.poolStats.feesCollected.isZero()) {
|
||||
expect(logs.length, 'Expect no events to be emitted').to.equal(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// It should have cleared the pool stats for prevEpoch
|
||||
const poolStats = PoolStats.fromArray(await stakingWrapper.poolStatsByEpoch(poolId, prevEpoch).callAsync());
|
||||
expect(poolStats).to.deep.equal({
|
||||
feesCollected: constants.ZERO_AMOUNT,
|
||||
weightedStake: constants.ZERO_AMOUNT,
|
||||
membersStake: constants.ZERO_AMOUNT,
|
||||
});
|
||||
|
||||
// uint256 rewards = _getUnfinalizedPoolRewardsFromPoolStats(poolStats, aggregatedStats);
|
||||
const rewards = BigNumber.min(
|
||||
cobbDouglas(beforeInfo.poolStats, beforeInfo.aggregatedStats),
|
||||
beforeInfo.aggregatedStats.rewardsAvailable.minus(beforeInfo.aggregatedStats.totalRewardsFinalized),
|
||||
);
|
||||
|
||||
// Check that a RewardsPaid event was emitted
|
||||
const events = filterLogsToArguments<StakingRewardsPaidEventArgs>(logs, StakingEvents.RewardsPaid);
|
||||
expect(events.length, 'Number of RewardsPaid events emitted').to.equal(1);
|
||||
const [rewardsPaidEvent] = events;
|
||||
expect(rewardsPaidEvent.poolId, 'RewardsPaid event: poolId').to.equal(poolId);
|
||||
expect(rewardsPaidEvent.epoch, 'RewardsPaid event: currentEpoch_').to.bignumber.equal(currentEpoch);
|
||||
|
||||
// Pull the operator and members' reward from the event
|
||||
const { operatorReward, membersReward } = rewardsPaidEvent;
|
||||
const totalReward = operatorReward.plus(membersReward);
|
||||
// Should be approximately equal to the rewards compute using the Cobb-Douglas reference function
|
||||
assertRoughlyEquals(totalReward, rewards, PRECISION);
|
||||
|
||||
// Operator takes their share of the rewards
|
||||
if (beforeInfo.poolStats.membersStake.isZero()) {
|
||||
expect(
|
||||
operatorReward,
|
||||
"operatorReward should equal totalReward if pool's membersStake is 0",
|
||||
).to.bignumber.equal(totalReward);
|
||||
} else {
|
||||
expect(operatorReward).to.bignumber.equal(
|
||||
ReferenceFunctions.getPartialAmountCeil(
|
||||
new BigNumber(pool.operatorShare),
|
||||
new BigNumber(stakingConstants.PPM),
|
||||
totalReward,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Pays the operator in WETH if the operator's reward is non-zero
|
||||
const expectedTransferEvents = operatorReward.isGreaterThan(0)
|
||||
? [
|
||||
{
|
||||
_from: deployment.staking.stakingProxy.address,
|
||||
_to: pool.operator,
|
||||
_value: operatorReward,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
// Check for WETH transfer event emitted when paying out operator's reward.
|
||||
verifyEventsFromLogs<WETH9TransferEventArgs>(logs, expectedTransferEvents, WETH9Events.Transfer);
|
||||
// Check that pool rewards have increased.
|
||||
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
|
||||
expect(poolRewards).to.bignumber.equal(beforeInfo.poolRewards.plus(membersReward));
|
||||
// Check that cumulative rewards have increased.
|
||||
const [
|
||||
mostRecentCumulativeRewards,
|
||||
cumulativeRewardsLastStored,
|
||||
] = await stakingWrapper.getMostRecentCumulativeReward(poolId).callAsync();
|
||||
expect(cumulativeRewardsLastStored).to.bignumber.equal(currentEpoch);
|
||||
let [numerator, denominator] = ReferenceFunctions.LibFractions.add(
|
||||
beforeInfo.mostRecentCumulativeRewards.numerator,
|
||||
beforeInfo.mostRecentCumulativeRewards.denominator,
|
||||
membersReward,
|
||||
beforeInfo.poolStats.membersStake,
|
||||
);
|
||||
[numerator, denominator] = ReferenceFunctions.LibFractions.normalize(numerator, denominator);
|
||||
expect(mostRecentCumulativeRewards).to.deep.equal({ numerator, denominator });
|
||||
|
||||
// Check that aggregated stats have been updated
|
||||
const aggregatedStats = AggregatedStats.fromArray(
|
||||
await stakingWrapper.aggregatedStatsByEpoch(prevEpoch).callAsync(),
|
||||
);
|
||||
expect(aggregatedStats).to.deep.equal({
|
||||
...beforeInfo.aggregatedStats,
|
||||
totalRewardsFinalized: beforeInfo.aggregatedStats.totalRewardsFinalized.plus(totalReward),
|
||||
numPoolsToFinalize: beforeInfo.aggregatedStats.numPoolsToFinalize.minus(1),
|
||||
});
|
||||
|
||||
// If there are no more unfinalized pools remaining, the epoch is finalized.
|
||||
const expectedEpochFinalizedEvents = aggregatedStats.numPoolsToFinalize.isZero()
|
||||
? [
|
||||
{
|
||||
epoch: prevEpoch,
|
||||
rewardsPaid: aggregatedStats.totalRewardsFinalized,
|
||||
rewardsRemaining: aggregatedStats.rewardsAvailable.minus(
|
||||
aggregatedStats.totalRewardsFinalized,
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
verifyEventsFromLogs<StakingEpochFinalizedEventArgs>(
|
||||
logs,
|
||||
expectedEpochFinalizedEvents,
|
||||
StakingEvents.EpochFinalized,
|
||||
);
|
||||
|
||||
// Update local state
|
||||
pool.lastFinalized = prevEpoch;
|
||||
},
|
||||
});
|
||||
}
|
||||
/* tslint:enable:no-unnecessary-type-assertion */
|
||||
@@ -79,13 +79,15 @@ export class FunctionAssertion<TArgs extends any[], TBefore, ReturnDataType> imp
|
||||
// Initialize the callResult so that the default success value is true.
|
||||
const callResult: FunctionResult = { success: true };
|
||||
|
||||
// Log function name, arguments, and txData
|
||||
logger.logFunctionAssertion(this._functionName, args, txData);
|
||||
|
||||
// Try to make the call to the function. If it is successful, pass the
|
||||
// result and receipt to the after condition.
|
||||
try {
|
||||
const functionWithArgs = (this._contractWrapper as any)[this._functionName](
|
||||
...args,
|
||||
) as ContractTxFunctionObj<ReturnDataType>;
|
||||
logger.logFunctionAssertion(this._functionName, args, txData);
|
||||
callResult.data = await functionWithArgs.callAsync(txData);
|
||||
callResult.receipt =
|
||||
functionWithArgs.awaitTransactionSuccessAsync !== undefined
|
||||
|
||||
@@ -11,16 +11,18 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
*/
|
||||
/* tslint:disable:no-unnecessary-type-assertion */
|
||||
/* tslint:disable:no-non-null-assertion */
|
||||
export function validJoinStakingPoolAssertion(deployment: DeploymentManager): FunctionAssertion<[string], {}, void> {
|
||||
export function validJoinStakingPoolAssertion(deployment: DeploymentManager): FunctionAssertion<[string], void, void> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion<[string], {}, void>(stakingWrapper, 'joinStakingPoolAsMaker', {
|
||||
after: async (_beforeInfo, _result: FunctionResult, args: [string], txData: Partial<TxData>) => {
|
||||
return new FunctionAssertion<[string], void, void>(stakingWrapper, 'joinStakingPoolAsMaker', {
|
||||
after: async (_beforeInfo: void, result: FunctionResult, args: [string], txData: Partial<TxData>) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const [poolId] = args;
|
||||
|
||||
expect(_result.success).to.be.true();
|
||||
|
||||
const logs = _result.receipt!.logs;
|
||||
// Verify a MakerStakingPoolSet event was emitted
|
||||
const logs = result.receipt!.logs;
|
||||
const logArgs = filterLogsToArguments<StakingMakerStakingPoolSetEventArgs>(
|
||||
logs,
|
||||
StakingEvents.MakerStakingPoolSet,
|
||||
@@ -31,6 +33,7 @@ export function validJoinStakingPoolAssertion(deployment: DeploymentManager): Fu
|
||||
poolId,
|
||||
},
|
||||
]);
|
||||
// Verify that the maker's pool id has been updated in storage
|
||||
const joinedPoolId = await deployment.staking.stakingWrapper.poolIdByMaker(txData.from!).callAsync();
|
||||
expect(joinedPoolId).to.be.eq(poolId);
|
||||
},
|
||||
|
||||
@@ -1,104 +1,156 @@
|
||||
import {
|
||||
GlobalStakeByStatus,
|
||||
decreaseNextBalance,
|
||||
increaseNextBalance,
|
||||
loadCurrentBalance,
|
||||
OwnerStakeByStatus,
|
||||
StakeInfo,
|
||||
StakeStatus,
|
||||
StakingPoolById,
|
||||
StoredBalance,
|
||||
} from '@0x/contracts-staking';
|
||||
import { constants, expect } from '@0x/contracts-test-utils';
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
function incrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber): void {
|
||||
_.update(stakeBalance, ['nextEpochBalance'], balance => (balance || constants.ZERO_AMOUNT).plus(amount));
|
||||
}
|
||||
|
||||
function decrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber): void {
|
||||
_.update(stakeBalance, ['nextEpochBalance'], balance => (balance || constants.ZERO_AMOUNT).minus(amount));
|
||||
}
|
||||
|
||||
function updateNextEpochBalances(
|
||||
globalStake: GlobalStakeByStatus,
|
||||
ownerStake: OwnerStakeByStatus,
|
||||
pools: StakingPoolById,
|
||||
from: StakeInfo,
|
||||
to: StakeInfo,
|
||||
amount: BigNumber,
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
): string[] {
|
||||
const { globalStake, stakingPools, currentEpoch } = simulationEnvironment;
|
||||
|
||||
// The on-chain state of these updated pools will be verified in the `after` of the assertion.
|
||||
const updatedPools = [];
|
||||
|
||||
// Decrement next epoch balances associated with the `from` stake
|
||||
if (from.status === StakeStatus.Undelegated) {
|
||||
// Decrement owner undelegated stake
|
||||
decrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount);
|
||||
ownerStake[StakeStatus.Undelegated] = decreaseNextBalance(
|
||||
ownerStake[StakeStatus.Undelegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
// Decrement global undelegated stake
|
||||
decrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount);
|
||||
globalStake[StakeStatus.Undelegated] = decreaseNextBalance(
|
||||
globalStake[StakeStatus.Undelegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
} else if (from.status === StakeStatus.Delegated) {
|
||||
// Decrement owner's delegated stake to this pool
|
||||
decrementNextEpochBalance(ownerStake[StakeStatus.Delegated][from.poolId], amount);
|
||||
ownerStake[StakeStatus.Delegated][from.poolId] = decreaseNextBalance(
|
||||
ownerStake[StakeStatus.Delegated][from.poolId],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
// Decrement owner's total delegated stake
|
||||
decrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount);
|
||||
ownerStake[StakeStatus.Delegated].total = decreaseNextBalance(
|
||||
ownerStake[StakeStatus.Delegated].total,
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
// Decrement global delegated stake
|
||||
decrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount);
|
||||
globalStake[StakeStatus.Delegated] = decreaseNextBalance(
|
||||
globalStake[StakeStatus.Delegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
// Decrement pool's delegated stake
|
||||
decrementNextEpochBalance(pools[from.poolId].delegatedStake, amount);
|
||||
stakingPools[from.poolId].delegatedStake = decreaseNextBalance(
|
||||
stakingPools[from.poolId].delegatedStake,
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
updatedPools.push(from.poolId);
|
||||
|
||||
// TODO: Check that delegator rewards have been withdrawn/synced
|
||||
}
|
||||
|
||||
// Increment next epoch balances associated with the `to` stake
|
||||
if (to.status === StakeStatus.Undelegated) {
|
||||
incrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount);
|
||||
incrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount);
|
||||
// Increment owner undelegated stake
|
||||
ownerStake[StakeStatus.Undelegated] = increaseNextBalance(
|
||||
ownerStake[StakeStatus.Undelegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
// Increment global undelegated stake
|
||||
globalStake[StakeStatus.Undelegated] = increaseNextBalance(
|
||||
globalStake[StakeStatus.Undelegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
} else if (to.status === StakeStatus.Delegated) {
|
||||
// Initializes the balance for this pool if the user has not previously delegated to it
|
||||
_.defaults(ownerStake[StakeStatus.Delegated], {
|
||||
[to.poolId]: new StoredBalance(),
|
||||
});
|
||||
// Increment owner's delegated stake to this pool
|
||||
incrementNextEpochBalance(ownerStake[StakeStatus.Delegated][to.poolId], amount);
|
||||
ownerStake[StakeStatus.Delegated][to.poolId] = increaseNextBalance(
|
||||
ownerStake[StakeStatus.Delegated][to.poolId],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
// Increment owner's total delegated stake
|
||||
incrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount);
|
||||
ownerStake[StakeStatus.Delegated].total = increaseNextBalance(
|
||||
ownerStake[StakeStatus.Delegated].total,
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
// Increment global delegated stake
|
||||
incrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount);
|
||||
globalStake[StakeStatus.Delegated] = increaseNextBalance(
|
||||
globalStake[StakeStatus.Delegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
// Increment pool's delegated stake
|
||||
incrementNextEpochBalance(pools[to.poolId].delegatedStake, amount);
|
||||
stakingPools[to.poolId].delegatedStake = increaseNextBalance(
|
||||
stakingPools[to.poolId].delegatedStake,
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
updatedPools.push(to.poolId);
|
||||
|
||||
// TODO: Check that delegator rewards have been withdrawn/synced
|
||||
}
|
||||
return updatedPools;
|
||||
}
|
||||
/**
|
||||
* Returns a FunctionAssertion for `moveStake` which assumes valid input is provided. The
|
||||
* FunctionAssertion checks that the staker's
|
||||
* Returns a FunctionAssertion for `moveStake` which assumes valid input is provided. Checks that
|
||||
* the owner's stake and global stake by status get updated correctly.
|
||||
*/
|
||||
/* tslint:disable:no-unnecessary-type-assertion */
|
||||
export function validMoveStakeAssertion(
|
||||
deployment: DeploymentManager,
|
||||
globalStake: GlobalStakeByStatus,
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
ownerStake: OwnerStakeByStatus,
|
||||
pools: StakingPoolById,
|
||||
): FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], {}, void> {
|
||||
): FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], void, void> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], {}, void>(stakingWrapper, 'moveStake', {
|
||||
return new FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], void, void>(stakingWrapper, 'moveStake', {
|
||||
after: async (
|
||||
_beforeInfo: {},
|
||||
_result: FunctionResult,
|
||||
_beforeInfo: void,
|
||||
result: FunctionResult,
|
||||
args: [StakeInfo, StakeInfo, BigNumber],
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const [from, to, amount] = args;
|
||||
const { stakingPools, globalStake, currentEpoch } = simulationEnvironment;
|
||||
|
||||
const owner = txData.from!; // tslint:disable-line:no-non-null-assertion
|
||||
|
||||
// Update local balances to match the expected result of this `moveStake` operation
|
||||
const updatedPools = updateNextEpochBalances(globalStake, ownerStake, pools, from, to, amount);
|
||||
const updatedPools = updateNextEpochBalances(ownerStake, from, to, amount, simulationEnvironment);
|
||||
|
||||
// Fetches on-chain owner stake balances and checks against local balances
|
||||
const ownerUndelegatedStake = {
|
||||
@@ -109,16 +161,24 @@ export function validMoveStakeAssertion(
|
||||
...new StoredBalance(),
|
||||
...(await stakingWrapper.getOwnerStakeByStatus(owner, StakeStatus.Delegated).callAsync()),
|
||||
};
|
||||
expect(ownerUndelegatedStake).to.deep.equal(ownerStake[StakeStatus.Undelegated]);
|
||||
expect(ownerDelegatedStake).to.deep.equal(ownerStake[StakeStatus.Delegated].total);
|
||||
expect(ownerUndelegatedStake).to.deep.equal(
|
||||
loadCurrentBalance(ownerStake[StakeStatus.Undelegated], currentEpoch),
|
||||
);
|
||||
expect(ownerDelegatedStake).to.deep.equal(
|
||||
loadCurrentBalance(ownerStake[StakeStatus.Delegated].total, currentEpoch),
|
||||
);
|
||||
|
||||
// Fetches on-chain global stake balances and checks against local balances
|
||||
const globalDelegatedStake = await stakingWrapper.getGlobalStakeByStatus(StakeStatus.Delegated).callAsync();
|
||||
const globalUndelegatedStake = await stakingWrapper
|
||||
.getGlobalStakeByStatus(StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const globalDelegatedStake = await stakingWrapper.getGlobalStakeByStatus(StakeStatus.Delegated).callAsync();
|
||||
expect(globalUndelegatedStake).to.deep.equal(globalStake[StakeStatus.Undelegated]);
|
||||
expect(globalDelegatedStake).to.deep.equal(globalStake[StakeStatus.Delegated]);
|
||||
expect(globalDelegatedStake).to.deep.equal(
|
||||
loadCurrentBalance(globalStake[StakeStatus.Delegated], currentEpoch),
|
||||
);
|
||||
expect(globalUndelegatedStake).to.deep.equal(
|
||||
loadCurrentBalance(globalStake[StakeStatus.Undelegated], currentEpoch),
|
||||
);
|
||||
|
||||
// Fetches on-chain pool stake balances and checks against local balances
|
||||
for (const poolId of updatedPools) {
|
||||
@@ -126,8 +186,12 @@ export function validMoveStakeAssertion(
|
||||
.getStakeDelegatedToPoolByOwner(owner, poolId)
|
||||
.callAsync();
|
||||
const totalStakeDelegated = await stakingWrapper.getTotalStakeDelegatedToPool(poolId).callAsync();
|
||||
expect(stakeDelegatedByOwner).to.deep.equal(ownerStake[StakeStatus.Delegated][poolId]);
|
||||
expect(totalStakeDelegated).to.deep.equal(pools[poolId].delegatedStake);
|
||||
expect(stakeDelegatedByOwner).to.deep.equal(
|
||||
loadCurrentBalance(ownerStake[StakeStatus.Delegated][poolId], currentEpoch),
|
||||
);
|
||||
expect(totalStakeDelegated).to.deep.equal(
|
||||
loadCurrentBalance(stakingPools[poolId].delegatedStake, currentEpoch),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { GlobalStakeByStatus, OwnerStakeByStatus, StakeStatus, StoredBalance } from '@0x/contracts-staking';
|
||||
import { increaseCurrentAndNextBalance, OwnerStakeByStatus, StakeStatus } from '@0x/contracts-staking';
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
|
||||
import { BlockchainBalanceStore } from '../balances/blockchain_balance_store';
|
||||
import { LocalBalanceStore } from '../balances/local_balance_store';
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
function expectedUndelegatedStake(
|
||||
initStake: OwnerStakeByStatus | GlobalStakeByStatus,
|
||||
amount: BigNumber,
|
||||
): StoredBalance {
|
||||
return {
|
||||
currentEpoch: initStake[StakeStatus.Undelegated].currentEpoch,
|
||||
currentEpochBalance: initStake[StakeStatus.Undelegated].currentEpochBalance.plus(amount),
|
||||
nextEpochBalance: initStake[StakeStatus.Undelegated].nextEpochBalance.plus(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `stake` which assumes valid input is provided. The
|
||||
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX decrease and increase,
|
||||
@@ -28,8 +17,7 @@ function expectedUndelegatedStake(
|
||||
/* tslint:disable:no-unnecessary-type-assertion */
|
||||
export function validStakeAssertion(
|
||||
deployment: DeploymentManager,
|
||||
balanceStore: BlockchainBalanceStore,
|
||||
globalStake: GlobalStakeByStatus,
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
ownerStake: OwnerStakeByStatus,
|
||||
): FunctionAssertion<[BigNumber], LocalBalanceStore, void> {
|
||||
const { stakingWrapper, zrxVault } = deployment.staking;
|
||||
@@ -37,11 +25,12 @@ export function validStakeAssertion(
|
||||
return new FunctionAssertion(stakingWrapper, 'stake', {
|
||||
before: async (args: [BigNumber], txData: Partial<TxData>) => {
|
||||
const [amount] = args;
|
||||
const { balanceStore } = simulationEnvironment;
|
||||
|
||||
// Simulates the transfer of ZRX from staker to vault
|
||||
const expectedBalances = LocalBalanceStore.create(balanceStore);
|
||||
expectedBalances.transferAsset(
|
||||
txData.from!, // tslint:disable-line:no-non-null-assertion
|
||||
txData.from as string,
|
||||
zrxVault.address,
|
||||
amount,
|
||||
deployment.assetDataEncoder.ERC20Token(deployment.tokens.zrx.address).getABIEncodedTransactionData(),
|
||||
@@ -50,33 +39,45 @@ export function validStakeAssertion(
|
||||
},
|
||||
after: async (
|
||||
expectedBalances: LocalBalanceStore,
|
||||
_result: FunctionResult,
|
||||
result: FunctionResult,
|
||||
args: [BigNumber],
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const [amount] = args;
|
||||
const { balanceStore, currentEpoch, globalStake } = simulationEnvironment;
|
||||
|
||||
// Checks that the ZRX transfer updated balances as expected.
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
balanceStore.assertEquals(expectedBalances);
|
||||
|
||||
// _increaseCurrentAndNextBalance
|
||||
ownerStake[StakeStatus.Undelegated] = increaseCurrentAndNextBalance(
|
||||
ownerStake[StakeStatus.Undelegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
globalStake[StakeStatus.Undelegated] = increaseCurrentAndNextBalance(
|
||||
globalStake[StakeStatus.Undelegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
|
||||
// Checks that the owner's undelegated stake has increased by the stake amount
|
||||
const ownerUndelegatedStake = await stakingWrapper
|
||||
.getOwnerStakeByStatus(txData.from!, StakeStatus.Undelegated) // tslint:disable-line:no-non-null-assertion
|
||||
.getOwnerStakeByStatus(txData.from as string, StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount);
|
||||
expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(expectedOwnerUndelegatedStake);
|
||||
// Updates local state accordingly
|
||||
ownerStake[StakeStatus.Undelegated] = expectedOwnerUndelegatedStake;
|
||||
expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(ownerStake[StakeStatus.Undelegated]);
|
||||
|
||||
// Checks that the global undelegated stake has also increased by the stake amount
|
||||
const globalUndelegatedStake = await stakingWrapper
|
||||
.getGlobalStakeByStatus(StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount);
|
||||
expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(expectedGlobalUndelegatedStake);
|
||||
// Updates local state accordingly
|
||||
globalStake[StakeStatus.Undelegated] = expectedGlobalUndelegatedStake;
|
||||
expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(
|
||||
globalStake[StakeStatus.Undelegated],
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,36 +1,23 @@
|
||||
import { GlobalStakeByStatus, OwnerStakeByStatus, StakeStatus, StoredBalance } from '@0x/contracts-staking';
|
||||
import { decreaseCurrentAndNextBalance, OwnerStakeByStatus, StakeStatus } from '@0x/contracts-staking';
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
|
||||
import { BlockchainBalanceStore } from '../balances/blockchain_balance_store';
|
||||
import { LocalBalanceStore } from '../balances/local_balance_store';
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
function expectedUndelegatedStake(
|
||||
initStake: OwnerStakeByStatus | GlobalStakeByStatus,
|
||||
amount: BigNumber,
|
||||
): StoredBalance {
|
||||
return {
|
||||
currentEpoch: initStake[StakeStatus.Undelegated].currentEpoch,
|
||||
currentEpochBalance: initStake[StakeStatus.Undelegated].currentEpochBalance.minus(amount),
|
||||
nextEpochBalance: initStake[StakeStatus.Undelegated].nextEpochBalance.minus(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `unstake` which assumes valid input is provided. The
|
||||
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX increase and decrease,
|
||||
* respectively, by the input amount.
|
||||
*/
|
||||
/* tslint:disable:no-unnecessary-type-assertion */
|
||||
/* tslint:disable:no-non-null-assertion */
|
||||
export function validUnstakeAssertion(
|
||||
deployment: DeploymentManager,
|
||||
balanceStore: BlockchainBalanceStore,
|
||||
globalStake: GlobalStakeByStatus,
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
ownerStake: OwnerStakeByStatus,
|
||||
): FunctionAssertion<[BigNumber], LocalBalanceStore, void> {
|
||||
const { stakingWrapper, zrxVault } = deployment.staking;
|
||||
@@ -38,12 +25,13 @@ export function validUnstakeAssertion(
|
||||
return new FunctionAssertion(stakingWrapper, 'unstake', {
|
||||
before: async (args: [BigNumber], txData: Partial<TxData>) => {
|
||||
const [amount] = args;
|
||||
const { balanceStore } = simulationEnvironment;
|
||||
|
||||
// Simulates the transfer of ZRX from vault to staker
|
||||
const expectedBalances = LocalBalanceStore.create(balanceStore);
|
||||
expectedBalances.transferAsset(
|
||||
zrxVault.address,
|
||||
txData.from!,
|
||||
txData.from as string,
|
||||
amount,
|
||||
deployment.assetDataEncoder.ERC20Token(deployment.tokens.zrx.address).getABIEncodedTransactionData(),
|
||||
);
|
||||
@@ -51,35 +39,46 @@ export function validUnstakeAssertion(
|
||||
},
|
||||
after: async (
|
||||
expectedBalances: LocalBalanceStore,
|
||||
_result: FunctionResult,
|
||||
result: FunctionResult,
|
||||
args: [BigNumber],
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const [amount] = args;
|
||||
const { balanceStore, currentEpoch, globalStake } = simulationEnvironment;
|
||||
|
||||
// Checks that the ZRX transfer updated balances as expected.
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
balanceStore.assertEquals(expectedBalances);
|
||||
|
||||
// _decreaseCurrentAndNextBalance
|
||||
ownerStake[StakeStatus.Undelegated] = decreaseCurrentAndNextBalance(
|
||||
ownerStake[StakeStatus.Undelegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
globalStake[StakeStatus.Undelegated] = decreaseCurrentAndNextBalance(
|
||||
globalStake[StakeStatus.Undelegated],
|
||||
amount,
|
||||
currentEpoch,
|
||||
);
|
||||
|
||||
// Checks that the owner's undelegated stake has decreased by the stake amount
|
||||
const ownerUndelegatedStake = await stakingWrapper
|
||||
.getOwnerStakeByStatus(txData.from!, StakeStatus.Undelegated)
|
||||
.getOwnerStakeByStatus(txData.from as string, StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount);
|
||||
expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(expectedOwnerUndelegatedStake);
|
||||
// Updates local state accordingly
|
||||
ownerStake[StakeStatus.Undelegated] = expectedOwnerUndelegatedStake;
|
||||
expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(ownerStake[StakeStatus.Undelegated]);
|
||||
|
||||
// Checks that the global undelegated stake has also decreased by the stake amount
|
||||
// Checks that the global undelegated stake has also increased by the stake amount
|
||||
const globalUndelegatedStake = await stakingWrapper
|
||||
.getGlobalStakeByStatus(StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount);
|
||||
expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(expectedGlobalUndelegatedStake);
|
||||
// Updates local state accordingly
|
||||
globalStake[StakeStatus.Undelegated] = expectedGlobalUndelegatedStake;
|
||||
expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(
|
||||
globalStake[StakeStatus.Undelegated],
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
/* tslint:enable:no-non-null-assertion */
|
||||
/* tslint:enable:no-unnecessary-type-assertion */
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { WETH9Events, WETH9TransferEventArgs } from '@0x/contracts-erc20';
|
||||
import { loadCurrentBalance, StoredBalance } from '@0x/contracts-staking';
|
||||
import { expect, filterLogsToArguments } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
interface WithdrawDelegatorRewardsBeforeInfo {
|
||||
delegatorStake: StoredBalance;
|
||||
poolRewards: BigNumber;
|
||||
wethReservedForPoolRewards: BigNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `withdrawDelegatorRewards` which assumes valid input is provided.
|
||||
* It checks that the delegator's stake gets synced and pool rewards are updated to reflect the
|
||||
* amount withdrawn.
|
||||
*/
|
||||
/* tslint:disable:no-unnecessary-type-assertion */
|
||||
export function validWithdrawDelegatorRewardsAssertion(
|
||||
deployment: DeploymentManager,
|
||||
simulationEnvironment: SimulationEnvironment,
|
||||
): FunctionAssertion<[string], WithdrawDelegatorRewardsBeforeInfo, void> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion(stakingWrapper, 'withdrawDelegatorRewards', {
|
||||
before: async (args: [string], txData: Partial<TxData>) => {
|
||||
const [poolId] = args;
|
||||
|
||||
const delegatorStake = await stakingWrapper
|
||||
.getStakeDelegatedToPoolByOwner(txData.from as string, poolId)
|
||||
.callAsync();
|
||||
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
|
||||
const wethReservedForPoolRewards = await stakingWrapper.wethReservedForPoolRewards().callAsync();
|
||||
return { delegatorStake, poolRewards, wethReservedForPoolRewards };
|
||||
},
|
||||
after: async (
|
||||
beforeInfo: WithdrawDelegatorRewardsBeforeInfo,
|
||||
result: FunctionResult,
|
||||
args: [string],
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
// Ensure that the tx succeeded.
|
||||
expect(result.success, `Error: ${result.data}`).to.be.true();
|
||||
|
||||
const [poolId] = args;
|
||||
const { currentEpoch } = simulationEnvironment;
|
||||
|
||||
// Check that delegator stake has been synced
|
||||
const expectedDelegatorStake = loadCurrentBalance(beforeInfo.delegatorStake, currentEpoch);
|
||||
const delegatorStake = await stakingWrapper
|
||||
.getStakeDelegatedToPoolByOwner(txData.from as string, poolId)
|
||||
.callAsync();
|
||||
expect(delegatorStake).to.deep.equal(expectedDelegatorStake);
|
||||
|
||||
// Check that pool rewards have been updated to reflect the amount withdrawn.
|
||||
const transferEvents = filterLogsToArguments<WETH9TransferEventArgs>(
|
||||
result.receipt!.logs, // tslint:disable-line:no-non-null-assertion
|
||||
WETH9Events.Transfer,
|
||||
);
|
||||
const expectedPoolRewards =
|
||||
transferEvents.length > 0
|
||||
? beforeInfo.poolRewards.minus(transferEvents[0]._value)
|
||||
: beforeInfo.poolRewards;
|
||||
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
|
||||
expect(poolRewards).to.bignumber.equal(expectedPoolRewards);
|
||||
|
||||
// TODO: Check CR
|
||||
},
|
||||
});
|
||||
}
|
||||
/* tslint:enable:no-unnecessary-type-assertion */
|
||||
@@ -393,7 +393,16 @@ export class DeploymentManager {
|
||||
stakingLogic.address,
|
||||
);
|
||||
|
||||
const stakingWrapper = new TestStakingContract(stakingProxy.address, environment.provider, txDefaults);
|
||||
const logDecoderDependencies = _.mapValues(
|
||||
{ ...stakingArtifacts, ...ERC20Artifacts },
|
||||
v => v.compilerOutput.abi,
|
||||
);
|
||||
const stakingWrapper = new TestStakingContract(
|
||||
stakingProxy.address,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
logDecoderDependencies,
|
||||
);
|
||||
|
||||
// Add the zrx vault and the weth contract to the staking proxy.
|
||||
await stakingWrapper.setWethContract(tokens.weth.address).awaitTransactionSuccessAsync({ from: owner });
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { GlobalStakeByStatus, StakeStatus, StakingPoolById, StoredBalance } from '@0x/contracts-staking';
|
||||
import {
|
||||
constants as stakingConstants,
|
||||
GlobalStakeByStatus,
|
||||
StakeStatus,
|
||||
StakingPoolById,
|
||||
StoredBalance,
|
||||
} from '@0x/contracts-staking';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { Maker } from './actors/maker';
|
||||
import { Actor } from './actors/base';
|
||||
import { AssertionResult } from './assertions/function_assertion';
|
||||
import { BlockchainBalanceStore } from './balances/blockchain_balance_store';
|
||||
import { DeploymentManager } from './deployment_manager';
|
||||
@@ -14,18 +21,27 @@ export class SimulationEnvironment {
|
||||
[StakeStatus.Delegated]: new StoredBalance(),
|
||||
};
|
||||
public stakingPools: StakingPoolById = {};
|
||||
public currentEpoch: BigNumber = stakingConstants.INITIAL_EPOCH;
|
||||
|
||||
public constructor(
|
||||
public readonly deployment: DeploymentManager,
|
||||
public balanceStore: BlockchainBalanceStore,
|
||||
public marketMakers: Maker[] = [],
|
||||
) {}
|
||||
public readonly actors: Actor[] = [],
|
||||
) {
|
||||
for (const actor of actors) {
|
||||
// Set the actor's simulation environment
|
||||
actor.simulationEnvironment = this;
|
||||
// Register each actor in the balance store
|
||||
this.balanceStore.registerTokenOwner(actor.address, actor.name);
|
||||
}
|
||||
}
|
||||
|
||||
public state(): any {
|
||||
return {
|
||||
globalStake: this.globalStake,
|
||||
stakingPools: this.stakingPools,
|
||||
balanceStore: this.balanceStore.toReadable(),
|
||||
currentEpoch: this.currentEpoch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class Logger {
|
||||
msg: `Function called: ${functionName}(${functionArgs
|
||||
.map(arg => JSON.stringify(arg).replace(/"/g, "'"))
|
||||
.join(', ')})`,
|
||||
step: this._step++,
|
||||
step: ++this._step,
|
||||
txData,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils';
|
||||
import * as seedrandom from 'seedrandom';
|
||||
|
||||
class PRNGWrapper {
|
||||
public readonly seed = process.env.UUID || Math.random().toString();
|
||||
public readonly seed = process.env.SEED || Math.random().toString();
|
||||
private readonly _rng = seedrandom(this.seed);
|
||||
|
||||
/*
|
||||
@@ -18,6 +18,21 @@ class PRNGWrapper {
|
||||
return arr[index];
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
public sampleSize<T>(arr: T[], n: number): T[] | undefined {
|
||||
if (arr.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const samples = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
samples.push(this.sample(arr) as T);
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
// tslint:disable:unified-signatures
|
||||
/*
|
||||
* Pseudorandom version of getRandomPortion/getRandomInteger. If two arguments are provided,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { blockchainTests } from '@0x/contracts-test-utils';
|
||||
|
||||
import { Actor } from '../framework/actors/base';
|
||||
import { PoolOperator } from '../framework/actors/pool_operator';
|
||||
import { filterActorsByRole } from '../framework/actors/utils';
|
||||
import { AssertionResult } from '../framework/assertions/function_assertion';
|
||||
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
||||
import { DeploymentManager } from '../framework/deployment_manager';
|
||||
@@ -10,16 +11,12 @@ import { Pseudorandom } from '../framework/utils/pseudorandom';
|
||||
|
||||
export class PoolManagementSimulation extends Simulation {
|
||||
protected async *_assertionGenerator(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { deployment } = this.environment;
|
||||
const operator = new PoolOperator({
|
||||
name: 'Operator',
|
||||
deployment,
|
||||
simulationEnvironment: this.environment,
|
||||
});
|
||||
const { actors } = this.environment;
|
||||
const operators = filterActorsByRole(actors, PoolOperator);
|
||||
|
||||
const actions = [
|
||||
operator.simulationActions.validCreateStakingPool,
|
||||
operator.simulationActions.validDecreaseStakingPoolOperatorShare,
|
||||
...operators.map(operator => operator.simulationActions.validCreateStakingPool),
|
||||
...operators.map(operator => operator.simulationActions.validDecreaseStakingPoolOperatorShare),
|
||||
];
|
||||
while (true) {
|
||||
const action = Pseudorandom.sample(actions);
|
||||
@@ -46,8 +43,12 @@ blockchainTests('Pool management fuzz test', env => {
|
||||
});
|
||||
const balanceStore = new BlockchainBalanceStore({}, {});
|
||||
|
||||
const simulationEnv = new SimulationEnvironment(deployment, balanceStore);
|
||||
const simulation = new PoolManagementSimulation(simulationEnv);
|
||||
const simulationEnvironment = new SimulationEnvironment(deployment, balanceStore, [
|
||||
new PoolOperator({ deployment, name: 'Operator 1' }),
|
||||
new PoolOperator({ deployment, name: 'Operator 2' }),
|
||||
]);
|
||||
|
||||
const simulation = new PoolManagementSimulation(simulationEnvironment);
|
||||
return simulation.fuzzAsync();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { blockchainTests, constants } from '@0x/contracts-test-utils';
|
||||
import { blockchainTests } from '@0x/contracts-test-utils';
|
||||
|
||||
import { Actor } from '../framework/actors/base';
|
||||
import { MakerTaker } from '../framework/actors/hybrids';
|
||||
import { Maker } from '../framework/actors/maker';
|
||||
import { PoolOperator } from '../framework/actors/pool_operator';
|
||||
import { Taker } from '../framework/actors/taker';
|
||||
import { filterActorsByRole } from '../framework/actors/utils';
|
||||
import { AssertionResult } from '../framework/assertions/function_assertion';
|
||||
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
||||
import { DeploymentManager } from '../framework/deployment_manager';
|
||||
@@ -10,21 +14,17 @@ import { Pseudorandom } from '../framework/utils/pseudorandom';
|
||||
|
||||
import { PoolManagementSimulation } from './pool_management_test';
|
||||
|
||||
class PoolMembershipSimulation extends Simulation {
|
||||
export class PoolMembershipSimulation extends Simulation {
|
||||
protected async *_assertionGenerator(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { deployment } = this.environment;
|
||||
const { actors } = this.environment;
|
||||
const makers = filterActorsByRole(actors, Maker);
|
||||
const takers = filterActorsByRole(actors, Taker);
|
||||
|
||||
const poolManagement = new PoolManagementSimulation(this.environment);
|
||||
|
||||
const member = new MakerTaker({
|
||||
name: 'member',
|
||||
deployment,
|
||||
simulationEnvironment: this.environment,
|
||||
});
|
||||
|
||||
const actions = [
|
||||
member.simulationActions.validJoinStakingPool,
|
||||
member.simulationActions.validFillOrderCompleteFill,
|
||||
...makers.map(maker => maker.simulationActions.validJoinStakingPool),
|
||||
...takers.map(taker => taker.simulationActions.validFillOrder),
|
||||
poolManagement.generator,
|
||||
];
|
||||
|
||||
@@ -36,45 +36,23 @@ class PoolMembershipSimulation extends Simulation {
|
||||
}
|
||||
|
||||
blockchainTests('pool membership fuzz test', env => {
|
||||
let deployment: DeploymentManager;
|
||||
let maker: Maker;
|
||||
|
||||
before(async function(): Promise<void> {
|
||||
if (process.env.FUZZ_TEST !== 'pool_membership') {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
deployment = await DeploymentManager.deployAsync(env, {
|
||||
numErc20TokensToDeploy: 2,
|
||||
after(async () => {
|
||||
Actor.reset();
|
||||
});
|
||||
|
||||
it('fuzz', async () => {
|
||||
const deployment = await DeploymentManager.deployAsync(env, {
|
||||
numErc20TokensToDeploy: 4,
|
||||
numErc721TokensToDeploy: 0,
|
||||
numErc1155TokensToDeploy: 0,
|
||||
});
|
||||
|
||||
const makerToken = deployment.tokens.erc20[0];
|
||||
const takerToken = deployment.tokens.erc20[1];
|
||||
|
||||
const orderConfig = {
|
||||
feeRecipientAddress: constants.NULL_ADDRESS,
|
||||
makerAssetData: deployment.assetDataEncoder.ERC20Token(makerToken.address).getABIEncodedTransactionData(),
|
||||
takerAssetData: deployment.assetDataEncoder.ERC20Token(takerToken.address).getABIEncodedTransactionData(),
|
||||
makerFeeAssetData: deployment.assetDataEncoder
|
||||
.ERC20Token(makerToken.address)
|
||||
.getABIEncodedTransactionData(),
|
||||
takerFeeAssetData: deployment.assetDataEncoder
|
||||
.ERC20Token(takerToken.address)
|
||||
.getABIEncodedTransactionData(),
|
||||
makerFee: constants.ZERO_AMOUNT,
|
||||
takerFee: constants.ZERO_AMOUNT,
|
||||
};
|
||||
|
||||
maker = new Maker({
|
||||
name: 'maker',
|
||||
deployment,
|
||||
orderConfig,
|
||||
});
|
||||
});
|
||||
|
||||
it('fuzz', async () => {
|
||||
const balanceStore = new BlockchainBalanceStore(
|
||||
{
|
||||
StakingProxy: deployment.staking.stakingProxy.address,
|
||||
@@ -83,8 +61,24 @@ blockchainTests('pool membership fuzz test', env => {
|
||||
{ erc20: { ZRX: deployment.tokens.zrx } },
|
||||
);
|
||||
|
||||
const simulationEnv = new SimulationEnvironment(deployment, balanceStore, [maker]);
|
||||
const simulation = new PoolMembershipSimulation(simulationEnv);
|
||||
const actors = [
|
||||
new Maker({ deployment, name: 'Maker 1' }),
|
||||
new Maker({ deployment, name: 'Maker 2' }),
|
||||
new Taker({ deployment, name: 'Taker 1' }),
|
||||
new Taker({ deployment, name: 'Taker 2' }),
|
||||
new MakerTaker({ deployment, name: 'Maker/Taker' }),
|
||||
new PoolOperator({ deployment, name: 'Operator 1' }),
|
||||
new PoolOperator({ deployment, name: 'Operator 2' }),
|
||||
];
|
||||
|
||||
const simulationEnvironment = new SimulationEnvironment(deployment, balanceStore, actors);
|
||||
|
||||
const takers = filterActorsByRole(actors, Taker);
|
||||
for (const taker of takers) {
|
||||
await taker.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address);
|
||||
}
|
||||
|
||||
const simulation = new PoolMembershipSimulation(simulationEnvironment);
|
||||
return simulation.fuzzAsync();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { blockchainTests } from '@0x/contracts-test-utils';
|
||||
|
||||
import { Actor } from '../framework/actors/base';
|
||||
import { StakerOperator } from '../framework/actors/hybrids';
|
||||
import { PoolOperator } from '../framework/actors/pool_operator';
|
||||
import { Staker } from '../framework/actors/staker';
|
||||
import { filterActorsByRole } from '../framework/actors/utils';
|
||||
import { AssertionResult } from '../framework/assertions/function_assertion';
|
||||
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
||||
import { DeploymentManager } from '../framework/deployment_manager';
|
||||
@@ -12,17 +15,15 @@ import { PoolManagementSimulation } from './pool_management_test';
|
||||
|
||||
export class StakeManagementSimulation extends Simulation {
|
||||
protected async *_assertionGenerator(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { deployment, balanceStore } = this.environment;
|
||||
const { actors } = this.environment;
|
||||
const stakers = filterActorsByRole(actors, Staker);
|
||||
|
||||
const poolManagement = new PoolManagementSimulation(this.environment);
|
||||
|
||||
const staker = new Staker({ name: 'Staker', deployment, simulationEnvironment: this.environment });
|
||||
await staker.configureERC20TokenAsync(deployment.tokens.zrx);
|
||||
balanceStore.registerTokenOwner(staker.address, staker.name);
|
||||
|
||||
const actions = [
|
||||
staker.simulationActions.validStake,
|
||||
staker.simulationActions.validUnstake,
|
||||
staker.simulationActions.validMoveStake,
|
||||
...stakers.map(staker => staker.simulationActions.validStake),
|
||||
...stakers.map(staker => staker.simulationActions.validUnstake),
|
||||
...stakers.map(staker => staker.simulationActions.validMoveStake),
|
||||
poolManagement.generator,
|
||||
];
|
||||
while (true) {
|
||||
@@ -57,8 +58,21 @@ blockchainTests('Stake management fuzz test', env => {
|
||||
{ erc20: { ZRX: deployment.tokens.zrx } },
|
||||
);
|
||||
|
||||
const simulationEnv = new SimulationEnvironment(deployment, balanceStore);
|
||||
const simulation = new StakeManagementSimulation(simulationEnv);
|
||||
const actors = [
|
||||
new Staker({ name: 'Staker 1', deployment }),
|
||||
new Staker({ name: 'Staker 2', deployment }),
|
||||
new StakerOperator({ name: 'Staker/Operator', deployment }),
|
||||
new PoolOperator({ name: 'Operator', deployment }),
|
||||
];
|
||||
|
||||
const simulationEnvironment = new SimulationEnvironment(deployment, balanceStore, actors);
|
||||
|
||||
const stakers = filterActorsByRole(actors, Staker);
|
||||
for (const staker of stakers) {
|
||||
await staker.configureERC20TokenAsync(deployment.tokens.zrx);
|
||||
}
|
||||
|
||||
const simulation = new StakeManagementSimulation(simulationEnvironment);
|
||||
return simulation.fuzzAsync();
|
||||
});
|
||||
});
|
||||
|
||||
120
contracts/integrations/test/fuzz_tests/staking_rewards_test.ts
Normal file
120
contracts/integrations/test/fuzz_tests/staking_rewards_test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { blockchainTests } from '@0x/contracts-test-utils';
|
||||
|
||||
import { Actor } from '../framework/actors/base';
|
||||
import {
|
||||
MakerTaker,
|
||||
OperatorStakerMaker,
|
||||
StakerKeeper,
|
||||
StakerMaker,
|
||||
StakerOperator,
|
||||
} from '../framework/actors/hybrids';
|
||||
import { Keeper } from '../framework/actors/keeper';
|
||||
import { Maker } from '../framework/actors/maker';
|
||||
import { PoolOperator } from '../framework/actors/pool_operator';
|
||||
import { Staker } from '../framework/actors/staker';
|
||||
import { Taker } from '../framework/actors/taker';
|
||||
import { filterActorsByRole } from '../framework/actors/utils';
|
||||
import { AssertionResult } from '../framework/assertions/function_assertion';
|
||||
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
|
||||
import { DeploymentManager } from '../framework/deployment_manager';
|
||||
import { Simulation, SimulationEnvironment } from '../framework/simulation';
|
||||
import { Pseudorandom } from '../framework/utils/pseudorandom';
|
||||
|
||||
import { PoolMembershipSimulation } from './pool_membership_test';
|
||||
import { StakeManagementSimulation } from './stake_management_test';
|
||||
|
||||
export class StakingRewardsSimulation extends Simulation {
|
||||
protected async *_assertionGenerator(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { actors } = this.environment;
|
||||
const stakers = filterActorsByRole(actors, Staker);
|
||||
const keepers = filterActorsByRole(actors, Keeper);
|
||||
|
||||
const poolMembership = new PoolMembershipSimulation(this.environment);
|
||||
const stakeManagement = new StakeManagementSimulation(this.environment);
|
||||
|
||||
const actions = [
|
||||
...stakers.map(staker => staker.simulationActions.validWithdrawDelegatorRewards),
|
||||
...keepers.map(keeper => keeper.simulationActions.validFinalizePool),
|
||||
...keepers.map(keeper => keeper.simulationActions.validEndEpoch),
|
||||
poolMembership.generator,
|
||||
stakeManagement.generator,
|
||||
];
|
||||
while (true) {
|
||||
const action = Pseudorandom.sample(actions);
|
||||
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockchainTests('Staking rewards fuzz test', env => {
|
||||
before(function(): void {
|
||||
if (process.env.FUZZ_TEST !== 'staking_rewards') {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
Actor.reset();
|
||||
});
|
||||
|
||||
it('fuzz', async () => {
|
||||
// Deploy contracts
|
||||
const deployment = await DeploymentManager.deployAsync(env, {
|
||||
numErc20TokensToDeploy: 4,
|
||||
numErc721TokensToDeploy: 0,
|
||||
numErc1155TokensToDeploy: 0,
|
||||
});
|
||||
const [ERC20TokenA, ERC20TokenB, ERC20TokenC, ERC20TokenD] = deployment.tokens.erc20;
|
||||
|
||||
// Set up balance store
|
||||
const balanceStore = new BlockchainBalanceStore(
|
||||
{
|
||||
StakingProxy: deployment.staking.stakingProxy.address,
|
||||
ZRXVault: deployment.staking.zrxVault.address,
|
||||
},
|
||||
{
|
||||
erc20: {
|
||||
ZRX: deployment.tokens.zrx,
|
||||
WETH: deployment.tokens.weth,
|
||||
ERC20TokenA,
|
||||
ERC20TokenB,
|
||||
ERC20TokenC,
|
||||
ERC20TokenD,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Spin up actors
|
||||
const actors = [
|
||||
new Maker({ deployment, name: 'Maker 1' }),
|
||||
new Maker({ deployment, name: 'Maker 2' }),
|
||||
new Taker({ deployment, name: 'Taker 1' }),
|
||||
new Taker({ deployment, name: 'Taker 2' }),
|
||||
new MakerTaker({ deployment, name: 'Maker/Taker' }),
|
||||
new Staker({ deployment, name: 'Staker 1' }),
|
||||
new Staker({ deployment, name: 'Staker 2' }),
|
||||
new Keeper({ deployment, name: 'Keeper' }),
|
||||
new StakerKeeper({ deployment, name: 'Staker/Keeper' }),
|
||||
new StakerMaker({ deployment, name: 'Staker/Maker' }),
|
||||
new PoolOperator({ deployment, name: 'Pool Operator' }),
|
||||
new StakerOperator({ deployment, name: 'Staker/Operator' }),
|
||||
new OperatorStakerMaker({ deployment, name: 'Operator/Staker/Maker' }),
|
||||
];
|
||||
|
||||
// Set up simulation environment
|
||||
const simulationEnvironment = new SimulationEnvironment(deployment, balanceStore, actors);
|
||||
|
||||
// Takers need to set a WETH allowance for the staking proxy in case they pay the protocol fee in WETH
|
||||
const takers = filterActorsByRole(actors, Taker);
|
||||
for (const taker of takers) {
|
||||
await taker.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address);
|
||||
}
|
||||
// Stakers need to set a ZRX allowance to deposit their ZRX into the zrxVault
|
||||
const stakers = filterActorsByRole(actors, Staker);
|
||||
for (const staker of stakers) {
|
||||
await staker.configureERC20TokenAsync(deployment.tokens.zrx);
|
||||
}
|
||||
const simulation = new StakingRewardsSimulation(simulationEnvironment);
|
||||
return simulation.fuzzAsync();
|
||||
});
|
||||
});
|
||||
@@ -102,15 +102,6 @@ contract TestMixinCumulativeRewards is
|
||||
_cumulativeRewardsByPoolLastStored[poolId] = epoch;
|
||||
}
|
||||
|
||||
/// @dev Returns the most recent cumulative reward for a given pool.
|
||||
function getMostRecentCumulativeReward(bytes32 poolId)
|
||||
public
|
||||
returns (IStructs.Fraction memory)
|
||||
{
|
||||
uint256 mostRecentEpoch = _cumulativeRewardsByPoolLastStored[poolId];
|
||||
return _cumulativeRewardsByPool[poolId][mostRecentEpoch];
|
||||
}
|
||||
|
||||
/// @dev Returns the raw cumulative reward for a given pool in an epoch.
|
||||
/// This is considered "raw" because the internal implementation
|
||||
/// (_getCumulativeRewardAtEpochRaw) will query other state variables
|
||||
@@ -122,4 +113,3 @@ contract TestMixinCumulativeRewards is
|
||||
return _cumulativeRewardsByPool[poolId][epoch];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ pragma experimental ABIEncoderV2;
|
||||
|
||||
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol";
|
||||
import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol";
|
||||
import "@0x/contracts-utils/contracts/src/LibFractions.sol";
|
||||
import "../src/Staking.sol";
|
||||
import "../src/interfaces/IStructs.sol";
|
||||
|
||||
|
||||
contract TestStaking is
|
||||
@@ -56,6 +58,16 @@ contract TestStaking is
|
||||
testZrxVaultAddress = zrxVaultAddress;
|
||||
}
|
||||
|
||||
// @dev Gets the most recent cumulative reward for a pool, and the epoch it was stored.
|
||||
function getMostRecentCumulativeReward(bytes32 poolId)
|
||||
external
|
||||
view
|
||||
returns (IStructs.Fraction memory cumulativeRewards, uint256 lastStoredEpoch)
|
||||
{
|
||||
lastStoredEpoch = _cumulativeRewardsByPoolLastStored[poolId];
|
||||
cumulativeRewards = _cumulativeRewardsByPool[poolId][lastStoredEpoch];
|
||||
}
|
||||
|
||||
/// @dev Overridden to use testWethAddress;
|
||||
function getWethContract()
|
||||
public
|
||||
|
||||
@@ -44,13 +44,21 @@ export { artifacts } from './artifacts';
|
||||
export { StakingRevertErrors, FixedMathRevertErrors } from '@0x/utils';
|
||||
export { constants } from './constants';
|
||||
export {
|
||||
AggregatedStats,
|
||||
StakeInfo,
|
||||
StakeStatus,
|
||||
StoredBalance,
|
||||
loadCurrentBalance,
|
||||
increaseNextBalance,
|
||||
decreaseNextBalance,
|
||||
increaseCurrentAndNextBalance,
|
||||
decreaseCurrentAndNextBalance,
|
||||
StakingPoolById,
|
||||
OwnerStakeByStatus,
|
||||
GlobalStakeByStatus,
|
||||
StakingPool,
|
||||
PoolStats,
|
||||
Numberish,
|
||||
} from './types';
|
||||
export {
|
||||
ContractArtifact,
|
||||
|
||||
@@ -67,6 +67,68 @@ export class StoredBalance {
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates _loadCurrentBalance. `shouldMutate` flag specifies whether or not to update the given
|
||||
* StoredBalance instance.
|
||||
*/
|
||||
export function loadCurrentBalance(balance: StoredBalance, epoch: BigNumber): StoredBalance {
|
||||
return new StoredBalance(
|
||||
epoch,
|
||||
epoch.isGreaterThan(balance.currentEpoch) ? balance.nextEpochBalance : balance.currentEpochBalance,
|
||||
balance.nextEpochBalance,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates _increaseNextBalance
|
||||
*/
|
||||
export function increaseNextBalance(balance: StoredBalance, amount: Numberish, epoch: BigNumber): StoredBalance {
|
||||
return {
|
||||
...loadCurrentBalance(balance, epoch),
|
||||
nextEpochBalance: balance.nextEpochBalance.plus(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates _decreaseNextBalance
|
||||
*/
|
||||
export function decreaseNextBalance(balance: StoredBalance, amount: Numberish, epoch: BigNumber): StoredBalance {
|
||||
return {
|
||||
...loadCurrentBalance(balance, epoch),
|
||||
nextEpochBalance: balance.nextEpochBalance.minus(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates _increaseCurrentAndNextBalance
|
||||
*/
|
||||
export function increaseCurrentAndNextBalance(
|
||||
balance: StoredBalance,
|
||||
amount: Numberish,
|
||||
epoch: BigNumber,
|
||||
): StoredBalance {
|
||||
return {
|
||||
...loadCurrentBalance(balance, epoch),
|
||||
currentEpochBalance: balance.currentEpochBalance.plus(amount),
|
||||
nextEpochBalance: balance.nextEpochBalance.plus(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates _decreaseCurrentAndNextBalance
|
||||
*/
|
||||
export function decreaseCurrentAndNextBalance(
|
||||
balance: StoredBalance,
|
||||
amount: Numberish,
|
||||
epoch: BigNumber,
|
||||
): StoredBalance {
|
||||
return {
|
||||
...loadCurrentBalance(balance, epoch),
|
||||
currentEpochBalance: balance.currentEpochBalance.minus(amount),
|
||||
nextEpochBalance: balance.nextEpochBalance.minus(amount),
|
||||
};
|
||||
}
|
||||
|
||||
export interface StakeBalanceByPool {
|
||||
[key: string]: StoredBalance;
|
||||
}
|
||||
@@ -151,8 +213,47 @@ export interface StakingPool {
|
||||
operator: string;
|
||||
operatorShare: number;
|
||||
delegatedStake: StoredBalance;
|
||||
lastFinalized: BigNumber; // Epoch during which the pool was most recently finalized
|
||||
}
|
||||
|
||||
export interface StakingPoolById {
|
||||
[poolId: string]: StakingPool;
|
||||
}
|
||||
|
||||
export class PoolStats {
|
||||
public feesCollected: BigNumber = constants.ZERO_AMOUNT;
|
||||
public weightedStake: BigNumber = constants.ZERO_AMOUNT;
|
||||
public membersStake: BigNumber = constants.ZERO_AMOUNT;
|
||||
|
||||
public static fromArray(arr: [BigNumber, BigNumber, BigNumber]): PoolStats {
|
||||
const poolStats = new PoolStats();
|
||||
[poolStats.feesCollected, poolStats.weightedStake, poolStats.membersStake] = arr;
|
||||
return poolStats;
|
||||
}
|
||||
}
|
||||
|
||||
export class AggregatedStats {
|
||||
public rewardsAvailable: BigNumber = constants.ZERO_AMOUNT;
|
||||
public numPoolsToFinalize: BigNumber = constants.ZERO_AMOUNT;
|
||||
public totalFeesCollected: BigNumber = constants.ZERO_AMOUNT;
|
||||
public totalWeightedStake: BigNumber = constants.ZERO_AMOUNT;
|
||||
public totalRewardsFinalized: BigNumber = constants.ZERO_AMOUNT;
|
||||
|
||||
public static fromArray(arr: [BigNumber, BigNumber, BigNumber, BigNumber, BigNumber]): AggregatedStats {
|
||||
const aggregatedStats = new AggregatedStats();
|
||||
[
|
||||
aggregatedStats.rewardsAvailable,
|
||||
aggregatedStats.numPoolsToFinalize,
|
||||
aggregatedStats.totalFeesCollected,
|
||||
aggregatedStats.totalWeightedStake,
|
||||
aggregatedStats.totalRewardsFinalized,
|
||||
] = arr;
|
||||
return aggregatedStats;
|
||||
}
|
||||
}
|
||||
|
||||
export interface AggregatedStatsByEpoch {
|
||||
[epoch: string]: AggregatedStats;
|
||||
}
|
||||
|
||||
export type Numberish = Numberish;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
|
||||
import { blockchainTests, constants, describe, expect, shortZip } from '@0x/contracts-test-utils';
|
||||
import { blockchainTests, constants, describe, expect, shortZip, toBaseUnitAmount } from '@0x/contracts-test-utils';
|
||||
import { BigNumber, StakingRevertErrors } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
@@ -9,7 +9,6 @@ import { FinalizerActor } from './actors/finalizer_actor';
|
||||
import { PoolOperatorActor } from './actors/pool_operator_actor';
|
||||
import { StakerActor } from './actors/staker_actor';
|
||||
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
|
||||
import { toBaseUnitAmount } from './utils/number_utils';
|
||||
|
||||
// tslint:disable:no-unnecessary-type-assertion
|
||||
// tslint:disable:max-file-line-count
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
|
||||
import { blockchainTests, describe } from '@0x/contracts-test-utils';
|
||||
import { blockchainTests, describe, toBaseUnitAmount } from '@0x/contracts-test-utils';
|
||||
import { BigNumber, StakingRevertErrors } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
@@ -7,7 +7,6 @@ import { StakeInfo, StakeStatus } from '../src/types';
|
||||
|
||||
import { StakerActor } from './actors/staker_actor';
|
||||
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
|
||||
import { toBaseUnitAmount } from './utils/number_utils';
|
||||
|
||||
// tslint:disable:no-unnecessary-type-assertion
|
||||
blockchainTests.resets('Stake Statuses', env => {
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import {
|
||||
assertIntegerRoughlyEquals as assertRoughlyEquals,
|
||||
blockchainTests,
|
||||
constants,
|
||||
expect,
|
||||
filterLogsToArguments,
|
||||
getRandomInteger,
|
||||
Numberish,
|
||||
randomAddress,
|
||||
toBaseUnitAmount,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { BigNumber, hexUtils } from '@0x/utils';
|
||||
import { LogEntry } from 'ethereum-types';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import {
|
||||
assertIntegerRoughlyEquals as assertRoughlyEquals,
|
||||
getRandomInteger,
|
||||
toBaseUnitAmount,
|
||||
} from '../utils/number_utils';
|
||||
|
||||
import {
|
||||
TestDelegatorRewardsContract,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
assertIntegerRoughlyEquals,
|
||||
blockchainTests,
|
||||
constants,
|
||||
expect,
|
||||
filterLogsToArguments,
|
||||
getRandomInteger,
|
||||
Numberish,
|
||||
shortZip,
|
||||
toBaseUnitAmount,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { BigNumber, hexUtils, StakingRevertErrors } from '@0x/utils';
|
||||
import { LogEntry } from 'ethereum-types';
|
||||
@@ -13,7 +16,6 @@ import * as _ from 'lodash';
|
||||
import { constants as stakingConstants } from '../../src/constants';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import { assertIntegerRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
|
||||
|
||||
import {
|
||||
IStakingEventsEpochEndedEventArgs,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { blockchainTests, Numberish } from '@0x/contracts-test-utils';
|
||||
import {
|
||||
assertRoughlyEquals,
|
||||
blockchainTests,
|
||||
getRandomInteger,
|
||||
getRandomPortion,
|
||||
Numberish,
|
||||
toDecimal,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { assertRoughlyEquals, getRandomInteger, getRandomPortion, toDecimal } from '../utils/number_utils';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import { TestCobbDouglasContract } from '../wrappers';
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils';
|
||||
import {
|
||||
assertRoughlyEquals,
|
||||
blockchainTests,
|
||||
expect,
|
||||
fromFixed,
|
||||
Numberish,
|
||||
toDecimal,
|
||||
toFixed,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { BigNumber, FixedMathRevertErrors, hexUtils } from '@0x/utils';
|
||||
import { Decimal } from 'decimal.js';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { assertRoughlyEquals, fromFixed, toDecimal, toFixed } from '../utils/number_utils';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import { TestLibFixedMathContract } from '../wrappers';
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { blockchainTests, expect } from '@0x/contracts-test-utils';
|
||||
import { blockchainTests, expect, toBaseUnitAmount } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants as stakingConstants } from '../../src/constants';
|
||||
import { toBaseUnitAmount } from '../utils/number_utils';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import { TestMixinCumulativeRewardsContract } from '../wrappers';
|
||||
@@ -74,7 +73,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
|
||||
await testContract
|
||||
.addCumulativeReward(testPoolId, testRewards[0].numerator, testRewards[0].denominator)
|
||||
.awaitTransactionSuccessAsync();
|
||||
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync();
|
||||
const [mostRecentCumulativeReward] = await testContract
|
||||
.getMostRecentCumulativeReward(testPoolId)
|
||||
.callAsync();
|
||||
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
|
||||
});
|
||||
|
||||
@@ -86,7 +87,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
|
||||
await testContract
|
||||
.addCumulativeReward(testPoolId, testRewards[1].numerator, testRewards[1].denominator)
|
||||
.awaitTransactionSuccessAsync();
|
||||
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync();
|
||||
const [mostRecentCumulativeReward] = await testContract
|
||||
.getMostRecentCumulativeReward(testPoolId)
|
||||
.callAsync();
|
||||
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
|
||||
});
|
||||
|
||||
@@ -98,7 +101,9 @@ blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
|
||||
await testContract
|
||||
.addCumulativeReward(testPoolId, testRewards[1].numerator, testRewards[1].denominator)
|
||||
.awaitTransactionSuccessAsync();
|
||||
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward(testPoolId).callAsync();
|
||||
const [mostRecentCumulativeReward] = await testContract
|
||||
.getMostRecentCumulativeReward(testPoolId)
|
||||
.callAsync();
|
||||
expect(mostRecentCumulativeReward).to.deep.equal(sumOfTestRewardsNormalized);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
constants,
|
||||
expect,
|
||||
filterLogsToArguments,
|
||||
getRandomInteger,
|
||||
Numberish,
|
||||
randomAddress,
|
||||
} from '@0x/contracts-test-utils';
|
||||
@@ -19,8 +20,6 @@ import {
|
||||
TestProtocolFeesEvents,
|
||||
} from '../wrappers';
|
||||
|
||||
import { getRandomInteger } from '../utils/number_utils';
|
||||
|
||||
blockchainTests('Protocol Fees unit tests', env => {
|
||||
let ownerAddress: string;
|
||||
let exchangeAddress: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BlockchainTestsEnvironment, expect, txDefaults } from '@0x/contracts-test-utils';
|
||||
import { BlockchainTestsEnvironment, expect, toBaseUnitAmount, txDefaults } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { DecodedLogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
@@ -8,7 +8,6 @@ import { artifacts } from '../artifacts';
|
||||
import { TestCumulativeRewardTrackingContract, TestCumulativeRewardTrackingEvents } from '../wrappers';
|
||||
|
||||
import { StakingApiWrapper } from './api_wrapper';
|
||||
import { toBaseUnitAmount } from './number_utils';
|
||||
|
||||
export enum TestAction {
|
||||
Finalize,
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import { expect, Numberish } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import * as crypto from 'crypto';
|
||||
import { Decimal } from 'decimal.js';
|
||||
|
||||
Decimal.set({ precision: 80 });
|
||||
|
||||
/**
|
||||
* Convert `x` to a `Decimal` type.
|
||||
*/
|
||||
export function toDecimal(x: Numberish): Decimal {
|
||||
if (BigNumber.isBigNumber(x)) {
|
||||
return new Decimal(x.toString(10));
|
||||
}
|
||||
return new Decimal(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random integer between `min` and `max`, inclusive.
|
||||
*/
|
||||
export function getRandomInteger(min: Numberish, max: Numberish): BigNumber {
|
||||
const range = new BigNumber(max).minus(min);
|
||||
return getRandomPortion(range).plus(min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random integer between `0` and `total`, inclusive.
|
||||
*/
|
||||
export function getRandomPortion(total: Numberish): BigNumber {
|
||||
return new BigNumber(total).times(getRandomFloat(0, 1)).integerValue(BigNumber.ROUND_HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random, high-precision decimal between `min` and `max`, inclusive.
|
||||
*/
|
||||
export function getRandomFloat(min: Numberish, max: Numberish): BigNumber {
|
||||
// Generate a really high precision number between [0, 1]
|
||||
const r = new BigNumber(crypto.randomBytes(32).toString('hex'), 16).dividedBy(new BigNumber(2).pow(256).minus(1));
|
||||
return new BigNumber(max)
|
||||
.minus(min)
|
||||
.times(r)
|
||||
.plus(min);
|
||||
}
|
||||
|
||||
export const FIXED_POINT_BASE = new BigNumber(2).pow(127);
|
||||
|
||||
/**
|
||||
* Convert `n` to fixed-point integer represenatation.
|
||||
*/
|
||||
export function toFixed(n: Numberish): BigNumber {
|
||||
return new BigNumber(n).times(FIXED_POINT_BASE).integerValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `n` from fixed-point integer represenatation.
|
||||
*/
|
||||
export function fromFixed(n: Numberish): BigNumber {
|
||||
return new BigNumber(n).dividedBy(FIXED_POINT_BASE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts two decimal numbers to integers with `precision` digits, then returns
|
||||
* the absolute difference.
|
||||
*/
|
||||
export function getNumericalDivergence(a: Numberish, b: Numberish, precision: number = 18): number {
|
||||
const _a = new BigNumber(a);
|
||||
const _b = new BigNumber(b);
|
||||
const maxIntegerDigits = Math.max(
|
||||
_a.integerValue(BigNumber.ROUND_DOWN).sd(true),
|
||||
_b.integerValue(BigNumber.ROUND_DOWN).sd(true),
|
||||
);
|
||||
const _toInteger = (n: BigNumber) => {
|
||||
const base = 10 ** (precision - maxIntegerDigits);
|
||||
return n.times(base).integerValue(BigNumber.ROUND_DOWN);
|
||||
};
|
||||
return _toInteger(_a)
|
||||
.minus(_toInteger(_b))
|
||||
.abs()
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that two numbers are equal up to `precision` digits.
|
||||
*/
|
||||
export function assertRoughlyEquals(actual: Numberish, expected: Numberish, precision: number = 18): void {
|
||||
if (getNumericalDivergence(actual, expected, precision) <= 1) {
|
||||
return;
|
||||
}
|
||||
expect(actual).to.bignumber.eq(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that two numbers are equal with up to `maxError` difference between them.
|
||||
*/
|
||||
export function assertIntegerRoughlyEquals(actual: Numberish, expected: Numberish, maxError: number = 1): void {
|
||||
const diff = new BigNumber(actual)
|
||||
.minus(expected)
|
||||
.abs()
|
||||
.toNumber();
|
||||
if (diff <= maxError) {
|
||||
return;
|
||||
}
|
||||
expect(actual).to.bignumber.eq(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts `amount` into a base unit amount with a specified number of digits. If
|
||||
* no digits are provided, this defaults to 18 digits.
|
||||
*/
|
||||
export function toBaseUnitAmount(amount: Numberish, decimals?: number): BigNumber {
|
||||
const amountAsBigNumber = new BigNumber(amount);
|
||||
const baseDecimals = decimals !== undefined ? decimals : 18;
|
||||
const baseUnitAmount = Web3Wrapper.toBaseUnitAmount(amountAsBigNumber, baseDecimals);
|
||||
return baseUnitAmount;
|
||||
}
|
||||
@@ -65,6 +65,7 @@
|
||||
"chai": "^4.0.1",
|
||||
"chai-as-promised": "^7.1.0",
|
||||
"chai-bignumber": "^3.0.0",
|
||||
"decimal.js": "^10.2.0",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"ethereum-types": "^3.0.0",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
|
||||
@@ -54,12 +54,15 @@ export { replaceKeysDeep, shortZip } from './lang_utils';
|
||||
export {
|
||||
assertIntegerRoughlyEquals,
|
||||
assertRoughlyEquals,
|
||||
fromFixed,
|
||||
getRandomFloat,
|
||||
getRandomInteger,
|
||||
getRandomPortion,
|
||||
getNumericalDivergence,
|
||||
getPercentageOfValue,
|
||||
toBaseUnitAmount,
|
||||
toDecimal,
|
||||
toFixed,
|
||||
} from './number_utils';
|
||||
export { orderHashUtils } from './order_hash';
|
||||
export { transactionHashUtils } from './transaction_hash';
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import * as crypto from 'crypto';
|
||||
import { Decimal } from 'decimal.js';
|
||||
|
||||
import { expect } from './chai_setup';
|
||||
import { constants } from './constants';
|
||||
import { Numberish } from './types';
|
||||
|
||||
Decimal.set({ precision: 80 });
|
||||
|
||||
/**
|
||||
* Convert `x` to a `Decimal` type.
|
||||
*/
|
||||
export function toDecimal(x: Numberish): Decimal {
|
||||
if (BigNumber.isBigNumber(x)) {
|
||||
return new Decimal(x.toString(10));
|
||||
}
|
||||
return new Decimal(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random integer between `min` and `max`, inclusive.
|
||||
*/
|
||||
@@ -33,6 +46,22 @@ export function getRandomFloat(min: Numberish, max: Numberish): BigNumber {
|
||||
.plus(min);
|
||||
}
|
||||
|
||||
export const FIXED_POINT_BASE = new BigNumber(2).pow(127);
|
||||
|
||||
/**
|
||||
* Convert `n` to fixed-point integer represenatation.
|
||||
*/
|
||||
export function toFixed(n: Numberish): BigNumber {
|
||||
return new BigNumber(n).times(FIXED_POINT_BASE).integerValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `n` from fixed-point integer represenatation.
|
||||
*/
|
||||
export function fromFixed(n: Numberish): BigNumber {
|
||||
return new BigNumber(n).dividedBy(FIXED_POINT_BASE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts two decimal numbers to integers with `precision` digits, then returns
|
||||
* the absolute difference.
|
||||
|
||||
Reference in New Issue
Block a user