Merge pull request #2287 from 0xProject/feat/staking/MixinStakingPool-unit-tests

Add MixinStakingPool unit tests.
This commit is contained in:
Lawrence Forman 2019-10-25 11:59:50 -04:00 committed by GitHub
commit 59210f5e5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 450 additions and 5 deletions

View File

@ -0,0 +1,53 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "../src/interfaces/IStructs.sol";
import "./TestStakingNoWETH.sol";
contract TestMixinStakingPool is
TestStakingNoWETH
{
function setLastPoolId(bytes32 poolId)
external
{
lastPoolId = poolId;
}
function setPoolIdByMaker(bytes32 poolId, address maker)
external
{
poolIdByMaker[maker] = poolId;
}
// solhint-disable no-empty-blocks
function testOnlyStakingPoolOperatorModifier(bytes32 poolId)
external
view
onlyStakingPoolOperator(poolId)
{}
function setPoolById(bytes32 poolId, IStructs.Pool memory pool)
public
{
_poolById[poolId] = pool;
}
}

View File

@ -37,7 +37,7 @@
},
"config": {
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|IZrxVaultBackstop|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|ReadOnlyProxy|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeStorage|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault|ZrxVaultBackstop).json"
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|IZrxVaultBackstop|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|ReadOnlyProxy|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeStorage|TestMixinStakingPool|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault|ZrxVaultBackstop).json"
},
"repository": {
"type": "git",

View File

@ -51,6 +51,7 @@ import * as TestLibSafeDowncast from '../generated-artifacts/TestLibSafeDowncast
import * as TestMixinParams from '../generated-artifacts/TestMixinParams.json';
import * as TestMixinStake from '../generated-artifacts/TestMixinStake.json';
import * as TestMixinStakeStorage from '../generated-artifacts/TestMixinStakeStorage.json';
import * as TestMixinStakingPool from '../generated-artifacts/TestMixinStakingPool.json';
import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json';
import * as TestStaking from '../generated-artifacts/TestStaking.json';
import * as TestStakingNoWETH from '../generated-artifacts/TestStakingNoWETH.json';
@ -107,6 +108,7 @@ export const artifacts = {
TestMixinParams: TestMixinParams as ContractArtifact,
TestMixinStake: TestMixinStake as ContractArtifact,
TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact,
TestMixinStakingPool: TestMixinStakingPool as ContractArtifact,
TestProtocolFees: TestProtocolFees as ContractArtifact,
TestStaking: TestStaking as ContractArtifact,
TestStakingNoWETH: TestStakingNoWETH as ContractArtifact,

View File

@ -49,6 +49,7 @@ export * from '../generated-wrappers/test_lib_safe_downcast';
export * from '../generated-wrappers/test_mixin_params';
export * from '../generated-wrappers/test_mixin_stake';
export * from '../generated-wrappers/test_mixin_stake_storage';
export * from '../generated-wrappers/test_mixin_staking_pool';
export * from '../generated-wrappers/test_protocol_fees';
export * from '../generated-wrappers/test_staking';
export * from '../generated-wrappers/test_staking_no_w_e_t_h';

View File

@ -0,0 +1,373 @@
import {
blockchainTests,
constants,
expect,
filterLogsToArguments,
hexLeftPad,
hexRandom,
toHex,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber, SafeMathRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
import {
artifacts,
TestMixinStakingPoolContract,
TestMixinStakingPoolEvents,
TestMixinStakingPoolStakingPoolCreatedEventArgs as StakingPoolCreated,
} from '../../src';
blockchainTests.resets('MixinStakingPool unit tests', env => {
let testContract: TestMixinStakingPoolContract;
let operator: string;
let maker: string;
let notOperatorOrMaker: string;
before(async () => {
[operator, maker, notOperatorOrMaker] = await env.getAccountAddressesAsync();
testContract = await TestMixinStakingPoolContract.deployFrom0xArtifactAsync(
artifacts.TestMixinStakingPool,
env.provider,
env.txDefaults,
artifacts,
);
});
function toNextPoolId(lastPoolId: string): string {
return hexLeftPad(new BigNumber(lastPoolId.slice(2), 16).plus(1));
}
function randomOperatorShare(): number {
return _.random(0, constants.PPM_100_PERCENT);
}
interface CreatePoolOpts {
poolId: string;
operator: string;
operatorShare: number;
}
async function createPoolAsync(opts?: Partial<CreatePoolOpts>): Promise<CreatePoolOpts> {
const _opts = {
poolId: hexRandom(),
operator,
operatorShare: randomOperatorShare(),
...opts,
};
await testContract.setPoolById.awaitTransactionSuccessAsync(_opts.poolId, {
operator: _opts.operator,
operatorShare: _opts.operatorShare,
});
return _opts;
}
async function addMakerToPoolAsync(poolId: string, _maker: string): Promise<void> {
await testContract.setPoolIdByMaker.awaitTransactionSuccessAsync(poolId, _maker);
}
describe('onlyStakingPoolOperator modifier', () => {
it('fails if not called by the pool operator', async () => {
const { poolId } = await createPoolAsync();
const tx = testContract.testOnlyStakingPoolOperatorModifier.callAsync(poolId, { from: notOperatorOrMaker });
const expectedError = new StakingRevertErrors.OnlyCallableByPoolOperatorError(notOperatorOrMaker, poolId);
return expect(tx).to.revertWith(expectedError);
});
it('fails if called by a pool maker', async () => {
const { poolId } = await createPoolAsync();
await addMakerToPoolAsync(poolId, maker);
const tx = testContract.testOnlyStakingPoolOperatorModifier.callAsync(poolId, { from: maker });
const expectedError = new StakingRevertErrors.OnlyCallableByPoolOperatorError(maker, poolId);
return expect(tx).to.revertWith(expectedError);
});
it('succeeds if called by the pool operator', async () => {
const { poolId } = await createPoolAsync();
await testContract.testOnlyStakingPoolOperatorModifier.callAsync(poolId, { from: operator });
});
});
describe('createStakingPool()', () => {
let nextPoolId: string;
before(async () => {
nextPoolId = toNextPoolId(await testContract.lastPoolId.callAsync());
});
it('fails if the next pool ID overflows', async () => {
await testContract.setLastPoolId.awaitTransactionSuccessAsync(toHex(constants.MAX_UINT256));
const tx = testContract.createStakingPool.awaitTransactionSuccessAsync(randomOperatorShare(), false);
const expectedError = new SafeMathRevertErrors.Uint256BinOpError(
SafeMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
constants.MAX_UINT256,
new BigNumber(1),
);
return expect(tx).to.revertWith(expectedError);
});
it('fails if the operator share is invalid', async () => {
const operatorShare = constants.PPM_100_PERCENT + 1;
const tx = testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, false);
const expectedError = new StakingRevertErrors.OperatorShareError(
StakingRevertErrors.OperatorShareErrorCodes.OperatorShareTooLarge,
nextPoolId,
operatorShare,
);
return expect(tx).to.revertWith(expectedError);
});
it('operator can create and own multiple pools', async () => {
const { logs: logs1 } = await testContract.createStakingPool.awaitTransactionSuccessAsync(
randomOperatorShare(),
false,
);
const { logs: logs2 } = await testContract.createStakingPool.awaitTransactionSuccessAsync(
randomOperatorShare(),
false,
);
const createEvents = filterLogsToArguments<StakingPoolCreated>(
[...logs1, ...logs2],
TestMixinStakingPoolEvents.StakingPoolCreated,
);
expect(createEvents).to.be.length(2);
const poolIds = createEvents.map(e => e.poolId);
expect(poolIds[0]).to.not.eq(poolIds[1]);
const pools = await Promise.all(poolIds.map(async poolId => testContract.getStakingPool.callAsync(poolId)));
expect(pools[0].operator).to.eq(pools[1].operator);
});
it('operator can only be maker of one pool', async () => {
await testContract.createStakingPool.awaitTransactionSuccessAsync(randomOperatorShare(), true);
const { logs } = await testContract.createStakingPool.awaitTransactionSuccessAsync(
randomOperatorShare(),
true,
);
const createEvents = filterLogsToArguments<StakingPoolCreated>(
logs,
TestMixinStakingPoolEvents.StakingPoolCreated,
);
const makerPool = await testContract.poolIdByMaker.callAsync(operator);
expect(makerPool).to.eq(createEvents[0].poolId);
});
it('computes correct next pool ID', async () => {
const { logs } = await testContract.createStakingPool.awaitTransactionSuccessAsync(
randomOperatorShare(),
false,
);
const createEvents = filterLogsToArguments<StakingPoolCreated>(
logs,
TestMixinStakingPoolEvents.StakingPoolCreated,
);
const poolId = createEvents[0].poolId;
expect(poolId).to.eq(nextPoolId);
});
it('increments last pool ID counter', async () => {
await testContract.createStakingPool.awaitTransactionSuccessAsync(randomOperatorShare(), false);
const lastPoolIdAfter = await testContract.lastPoolId.callAsync();
expect(lastPoolIdAfter).to.eq(nextPoolId);
});
it('records pool details', async () => {
const operatorShare = randomOperatorShare();
await testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, false, { from: operator });
const pool = await testContract.getStakingPool.callAsync(nextPoolId);
expect(pool.operator).to.eq(operator);
expect(pool.operatorShare).to.bignumber.eq(operatorShare);
});
it('returns the next pool ID', async () => {
const poolId = await testContract.createStakingPool.callAsync(randomOperatorShare(), false, {
from: operator,
});
expect(poolId).to.eq(nextPoolId);
});
it('can add operator as a maker', async () => {
const operatorShare = randomOperatorShare();
await testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, true, { from: operator });
const makerPoolId = await testContract.poolIdByMaker.callAsync(operator);
expect(makerPoolId).to.eq(nextPoolId);
});
it('emits a `StakingPoolCreated` event', async () => {
const operatorShare = randomOperatorShare();
const { logs } = await testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, false, {
from: operator,
});
verifyEventsFromLogs(
logs,
[
{
poolId: nextPoolId,
operator,
operatorShare,
},
],
TestMixinStakingPoolEvents.StakingPoolCreated,
);
});
it('emits a `MakerStakingPoolSet` event when also joining as a maker', async () => {
const operatorShare = randomOperatorShare();
const { logs } = await testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, true, {
from: operator,
});
verifyEventsFromLogs(
logs,
[
{
makerAddress: operator,
poolId: nextPoolId,
},
],
TestMixinStakingPoolEvents.MakerStakingPoolSet,
);
});
});
describe('decreaseStakingPoolOperatorShare()', () => {
it('fails if not called by operator', async () => {
const { poolId, operatorShare } = await createPoolAsync();
const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
operatorShare - 1,
{ from: notOperatorOrMaker },
);
const expectedError = new StakingRevertErrors.OnlyCallableByPoolOperatorError(notOperatorOrMaker, poolId);
return expect(tx).to.revertWith(expectedError);
});
it('fails if called by maker', async () => {
const { poolId, operatorShare } = await createPoolAsync();
await addMakerToPoolAsync(poolId, maker);
const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
operatorShare - 1,
{ from: maker },
);
const expectedError = new StakingRevertErrors.OnlyCallableByPoolOperatorError(maker, poolId);
return expect(tx).to.revertWith(expectedError);
});
it('fails if operator share is equal to current', async () => {
const { poolId, operatorShare } = await createPoolAsync();
const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
operatorShare,
{ from: operator },
);
const expectedError = new StakingRevertErrors.OperatorShareError(
StakingRevertErrors.OperatorShareErrorCodes.CanOnlyDecreaseOperatorShare,
poolId,
operatorShare,
);
return expect(tx).to.revertWith(expectedError);
});
it('fails if operator share is greater than current', async () => {
const { poolId, operatorShare } = await createPoolAsync();
const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
operatorShare + 1,
{ from: operator },
);
const expectedError = new StakingRevertErrors.OperatorShareError(
StakingRevertErrors.OperatorShareErrorCodes.CanOnlyDecreaseOperatorShare,
poolId,
operatorShare + 1,
);
return expect(tx).to.revertWith(expectedError);
});
it('fails if operator share is greater than PPM_100_PERCENT', async () => {
const { poolId } = await createPoolAsync();
const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
constants.PPM_100_PERCENT + 1,
{ from: operator },
);
const expectedError = new StakingRevertErrors.OperatorShareError(
StakingRevertErrors.OperatorShareErrorCodes.OperatorShareTooLarge,
poolId,
constants.PPM_100_PERCENT + 1,
);
return expect(tx).to.revertWith(expectedError);
});
it('records new operator share', async () => {
const { poolId, operatorShare } = await createPoolAsync();
await testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
operatorShare - 1,
{ from: operator },
);
const pool = await testContract.getStakingPool.callAsync(poolId);
expect(pool.operatorShare).to.bignumber.eq(operatorShare - 1);
});
it('does not modify operator', async () => {
const { poolId, operatorShare } = await createPoolAsync();
await testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
operatorShare - 1,
{ from: operator },
);
const pool = await testContract.getStakingPool.callAsync(poolId);
expect(pool.operator).to.eq(operator);
});
it('emits an `OperatorShareDecreased` event', async () => {
const { poolId, operatorShare } = await createPoolAsync();
const { logs } = await testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
operatorShare - 1,
{ from: operator },
);
verifyEventsFromLogs(
logs,
[
{
poolId,
oldOperatorShare: operatorShare,
newOperatorShare: operatorShare - 1,
},
],
TestMixinStakingPoolEvents.OperatorShareDecreased,
);
});
});
describe('joinStakingPoolAsMaker()', () => {
it('records sender as maker for the pool', async () => {
const { poolId } = await createPoolAsync();
await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: maker });
const makerPoolId = await testContract.poolIdByMaker.callAsync(maker);
expect(makerPoolId).to.eq(poolId);
});
it('operator can join as maker for the pool', async () => {
const { poolId } = await createPoolAsync();
await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: operator });
const makerPoolId = await testContract.poolIdByMaker.callAsync(operator);
expect(makerPoolId).to.eq(poolId);
});
it('can join the same pool as a maker twice', async () => {
const { poolId } = await createPoolAsync();
await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: maker });
await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: maker });
const makerPoolId = await testContract.poolIdByMaker.callAsync(maker);
expect(makerPoolId).to.eq(poolId);
});
it('can only be a maker in one pool at a time', async () => {
const { poolId: poolId1 } = await createPoolAsync();
const { poolId: poolId2 } = await createPoolAsync();
await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId1, { from: maker });
let makerPoolId = await testContract.poolIdByMaker.callAsync(maker);
expect(makerPoolId).to.eq(poolId1);
await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId2, { from: maker });
makerPoolId = await testContract.poolIdByMaker.callAsync(maker);
expect(makerPoolId).to.eq(poolId2);
});
it('emits a `MakerStakingPoolSet` event', async () => {
const { poolId } = await createPoolAsync();
const { logs } = await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, {
from: maker,
});
verifyEventsFromLogs(
logs,
[
{
makerAddress: maker,
poolId,
},
],
TestMixinStakingPoolEvents.MakerStakingPoolSet,
);
});
});
});
// tslint:disable: max-file-line-count

View File

@ -49,6 +49,7 @@
"generated-artifacts/TestMixinParams.json",
"generated-artifacts/TestMixinStake.json",
"generated-artifacts/TestMixinStakeStorage.json",
"generated-artifacts/TestMixinStakingPool.json",
"generated-artifacts/TestProtocolFees.json",
"generated-artifacts/TestStaking.json",
"generated-artifacts/TestStakingNoWETH.json",

View File

@ -97,6 +97,10 @@
{
"note": "Add `number_utils.ts` and `hexSize()`",
"pr": 2220
},
{
"note": "Add `verifyEventsFromLogs()`",
"pr": 2287
}
],
"timestamp": 1570135330

View File

@ -15,7 +15,7 @@ export {
export { getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync } from './block_timestamp';
export { provider, txDefaults, web3Wrapper } from './web3_wrapper';
export { LogDecoder } from './log_decoder';
export { filterLogs, filterLogsToArguments, verifyEvents } from './log_utils';
export { filterLogs, filterLogsToArguments, verifyEvents, verifyEventsFromLogs } from './log_utils';
export { signingUtils } from './signing_utils';
export { orderUtils } from './order_utils';
export { typeEncodingUtils } from './type_encoding_utils';

View File

@ -26,9 +26,20 @@ export function verifyEvents<TEventArgs>(
expectedEvents: TEventArgs[],
eventName: string,
): void {
const logs = filterLogsToArguments<TEventArgs>(txReceipt.logs, eventName);
expect(logs.length).to.eq(expectedEvents.length);
logs.forEach((log, index) => {
return verifyEventsFromLogs(txReceipt.logs, expectedEvents, eventName);
}
/**
* Given a collection of logs, verifies that matching events are identical.
*/
export function verifyEventsFromLogs<TEventArgs>(
logs: LogEntry[],
expectedEvents: TEventArgs[],
eventName: string,
): void {
const _logs = filterLogsToArguments<TEventArgs>(logs, eventName);
expect(_logs.length).to.eq(expectedEvents.length);
_logs.forEach((log, index) => {
expect(log).to.deep.equal(expectedEvents[index]);
});
}