@0x/contracts-staking: Updating tests and making the contracts testable.

This commit is contained in:
Lawrence Forman
2019-09-21 23:50:34 -04:00
parent 3ad7728a0e
commit 6a29654d7d
18 changed files with 283 additions and 348 deletions

View File

@@ -192,24 +192,23 @@ contract MixinExchangeFees is
private private
view view
{ {
if (protocolFeePaid != 0) { if (protocolFeePaid == 0) {
return;
}
if (msg.value == protocolFeePaid || msg.value == 0) {
return;
}
LibRichErrors.rrevert( LibRichErrors.rrevert(
LibStakingRichErrors.InvalidProtocolFeePaymentError( LibStakingRichErrors.InvalidProtocolFeePaymentError(
protocolFeePaid == 0 ? LibStakingRichErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid,
LibStakingRichErrors protocolFeePaid,
.ProtocolFeePaymentErrorCodes msg.value
.ZeroProtocolFeePaid : )
LibStakingRichErrors );
.ProtocolFeePaymentErrorCodes }
.MismatchedFeeAndPayment, if (msg.value != protocolFeePaid && msg.value != 0) {
LibRichErrors.rrevert(
LibStakingRichErrors.InvalidProtocolFeePaymentError(
LibStakingRichErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment,
protocolFeePaid, protocolFeePaid,
msg.value msg.value
) )
); );
} }
} }
}

View File

@@ -26,20 +26,6 @@ import "../interfaces/IStructs.sol";
/// cyclical dependencies. /// cyclical dependencies.
contract MixinAbstract { contract MixinAbstract {
/// @dev Computes the reward owed to a pool during finalization.
/// Does nothing if the pool is already finalized.
/// @param poolId The pool's ID.
/// @return totalReward The total reward owed to a pool.
/// @return membersStake The total stake for all non-operator members in
/// this pool.
function _getUnfinalizedPoolRewards(bytes32 poolId)
internal
view
returns (
uint256 totalReward,
uint256 membersStake
);
/// @dev Instantly finalizes a single pool that was active in the previous /// @dev Instantly finalizes a single pool that was active in the previous
/// epoch, crediting it rewards and sending those rewards to the reward /// epoch, crediting it rewards and sending those rewards to the reward
/// and eth vault. This can be called by internal functions that need /// and eth vault. This can be called by internal functions that need
@@ -57,4 +43,18 @@ contract MixinAbstract {
uint256 membersReward, uint256 membersReward,
uint256 membersStake uint256 membersStake
); );
/// @dev Computes the reward owed to a pool during finalization.
/// Does nothing if the pool is already finalized.
/// @param poolId The pool's ID.
/// @return totalReward The total reward owed to a pool.
/// @return membersStake The total stake for all non-operator members in
/// this pool.
function _getUnfinalizedPoolRewards(bytes32 poolId)
internal
view
returns (
uint256 totalReward,
uint256 membersStake
);
} }

View File

@@ -248,7 +248,7 @@ contract MixinFinalizer is
IEtherToken weth = IEtherToken(_getWETHAddress()); IEtherToken weth = IEtherToken(_getWETHAddress());
uint256 ethBalance = address(this).balance; uint256 ethBalance = address(this).balance;
if (ethBalance != 0) { if (ethBalance != 0) {
weth.deposit.value((address(this).balance)); weth.deposit.value((address(this).balance))();
} }
balance = weth.balanceOf(address(this)); balance = weth.balanceOf(address(this));
return balance; return balance;

View File

@@ -235,7 +235,7 @@ contract MixinParams is
address[2] memory oldSpenders, address[2] memory oldSpenders,
address[2] memory newSpenders address[2] memory newSpenders
) )
private internal
{ {
IEtherToken weth = IEtherToken(_getWETHAddress()); IEtherToken weth = IEtherToken(_getWETHAddress());
// Grant new allowances. // Grant new allowances.

View File

@@ -21,7 +21,6 @@ pragma solidity ^0.5.9;
import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../interfaces/IEthVault.sol"; import "../interfaces/IEthVault.sol";
import "../immutable/MixinDeploymentConstants.sol";
import "./MixinVaultCore.sol"; import "./MixinVaultCore.sol";
@@ -29,15 +28,21 @@ import "./MixinVaultCore.sol";
contract EthVault is contract EthVault is
IEthVault, IEthVault,
IVaultCore, IVaultCore,
MixinDeploymentConstants,
Ownable, Ownable,
MixinVaultCore MixinVaultCore
{ {
using LibSafeMath for uint256; using LibSafeMath for uint256;
// Address of the WETH contract.
IEtherToken public weth;
// mapping from Owner to WETH balance // mapping from Owner to WETH balance
mapping (address => uint256) internal _balances; mapping (address => uint256) internal _balances;
/// @param wethAddress Address of the WETH contract.
constructor(address wethAddress) public {
weth = IEtherToken(wethAddress);
}
/// @dev Deposit an `amount` of WETH for `owner` into the vault. /// @dev Deposit an `amount` of WETH for `owner` into the vault.
/// The staking contract should have granted the vault an allowance /// The staking contract should have granted the vault an allowance
/// because it will pull the WETH via `transferFrom()`. /// because it will pull the WETH via `transferFrom()`.
@@ -49,7 +54,7 @@ contract EthVault is
onlyStakingProxy onlyStakingProxy
{ {
// Transfer WETH from the staking contract into this contract. // Transfer WETH from the staking contract into this contract.
IEtherToken(_getWETHAddress()).transferFrom(msg.sender, address(this), amount); weth.transferFrom(msg.sender, address(this), amount);
// Credit the owner. // Credit the owner.
_balances[owner] = _balances[owner].safeAdd(amount); _balances[owner] = _balances[owner].safeAdd(amount);
emit EthDepositedIntoVault(msg.sender, owner, amount); emit EthDepositedIntoVault(msg.sender, owner, amount);
@@ -97,7 +102,7 @@ contract EthVault is
_balances[owner] = _balances[owner].safeSub(amount); _balances[owner] = _balances[owner].safeSub(amount);
// withdraw WETH to owner // withdraw WETH to owner
IEtherToken(_getWETHAddress()).transfer(msg.sender, amount); weth.transfer(msg.sender, amount);
// notify // notify
emit EthWithdrawnFromVault(msg.sender, owner, amount); emit EthWithdrawnFromVault(msg.sender, owner, amount);

View File

@@ -26,22 +26,27 @@ import "../libs/LibStakingRichErrors.sol";
import "../libs/LibSafeDowncast.sol"; import "../libs/LibSafeDowncast.sol";
import "./MixinVaultCore.sol"; import "./MixinVaultCore.sol";
import "../interfaces/IStakingPoolRewardVault.sol"; import "../interfaces/IStakingPoolRewardVault.sol";
import "../immutable/MixinDeploymentConstants.sol";
/// @dev This vault manages staking pool rewards. /// @dev This vault manages staking pool rewards.
contract StakingPoolRewardVault is contract StakingPoolRewardVault is
IStakingPoolRewardVault, IStakingPoolRewardVault,
IVaultCore, IVaultCore,
MixinDeploymentConstants,
Ownable, Ownable,
MixinVaultCore MixinVaultCore
{ {
using LibSafeMath for uint256; using LibSafeMath for uint256;
// Address of the WETH contract.
IEtherToken public weth;
// mapping from poolId to Pool metadata // mapping from poolId to Pool metadata
mapping (bytes32 => uint256) internal _balanceByPoolId; mapping (bytes32 => uint256) internal _balanceByPoolId;
/// @param wethAddress Address of the WETH contract.
constructor(address wethAddress) public {
weth = IEtherToken(wethAddress);
}
/// @dev Deposit an amount of WETH for `poolId` into the vault. /// @dev Deposit an amount of WETH for `poolId` into the vault.
/// The staking contract should have granted the vault an allowance /// The staking contract should have granted the vault an allowance
/// because it will pull the WETH via `transferFrom()`. /// because it will pull the WETH via `transferFrom()`.
@@ -53,7 +58,7 @@ contract StakingPoolRewardVault is
onlyStakingProxy onlyStakingProxy
{ {
// Transfer WETH from the staking contract into this contract. // Transfer WETH from the staking contract into this contract.
IEtherToken(_getWETHAddress()).transferFrom(msg.sender, address(this), amount); weth.transferFrom(msg.sender, address(this), amount);
// Credit the pool. // Credit the pool.
_balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeAdd(amount); _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeAdd(amount);
emit EthDepositedIntoVault(msg.sender, poolId, amount); emit EthDepositedIntoVault(msg.sender, poolId, amount);
@@ -73,7 +78,7 @@ contract StakingPoolRewardVault is
onlyStakingProxy onlyStakingProxy
{ {
_balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeSub(amount); _balanceByPoolId[poolId] = _balanceByPoolId[poolId].safeSub(amount);
IEtherToken(_getWETHAddress()).transfer(to, amount); weth.transfer(to, amount);
emit PoolRewardTransferred( emit PoolRewardTransferred(
poolId, poolId,
to, to,

View File

@@ -21,6 +21,7 @@ pragma experimental ABIEncoderV2;
import "./TestStaking.sol"; import "./TestStaking.sol";
// solhint-disable no-empty-blocks
contract TestCumulativeRewardTracking is contract TestCumulativeRewardTracking is
TestStaking TestStaking
{ {
@@ -39,7 +40,8 @@ contract TestCumulativeRewardTracking is
uint256 epoch uint256 epoch
); );
// solhint-disable-next-line no-empty-blocks constructor(address wethAddress) public TestStaking(wethAddress) {}
function init(address, address, address payable, address) public {} function init(address, address, address payable, address) public {}
function _forceSetCumulativeReward( function _forceSetCumulativeReward(

View File

@@ -0,0 +1,54 @@
/*
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/IEthVault.sol";
import "../src/interfaces/IStakingPoolRewardVault.sol";
import "../src/sys/MixinParams.sol";
// solhint-disable no-empty-blocks
contract TestMixinParams is
MixinParams
{
event WETHApprove(address spender, uint256 amount);
/// @dev Sets the eth and reward vault addresses.
function setVaultAddresses(
address ethVaultAddress,
address rewardVaultAddress
)
external
{
ethVault = IEthVault(ethVaultAddress);
rewardVault = IStakingPoolRewardVault(rewardVaultAddress);
}
/// @dev WETH `approve()` function that just logs events.
function approve(address spender, uint256 amount) external returns (bool) {
emit WETHApprove(spender, amount);
}
/// @dev Overridden return this contract's address.
function _getWETHAddress() internal view returns (address) {
return address(this);
}
}

View File

@@ -21,11 +21,11 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol"; import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol";
import "../src/interfaces/IStructs.sol"; import "../src/interfaces/IStructs.sol";
import "../src/Staking.sol"; import "./TestStakingNoWETH.sol";
contract TestProtocolFees is contract TestProtocolFees is
Staking TestStakingNoWETH
{ {
struct TestPool { struct TestPool {
uint256 operatorStake; uint256 operatorStake;
@@ -33,13 +33,22 @@ contract TestProtocolFees is
mapping(address => bool) isMaker; mapping(address => bool) isMaker;
} }
event ERC20ProxyTransferFrom(
bytes assetData,
address from,
address to,
uint256 amount
);
mapping(bytes32 => TestPool) private _testPools; mapping(bytes32 => TestPool) private _testPools;
mapping(address => bytes32) private _makersToTestPoolIds; mapping(address => bytes32) private _makersToTestPoolIds;
constructor(address exchangeAddress, address wethProxyAddress) public { constructor(address exchangeAddress) public {
init( init(
wethProxyAddress, // Use this contract as the ERC20Proxy.
address(1), // vault addresses must be non-zero address(this),
// vault addresses must be non-zero
address(1),
address(1), address(1),
address(1) address(1)
); );
@@ -81,6 +90,18 @@ contract TestProtocolFees is
} }
} }
/// @dev The ERC20Proxy `transferFrom()` function.
function transferFrom(
bytes calldata assetData,
address from,
address to,
uint256 amount
)
external
{
emit ERC20ProxyTransferFrom(assetData, from, to, amount);
}
/// @dev Overridden to use test pools. /// @dev Overridden to use test pools.
function getStakingPoolIdOfMaker(address makerAddress) function getStakingPoolIdOfMaker(address makerAddress)
public public

View File

@@ -25,23 +25,17 @@ import "../src/Staking.sol";
contract TestStaking is contract TestStaking is
Staking Staking
{ {
address internal _wethAddress; address public testWethAddress;
constructor(address wethAddress) public { constructor(address wethAddress) public {
_wethAddress = wethAddress; testWethAddress = wethAddress;
} }
/// @dev Overridden to avoid hard-coded WETH. /// @dev Overridden to use testWethAddress;
function getTotalBalance()
external
view
returns (uint256 totalBalance)
{
totalBalance = address(this).balance;
}
/// @dev Overridden to use _wethAddress;
function _getWETHAddress() internal view returns (address) { function _getWETHAddress() internal view returns (address) {
return _wethAddress; // `testWethAddress` will not be set on the proxy this contract is
// attached to, so we need to access the storage of the deployed
// instance of this contract.
return TestStaking(address(uint160(stakingContract))).testWethAddress();
} }
} }

View File

@@ -17,28 +17,28 @@
*/ */
pragma solidity ^0.5.9; pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-asset-proxy/contracts/src/ERC20Proxy.sol"; import "../src/Staking.sol";
contract TestProtocolFeesERC20Proxy is // solhint-disable no-empty-blocks
ERC20Proxy /// @dev A version of the staking contract with WETH-related functions
/// overridden to do nothing.
contract TestStakingNoWETH is
Staking
{ {
event TransferFromCalled( function _transferWETHAllownces(
bytes assetData, address[2] memory oldSpenders,
address from, address[2] memory newSpenders
address to,
uint256 amount
);
function transferFrom(
bytes calldata assetData,
address from,
address to,
uint256 amount
) )
external internal
{}
function _wrapBalanceToWETHAndGetBalance()
internal
returns (uint256 balance)
{ {
emit TransferFromCalled(assetData, from, to, amount); return address(this).balance;
} }
} }

View File

@@ -2,8 +2,6 @@ import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { blockchainTests, describe } from '@0x/contracts-test-utils'; import { blockchainTests, describe } from '@0x/contracts-test-utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { artifacts } from '../src';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper'; import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { CumulativeRewardTrackingSimulation, TestAction } from './utils/cumulative_reward_tracking_simulation'; import { CumulativeRewardTrackingSimulation, TestAction } from './utils/cumulative_reward_tracking_simulation';

View File

@@ -1,20 +1,28 @@
import { blockchainTests, expect, filterLogsToArguments } from '@0x/contracts-test-utils'; import { blockchainTests, expect, filterLogsToArguments } from '@0x/contracts-test-utils';
import { AuthorizableRevertErrors, BigNumber } from '@0x/utils'; import { AuthorizableRevertErrors, BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { artifacts, IStakingEventsParamsSetEventArgs, MixinParamsContract } from '../src/'; import {
artifacts,
IStakingEventsParamsSetEventArgs,
TestMixinParamsContract,
TestMixinParamsEvents,
TestMixinParamsWETHApproveEventArgs,
} from '../src/';
import { constants as stakingConstants } from './utils/constants'; import { constants as stakingConstants } from './utils/constants';
import { StakingParams } from './utils/types'; import { StakingParams } from './utils/types';
blockchainTests('Configurable Parameters unit tests', env => { blockchainTests('Configurable Parameters unit tests', env => {
let testContract: MixinParamsContract; let testContract: TestMixinParamsContract;
let authorizedAddress: string; let authorizedAddress: string;
let notAuthorizedAddress: string; let notAuthorizedAddress: string;
before(async () => { before(async () => {
[authorizedAddress, notAuthorizedAddress] = await env.getAccountAddressesAsync(); [authorizedAddress, notAuthorizedAddress] = await env.getAccountAddressesAsync();
testContract = await MixinParamsContract.deployFrom0xArtifactAsync( testContract = await TestMixinParamsContract.deployFrom0xArtifactAsync(
artifacts.MixinParams, artifacts.TestMixinParams,
env.provider, env.provider,
env.txDefaults, env.txDefaults,
artifacts, artifacts,
@@ -22,7 +30,7 @@ blockchainTests('Configurable Parameters unit tests', env => {
}); });
blockchainTests.resets('setParams()', () => { blockchainTests.resets('setParams()', () => {
async function setParamsAndAssertAsync(params: Partial<StakingParams>, from?: string): Promise<void> { async function setParamsAndAssertAsync(params: Partial<StakingParams>, from?: string): Promise<TransactionReceiptWithDecodedLogs> {
const _params = { const _params = {
...stakingConstants.DEFAULT_PARAMS, ...stakingConstants.DEFAULT_PARAMS,
...params, ...params,
@@ -41,8 +49,9 @@ blockchainTests('Configurable Parameters unit tests', env => {
{ from }, { from },
); );
// Assert event. // Assert event.
expect(receipt.logs.length).to.eq(1); const events = filterLogsToArguments<IStakingEventsParamsSetEventArgs>(receipt.logs, 'ParamsSet');
const event = filterLogsToArguments<IStakingEventsParamsSetEventArgs>(receipt.logs, 'ParamsSet')[0]; expect(events.length).to.eq(1);
const event = events[0];
expect(event.epochDurationInSeconds).to.bignumber.eq(_params.epochDurationInSeconds); expect(event.epochDurationInSeconds).to.bignumber.eq(_params.epochDurationInSeconds);
expect(event.rewardDelegatedStakeWeight).to.bignumber.eq(_params.rewardDelegatedStakeWeight); expect(event.rewardDelegatedStakeWeight).to.bignumber.eq(_params.rewardDelegatedStakeWeight);
expect(event.minimumPoolStake).to.bignumber.eq(_params.minimumPoolStake); expect(event.minimumPoolStake).to.bignumber.eq(_params.minimumPoolStake);
@@ -65,6 +74,7 @@ blockchainTests('Configurable Parameters unit tests', env => {
expect(actual[7]).to.eq(_params.ethVaultAddress); expect(actual[7]).to.eq(_params.ethVaultAddress);
expect(actual[8]).to.eq(_params.rewardVaultAddress); expect(actual[8]).to.eq(_params.rewardVaultAddress);
expect(actual[9]).to.eq(_params.zrxVaultAddress); expect(actual[9]).to.eq(_params.zrxVaultAddress);
return receipt;
} }
it('throws if not called by an authorized address', async () => { it('throws if not called by an authorized address', async () => {

View File

@@ -17,8 +17,8 @@ import {
IStakingEventsEvents, IStakingEventsEvents,
IStakingEventsStakingPoolActivatedEventArgs, IStakingEventsStakingPoolActivatedEventArgs,
TestProtocolFeesContract, TestProtocolFeesContract,
TestProtocolFeesERC20ProxyContract, TestProtocolFeesERC20ProxyTransferFromEventArgs,
TestProtocolFeesERC20ProxyTransferFromCalledEventArgs, TestProtocolFeesEvents,
} from '../src'; } from '../src';
import { getRandomInteger } from './utils/number_utils'; import { getRandomInteger } from './utils/number_utils';
@@ -34,14 +34,6 @@ blockchainTests('Protocol Fee Unit Tests', env => {
before(async () => { before(async () => {
[ownerAddress, exchangeAddress, notExchangeAddress] = await env.web3Wrapper.getAvailableAddressesAsync(); [ownerAddress, exchangeAddress, notExchangeAddress] = await env.web3Wrapper.getAvailableAddressesAsync();
// Deploy the erc20Proxy for testing.
const proxy = await TestProtocolFeesERC20ProxyContract.deployFrom0xArtifactAsync(
artifacts.TestProtocolFeesERC20Proxy,
env.provider,
env.txDefaults,
{},
);
// Deploy the protocol fees contract. // Deploy the protocol fees contract.
testContract = await TestProtocolFeesContract.deployFrom0xArtifactAsync( testContract = await TestProtocolFeesContract.deployFrom0xArtifactAsync(
artifacts.TestProtocolFees, artifacts.TestProtocolFees,
@@ -52,7 +44,6 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}, },
artifacts, artifacts,
exchangeAddress, exchangeAddress,
proxy.address,
); );
wethAssetData = await testContract.getWethAssetData.callAsync(); wethAssetData = await testContract.getWethAssetData.callAsync();
@@ -168,9 +159,9 @@ blockchainTests('Protocol Fee Unit Tests', env => {
describe('ETH fees', () => { describe('ETH fees', () => {
function assertNoWETHTransferLogs(logs: LogEntry[]): void { function assertNoWETHTransferLogs(logs: LogEntry[]): void {
const logsArgs = filterLogsToArguments<TestProtocolFeesERC20ProxyTransferFromCalledEventArgs>( const logsArgs = filterLogsToArguments<TestProtocolFeesERC20ProxyTransferFromEventArgs>(
logs, logs,
'TransferFromCalled', TestProtocolFeesEvents.ERC20ProxyTransferFrom,
); );
expect(logsArgs).to.deep.eq([]); expect(logsArgs).to.deep.eq([]);
} }
@@ -233,9 +224,9 @@ blockchainTests('Protocol Fee Unit Tests', env => {
describe('WETH fees', () => { describe('WETH fees', () => {
function assertWETHTransferLogs(logs: LogEntry[], fromAddress: string, amount: BigNumber): void { function assertWETHTransferLogs(logs: LogEntry[], fromAddress: string, amount: BigNumber): void {
const logsArgs = filterLogsToArguments<TestProtocolFeesERC20ProxyTransferFromCalledEventArgs>( const logsArgs = filterLogsToArguments<TestProtocolFeesERC20ProxyTransferFromEventArgs>(
logs, logs,
'TransferFromCalled', TestProtocolFeesEvents.ERC20ProxyTransferFrom,
); );
expect(logsArgs.length).to.eq(1); expect(logsArgs.length).to.eq(1);
for (const args of logsArgs) { for (const args of logsArgs) {

View File

@@ -91,36 +91,35 @@ blockchainTests.resets('finalizer unit tests', env => {
} }
interface FinalizationState { interface FinalizationState {
balance: Numberish; rewardsAvailable: Numberish;
currentEpoch: number; poolsRemaining: number;
closingEpoch: number; totalFeesCollected: Numberish;
numActivePoolsThisEpoch: number; totalWeightedStake: Numberish;
totalFeesCollectedThisEpoch: Numberish; totalRewardsFinalized: Numberish;
totalWeightedStakeThisEpoch: Numberish;
unfinalizedPoolsRemaining: number;
unfinalizedRewardsAvailable: Numberish;
unfinalizedTotalFeesCollected: Numberish;
unfinalizedTotalWeightedStake: Numberish;
} }
async function getFinalizationStateAsync(): Promise<FinalizationState> { async function getUnfinalizedStateAsync(): Promise<FinalizationState> {
const r = await testContract.getFinalizationState.callAsync(); const r = await testContract.unfinalizedState.callAsync();
return { return {
balance: r[0], rewardsAvailable: r[0],
currentEpoch: r[1].toNumber(), poolsRemaining: r[1].toNumber(),
closingEpoch: r[2].toNumber(), totalFeesCollected: r[2],
numActivePoolsThisEpoch: r[3].toNumber(), totalWeightedStake: r[3],
totalFeesCollectedThisEpoch: r[4], totalRewardsFinalized: r[4],
totalWeightedStakeThisEpoch: r[5],
unfinalizedPoolsRemaining: r[6].toNumber(),
unfinalizedRewardsAvailable: r[7],
unfinalizedTotalFeesCollected: r[8],
unfinalizedTotalWeightedStake: r[9],
}; };
} }
async function finalizePoolsAsync(poolIds: string[]): Promise<LogEntry[]> {
const logs = [] as LogEntry[];
for (const poolId of poolIds) {
const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(poolId);
logs.splice(logs.length - 1, 0, ...receipt.logs);
}
return logs;
}
async function assertFinalizationStateAsync(expected: Partial<FinalizationState>): Promise<void> { async function assertFinalizationStateAsync(expected: Partial<FinalizationState>): Promise<void> {
const actual = await getFinalizationStateAsync(); const actual = await getUnfinalizedStateAsync();
assertEqualNumberFields(actual, expected); assertEqualNumberFields(actual, expected);
} }
@@ -248,7 +247,7 @@ blockchainTests.resets('finalizer unit tests', env => {
if (new BigNumber(pool.membersStake).isZero()) { if (new BigNumber(pool.membersStake).isZero()) {
return [new BigNumber(totalReward), ZERO_AMOUNT]; return [new BigNumber(totalReward), ZERO_AMOUNT];
} }
const operatorShare = new BigNumber(totalReward).times(pool.operatorShare).integerValue(BigNumber.ROUND_DOWN); const operatorShare = new BigNumber(totalReward).times(pool.operatorShare).integerValue(BigNumber.ROUND_UP);
const membersShare = new BigNumber(totalReward).minus(operatorShare); const membersShare = new BigNumber(totalReward).minus(operatorShare);
return [operatorShare, membersShare]; return [operatorShare, membersShare];
} }
@@ -337,12 +336,12 @@ blockchainTests.resets('finalizer unit tests', env => {
// Add a pool so there is state to clear. // Add a pool so there is state to clear.
await addActivePoolAsync(); await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
const epoch = await testContract.currentEpoch.callAsync();
expect(epoch).to.bignumber.eq(INITIAL_EPOCH + 1);
return assertFinalizationStateAsync({ return assertFinalizationStateAsync({
currentEpoch: INITIAL_EPOCH + 1, poolsRemaining: 0,
closingEpoch: INITIAL_EPOCH, totalFeesCollected: 0,
numActivePoolsThisEpoch: 0, totalWeightedStake: 0,
totalFeesCollectedThisEpoch: 0,
totalWeightedStakeThisEpoch: 0,
}); });
}); });
@@ -351,10 +350,10 @@ blockchainTests.resets('finalizer unit tests', env => {
const pool = await addActivePoolAsync(); const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
return assertFinalizationStateAsync({ return assertFinalizationStateAsync({
unfinalizedPoolsRemaining: 1, poolsRemaining: 1,
unfinalizedRewardsAvailable: INITIAL_BALANCE, rewardsAvailable: INITIAL_BALANCE,
unfinalizedTotalFeesCollected: pool.feesCollected, totalFeesCollected: pool.feesCollected,
unfinalizedTotalWeightedStake: pool.weightedStake, totalWeightedStake: pool.weightedStake,
}); });
}); });
@@ -367,181 +366,35 @@ blockchainTests.resets('finalizer unit tests', env => {
}); });
}); });
describe('finalizePools()', () => {
it('does nothing if there were no active pools', async () => {
await testContract.endEpoch.awaitTransactionSuccessAsync();
const poolId = hexRandom();
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([poolId]);
expect(receipt.logs).to.deep.eq([]);
});
it('does nothing if no pools are passed in', async () => {
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([]);
expect(receipt.logs).to.deep.eq([]);
});
it('can finalize a single pool', async () => {
const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]);
return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], receipt.logs);
});
it('can finalize multiple pools', async () => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
const poolIds = pools.map(p => p.poolId);
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, receipt.logs);
});
it('can finalize multiple pools over multiple transactions', async () => {
const pools = await Promise.all(_.times(2, async () => addActivePoolAsync()));
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipts = await Promise.all(
pools.map(pool => testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId])),
);
const allLogs = _.flatten(receipts.map(r => r.logs));
return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, allLogs);
});
it('can finalize with no rewards', async () => {
await testContract.drainBalance.awaitTransactionSuccessAsync();
const pools = await Promise.all(_.times(2, async () => addActivePoolAsync()));
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipts = await Promise.all(
pools.map(pool => testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId])),
);
const allLogs = _.flatten(receipts.map(r => r.logs));
return assertFinalizationLogsAndBalancesAsync(0, pools, allLogs);
});
it('ignores a non-active pool', async () => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
const nonActivePoolId = hexRandom();
const poolIds = _.shuffle([...pools.map(p => p.poolId), nonActivePoolId]);
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs);
expect(rewardsPaidEvents.length).to.eq(pools.length);
for (const event of rewardsPaidEvents) {
expect(event.poolId).to.not.eq(nonActivePoolId);
}
});
it('ignores a finalized pool', async () => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
const poolIds = pools.map(p => p.poolId);
await testContract.endEpoch.awaitTransactionSuccessAsync();
const [finalizedPool] = _.sampleSize(pools, 1);
await testContract.finalizePools.awaitTransactionSuccessAsync([finalizedPool.poolId]);
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs);
expect(rewardsPaidEvents.length).to.eq(pools.length - 1);
for (const event of rewardsPaidEvents) {
expect(event.poolId).to.not.eq(finalizedPool.poolId);
}
});
it('resets pool state after finalizing it', async () => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
const pool = _.sample(pools) as ActivePoolOpts;
await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]);
const poolState = await testContract.getActivePoolFromEpoch.callAsync(
new BigNumber(INITIAL_EPOCH),
pool.poolId,
);
expect(poolState.feesCollected).to.bignumber.eq(0);
expect(poolState.weightedStake).to.bignumber.eq(0);
expect(poolState.membersStake).to.bignumber.eq(0);
});
it('`rewardsPaid` is the sum of all pool rewards', async () => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
const poolIds = pools.map(p => p.poolId);
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs);
const expectedTotalRewardsPaid = BigNumber.sum(
...rewardsPaidEvents.map(e => e.membersReward.plus(e.operatorReward)),
);
const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0];
expect(totalRewardsPaid).to.bignumber.eq(expectedTotalRewardsPaid);
});
it('`rewardsPaid` <= `rewardsAvailable` <= contract balance at the end of the epoch', async () => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
const poolIds = pools.map(p => p.poolId);
let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE);
receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0];
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
});
it('`rewardsPaid` <= `rewardsAvailable` with two equal pools', async () => {
const pool1 = await addActivePoolAsync();
const pool2 = await addActivePoolAsync(_.omit(pool1, 'poolId'));
const poolIds = [pool1, pool2].map(p => p.poolId);
let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0];
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
});
blockchainTests.optional('`rewardsPaid` fuzzing', async () => {
const numTests = 32;
for (const i of _.times(numTests)) {
const numPools = _.random(1, 32);
it(`${i + 1}/${numTests} \`rewardsPaid\` <= \`rewardsAvailable\` (${numPools} pools)`, async () => {
const pools = await Promise.all(_.times(numPools, async () => addActivePoolAsync()));
const poolIds = pools.map(p => p.poolId);
let receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
const { rewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0];
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
});
}
});
});
describe('_finalizePool()', () => { describe('_finalizePool()', () => {
it('does nothing if there were no active pools', async () => { it('does nothing if there were no active pools', async () => {
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
const poolId = hexRandom(); const poolId = hexRandom();
const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(poolId); const logs = await finalizePoolsAsync([poolId]);
expect(receipt.logs).to.deep.eq([]); expect(logs).to.deep.eq([]);
}); });
it('can finalize a pool', async () => { it('can finalize a pool', async () => {
const pool = await addActivePoolAsync(); const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId); const logs = await finalizePoolsAsync([pool.poolId]);
return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], receipt.logs); return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], logs);
}); });
it('can finalize multiple pools over multiple transactions', async () => { it('can finalize multiple pools over multiple transactions', async () => {
const pools = await Promise.all(_.times(2, async () => addActivePoolAsync())); const pools = await Promise.all(_.times(2, async () => addActivePoolAsync()));
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipts = await Promise.all( const logs = await finalizePoolsAsync(pools.map(p => p.poolId));
pools.map(pool => testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId)), return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, logs);
);
const allLogs = _.flatten(receipts.map(r => r.logs));
return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, allLogs);
}); });
it('ignores a finalized pool', async () => { it('ignores a finalized pool', async () => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
const [finalizedPool] = _.sampleSize(pools, 1); const [finalizedPool] = _.sampleSize(pools, 1);
await testContract.finalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); await finalizePoolsAsync([finalizedPool.poolId]);
const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId); const logs = await finalizePoolsAsync([finalizedPool.poolId]);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); const rewardsPaidEvents = getRewardsPaidEvents(logs);
expect(rewardsPaidEvents).to.deep.eq([]); expect(rewardsPaidEvents).to.deep.eq([]);
}); });
@@ -549,7 +402,7 @@ blockchainTests.resets('finalizer unit tests', env => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
const pool = _.sample(pools) as ActivePoolOpts; const pool = _.sample(pools) as ActivePoolOpts;
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId); await finalizePoolsAsync([pool.poolId]);
const poolState = await testContract.getActivePoolFromEpoch.callAsync( const poolState = await testContract.getActivePoolFromEpoch.callAsync(
new BigNumber(INITIAL_EPOCH), new BigNumber(INITIAL_EPOCH),
pool.poolId, pool.poolId,
@@ -564,11 +417,8 @@ blockchainTests.resets('finalizer unit tests', env => {
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE); expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE);
const receipts = await Promise.all( const logs = await finalizePoolsAsync(pools.map(r => r.poolId));
pools.map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), const { rewardsPaid } = getEpochFinalizedEvents(logs)[0];
);
const allLogs = _.flatten(receipts.map(r => r.logs));
const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0];
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
}); });
@@ -577,11 +427,8 @@ blockchainTests.resets('finalizer unit tests', env => {
const pool2 = await addActivePoolAsync(_.omit(pool1, 'poolId')); const pool2 = await addActivePoolAsync(_.omit(pool1, 'poolId'));
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
const receipts = await Promise.all( const logs = await finalizePoolsAsync([pool1, pool2].map(r => r.poolId));
[pool1, pool2].map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), const { rewardsPaid } = getEpochFinalizedEvents(logs)[0];
);
const allLogs = _.flatten(receipts.map(r => r.logs));
const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0];
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
}); });
@@ -593,11 +440,8 @@ blockchainTests.resets('finalizer unit tests', env => {
const pools = await Promise.all(_.times(numPools, async () => addActivePoolAsync())); const pools = await Promise.all(_.times(numPools, async () => addActivePoolAsync()));
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
const receipts = await Promise.all( const logs = await finalizePoolsAsync(pools.map(r => r.poolId));
pools.map(p => testContract.finalizePool.awaitTransactionSuccessAsync(p.poolId)), const { rewardsPaid } = getEpochFinalizedEvents(logs)[0];
);
const allLogs = _.flatten(receipts.map(r => r.logs));
const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0];
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable); expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
}); });
} }
@@ -608,7 +452,7 @@ blockchainTests.resets('finalizer unit tests', env => {
it('can advance the epoch after the prior epoch is finalized', async () => { it('can advance the epoch after the prior epoch is finalized', async () => {
const pool = await addActivePoolAsync(); const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); await finalizePoolsAsync([pool.poolId]);
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
return expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 2); return expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 2);
}); });
@@ -616,25 +460,25 @@ blockchainTests.resets('finalizer unit tests', env => {
it('does not reward a pool that was only active 2 epochs ago', async () => { it('does not reward a pool that was only active 2 epochs ago', async () => {
const pool1 = await addActivePoolAsync(); const pool1 = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); await finalizePoolsAsync([pool1.poolId]);
await addActivePoolAsync(); await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 2); expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 2);
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); const logs = await finalizePoolsAsync([pool1.poolId]);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); const rewardsPaidEvents = getRewardsPaidEvents(logs);
expect(rewardsPaidEvents).to.deep.eq([]); expect(rewardsPaidEvents).to.deep.eq([]);
}); });
it('does not reward a pool that was only active 3 epochs ago', async () => { it('does not reward a pool that was only active 3 epochs ago', async () => {
const pool1 = await addActivePoolAsync(); const pool1 = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); await finalizePoolsAsync([pool1.poolId]);
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
await addActivePoolAsync(); await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 3); expect(getCurrentEpochAsync()).to.become(INITIAL_EPOCH + 3);
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync([pool1.poolId]); const logs = await finalizePoolsAsync([pool1.poolId]);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs); const rewardsPaidEvents = getRewardsPaidEvents(logs);
expect(rewardsPaidEvents).to.deep.eq([]); expect(rewardsPaidEvents).to.deep.eq([]);
}); });
@@ -642,11 +486,11 @@ blockchainTests.resets('finalizer unit tests', env => {
const poolIds = _.times(3, () => hexRandom()); const poolIds = _.times(3, () => hexRandom());
await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id }))); await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id })));
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
let receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds); const finalizeLogs = await finalizePoolsAsync(poolIds);
const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(receipt.logs)[0]; const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(finalizeLogs)[0];
await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id }))); await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id })));
receipt = await testContract.endEpoch.awaitTransactionSuccessAsync(); const {logs: endEpochLogs } = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0]; const { rewardsAvailable } = getEpochEndedEvents(endEpochLogs)[0];
expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards); expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards);
}); });
}); });
@@ -694,7 +538,7 @@ blockchainTests.resets('finalizer unit tests', env => {
it('returns empty if pool was only active in the 2 epochs ago', async () => { it('returns empty if pool was only active in the 2 epochs ago', async () => {
const pool = await addActivePoolAsync(); const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); await finalizePoolsAsync([pool.poolId]);
return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS); return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS);
}); });
@@ -702,7 +546,7 @@ blockchainTests.resets('finalizer unit tests', env => {
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync())); const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
const [pool] = _.sampleSize(pools, 1); const [pool] = _.sampleSize(pools, 1);
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]); await finalizePoolsAsync([pool.poolId]);
return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS); return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS);
}); });

View File

@@ -3,7 +3,7 @@ import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } f
import { BlockchainTestsEnvironment, constants, filterLogsToArguments, txDefaults } from '@0x/contracts-test-utils'; import { BlockchainTestsEnvironment, constants, filterLogsToArguments, txDefaults } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } from '@0x/utils'; import { BigNumber, logUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper'; import { Web3Wrapper } from '@0x/web3-wrapper';
import { BlockParamLiteral, ContractArtifact, DecodedLogArgs, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { BlockParamLiteral, ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { import {
@@ -12,29 +12,29 @@ import {
IStakingEventsEpochEndedEventArgs, IStakingEventsEpochEndedEventArgs,
IStakingEventsStakingPoolActivatedEventArgs, IStakingEventsStakingPoolActivatedEventArgs,
ReadOnlyProxyContract, ReadOnlyProxyContract,
StakingContract,
StakingEvents,
StakingPoolRewardVaultContract, StakingPoolRewardVaultContract,
StakingProxyContract, StakingProxyContract,
TestCobbDouglasContract, TestCobbDouglasContract,
TestStakingContract, TestStakingContract,
TestStakingEvents,
ZrxVaultContract, ZrxVaultContract,
} from '../../src'; } from '../../src';
import { constants as stakingConstants } from './constants'; import { constants as stakingConstants } from './constants';
import { EndOfEpochInfo, StakingParams } from './types'; import { DecodedLogs, EndOfEpochInfo, StakingParams } from './types';
export class StakingApiWrapper { export class StakingApiWrapper {
// The address of the real Staking.sol contract // The address of the real Staking.sol contract
public stakingContractAddress: string; public stakingContractAddress: string;
// The StakingProxy.sol contract wrapped as a StakingContract to borrow API // The StakingProxy.sol contract wrapped as a StakingContract to borrow API
public stakingContract: StakingContract; public stakingContract: TestStakingContract;
// The StakingProxy.sol contract as a StakingProxyContract // The StakingProxy.sol contract as a StakingProxyContract
public stakingProxyContract: StakingProxyContract; public stakingProxyContract: StakingProxyContract;
public zrxVaultContract: ZrxVaultContract; public zrxVaultContract: ZrxVaultContract;
public ethVaultContract: EthVaultContract; public ethVaultContract: EthVaultContract;
public rewardVaultContract: StakingPoolRewardVaultContract; public rewardVaultContract: StakingPoolRewardVaultContract;
public zrxTokenContract: DummyERC20TokenContract; public zrxTokenContract: DummyERC20TokenContract;
public wethContract: WETH9Contract;
public cobbDouglasContract: TestCobbDouglasContract; public cobbDouglasContract: TestCobbDouglasContract;
public utils = { public utils = {
// Epoch Utils // Epoch Utils
@@ -49,17 +49,17 @@ export class StakingApiWrapper {
await this._web3Wrapper.mineBlockAsync(); await this._web3Wrapper.mineBlockAsync();
}, },
skipToNextEpochAndFinalizeAsync: async (): Promise<DecodedLogArgs[]> => { skipToNextEpochAndFinalizeAsync: async (): Promise<DecodedLogs> => {
await this.utils.fastForwardToNextEpochAsync(); await this.utils.fastForwardToNextEpochAsync();
const endOfEpochInfo = await this.utils.endEpochAsync(); const endOfEpochInfo = await this.utils.endEpochAsync();
let totalGasUsed = 0; let totalGasUsed = 0;
const allLogs = [] as LogEntry[]; const allLogs = [] as DecodedLogs;
for (const poolId of endOfEpochInfo.activePoolIds) { for (const poolId of endOfEpochInfo.activePoolIds) {
const receipt = await this.stakingContract.finalizePool.awaitTransactionSuccessAsync( const receipt = await this.stakingContract.finalizePool.awaitTransactionSuccessAsync(
poolId, poolId,
); );
totalGasUsed += receipt.gasUsed; totalGasUsed += receipt.gasUsed;
allLogs.splice(allLogs.length, 0, receipt.logs); allLogs.splice(allLogs.length, 0, ...(receipt.logs as DecodedLogs));
} }
logUtils.log(`Finalization cost ${totalGasUsed} gas`); logUtils.log(`Finalization cost ${totalGasUsed} gas`);
return allLogs; return allLogs;
@@ -70,7 +70,7 @@ export class StakingApiWrapper {
const receipt = await this.stakingContract.endEpoch.awaitTransactionSuccessAsync(); const receipt = await this.stakingContract.endEpoch.awaitTransactionSuccessAsync();
const [epochEndedEvent] = filterLogsToArguments<IStakingEventsEpochEndedEventArgs>( const [epochEndedEvent] = filterLogsToArguments<IStakingEventsEpochEndedEventArgs>(
receipt.logs, receipt.logs,
StakingEvents.EpochEnded, TestStakingEvents.EpochEnded,
); );
return { return {
closingEpoch: epochEndedEvent.epoch, closingEpoch: epochEndedEvent.epoch,
@@ -85,11 +85,11 @@ export class StakingApiWrapper {
const _epoch = epoch !== undefined ? epoch : await this.stakingContract.currentEpoch.callAsync(); const _epoch = epoch !== undefined ? epoch : await this.stakingContract.currentEpoch.callAsync();
const events = filterLogsToArguments<IStakingEventsStakingPoolActivatedEventArgs>( const events = filterLogsToArguments<IStakingEventsStakingPoolActivatedEventArgs>(
await this.stakingContract.getLogsAsync( await this.stakingContract.getLogsAsync(
StakingEvents.StakingPoolActivated, TestStakingEvents.StakingPoolActivated,
{ fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest }, { fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest },
{ epoch: new BigNumber(_epoch) }, { epoch: new BigNumber(_epoch) },
), ),
StakingEvents.StakingPoolActivated, TestStakingEvents.StakingPoolActivated,
); );
return events.map(e => e.poolId); return events.map(e => e.poolId);
}, },
@@ -177,11 +177,12 @@ export class StakingApiWrapper {
env: BlockchainTestsEnvironment, env: BlockchainTestsEnvironment,
ownerAddress: string, ownerAddress: string,
stakingProxyContract: StakingProxyContract, stakingProxyContract: StakingProxyContract,
stakingContract: StakingContract, stakingContract: TestStakingContract,
zrxVaultContract: ZrxVaultContract, zrxVaultContract: ZrxVaultContract,
ethVaultContract: EthVaultContract, ethVaultContract: EthVaultContract,
rewardVaultContract: StakingPoolRewardVaultContract, rewardVaultContract: StakingPoolRewardVaultContract,
zrxTokenContract: DummyERC20TokenContract, zrxTokenContract: DummyERC20TokenContract,
wethContract: WETH9Contract,
cobbDouglasContract: TestCobbDouglasContract, cobbDouglasContract: TestCobbDouglasContract,
) { ) {
this._web3Wrapper = env.web3Wrapper; this._web3Wrapper = env.web3Wrapper;
@@ -189,13 +190,14 @@ export class StakingApiWrapper {
this.ethVaultContract = ethVaultContract; this.ethVaultContract = ethVaultContract;
this.rewardVaultContract = rewardVaultContract; this.rewardVaultContract = rewardVaultContract;
this.zrxTokenContract = zrxTokenContract; this.zrxTokenContract = zrxTokenContract;
this.wethContract = wethContract;
this.cobbDouglasContract = cobbDouglasContract; this.cobbDouglasContract = cobbDouglasContract;
this.stakingContractAddress = stakingContract.address; this.stakingContractAddress = stakingContract.address;
this.stakingProxyContract = stakingProxyContract; this.stakingProxyContract = stakingProxyContract;
// disguise the staking proxy as a StakingContract // disguise the staking proxy as a StakingContract
const logDecoderDependencies = _.mapValues({ ...artifacts, ...erc20Artifacts }, v => v.compilerOutput.abi); const logDecoderDependencies = _.mapValues({ ...artifacts, ...erc20Artifacts }, v => v.compilerOutput.abi);
this.stakingContract = new StakingContract( this.stakingContract = new TestStakingContract(
stakingProxyContract.address, stakingProxyContract.address,
env.provider, env.provider,
{ {
@@ -256,6 +258,7 @@ export async function deployAndConfigureContractsAsync(
env.provider, env.provider,
env.txDefaults, env.txDefaults,
artifacts, artifacts,
wethContract.address,
); );
// deploy reward vault // deploy reward vault
const rewardVaultContract = await StakingPoolRewardVaultContract.deployFrom0xArtifactAsync( const rewardVaultContract = await StakingPoolRewardVaultContract.deployFrom0xArtifactAsync(
@@ -263,6 +266,7 @@ export async function deployAndConfigureContractsAsync(
env.provider, env.provider,
env.txDefaults, env.txDefaults,
artifacts, artifacts,
wethContract.address,
); );
// deploy zrx vault // deploy zrx vault
const zrxVaultContract = await ZrxVaultContract.deployFrom0xArtifactAsync( const zrxVaultContract = await ZrxVaultContract.deployFrom0xArtifactAsync(
@@ -311,6 +315,7 @@ export async function deployAndConfigureContractsAsync(
ethVaultContract, ethVaultContract,
rewardVaultContract, rewardVaultContract,
zrxTokenContract, zrxTokenContract,
wethContract,
cobbDouglasContract, cobbDouglasContract,
); );
} }

View File

@@ -3,11 +3,11 @@ import { BigNumber } from '@0x/utils';
import { DecodedLogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { DecodedLogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { artifacts, TestCumulativeRewardTrackingContract, IStakingEvents } from '../../src'; import { artifacts, TestCumulativeRewardTrackingContract, TestCumulativeRewardTrackingEvents } from '../../src';
import { StakingApiWrapper } from './api_wrapper'; import { StakingApiWrapper } from './api_wrapper';
import { toBaseUnitAmount } from './number_utils'; import { toBaseUnitAmount } from './number_utils';
import { StakeInfo, StakeStatus } from './types'; import { DecodedLogs, StakeInfo, StakeStatus } from './types';
export enum TestAction { export enum TestAction {
Finalize, Finalize,
@@ -33,22 +33,22 @@ export class CumulativeRewardTrackingSimulation {
private _testCumulativeRewardTrackingContract?: TestCumulativeRewardTrackingContract; private _testCumulativeRewardTrackingContract?: TestCumulativeRewardTrackingContract;
private _poolId: string; private _poolId: string;
private static _extractTestLogs(txReceiptLogs: DecodedLogArgs[]): TestLog[] { private static _extractTestLogs(txReceiptLogs: DecodedLogs): TestLog[] {
const logs = []; const logs = [];
for (const log of txReceiptLogs) { for (const log of txReceiptLogs) {
if (log.event === 'SetMostRecentCumulativeReward') { if (log.event === TestCumulativeRewardTrackingEvents.SetMostRecentCumulativeReward) {
logs.push({ logs.push({
event: 'SetMostRecentCumulativeReward', event: log.event,
epoch: log.args.epoch.toNumber(), epoch: log.args.epoch.toNumber(),
}); });
} else if (log.event === 'SetCumulativeReward') { } else if (log.event === TestCumulativeRewardTrackingEvents.SetCumulativeReward) {
logs.push({ logs.push({
event: 'SetCumulativeReward', event: log.event,
epoch: log.args.epoch.toNumber(), epoch: log.args.epoch.toNumber(),
}); });
} else if (log.event === 'UnsetCumulativeReward') { } else if (log.event === TestCumulativeRewardTrackingEvents.UnsetCumulativeReward) {
logs.push({ logs.push({
event: 'UnsetCumulativeReward', event: log.event,
epoch: log.args.epoch.toNumber(), epoch: log.args.epoch.toNumber(),
}); });
} }
@@ -56,7 +56,7 @@ export class CumulativeRewardTrackingSimulation {
return logs; return logs;
} }
private static _assertTestLogs(expectedSequence: TestLog[], txReceiptLogs: DecodedLogArgs[]): void { private static _assertTestLogs(expectedSequence: TestLog[], txReceiptLogs: DecodedLogs): void {
const logs = CumulativeRewardTrackingSimulation._extractTestLogs(txReceiptLogs); const logs = CumulativeRewardTrackingSimulation._extractTestLogs(txReceiptLogs);
expect(logs.length).to.be.equal(expectedSequence.length); expect(logs.length).to.be.equal(expectedSequence.length);
for (let i = 0; i < expectedSequence.length; i++) { for (let i = 0; i < expectedSequence.length; i++) {
@@ -90,6 +90,7 @@ export class CumulativeRewardTrackingSimulation {
env.provider, env.provider,
txDefaults, txDefaults,
artifacts, artifacts,
this._stakingApiWrapper.wethContract.address,
); );
} }
@@ -117,11 +118,11 @@ export class CumulativeRewardTrackingSimulation {
CumulativeRewardTrackingSimulation._assertTestLogs(expectedTestLogs, testLogs); CumulativeRewardTrackingSimulation._assertTestLogs(expectedTestLogs, testLogs);
} }
private async _executeActionsAsync(actions: TestAction[]): Promise<Array<DecodedLogEntry<any>>> { private async _executeActionsAsync(actions: TestAction[]): Promise<DecodedLogs> {
const combinedLogs = [] as Array<DecodedLogEntry<any>>; const combinedLogs = [] as DecodedLogs;
for (const action of actions) { for (const action of actions) {
let receipt: TransactionReceiptWithDecodedLogs; let receipt: TransactionReceiptWithDecodedLogs | undefined;
let logs = [] as DecodedLogEntry<any>; let logs = [] as DecodedLogs;
switch (action) { switch (action) {
case TestAction.Finalize: case TestAction.Finalize:
logs = await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); logs = await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
@@ -163,15 +164,18 @@ export class CumulativeRewardTrackingSimulation {
true, true,
{ from: this._poolOperator }, { from: this._poolOperator },
); );
const createStakingPoolLog = logs[0]; const createStakingPoolLog = receipt.logs[0];
// tslint:disable-next-line no-unnecessary-type-assertion // tslint:disable-next-line no-unnecessary-type-assertion
this._poolId = (createStakingPoolLog as DecodedLogArgs).args.poolId; this._poolId = (createStakingPoolLog as DecodedLogEntry<any>).args.poolId;
break; break;
default: default:
throw new Error('Unrecognized test action'); throw new Error('Unrecognized test action');
} }
combinedLogs.splice(combinedLogs.length - 1, 0, logs); if (receipt !== undefined) {
logs = receipt.logs as DecodedLogs;
}
combinedLogs.splice(combinedLogs.length - 1, 0, ...logs);
} }
return combinedLogs; return combinedLogs;
} }

View File

@@ -1,5 +1,6 @@
import { Numberish } from '@0x/contracts-test-utils'; import { Numberish } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { DecodedLogArgs, LogWithDecodedArgs } from 'ethereum-types';
import { constants } from './constants'; import { constants } from './constants';
@@ -133,3 +134,5 @@ export interface OperatorByPoolId {
export interface DelegatorsByPoolId { export interface DelegatorsByPoolId {
[key: string]: string[]; [key: string]: string[];
} }
export type DecodedLogs = Array<LogWithDecodedArgs<DecodedLogArgs>>;