@0x/contracts-staking: All tests back up and running.

This commit is contained in:
Lawrence Forman 2019-09-22 12:03:53 -04:00
parent 6a29654d7d
commit c72a15b488
18 changed files with 113 additions and 213 deletions

View File

@ -57,12 +57,6 @@ library LibStakingRichErrors {
PoolIsFull PoolIsFull
} }
enum CumulativeRewardIntervalErrorCode {
BeginEpochMustBeLessThanEndEpoch,
BeginEpochDoesNotHaveReward,
EndEpochDoesNotHaveReward
}
// bytes4(keccak256("MiscalculatedRewardsError(uint256,uint256)")) // bytes4(keccak256("MiscalculatedRewardsError(uint256,uint256)"))
bytes4 internal constant MISCALCULATED_REWARDS_ERROR_SELECTOR = bytes4 internal constant MISCALCULATED_REWARDS_ERROR_SELECTOR =
0xf7806c4e; 0xf7806c4e;
@ -159,10 +153,6 @@ library LibStakingRichErrors {
bytes internal constant INVALID_WETH_ASSET_DATA_ERROR = bytes internal constant INVALID_WETH_ASSET_DATA_ERROR =
hex"24bf322c"; hex"24bf322c";
// bytes4(keccak256("CumulativeRewardIntervalError(uint8,bytes32,uint256,uint256)"))
bytes4 internal constant CUMULATIVE_REWARD_INTERVAL_ERROR_SELECTOR =
0x1f806d55;
// bytes4(keccak256("PreviousEpochNotFinalizedError(uint256,uint256)")) // bytes4(keccak256("PreviousEpochNotFinalizedError(uint256,uint256)"))
bytes4 internal constant PREVIOUS_EPOCH_NOT_FINALIZED_ERROR_SELECTOR = bytes4 internal constant PREVIOUS_EPOCH_NOT_FINALIZED_ERROR_SELECTOR =
0x614b800a; 0x614b800a;
@ -478,25 +468,6 @@ library LibStakingRichErrors {
return INVALID_WETH_ASSET_DATA_ERROR; return INVALID_WETH_ASSET_DATA_ERROR;
} }
function CumulativeRewardIntervalError(
CumulativeRewardIntervalErrorCode errorCode,
bytes32 poolId,
uint256 beginEpoch,
uint256 endEpoch
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
CUMULATIVE_REWARD_INTERVAL_ERROR_SELECTOR,
errorCode,
poolId,
beginEpoch,
endEpoch
);
}
function PreviousEpochNotFinalizedError( function PreviousEpochNotFinalizedError(
uint256 unfinalizedEpoch, uint256 unfinalizedEpoch,
uint256 unfinalizedPoolsRemaining uint256 unfinalizedPoolsRemaining

View File

@ -247,50 +247,17 @@ contract MixinCumulativeRewards is
} }
// Sanity check interval // Sanity check interval
if (beginEpoch > endEpoch) { require(beginEpoch <= endEpoch, "CR_INTERVAL_INVALID");
LibRichErrors.rrevert(
LibStakingRichErrors.CumulativeRewardIntervalError(
LibStakingRichErrors
.CumulativeRewardIntervalErrorCode
.BeginEpochMustBeLessThanEndEpoch,
poolId,
beginEpoch,
endEpoch
)
);
}
// Sanity check begin reward // Sanity check begin reward
IStructs.Fraction memory beginReward = IStructs.Fraction memory beginReward =
_cumulativeRewardsByPool[poolId][beginEpoch]; _cumulativeRewardsByPool[poolId][beginEpoch];
if (!_isCumulativeRewardSet(beginReward)) { require(_isCumulativeRewardSet(beginReward), "CR_INTERVAL_INVALID_BEGIN");
LibRichErrors.rrevert(
LibStakingRichErrors.CumulativeRewardIntervalError(
LibStakingRichErrors
.CumulativeRewardIntervalErrorCode
.BeginEpochDoesNotHaveReward,
poolId,
beginEpoch,
endEpoch
)
);
}
// Sanity check end reward // Sanity check end reward
IStructs.Fraction memory endReward = IStructs.Fraction memory endReward =
_cumulativeRewardsByPool[poolId][endEpoch]; _cumulativeRewardsByPool[poolId][endEpoch];
if (!_isCumulativeRewardSet(endReward)) { require(_isCumulativeRewardSet(endReward), "CR_INTERVAL_INVALID_END");
LibRichErrors.rrevert(
LibStakingRichErrors.CumulativeRewardIntervalError(
LibStakingRichErrors
.CumulativeRewardIntervalErrorCode
.EndEpochDoesNotHaveReward,
poolId,
beginEpoch,
endEpoch
)
);
}
// Compute reward // Compute reward
reward = LibFractions.scaleDifference( reward = LibFractions.scaleDifference(

View File

@ -356,7 +356,7 @@ contract MixinStakingPoolRewards is
// If the stake has been touched since the last reward epoch, // If the stake has been touched since the last reward epoch,
// it has already been claimed. // it has already been claimed.
if (unsyncedStake.currentEpoch >= lastRewardEpoch) { if (unsyncedStake.currentEpoch >= lastRewardEpoch) {
return 0; return reward;
} }
// From here we know: `unsyncedStake.currentEpoch < currentEpoch > 0`. // From here we know: `unsyncedStake.currentEpoch < currentEpoch > 0`.

View File

@ -66,19 +66,19 @@ contract MixinFinalizer is
returns (uint256 poolsRemaining) returns (uint256 poolsRemaining)
{ {
uint256 closingEpoch = currentEpoch; uint256 closingEpoch = currentEpoch;
IStructs.UnfinalizedState memory state = unfinalizedState;
// Make sure the previous epoch has been fully finalized. // Make sure the previous epoch has been fully finalized.
if (poolsRemaining != 0) { if (state.poolsRemaining != 0) {
LibRichErrors.rrevert( LibRichErrors.rrevert(
LibStakingRichErrors.PreviousEpochNotFinalizedError( LibStakingRichErrors.PreviousEpochNotFinalizedError(
closingEpoch.safeSub(1), closingEpoch - 1,
poolsRemaining state.poolsRemaining
) )
); );
} }
// Set up unfinalized state. // Set up unfinalized state.
IStructs.UnfinalizedState memory state;
state.rewardsAvailable = _wrapBalanceToWETHAndGetBalance(); state.rewardsAvailable = _wrapBalanceToWETHAndGetBalance();
state.poolsRemaining = poolsRemaining = numActivePoolsThisEpoch; state.poolsRemaining = poolsRemaining = numActivePoolsThisEpoch;
state.totalFeesCollected = totalFeesCollectedThisEpoch; state.totalFeesCollected = totalFeesCollectedThisEpoch;

View File

@ -172,6 +172,26 @@ contract MixinParams is
} }
} }
/// @dev Rescind the WETH allowance for `oldSpenders` and grant `newSpenders`
/// an unlimited allowance.
/// @param oldSpenders Addresses to remove allowance from.
/// @param newSpenders Addresses to grant allowance to.
function _transferWETHAllownces(
address[2] memory oldSpenders,
address[2] memory newSpenders
)
internal
{
IEtherToken weth = IEtherToken(_getWETHAddress());
// Grant new allowances.
for (uint256 i = 0; i < oldSpenders.length; i++) {
// Rescind old allowance.
weth.approve(oldSpenders[i], 0);
// Grant new allowance.
weth.approve(newSpenders[i], uint256(-1));
}
}
/// @dev Set all configurable parameters at once. /// @dev Set all configurable parameters at once.
/// @param _epochDurationInSeconds Minimum seconds between epochs. /// @param _epochDurationInSeconds Minimum seconds between epochs.
/// @param _rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm. /// @param _rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm.

View File

@ -22,11 +22,11 @@ pragma experimental ABIEncoderV2;
import "../src/interfaces/IStructs.sol"; import "../src/interfaces/IStructs.sol";
import "../src/interfaces/IStakingPoolRewardVault.sol"; import "../src/interfaces/IStakingPoolRewardVault.sol";
import "../src/interfaces/IEthVault.sol"; import "../src/interfaces/IEthVault.sol";
import "./TestStaking.sol"; import "./TestStakingNoWETH.sol";
contract TestDelegatorRewards is contract TestDelegatorRewards is
TestStaking TestStakingNoWETH
{ {
event RecordDepositToEthVault( event RecordDepositToEthVault(
address owner, address owner,
@ -67,9 +67,9 @@ contract TestDelegatorRewards is
mapping (uint256 => mapping (bytes32 => UnfinalizedPoolReward)) private mapping (uint256 => mapping (bytes32 => UnfinalizedPoolReward)) private
unfinalizedPoolRewardsByEpoch; unfinalizedPoolRewardsByEpoch;
/// @dev Expose _finalizePool /// @dev Expose the original finalizePool
function internalFinalizePool(bytes32 poolId) external { function originalFinalizePool(bytes32 poolId) external {
_finalizePool(poolId); MixinFinalizer.finalizePool(poolId);
} }
/// @dev Set unfinalized rewards for a pool in the current epoch. /// @dev Set unfinalized rewards for a pool in the current epoch.
@ -233,10 +233,11 @@ contract TestDelegatorRewards is
); );
} }
// solhint-disable no-simple-event-func-name
/// @dev Overridden to realize `unfinalizedPoolRewardsByEpoch` in /// @dev Overridden to realize `unfinalizedPoolRewardsByEpoch` in
/// the current epoch and emit a event, /// the current epoch and emit a event,
function _finalizePool(bytes32 poolId) function finalizePool(bytes32 poolId)
internal public
returns ( returns (
uint256 operatorReward, uint256 operatorReward,
uint256 membersReward, uint256 membersReward,

View File

@ -21,21 +21,16 @@ pragma experimental ABIEncoderV2;
import "../src/interfaces/IStructs.sol"; import "../src/interfaces/IStructs.sol";
import "../src/libs/LibCobbDouglas.sol"; import "../src/libs/LibCobbDouglas.sol";
import "./TestStaking.sol"; import "./TestStakingNoWETH.sol";
contract TestFinalizer is contract TestFinalizer is
TestStaking TestStakingNoWETH
{ {
event RecordStakingPoolRewards(
bytes32 poolId,
uint256 totalReward,
uint256 membersStake
);
event DepositStakingPoolRewards( event DepositStakingPoolRewards(
uint256 operatorReward, bytes32 poolId,
uint256 membersReward uint256 reward,
uint256 membersStake
); );
struct UnfinalizedPoolReward { struct UnfinalizedPoolReward {
@ -160,7 +155,7 @@ contract TestFinalizer is
_computeSplitStakingPoolRewards(operatorShare, reward, membersStake); _computeSplitStakingPoolRewards(operatorShare, reward, membersStake);
address(_operatorRewardsReceiver).transfer(operatorReward); address(_operatorRewardsReceiver).transfer(operatorReward);
address(_membersRewardsReceiver).transfer(membersReward); address(_membersRewardsReceiver).transfer(membersReward);
emit DepositStakingPoolRewards(operatorReward, membersReward); emit DepositStakingPoolRewards(poolId, reward, membersStake);
} }
/// @dev Overriden to just increase the epoch counter. /// @dev Overriden to just increase the epoch counter.

View File

@ -68,18 +68,14 @@ contract TestProtocolFees is
currentEpoch += 1; currentEpoch += 1;
} }
function getWethAssetData() external pure returns (bytes memory) {
return WETH_ASSET_DATA;
}
/// @dev Create a test pool. /// @dev Create a test pool.
function createTestPool( function createTestPool(
bytes32 poolId, bytes32 poolId,
uint256 operatorStake, uint256 operatorStake,
uint256 membersStake, uint256 membersStake,
address[] memory makerAddresses address[] calldata makerAddresses
) )
public external
{ {
TestPool storage pool = _testPools[poolId]; TestPool storage pool = _testPools[poolId];
pool.operatorStake = operatorStake; pool.operatorStake = operatorStake;
@ -102,6 +98,10 @@ contract TestProtocolFees is
emit ERC20ProxyTransferFrom(assetData, from, to, amount); emit ERC20ProxyTransferFrom(assetData, from, to, amount);
} }
function getWethAssetData() external pure returns (bytes memory) {
return WETH_ASSET_DATA;
}
/// @dev Overridden to use test pools. /// @dev Overridden to use test pools.
function getStakingPoolIdOfMaker(address makerAddress) function getStakingPoolIdOfMaker(address makerAddress)
public public

View File

@ -155,6 +155,18 @@ export class StakerActor extends BaseActor {
); );
} }
public async syncDelegatorRewardsAsync(poolId: string, revertError?: RevertError): Promise<void> {
const txReceiptPromise = this._stakingApiWrapper.stakingContract.syncDelegatorRewards.awaitTransactionSuccessAsync(
poolId,
{ from: this._owner },
);
if (revertError !== undefined) {
await expect(txReceiptPromise, 'expected revert error').to.revertWith(revertError);
return;
}
await txReceiptPromise;
}
public async goToNextEpochAsync(): Promise<void> { public async goToNextEpochAsync(): Promise<void> {
// cache balances // cache balances
const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync(); const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync();

View File

@ -2,8 +2,6 @@ import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { blockchainTests, expect } from '@0x/contracts-test-utils'; import { blockchainTests, expect } 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 { constants as stakingConstants } from './utils/constants'; import { constants as stakingConstants } from './utils/constants';

View File

@ -30,7 +30,10 @@ blockchainTests('Configurable Parameters unit tests', env => {
}); });
blockchainTests.resets('setParams()', () => { blockchainTests.resets('setParams()', () => {
async function setParamsAndAssertAsync(params: Partial<StakingParams>, from?: string): Promise<TransactionReceiptWithDecodedLogs> { async function setParamsAndAssertAsync(
params: Partial<StakingParams>,
from?: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const _params = { const _params = {
...stakingConstants.DEFAULT_PARAMS, ...stakingConstants.DEFAULT_PARAMS,
...params, ...params,

View File

@ -3,8 +3,6 @@ import { blockchainTests, describe, expect, shortZip } from '@0x/contracts-test-
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { artifacts } from '../src';
import { FinalizerActor } from './actors/finalizer_actor'; import { FinalizerActor } from './actors/finalizer_actor';
import { PoolOperatorActor } from './actors/pool_operator_actor'; import { PoolOperatorActor } from './actors/pool_operator_actor';
import { StakerActor } from './actors/staker_actor'; import { StakerActor } from './actors/staker_actor';
@ -45,7 +43,7 @@ blockchainTests.resets('Testing Rewards', env => {
stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper); stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper);
// set up staking parameters // set up staking parameters
await stakingApiWrapper.utils.setParamsAsync({ await stakingApiWrapper.utils.setParamsAsync({
minimumPoolStake: new BigNumber(1), minimumPoolStake: new BigNumber(2),
cobbDouglasAlphaNumerator: new BigNumber(1), cobbDouglasAlphaNumerator: new BigNumber(1),
cobbDouglasAlphaDenominator: new BigNumber(6), cobbDouglasAlphaDenominator: new BigNumber(6),
rewardVaultAddress: stakingApiWrapper.rewardVaultContract.address, rewardVaultAddress: stakingApiWrapper.rewardVaultContract.address,
@ -60,7 +58,7 @@ blockchainTests.resets('Testing Rewards', env => {
poolId = await poolOperator.createStakingPoolAsync(0, true); poolId = await poolOperator.createStakingPoolAsync(0, true);
// Stake something in the pool or else it won't get any rewards. // Stake something in the pool or else it won't get any rewards.
poolOperatorStaker = new StakerActor(poolOperator.getOwner(), stakingApiWrapper); poolOperatorStaker = new StakerActor(poolOperator.getOwner(), stakingApiWrapper);
await poolOperatorStaker.stakeWithPoolAsync(poolId, new BigNumber(1)); await poolOperatorStaker.stakeWithPoolAsync(poolId, new BigNumber(2));
// set exchange address // set exchange address
await stakingApiWrapper.stakingContract.addExchangeAddress.awaitTransactionSuccessAsync(exchangeAddress); await stakingApiWrapper.stakingContract.addExchangeAddress.awaitTransactionSuccessAsync(exchangeAddress);
// associate operators for tracking in Finalizer // associate operators for tracking in Finalizer
@ -609,18 +607,14 @@ blockchainTests.resets('Testing Rewards', env => {
// finalize // finalize
const reward = toBaseUnitAmount(10); const reward = toBaseUnitAmount(10);
await payProtocolFeeAndFinalize(reward); await payProtocolFeeAndFinalize(reward);
// Undelegate 0 stake to move the rewards into the EthVault. // Sync rewards to move the rewards into the EthVault.
await staker.moveStakeAsync( await staker.syncDelegatorRewardsAsync(poolId);
new StakeInfo(StakeStatus.Delegated, poolId),
new StakeInfo(StakeStatus.Active),
toBaseUnitAmount(0),
);
await validateEndBalances({ await validateEndBalances({
stakerRewardVaultBalance_1: toBaseUnitAmount(0), stakerRewardVaultBalance_1: toBaseUnitAmount(0),
stakerEthVaultBalance_1: reward, stakerEthVaultBalance_1: reward,
}); });
}); });
it(`should split payout between two delegators when undelegating`, async () => { it(`should split payout between two delegators when syncing rewards`, async () => {
const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)];
const totalStakeAmount = BigNumber.sum(...stakeAmounts); const totalStakeAmount = BigNumber.sum(...stakeAmounts);
// stake and delegate both // stake and delegate both
@ -633,13 +627,9 @@ blockchainTests.resets('Testing Rewards', env => {
// finalize // finalize
const reward = toBaseUnitAmount(10); const reward = toBaseUnitAmount(10);
await payProtocolFeeAndFinalize(reward); await payProtocolFeeAndFinalize(reward);
// Undelegate 0 stake to move rewards from RewardVault into the EthVault. // Sync rewards to move rewards from RewardVault into the EthVault.
for (const [staker] of _.reverse(stakersAndStake)) { for (const [staker] of _.reverse(stakersAndStake)) {
await staker.moveStakeAsync( await staker.syncDelegatorRewardsAsync(poolId);
new StakeInfo(StakeStatus.Delegated, poolId),
new StakeInfo(StakeStatus.Active),
toBaseUnitAmount(0),
);
} }
const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
await validateEndBalances({ await validateEndBalances({
@ -651,7 +641,7 @@ blockchainTests.resets('Testing Rewards', env => {
membersRewardVaultBalance: new BigNumber(1), // Rounding error membersRewardVaultBalance: new BigNumber(1), // Rounding error
}); });
}); });
it(`delegator should not be credited payout twice by undelegating twice`, async () => { it(`delegator should not be credited payout twice by syncing rewards twice`, async () => {
const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)];
const totalStakeAmount = BigNumber.sum(...stakeAmounts); const totalStakeAmount = BigNumber.sum(...stakeAmounts);
// stake and delegate both // stake and delegate both
@ -673,17 +663,10 @@ blockchainTests.resets('Testing Rewards', env => {
poolRewardVaultBalance: reward, poolRewardVaultBalance: reward,
membersRewardVaultBalance: reward, membersRewardVaultBalance: reward,
}); });
const undelegateZeroAsync = async (staker: StakerActor) => { // First staker will sync rewards to get rewards transferred to EthVault.
return staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolId),
new StakeInfo(StakeStatus.Active),
toBaseUnitAmount(0),
);
};
// First staker will undelegate 0 to get rewards transferred to EthVault.
const sneakyStaker = stakers[0]; const sneakyStaker = stakers[0];
const sneakyStakerExpectedEthVaultBalance = expectedStakerRewards[0]; const sneakyStakerExpectedEthVaultBalance = expectedStakerRewards[0];
await undelegateZeroAsync(sneakyStaker); await sneakyStaker.syncDelegatorRewardsAsync(poolId);
// Should have been credited the correct amount of rewards. // Should have been credited the correct amount of rewards.
let sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync( let sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync(
sneakyStaker.getOwner(), sneakyStaker.getOwner(),
@ -692,7 +675,7 @@ blockchainTests.resets('Testing Rewards', env => {
sneakyStakerExpectedEthVaultBalance, sneakyStakerExpectedEthVaultBalance,
); );
// Now he'll try to do it again to see if he gets credited twice. // Now he'll try to do it again to see if he gets credited twice.
await undelegateZeroAsync(sneakyStaker); await sneakyStaker.syncDelegatorRewardsAsync(poolId);
/// The total amount credited should remain the same. /// The total amount credited should remain the same.
sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync( sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync(
sneakyStaker.getOwner(), sneakyStaker.getOwner(),

View File

@ -4,8 +4,6 @@ import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { artifacts } from '../src';
import { StakerActor } from './actors/staker_actor'; import { StakerActor } from './actors/staker_actor';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper'; import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { toBaseUnitAmount } from './utils/number_utils'; import { toBaseUnitAmount } from './utils/number_utils';

View File

@ -223,7 +223,7 @@ blockchainTests.resets('delegator unit rewards', env => {
} }
async function finalizePoolAsync(poolId: string): Promise<ResultWithDeposits<{}>> { async function finalizePoolAsync(poolId: string): Promise<ResultWithDeposits<{}>> {
const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(poolId); const receipt = await testContract.originalFinalizePool.awaitTransactionSuccessAsync(poolId);
const [ethVaultDeposit, rewardVaultDeposit] = getDepositsFromLogs(receipt.logs, poolId); const [ethVaultDeposit, rewardVaultDeposit] = getDepositsFromLogs(receipt.logs, poolId);
return { return {
ethVaultDeposit, ethVaultDeposit,
@ -379,7 +379,7 @@ blockchainTests.resets('delegator unit rewards', env => {
assertRoughlyEquals(delegatorReward, expectedDelegatorRewards); assertRoughlyEquals(delegatorReward, expectedDelegatorRewards);
}); });
it('has correct reward immediately after unstaking', async () => { it('has correct reward immediately after undelegating', async () => {
const poolId = hexRandom(); const poolId = hexRandom();
const { delegator, stake } = await delegateStakeAsync(poolId); const { delegator, stake } = await delegateStakeAsync(poolId);
await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 1 (stake now active)
@ -392,7 +392,7 @@ blockchainTests.resets('delegator unit rewards', env => {
expect(delegatorReward).to.bignumber.eq(0); expect(delegatorReward).to.bignumber.eq(0);
}); });
it('has correct reward immediately after unstaking and restaking', async () => { it('has correct reward immediately after undelegating and redelegating', async () => {
const poolId = hexRandom(); const poolId = hexRandom();
const { delegator, stake } = await delegateStakeAsync(poolId); const { delegator, stake } = await delegateStakeAsync(poolId);
await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 1 (stake now active)
@ -406,7 +406,7 @@ blockchainTests.resets('delegator unit rewards', env => {
expect(delegatorReward).to.bignumber.eq(0); expect(delegatorReward).to.bignumber.eq(0);
}); });
it('has correct reward immediately after unstaking, restaking, and rewarding fees', async () => { it('has correct reward immediately after undelegating, redelegating, and rewarding fees', async () => {
const poolId = hexRandom(); const poolId = hexRandom();
const { delegator, stake } = await delegateStakeAsync(poolId); const { delegator, stake } = await delegateStakeAsync(poolId);
await advanceEpochAsync(); // epoch 1 (stake now active) await advanceEpochAsync(); // epoch 1 (stake now active)

View File

@ -21,13 +21,8 @@ import {
TestFinalizerContract, TestFinalizerContract,
TestFinalizerDepositStakingPoolRewardsEventArgs as DepositStakingPoolRewardsEventArgs, TestFinalizerDepositStakingPoolRewardsEventArgs as DepositStakingPoolRewardsEventArgs,
TestFinalizerEvents, TestFinalizerEvents,
TestFinalizerRecordStakingPoolRewardsEventArgs as RecordStakingPoolRewardsEventArgs,
} from '../../src'; } from '../../src';
import { import { assertIntegerRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
assertRoughlyEquals as _assertIntegerRoughlyEquals,
getRandomInteger,
toBaseUnitAmount,
} from '../utils/number_utils';
blockchainTests.resets('finalizer unit tests', env => { blockchainTests.resets('finalizer unit tests', env => {
const { ZERO_AMOUNT } = constants; const { ZERO_AMOUNT } = constants;
@ -90,7 +85,7 @@ blockchainTests.resets('finalizer unit tests', env => {
return _opts; return _opts;
} }
interface FinalizationState { interface UnfinalizedState {
rewardsAvailable: Numberish; rewardsAvailable: Numberish;
poolsRemaining: number; poolsRemaining: number;
totalFeesCollected: Numberish; totalFeesCollected: Numberish;
@ -98,7 +93,7 @@ blockchainTests.resets('finalizer unit tests', env => {
totalRewardsFinalized: Numberish; totalRewardsFinalized: Numberish;
} }
async function getUnfinalizedStateAsync(): Promise<FinalizationState> { async function getUnfinalizedStateAsync(): Promise<UnfinalizedState> {
const r = await testContract.unfinalizedState.callAsync(); const r = await testContract.unfinalizedState.callAsync();
return { return {
rewardsAvailable: r[0], rewardsAvailable: r[0],
@ -113,12 +108,12 @@ blockchainTests.resets('finalizer unit tests', env => {
const logs = [] as LogEntry[]; const logs = [] as LogEntry[];
for (const poolId of poolIds) { for (const poolId of poolIds) {
const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(poolId); const receipt = await testContract.finalizePool.awaitTransactionSuccessAsync(poolId);
logs.splice(logs.length - 1, 0, ...receipt.logs); logs.splice(logs.length, 0, ...receipt.logs);
} }
return logs; return logs;
} }
async function assertFinalizationStateAsync(expected: Partial<FinalizationState>): Promise<void> { async function assertUnfinalizedStateAsync(expected: Partial<UnfinalizedState>): Promise<void> {
const actual = await getUnfinalizedStateAsync(); const actual = await getUnfinalizedStateAsync();
assertEqualNumberFields(actual, expected); assertEqualNumberFields(actual, expected);
} }
@ -135,10 +130,6 @@ blockchainTests.resets('finalizer unit tests', env => {
assertEqualNumberFields(events[0], args); assertEqualNumberFields(events[0], args);
} }
function assertRoughlyEquals(actual: Numberish, expected: Numberish): void {
_assertIntegerRoughlyEquals(actual, expected, 5);
}
function assertEqualNumberFields<T>(actual: T, expected: Partial<T>): void { function assertEqualNumberFields<T>(actual: T, expected: Partial<T>): void {
for (const key of Object.keys(actual)) { for (const key of Object.keys(actual)) {
const a = (actual as any)[key] as BigNumber; const a = (actual as any)[key] as BigNumber;
@ -171,42 +162,33 @@ blockchainTests.resets('finalizer unit tests', env => {
const reward = poolRewards[i]; const reward = poolRewards[i];
const [operatorReward, membersReward] = splitRewards(pool, reward); const [operatorReward, membersReward] = splitRewards(pool, reward);
expect(event.epoch).to.bignumber.eq(currentEpoch); expect(event.epoch).to.bignumber.eq(currentEpoch);
assertRoughlyEquals(event.operatorReward, operatorReward); assertIntegerRoughlyEquals(event.operatorReward, operatorReward);
assertRoughlyEquals(event.membersReward, membersReward); assertIntegerRoughlyEquals(event.membersReward, membersReward);
}
// Assert the `RecordStakingPoolRewards` logs.
const recordStakingPoolRewardsEvents = getRecordStakingPoolRewardsEvents(finalizationLogs);
expect(recordStakingPoolRewardsEvents.length).to.eq(poolsWithStake.length);
for (const i of _.times(recordStakingPoolRewardsEvents.length)) {
const event = recordStakingPoolRewardsEvents[i];
const pool = poolsWithStake[i];
const reward = poolRewards[i];
expect(event.poolId).to.eq(pool.poolId);
assertRoughlyEquals(event.totalReward, reward);
assertRoughlyEquals(event.membersStake, pool.membersStake);
} }
// Assert the `DepositStakingPoolRewards` logs. // Assert the `DepositStakingPoolRewards` logs.
// Make sure they all sum up to the totals.
const depositStakingPoolRewardsEvents = getDepositStakingPoolRewardsEvents(finalizationLogs); const depositStakingPoolRewardsEvents = getDepositStakingPoolRewardsEvents(finalizationLogs);
expect(depositStakingPoolRewardsEvents.length).to.eq(poolsWithStake.length);
for (const i of _.times(depositStakingPoolRewardsEvents.length)) {
const event = depositStakingPoolRewardsEvents[i];
const pool = poolsWithStake[i];
const reward = poolRewards[i];
expect(event.poolId).to.eq(pool.poolId);
assertIntegerRoughlyEquals(event.reward, reward);
assertIntegerRoughlyEquals(event.membersStake, pool.membersStake);
}
// Make sure they all sum up to the totals.
if (depositStakingPoolRewardsEvents.length > 0) { if (depositStakingPoolRewardsEvents.length > 0) {
const totalDepositedOperatorRewards = BigNumber.sum( const totalDepositRewards = BigNumber.sum(...depositStakingPoolRewardsEvents.map(e => e.reward));
...depositStakingPoolRewardsEvents.map(e => e.operatorReward), assertIntegerRoughlyEquals(totalDepositRewards, totalRewards);
);
const totalDepositedMembersRewards = BigNumber.sum(
...depositStakingPoolRewardsEvents.map(e => e.membersReward),
);
assertRoughlyEquals(totalDepositedOperatorRewards, totalOperatorRewards);
assertRoughlyEquals(totalDepositedMembersRewards, totalMembersRewards);
} }
// Assert the `EpochFinalized` logs. // Assert the `EpochFinalized` logs.
const epochFinalizedEvents = getEpochFinalizedEvents(finalizationLogs); const epochFinalizedEvents = getEpochFinalizedEvents(finalizationLogs);
expect(epochFinalizedEvents.length).to.eq(1); expect(epochFinalizedEvents.length).to.eq(1);
expect(epochFinalizedEvents[0].epoch).to.bignumber.eq(currentEpoch - 1); expect(epochFinalizedEvents[0].epoch).to.bignumber.eq(currentEpoch - 1);
assertRoughlyEquals(epochFinalizedEvents[0].rewardsPaid, totalRewards); assertIntegerRoughlyEquals(epochFinalizedEvents[0].rewardsPaid, totalRewards);
assertRoughlyEquals(epochFinalizedEvents[0].rewardsRemaining, rewardsRemaining); assertIntegerRoughlyEquals(epochFinalizedEvents[0].rewardsRemaining, rewardsRemaining);
// Assert the receiver balances. // Assert the receiver balances.
await assertReceiverBalancesAsync(totalOperatorRewards, totalMembersRewards); await assertReceiverBalancesAsync(totalOperatorRewards, totalMembersRewards);
@ -214,9 +196,9 @@ blockchainTests.resets('finalizer unit tests', env => {
async function assertReceiverBalancesAsync(operatorRewards: Numberish, membersRewards: Numberish): Promise<void> { async function assertReceiverBalancesAsync(operatorRewards: Numberish, membersRewards: Numberish): Promise<void> {
const operatorRewardsBalance = await getBalanceOfAsync(operatorRewardsReceiver); const operatorRewardsBalance = await getBalanceOfAsync(operatorRewardsReceiver);
assertRoughlyEquals(operatorRewardsBalance, operatorRewards); assertIntegerRoughlyEquals(operatorRewardsBalance, operatorRewards);
const membersRewardsBalance = await getBalanceOfAsync(membersRewardsReceiver); const membersRewardsBalance = await getBalanceOfAsync(membersRewardsReceiver);
assertRoughlyEquals(membersRewardsBalance, membersRewards); assertIntegerRoughlyEquals(membersRewardsBalance, membersRewards);
} }
async function calculatePoolRewardsAsync( async function calculatePoolRewardsAsync(
@ -269,13 +251,6 @@ blockchainTests.resets('finalizer unit tests', env => {
return filterLogsToArguments<IStakingEventsEpochFinalizedEventArgs>(logs, IStakingEventsEvents.EpochFinalized); return filterLogsToArguments<IStakingEventsEpochFinalizedEventArgs>(logs, IStakingEventsEvents.EpochFinalized);
} }
function getRecordStakingPoolRewardsEvents(logs: LogEntry[]): RecordStakingPoolRewardsEventArgs[] {
return filterLogsToArguments<RecordStakingPoolRewardsEventArgs>(
logs,
TestFinalizerEvents.RecordStakingPoolRewards,
);
}
function getDepositStakingPoolRewardsEvents(logs: LogEntry[]): DepositStakingPoolRewardsEventArgs[] { function getDepositStakingPoolRewardsEvents(logs: LogEntry[]): DepositStakingPoolRewardsEventArgs[] {
return filterLogsToArguments<DepositStakingPoolRewardsEventArgs>( return filterLogsToArguments<DepositStakingPoolRewardsEventArgs>(
logs, logs,
@ -338,18 +313,19 @@ blockchainTests.resets('finalizer unit tests', env => {
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
const epoch = await testContract.currentEpoch.callAsync(); const epoch = await testContract.currentEpoch.callAsync();
expect(epoch).to.bignumber.eq(INITIAL_EPOCH + 1); expect(epoch).to.bignumber.eq(INITIAL_EPOCH + 1);
return assertFinalizationStateAsync({ const numActivePools = await testContract.numActivePoolsThisEpoch.callAsync();
poolsRemaining: 0, const totalFees = await testContract.totalFeesCollectedThisEpoch.callAsync();
totalFeesCollected: 0, const totalStake = await testContract.totalWeightedStakeThisEpoch.callAsync();
totalWeightedStake: 0, expect(numActivePools).to.bignumber.eq(0);
}); expect(totalFees).to.bignumber.eq(0);
expect(totalStake).to.bignumber.eq(0);
}); });
it('prepares finalization state', async () => { it('prepares unfinalized state', async () => {
// Add a pool so there is state to clear. // Add a pool so there is state to clear.
const pool = await addActivePoolAsync(); const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync(); await testContract.endEpoch.awaitTransactionSuccessAsync();
return assertFinalizationStateAsync({ return assertUnfinalizedStateAsync({
poolsRemaining: 1, poolsRemaining: 1,
rewardsAvailable: INITIAL_BALANCE, rewardsAvailable: INITIAL_BALANCE,
totalFeesCollected: pool.feesCollected, totalFeesCollected: pool.feesCollected,
@ -489,7 +465,7 @@ blockchainTests.resets('finalizer unit tests', env => {
const finalizeLogs = await finalizePoolsAsync(poolIds); const finalizeLogs = await finalizePoolsAsync(poolIds);
const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(finalizeLogs)[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 })));
const {logs: endEpochLogs } = await testContract.endEpoch.awaitTransactionSuccessAsync(); const { logs: endEpochLogs } = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(endEpochLogs)[0]; const { rewardsAvailable } = getEpochEndedEvents(endEpochLogs)[0];
expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards); expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards);
}); });

View File

@ -55,9 +55,7 @@ export class StakingApiWrapper {
let totalGasUsed = 0; let totalGasUsed = 0;
const allLogs = [] as DecodedLogs; 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 as DecodedLogs)); allLogs.splice(allLogs.length, 0, ...(receipt.logs as DecodedLogs));
} }

View File

@ -175,7 +175,7 @@ export class CumulativeRewardTrackingSimulation {
if (receipt !== undefined) { if (receipt !== undefined) {
logs = receipt.logs as DecodedLogs; logs = receipt.logs as DecodedLogs;
} }
combinedLogs.splice(combinedLogs.length - 1, 0, ...logs); combinedLogs.splice(combinedLogs.length, 0, ...logs);
} }
return combinedLogs; return combinedLogs;
} }

View File

@ -40,12 +40,6 @@ export enum InitializationErrorCode {
MixinParamsAlreadyInitialized, MixinParamsAlreadyInitialized,
} }
export enum CumulativeRewardIntervalErrorCode {
BeginEpochMustBeLessThanEndEpoch,
BeginEpochDoesNotHaveReward,
EndEpochDoesNotHaveReward,
}
export class MiscalculatedRewardsError extends RevertError { export class MiscalculatedRewardsError extends RevertError {
constructor(totalRewardsPaid?: BigNumber | number | string, initialContractBalance?: BigNumber | number | string) { constructor(totalRewardsPaid?: BigNumber | number | string, initialContractBalance?: BigNumber | number | string) {
super( super(
@ -244,21 +238,6 @@ export class ProxyDestinationCannotBeNilError extends RevertError {
} }
} }
export class CumulativeRewardIntervalError extends RevertError {
constructor(
errorCode?: CumulativeRewardIntervalErrorCode,
poolId?: string,
beginEpoch?: BigNumber | number | string,
endEpoch?: BigNumber | number | string,
) {
super(
'CumulativeRewardIntervalError',
'CumulativeRewardIntervalError(uint8 errorCode, bytes32 poolId, uint256 beginEpoch, uint256 endEpoch)',
{ errorCode, poolId, beginEpoch, endEpoch },
);
}
}
export class PreviousEpochNotFinalizedError extends RevertError { export class PreviousEpochNotFinalizedError extends RevertError {
constructor(closingEpoch?: BigNumber | number | string, unfinalizedPoolsRemaining?: BigNumber | number | string) { constructor(closingEpoch?: BigNumber | number | string, unfinalizedPoolsRemaining?: BigNumber | number | string) {
super( super(
@ -272,7 +251,6 @@ export class PreviousEpochNotFinalizedError extends RevertError {
const types = [ const types = [
AmountExceedsBalanceOfPoolError, AmountExceedsBalanceOfPoolError,
BlockTimestampTooLowError, BlockTimestampTooLowError,
CumulativeRewardIntervalError,
EthVaultNotSetError, EthVaultNotSetError,
ExchangeAddressAlreadyRegisteredError, ExchangeAddressAlreadyRegisteredError,
ExchangeAddressNotRegisteredError, ExchangeAddressNotRegisteredError,