@0x/contracts-staking: Fixing tests.

This commit is contained in:
Lawrence Forman 2019-09-13 14:19:45 -04:00 committed by Lawrence Forman
parent 58a5ab4550
commit d548ddac0d
14 changed files with 260 additions and 139 deletions

View File

@ -0,0 +1,10 @@
{
"overrides": [
{
"files": "./test/**.ts",
"options": {
"printWidth": 80
}
}
]
}

View File

@ -8,7 +8,12 @@
"optimizer": {
"enabled": true,
"runs": 1000000,
"details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true }
"details": {
"yul": true,
"deduplicate": true,
"cse": true,
"constantOptimizer": true
}
},
"outputSelection": {
"*": {

View File

@ -21,6 +21,7 @@ pragma experimental ABIEncoderV2;
import "./interfaces/IStaking.sol";
import "./sys/MixinParams.sol";
import "./sys/MixinFinalizer.sol";
import "./stake/MixinStake.sol";
import "./staking_pools/MixinStakingPool.sol";
import "./fees/MixinExchangeFees.sol";
@ -31,7 +32,7 @@ contract Staking is
MixinParams,
MixinStakingPool,
MixinStake,
MixinExchangeFees
MixinExchangeFees,
{
// this contract can receive ETH
// solhint-disable no-empty-blocks

View File

@ -335,6 +335,15 @@ contract MixinFinalizer is
rewards.membersStake = pool.delegatedStake;
}
/// @dev Converts the entire WETH balance of the contract into ETH.
function _unwrapWETH() internal {
uint256 wethBalance = IEtherToken(WETH_ADDRESS)
.balanceOf(address(this));
if (wethBalance != 0) {
IEtherToken(WETH_ADDRESS).withdraw(wethBalance);
}
}
/// @dev Computes the reward owed to a pool during finalization and
/// credits it to that pool for the CURRENT epoch.
/// @param poolId The pool's ID.
@ -377,13 +386,4 @@ contract MixinFinalizer is
);
}
}
/// @dev Converts the entire WETH balance of the contract into ETH.
function _unwrapWETH() private {
uint256 wethBalance = IEtherToken(WETH_ADDRESS)
.balanceOf(address(this));
if (wethBalance != 0) {
IEtherToken(WETH_ADDRESS).withdraw(wethBalance);
}
}
}

View File

@ -0,0 +1,29 @@
/*
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/Staking.sol";
contract TestDelegatorRewards is
Staking
{
// TODO
}

View File

@ -3,7 +3,7 @@
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
* -----------------------------------------------------------------------------
*/
import { ContractArtifact } from 'ethereum-types';
import { ContractArtifact } from "ethereum-types";
import * as EthVault from '../generated-artifacts/EthVault.json';
import * as IEthVault from '../generated-artifacts/IEthVault.json';

View File

@ -62,7 +62,7 @@ export class FinalizerActor extends BaseActor {
memberRewardByPoolId,
);
// finalize
await this._stakingApiWrapper.utils.skipToNextEpochAsync();
await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
// assert reward vault changes
const finalRewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds);
expect(finalRewardVaultBalanceByPoolId, 'final pool balances in reward vault').to.be.deep.equal(

View File

@ -151,7 +151,7 @@ export class StakerActor extends BaseActor {
const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync();
const initBalances = await this._getBalancesAsync();
// go to next epoch
await this._stakingApiWrapper.utils.skipToNextEpochAsync();
await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
// check balances
const expectedBalances = this._getNextEpochBalances(initBalances);
await this._assertBalancesAsync(expectedBalances);

View File

@ -38,7 +38,7 @@ blockchainTests('Epochs', env => {
expect(currentEpoch).to.be.bignumber.equal(stakingConstants.INITIAL_EPOCH);
}
///// 3/3 Increment Epoch (TimeLock Should Not Increment) /////
await stakingApiWrapper.utils.skipToNextEpochAsync();
await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
{
// epoch
const currentEpoch = await stakingApiWrapper.stakingContract.currentEpoch.callAsync();

View File

@ -701,8 +701,7 @@ blockchainTests.resets('Testing Rewards', env => {
const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)];
const totalStakeAmount = BigNumber.sum(...stakeAmounts);
// stake and delegate both
const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as
Array<[StakerActor, BigNumber]>;
const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>;
for (const [staker, stakeAmount] of stakersAndStake) {
await staker.stakeAsync(stakeAmount);
await staker.moveStakeAsync(
@ -724,8 +723,7 @@ blockchainTests.resets('Testing Rewards', env => {
toBaseUnitAmount(0),
);
}
const expectedStakerRewards = stakeAmounts
.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
await validateEndBalances({
stakerRewardVaultBalance_1: toBaseUnitAmount(0),
stakerRewardVaultBalance_2: toBaseUnitAmount(0),
@ -739,8 +737,7 @@ blockchainTests.resets('Testing Rewards', env => {
const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)];
const totalStakeAmount = BigNumber.sum(...stakeAmounts);
// stake and delegate both
const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as
Array<[StakerActor, BigNumber]>;
const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>;
for (const [staker, stakeAmount] of stakersAndStake) {
await staker.stakeAsync(stakeAmount);
await staker.moveStakeAsync(
@ -754,8 +751,7 @@ blockchainTests.resets('Testing Rewards', env => {
// finalize
const reward = toBaseUnitAmount(10);
await payProtocolFeeAndFinalize(reward);
const expectedStakerRewards = stakeAmounts
.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
await validateEndBalances({
stakerRewardVaultBalance_1: expectedStakerRewards[0],
stakerRewardVaultBalance_2: expectedStakerRewards[1],
@ -776,19 +772,21 @@ blockchainTests.resets('Testing Rewards', env => {
const sneakyStakerExpectedEthVaultBalance = expectedStakerRewards[0];
await undelegateZeroAsync(sneakyStaker);
// Should have been credited the correct amount of rewards.
let sneakyStakerEthVaultBalance = await stakingApiWrapper
.ethVaultContract.balanceOf
.callAsync(sneakyStaker.getOwner());
expect(sneakyStakerEthVaultBalance, 'EthVault balance after first undelegate')
.to.bignumber.eq(sneakyStakerExpectedEthVaultBalance);
let sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync(
sneakyStaker.getOwner(),
);
expect(sneakyStakerEthVaultBalance, 'EthVault balance after first undelegate').to.bignumber.eq(
sneakyStakerExpectedEthVaultBalance,
);
// Now he'll try to do it again to see if he gets credited twice.
await undelegateZeroAsync(sneakyStaker);
/// The total amount credited should remain the same.
sneakyStakerEthVaultBalance = await stakingApiWrapper
.ethVaultContract.balanceOf
.callAsync(sneakyStaker.getOwner());
expect(sneakyStakerEthVaultBalance, 'EthVault balance after second undelegate')
.to.bignumber.eq(sneakyStakerExpectedEthVaultBalance);
sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync(
sneakyStaker.getOwner(),
);
expect(sneakyStakerEthVaultBalance, 'EthVault balance after second undelegate').to.bignumber.eq(
sneakyStakerExpectedEthVaultBalance,
);
});
});
});

View File

@ -0,0 +1,22 @@
import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils';
import { artifacts, TestDelegatorRewardsContract } from '../../src';
blockchainTests('delegator rewards', env => {
let testContract: TestDelegatorRewardsContract;
before(async () => {
testContract = await TestDelegatorRewardsContract.deployFrom0xArtifactAsync(
artifacts.TestLibFixedMath,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('computeRewardBalanceOfDelegator()', () => {
it('does stuff', () => {
// TODO
});
});
});

View File

@ -8,6 +8,7 @@ import {
testCombinatoriallyWithReferenceFunc,
} from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { cartesianProduct } from 'js-combinatorics';
import { artifacts, TestLibProxyContract, TestLibProxyReceiverContract } from '../../src';
@ -196,25 +197,51 @@ blockchainTests.resets('LibProxy', env => {
describe('Combinatorial Tests', () => {
// Combinatorial Scenarios for `proxyCall()`.
const revertRuleScenarios: RevertRule[] = [
function getCombinatorialTestDescription(params: [RevertRule, boolean, string, string]): string {
const REVERT_RULE_NAMES = [
'RevertOnError',
'AlwaysRevert',
'NeverRevert',
];
return [
`revertRule: ${REVERT_RULE_NAMES[params[0]]}`,
`ignoreIngressSelector: ${params[1]}`,
`customEgressSelector: ${params[2]}`,
`calldata: ${
params[3].length / 2 - 2 > 4
? // tslint:disable-next-line
hexSlice(params[3], 0, 4) + '...'
: params[3]
}`,
].join(', ');
}
const scenarios = [
// revertRule
[
RevertRule.RevertOnError,
RevertRule.AlwaysRevert,
RevertRule.NeverRevert,
];
const ignoreIngressScenarios: boolean[] = [false, true];
const customEgressScenarios: string[] = [
],
// ignoreIngressSelector
[false, true],
// customEgressSelector
[
constants.NULL_BYTES4,
constructRandomFailureCalldata(), // Random failure calldata is used because it is nonzero and won't collide.
];
const calldataScenarios: string[] = [constructRandomFailureCalldata(), constructRandomSuccessCalldata()];
// Random failure calldata is used because it is nonzero and
// won't collide.
constructRandomFailureCalldata(),
],
// calldata
[
constructRandomFailureCalldata(),
constructRandomSuccessCalldata(),
],
] as [RevertRule[], boolean[], string[], string[]];
// A reference function that returns the expected success and returndata values of a given call to `proxyCall()`.
async function referenceFuncAsync(
revertRule: RevertRule,
customEgressSelector: string,
shouldIgnoreIngressSelector: boolean,
calldata: string,
): Promise<[boolean, string]> {
for (const params of cartesianProduct(...scenarios).toArray()) {
const [revertRule, shouldIgnoreIngressSelector, customEgressSelector, calldata] = params;
it(getCombinatorialTestDescription(params), async () => {
// Determine whether or not the call should succeed.
let shouldSucceed = true;
if (
@ -242,33 +269,16 @@ blockchainTests.resets('LibProxy', env => {
returnData = hexConcat(customEgressSelector, returnData);
}
// Return the success and return data values.
return [shouldSucceed, returnData];
}
// A wrapper for `publicProxyCall()` that allow us to combinatorially test `proxyCall()` for the
// scenarios defined above.
async function testFuncAsync(
revertRule: RevertRule,
customEgressSelector: string,
shouldIgnoreIngressSelector: boolean,
calldata: string,
): Promise<[boolean, string]> {
return publicProxyCallAsync({
const [didSucceed, actualReturnData] = await publicProxyCallAsync({
calldata,
customEgressSelector,
ignoreIngressSelector: shouldIgnoreIngressSelector,
revertRule,
});
expect(didSucceed).to.be.eq(shouldSucceed);
expect(actualReturnData).to.be.eq(returnData);
});
}
// Combinatorially test proxy call.
testCombinatoriallyWithReferenceFunc('proxyCall', referenceFuncAsync, testFuncAsync, [
revertRuleScenarios,
customEgressScenarios,
ignoreIngressScenarios,
calldataScenarios,
]);
});
});
});

View File

@ -1,28 +1,34 @@
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
import { BlockchainTestsEnvironment, constants } from '@0x/contracts-test-utils';
import { BlockchainTestsEnvironment, constants, filterLogsToArguments, txDefaults } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { BlockParamLiteral, ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import {
artifacts,
EthVaultContract,
IStakingEventsEpochEndedEventArgs,
IStakingEventsStakingPoolActivatedEventArgs,
ReadOnlyProxyContract,
StakingContract,
StakingEvents,
StakingPoolRewardVaultContract,
StakingProxyContract,
ZrxVaultContract,
} from '../../src';
import { constants as stakingConstants } from './constants';
import { StakingParams } from './types';
import { EndOfEpochInfo, StakingParams } from './types';
export class StakingApiWrapper {
public stakingContractAddress: string; // The address of the real Staking.sol contract
public stakingContract: StakingContract; // The StakingProxy.sol contract wrapped as a StakingContract to borrow API
public stakingProxyContract: StakingProxyContract; // The StakingProxy.sol contract as a StakingProxyContract
// The address of the real Staking.sol contract
public stakingContractAddress: string;
// The StakingProxy.sol contract wrapped as a StakingContract to borrow API
public stakingContract: StakingContract;
// The StakingProxy.sol contract as a StakingProxyContract
public stakingProxyContract: StakingProxyContract;
public zrxVaultContract: ZrxVaultContract;
public ethVaultContract: EthVaultContract;
public rewardVaultContract: StakingPoolRewardVaultContract;
@ -30,21 +36,53 @@ export class StakingApiWrapper {
public utils = {
// Epoch Utils
fastForwardToNextEpochAsync: async (): Promise<void> => {
// increase timestamp of next block
const { epochDurationInSeconds } = await this.utils.getParamsAsync();
await this._web3Wrapper.increaseTimeAsync(epochDurationInSeconds.toNumber());
// increase timestamp of next block by how many seconds we need to
// get to the next epoch.
const epochEndTime = await this.stakingContract.getCurrentEpochEarliestEndTimeInSeconds.callAsync();
const lastBlockTime = await this._web3Wrapper.getBlockTimestampAsync('latest');
const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber());
await this._web3Wrapper.increaseTimeAsync(dt);
// mine next block
await this._web3Wrapper.mineBlockAsync();
},
skipToNextEpochAsync: async (): Promise<TransactionReceiptWithDecodedLogs> => {
skipToNextEpochAndFinalizeAsync: async (): Promise<TransactionReceiptWithDecodedLogs> => {
await this.utils.fastForwardToNextEpochAsync();
// increment epoch in contracts
const txReceipt = await this.stakingContract.finalizeFees.awaitTransactionSuccessAsync();
logUtils.log(`Finalization costed ${txReceipt.gasUsed} gas`);
// mine next block
await this._web3Wrapper.mineBlockAsync();
return txReceipt;
const endOfEpochInfo = await this.utils.endEpochAsync();
const receipt = await this.stakingContract.finalizePools.awaitTransactionSuccessAsync(
endOfEpochInfo.activePoolIds,
);
logUtils.log(`Finalization cost ${receipt.gasUsed} gas`);
return receipt;
},
endEpochAsync: async (): Promise<EndOfEpochInfo> => {
const activePoolIds = await this.utils.findActivePoolIdsAsync();
const receipt = await this.stakingContract.endEpoch.awaitTransactionSuccessAsync();
const [epochEndedEvent] = filterLogsToArguments<IStakingEventsEpochEndedEventArgs>(
receipt.logs,
StakingEvents.EpochEnded,
);
return {
closingEpoch: epochEndedEvent.epoch,
activePoolIds,
rewardsAvailable: epochEndedEvent.rewardsAvailable,
totalFeesCollected: epochEndedEvent.totalFeesCollected,
totalWeightedStake: epochEndedEvent.totalWeightedStake,
};
},
findActivePoolIdsAsync: async (epoch?: number): Promise<string[]> => {
const _epoch = epoch !== undefined ? epoch : await this.stakingContract.getCurrentEpoch.callAsync();
const events = filterLogsToArguments<IStakingEventsStakingPoolActivatedEventArgs>(
await this.stakingContract.getLogsAsync(
StakingEvents.StakingPoolActivated,
{ fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest },
{ epoch: _epoch },
),
StakingEvents.StakingPoolActivated,
);
return events.map(e => e.poolId);
},
// Other Utils

View File

@ -53,6 +53,14 @@ export interface SimulationParams {
withdrawByUndelegating: boolean;
}
export interface EndOfEpochInfo {
closingEpoch: BigNumber;
activePoolIds: string[];
rewardsAvailable: BigNumber;
totalFeesCollected: BigNumber;
totalWeightedStake: BigNumber;
}
export interface StakeBalance {
currentEpochBalance: BigNumber;
nextEpochBalance: BigNumber;