Refactored finalization state.

1. Removes state variables:
   - totalWeightedStakeThisEpoch
   - totalFeesCollectedThisEpoch
   - numActivePoolsThisEpoch

2. No longer indexes by epoch % 2

3. Renamed event StakingPoolActivated → StakingPoolEarnedRewards.

4. Renamed structs:
   - ActivePool → PoolStats. This holds stats for a pool that earned rewards.
   - UnfinalizedState → AggregatedStats. This aggregates stats from the former struct.
This commit is contained in:
Greg Hysen
2019-10-21 10:07:06 -07:00
parent e7dc7167d0
commit 6617ad9531
13 changed files with 222 additions and 274 deletions

View File

@@ -81,49 +81,46 @@ contract MixinExchangeFees is
return;
}
// Look up the pool for this epoch.
// Look up the pool stats and aggregated stats for this epoch.
uint256 currentEpoch_ = currentEpoch;
mapping (bytes32 => IStructs.ActivePool) storage activePoolsThisEpoch =
_getActivePoolsFromEpoch(currentEpoch_);
IStructs.ActivePool memory pool = activePoolsThisEpoch[poolId];
IStructs.PoolStats memory poolStats = poolStatsByEpoch[poolId][currentEpoch_];
IStructs.AggregatedStats memory aggregatedStats = aggregatedStatsByEpoch[currentEpoch_];
// If the pool was previously inactive in this epoch, initialize it.
if (pool.feesCollected == 0) {
if (poolStats.feesCollected == 0) {
// Compute member and total weighted stake.
(pool.membersStake, pool.weightedStake) = _computeMembersAndWeightedStake(poolId, poolStake);
(poolStats.membersStake, poolStats.weightedStake) = _computeMembersAndWeightedStake(poolId, poolStake);
// Increase the total weighted stake.
totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch.safeAdd(pool.weightedStake);
aggregatedStats.totalWeightedStake = aggregatedStats.totalWeightedStake.safeAdd(poolStats.weightedStake);
// Increase the number of active pools.
numActivePoolsThisEpoch = numActivePoolsThisEpoch.safeAdd(1);
aggregatedStats.poolsToFinalize = aggregatedStats.poolsToFinalize.safeAdd(1);
// Emit an event so keepers know what pools to pass into
// `finalize()`.
emit StakingPoolActivated(currentEpoch_, poolId);
// Emit an event so keepers know what pools earned rewards this epoch.
emit StakingPoolEarnedRewardsInEpoch(currentEpoch_, poolId);
}
// Credit the fees to the pool.
pool.feesCollected = pool.feesCollected.safeAdd(protocolFeePaid);
poolStats.feesCollected = poolStats.feesCollected.safeAdd(protocolFeePaid);
// Increase the total fees collected this epoch.
totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd(protocolFeePaid);
aggregatedStats.totalFeesCollected = aggregatedStats.totalFeesCollected.safeAdd(protocolFeePaid);
// Store the pool.
activePoolsThisEpoch[poolId] = pool;
// Store the updated stats.
poolStatsByEpoch[poolId][currentEpoch_] = poolStats;
aggregatedStatsByEpoch[currentEpoch_] = aggregatedStats;
}
/// @dev Get information on an active staking pool in this epoch.
/// @param poolId Pool Id to query.
/// @return pool ActivePool struct.
function getActiveStakingPoolThisEpoch(bytes32 poolId)
/// @return PoolStats struct for pool id.
function getStakingPoolStatsThisEpoch(bytes32 poolId)
external
view
returns (IStructs.ActivePool memory pool)
returns (IStructs.PoolStats memory)
{
pool = _getActivePoolFromEpoch(currentEpoch, poolId);
return pool;
return poolStatsByEpoch[poolId][currentEpoch];
}
/// @dev Computes the members and weighted stake for a pool at the current

View File

@@ -101,25 +101,15 @@ contract MixinStorage is
// Denominator for cobb douglas alpha factor.
uint32 public cobbDouglasAlphaDenominator;
/* Finalization states */
/* State for finalization */
/// @dev The total fees collected in the current epoch, built up iteratively
/// in `payProtocolFee()`.
uint256 public totalFeesCollectedThisEpoch;
/// @dev Stats for each pool that generated fees with sufficient stake to earn rewards.
/// See `_minimumPoolStake` in MixinParams.
mapping (bytes32 => mapping (uint256 => IStructs.PoolStats)) public poolStatsByEpoch;
/// @dev The total weighted stake in the current epoch, built up iteratively
/// in `payProtocolFee()`.
uint256 public totalWeightedStakeThisEpoch;
/// @dev State information for each active pool in an epoch.
/// In practice, we only store state for `currentEpoch % 2`.
mapping (uint256 => mapping (bytes32 => IStructs.ActivePool)) internal _activePoolsByEpoch;
/// @dev Number of pools activated in the current epoch.
uint256 public numActivePoolsThisEpoch;
/// @dev State for unfinalized rewards.
IStructs.UnfinalizedState public unfinalizedState;
/// @dev Aggregated stats across all pools that generated fees with sufficient stake to earn rewards.
/// See `_minimumPoolStake` in MixinParams.
mapping (uint256 => IStructs.AggregatedStats) public aggregatedStatsByEpoch;
/// @dev The WETH balance of this contract that is reserved for pool reward payouts.
uint256 public wethReservedForPoolRewards;

View File

@@ -43,24 +43,23 @@ interface IStakingEvents {
address exchangeAddress
);
/// @dev Emitted by MixinExchangeFees when a pool pays protocol fees
/// for the first time in an epoch.
/// @param epoch The epoch in which the pool was activated.
/// @dev Emitted by MixinExchangeFees when a pool will earn rewards.
/// @param epoch The epoch in which the pool earned rewards.
/// @param poolId The ID of the pool.
event StakingPoolActivated(
event StakingPoolEarnedRewardsInEpoch(
uint256 indexed epoch,
bytes32 indexed poolId
);
/// @dev Emitted by MixinFinalizer when an epoch has ended.
/// @param epoch The closing epoch.
/// @param numActivePools Number of active pools in the closing epoch.
/// @param poolsToFinalize Number of pools to finalize in the closing epoch.
/// @param rewardsAvailable Rewards available to all active pools.
/// @param totalWeightedStake Total weighted stake across all active pools.
/// @param totalFeesCollected Total fees collected across all active pools.
event EpochEnded(
uint256 indexed epoch,
uint256 numActivePools,
uint256 poolsToFinalize,
uint256 rewardsAvailable,
uint256 totalFeesCollected,
uint256 totalWeightedStake

View File

@@ -65,7 +65,7 @@ interface IStorage {
view
returns (uint256);
function activePoolsThisEpoch()
function poolsToFinalizeThisEpoch()
external
view
returns (bytes32[] memory);

View File

@@ -29,29 +29,27 @@ interface IStructs {
uint96 lastSetTimestamp;
}
/// @dev Status for a pool that actively traded during the current epoch.
/// (see MixinExchangeFees).
/// @dev Stats for a pool that actively traded.
/// @param feesCollected Fees collected in ETH by this pool.
/// @param weightedStake Amount of weighted stake in the pool.
/// @param membersStake Amount of non-operator stake in the pool.
struct ActivePool {
struct PoolStats {
uint256 feesCollected;
uint256 weightedStake;
uint256 membersStake;
}
/// @dev Holds state for unfinalized epoch rewards.
/// @dev Holds stats aggregated across a set of pools.
/// @param rewardsAvailable Rewards (ETH) available to the epoch
/// being finalized (the previous epoch). This is simply the balance
/// of the contract at the end of the epoch.
/// @param poolsRemaining The number of active pools in the last
/// epoch that have yet to be finalized through `finalizePools()`.
/// @param poolsToFinalize The number of pools that have yet to be finalized through `finalizePools()`.
/// @param totalFeesCollected The total fees collected for the epoch being finalized.
/// @param totalWeightedStake The total fees collected for the epoch being finalized.
/// @param totalRewardsFinalized Amount of rewards that have been paid during finalization.
struct UnfinalizedState {
struct AggregatedStats {
uint256 rewardsAvailable;
uint256 poolsRemaining;
uint256 poolsToFinalize;
uint256 totalFeesCollected;
uint256 totalWeightedStake;
uint256 totalRewardsFinalized;

View File

@@ -38,56 +38,54 @@ contract MixinFinalizer is
/// If there were no active pools in the closing epoch, the epoch
/// will be instantly finalized here. Otherwise, `finalizePool()`
/// should be called on each active pool afterwards.
/// @return poolsRemaining The number of unfinalized pools.
/// @return poolsToFinalize The number of unfinalized pools.
function endEpoch()
external
returns (uint256 poolsRemaining)
returns (uint256)
{
uint256 closingEpoch = currentEpoch;
IStructs.UnfinalizedState memory state = unfinalizedState;
uint256 prevEpoch = closingEpoch.safeSub(1);
// Make sure the previous epoch has been fully finalized.
if (state.poolsRemaining != 0) {
uint256 poolsToFinalizeFromPrevEpoch = aggregatedStatsByEpoch[prevEpoch].poolsToFinalize;
if (poolsToFinalizeFromPrevEpoch != 0) {
LibRichErrors.rrevert(
LibStakingRichErrors.PreviousEpochNotFinalizedError(
closingEpoch.safeSub(1),
state.poolsRemaining
prevEpoch,
poolsToFinalizeFromPrevEpoch
)
);
}
// Convert all ETH to WETH
// Since it is finalized, we no longer need stats for the previous epoch.
delete aggregatedStatsByEpoch[prevEpoch];
// Convert all ETH to WETH; the WETH balance of this contract is the total rewards.
_wrapEth();
// Set up unfinalized state.
state.rewardsAvailable = _getAvailableWethBalance();
state.poolsRemaining = poolsRemaining = numActivePoolsThisEpoch;
state.totalFeesCollected = totalFeesCollectedThisEpoch;
state.totalWeightedStake = totalWeightedStakeThisEpoch;
state.totalRewardsFinalized = 0;
unfinalizedState = state;
// Load aggregated stats for the epoch we're ending.
IStructs.AggregatedStats memory aggregatedStats = aggregatedStatsByEpoch[closingEpoch];
aggregatedStatsByEpoch[closingEpoch].rewardsAvailable =
aggregatedStats.rewardsAvailable = _getAvailableWethBalance();
// Emit an event.
emit EpochEnded(
closingEpoch,
state.poolsRemaining,
state.rewardsAvailable,
state.totalFeesCollected,
state.totalWeightedStake
aggregatedStats.poolsToFinalize,
aggregatedStats.rewardsAvailable,
aggregatedStats.totalFeesCollected,
aggregatedStats.totalWeightedStake
);
// Reset current epoch state.
totalFeesCollectedThisEpoch = 0;
totalWeightedStakeThisEpoch = 0;
numActivePoolsThisEpoch = 0;
// Advance the epoch. This will revert if not enough time has passed.
_goToNextEpoch();
// If there were no active pools, the epoch is already finalized.
if (poolsRemaining == 0) {
emit EpochFinalized(closingEpoch, 0, state.rewardsAvailable);
if (aggregatedStats.poolsToFinalize == 0) {
emit EpochFinalized(closingEpoch, 0, aggregatedStats.rewardsAvailable);
}
return aggregatedStats.poolsToFinalize;
}
/// @dev Instantly finalizes a single pool that was active in the previous
@@ -99,29 +97,28 @@ contract MixinFinalizer is
function finalizePool(bytes32 poolId)
external
{
// Load the finalization and pool state into memory.
IStructs.UnfinalizedState memory state = unfinalizedState;
// Noop if all active pools have been finalized.
if (state.poolsRemaining == 0) {
return;
}
// Compute relevant epochs
uint256 currentEpoch_ = currentEpoch;
uint256 prevEpoch = currentEpoch_.safeSub(1);
IStructs.ActivePool memory pool = _getActivePoolFromEpoch(prevEpoch, poolId);
// Noop if the pool was not active or already finalized (has no fees).
if (pool.feesCollected == 0) {
// Load the aggregated stats into memory; noop if no pools to finalize.
IStructs.AggregatedStats memory aggregatedStats = aggregatedStatsByEpoch[prevEpoch];
if (aggregatedStats.poolsToFinalize == 0) {
return;
}
// Clear the pool state so we don't finalize it again, and to recoup
// Noop if the pool was not active or already finalized (has no fees).
IStructs.PoolStats memory poolStats = poolStatsByEpoch[poolId][prevEpoch];
if (poolStats.feesCollected == 0) {
return;
}
// Clear the pool stats so we don't finalize it again, and to recoup
// some gas.
delete _getActivePoolsFromEpoch(prevEpoch)[poolId];
delete poolStatsByEpoch[poolId][prevEpoch];
// Compute the rewards.
uint256 rewards = _getUnfinalizedPoolRewardsFromState(pool, state);
uint256 rewards = _getUnfinalizedPoolRewardsFromPoolStats(poolStats, aggregatedStats);
// Pay the operator and update rewards for the pool.
// Note that we credit at the CURRENT epoch even though these rewards
@@ -129,7 +126,7 @@ contract MixinFinalizer is
(uint256 operatorReward, uint256 membersReward) = _syncPoolRewards(
poolId,
rewards,
pool.membersStake
poolStats.membersStake
);
// Emit an event.
@@ -143,22 +140,22 @@ contract MixinFinalizer is
uint256 totalReward = operatorReward.safeAdd(membersReward);
// Increase `totalRewardsFinalized`.
unfinalizedState.totalRewardsFinalized =
state.totalRewardsFinalized =
state.totalRewardsFinalized.safeAdd(totalReward);
aggregatedStatsByEpoch[prevEpoch].totalRewardsFinalized =
aggregatedStats.totalRewardsFinalized =
aggregatedStats.totalRewardsFinalized.safeAdd(totalReward);
// Decrease the number of unfinalized pools left.
unfinalizedState.poolsRemaining =
state.poolsRemaining =
state.poolsRemaining.safeSub(1);
aggregatedStatsByEpoch[prevEpoch].poolsToFinalize =
aggregatedStats.poolsToFinalize =
aggregatedStats.poolsToFinalize.safeSub(1);
// If there are no more unfinalized pools remaining, the epoch is
// finalized.
if (state.poolsRemaining == 0) {
if (aggregatedStats.poolsToFinalize == 0) {
emit EpochFinalized(
prevEpoch,
state.totalRewardsFinalized,
state.rewardsAvailable.safeSub(state.totalRewardsFinalized)
aggregatedStats.totalRewardsFinalized,
aggregatedStats.rewardsAvailable.safeSub(aggregatedStats.totalRewardsFinalized)
);
}
}
@@ -177,44 +174,10 @@ contract MixinFinalizer is
uint256 membersStake
)
{
IStructs.ActivePool memory pool = _getActivePoolFromEpoch(
currentEpoch.safeSub(1),
poolId
);
reward = _getUnfinalizedPoolRewardsFromState(pool, unfinalizedState);
membersStake = pool.membersStake;
}
/// @dev Get an active pool from an epoch by its ID.
/// @param epoch The epoch the pool was/will be active in.
/// @param poolId The ID of the pool.
/// @return pool The pool with ID `poolId` that was active in `epoch`.
function _getActivePoolFromEpoch(
uint256 epoch,
bytes32 poolId
)
internal
view
returns (IStructs.ActivePool memory pool)
{
pool = _getActivePoolsFromEpoch(epoch)[poolId];
return pool;
}
/// @dev Get a mapping of active pools from an epoch.
/// This uses the formula `epoch % 2` as the epoch index in order
/// to reuse state, because we only need to remember, at most, two
/// epochs at once.
/// @return activePools The pools that were active in `epoch`.
function _getActivePoolsFromEpoch(
uint256 epoch
)
internal
view
returns (mapping (bytes32 => IStructs.ActivePool) storage activePools)
{
activePools = _activePoolsByEpoch[epoch % 2];
return activePools;
uint256 prevEpoch = currentEpoch.safeSub(1);
IStructs.PoolStats memory poolStats = poolStatsByEpoch[poolId][prevEpoch];
reward = _getUnfinalizedPoolRewardsFromPoolStats(poolStats, aggregatedStatsByEpoch[prevEpoch]);
membersStake = poolStats.membersStake;
}
/// @dev Converts the entire ETH balance of this contract into WETH.
@@ -247,10 +210,10 @@ contract MixinFinalizer is
view
{
uint256 prevEpoch = currentEpoch.safeSub(1);
IStructs.ActivePool memory pool = _getActivePoolFromEpoch(prevEpoch, poolId);
IStructs.PoolStats memory poolStats = poolStatsByEpoch[poolId][prevEpoch];
// A pool that has any fees remaining has not been finalized
if (pool.feesCollected != 0) {
if (poolStats.feesCollected != 0) {
LibRichErrors.rrevert(
LibStakingRichErrors.PoolNotFinalizedError(
poolId,
@@ -261,12 +224,12 @@ contract MixinFinalizer is
}
/// @dev Computes the reward owed to a pool during finalization.
/// @param pool The active pool.
/// @param state The current state of finalization.
/// @return rewards Unfinalized rewards for this pool.
function _getUnfinalizedPoolRewardsFromState(
IStructs.ActivePool memory pool,
IStructs.UnfinalizedState memory state
/// @param poolStats Stats for a specific pool.
/// @param aggregatedStats Stats aggregated across all pools.
/// @return rewards Unfinalized rewards for the input pool.
function _getUnfinalizedPoolRewardsFromPoolStats(
IStructs.PoolStats memory poolStats,
IStructs.AggregatedStats memory aggregatedStats
)
private
view
@@ -274,17 +237,17 @@ contract MixinFinalizer is
{
// There can't be any rewards if the pool was active or if it has
// no stake.
if (pool.feesCollected == 0) {
if (poolStats.feesCollected == 0) {
return rewards;
}
// Use the cobb-douglas function to compute the total reward.
rewards = LibCobbDouglas.cobbDouglas(
state.rewardsAvailable,
pool.feesCollected,
state.totalFeesCollected,
pool.weightedStake,
state.totalWeightedStake,
aggregatedStats.rewardsAvailable,
poolStats.feesCollected,
aggregatedStats.totalFeesCollected,
poolStats.weightedStake,
aggregatedStats.totalWeightedStake,
cobbDouglasAlphaNumerator,
cobbDouglasAlphaDenominator
);
@@ -292,7 +255,7 @@ contract MixinFinalizer is
// Clip the reward to always be under
// `rewardsAvailable - totalRewardsPaid`,
// in case cobb-douglas overflows, which should be unlikely.
uint256 rewardsRemaining = state.rewardsAvailable.safeSub(state.totalRewardsFinalized);
uint256 rewardsRemaining = aggregatedStats.rewardsAvailable.safeSub(aggregatedStats.totalRewardsFinalized);
if (rewardsRemaining < rewards) {
rewards = rewardsRemaining;
}

View File

@@ -74,20 +74,19 @@ contract TestFinalizer is
external
{
require(feesCollected > 0, "FEES_MUST_BE_NONZERO");
mapping (bytes32 => IStructs.ActivePool) storage activePools = _getActivePoolsFromEpoch(
currentEpoch
);
IStructs.ActivePool memory pool = activePools[poolId];
require(pool.feesCollected == 0, "POOL_ALREADY_ADDED");
uint256 currentEpoch_ = currentEpoch;
IStructs.PoolStats memory poolStats = poolStatsByEpoch[poolId][currentEpoch_];
require(poolStats.feesCollected == 0, "POOL_ALREADY_ADDED");
_operatorSharesByPool[poolId] = operatorShare;
activePools[poolId] = IStructs.ActivePool({
poolStatsByEpoch[poolId][currentEpoch_] = IStructs.PoolStats({
feesCollected: feesCollected,
membersStake: membersStake,
weightedStake: weightedStake
});
totalFeesCollectedThisEpoch += feesCollected;
totalWeightedStakeThisEpoch += weightedStake;
numActivePoolsThisEpoch += 1;
aggregatedStatsByEpoch[currentEpoch_].totalFeesCollected += feesCollected;
aggregatedStatsByEpoch[currentEpoch_].totalWeightedStake += weightedStake;
aggregatedStatsByEpoch[currentEpoch_].poolsToFinalize += 1;
}
/// @dev Drain the balance of this contract.
@@ -131,13 +130,21 @@ contract TestFinalizer is
);
}
/// @dev Expose `_getActivePoolFromEpoch`.
function getActivePoolFromEpoch(uint256 epoch, bytes32 poolId)
/// @dev Expose pool stats for the input epoch.
function getPoolStatsFromEpoch(uint256 epoch, bytes32 poolId)
external
view
returns (IStructs.ActivePool memory pool)
returns (IStructs.PoolStats memory)
{
pool = _getActivePoolFromEpoch(epoch, poolId);
return poolStatsByEpoch[poolId][epoch];
}
function getAggregatedStatsForPreviousEpoch()
external
view
returns (IStructs.AggregatedStats memory)
{
return aggregatedStatsByEpoch[currentEpoch - 1];
}
/// @dev Overridden to log and transfer to receivers.

View File

@@ -86,6 +86,14 @@ contract TestProtocolFees is
return true;
}
function getAggregatedStatsForCurrentEpoch()
external
view
returns (IStructs.AggregatedStats memory)
{
return aggregatedStatsByEpoch[currentEpoch];
}
/// @dev Overridden to use test pools.
function getStakingPoolIdOfMaker(address makerAddress)
public

View File

@@ -275,45 +275,21 @@ contract TestStorageLayoutAndConstants is
offset := 0x0
assertSlotAndOffset(
totalFeesCollectedThisEpoch_slot,
totalFeesCollectedThisEpoch_offset,
poolStatsByEpoch_slot,
poolStatsByEpoch_offset,
slot,
offset
)
slot := add(slot, 0x1)
assertSlotAndOffset(
totalWeightedStakeThisEpoch_slot,
totalWeightedStakeThisEpoch_offset,
aggregatedStatsByEpoch_slot,
aggregatedStatsByEpoch_offset,
slot,
offset
)
slot := add(slot, 0x1)
assertSlotAndOffset(
_activePoolsByEpoch_slot,
_activePoolsByEpoch_offset,
slot,
offset
)
slot := add(slot, 0x1)
assertSlotAndOffset(
numActivePoolsThisEpoch_slot,
numActivePoolsThisEpoch_offset,
slot,
offset
)
slot := add(slot, 0x1)
assertSlotAndOffset(
unfinalizedState_slot,
unfinalizedState_offset,
slot,
offset
)
slot := add(slot, 0x5)
assertSlotAndOffset(
wethReservedForPoolRewards_slot,
wethReservedForPoolRewards_offset,

View File

@@ -238,7 +238,7 @@ export class FinalizerActor extends BaseActor {
private async _getRewardByPoolIdAsync(poolIds: string[]): Promise<RewardByPoolId> {
const activePools = await Promise.all(
poolIds.map(async poolId =>
this._stakingApiWrapper.stakingContract.getActiveStakingPoolThisEpoch.callAsync(poolId),
this._stakingApiWrapper.stakingContract.getStakingPoolStatsThisEpoch.callAsync(poolId),
),
);
const totalRewards = await this._stakingApiWrapper.utils.getAvailableRewardsBalanceAsync();

View File

@@ -87,21 +87,14 @@ blockchainTests.resets('Finalizer unit tests', env => {
interface UnfinalizedState {
rewardsAvailable: Numberish;
poolsRemaining: number;
poolsToFinalize: Numberish;
totalFeesCollected: Numberish;
totalWeightedStake: Numberish;
totalRewardsFinalized: Numberish;
}
async function getUnfinalizedStateAsync(): Promise<UnfinalizedState> {
const r = await testContract.unfinalizedState.callAsync();
return {
rewardsAvailable: r[0],
poolsRemaining: r[1].toNumber(),
totalFeesCollected: r[2],
totalWeightedStake: r[3],
totalRewardsFinalized: r[4],
};
return testContract.getAggregatedStatsForPreviousEpoch.callAsync();
}
async function finalizePoolsAsync(poolIds: string[]): Promise<LogEntry[]> {
@@ -142,16 +135,16 @@ blockchainTests.resets('Finalizer unit tests', env => {
async function assertFinalizationLogsAndBalancesAsync(
rewardsAvailable: Numberish,
activePools: ActivePoolOpts[],
poolsToFinalize: ActivePoolOpts[],
finalizationLogs: LogEntry[],
): Promise<void> {
const currentEpoch = await getCurrentEpochAsync();
// Compute the expected rewards for each pool.
const poolsWithStake = activePools.filter(p => !new BigNumber(p.weightedStake).isZero());
const poolsWithStake = poolsToFinalize.filter(p => !new BigNumber(p.weightedStake).isZero());
const poolRewards = await calculatePoolRewardsAsync(rewardsAvailable, poolsWithStake);
const totalRewards = BigNumber.sum(...poolRewards);
const rewardsRemaining = new BigNumber(rewardsAvailable).minus(totalRewards);
const [totalOperatorRewards, totalMembersRewards] = getTotalSplitRewards(activePools, poolRewards);
const [totalOperatorRewards, totalMembersRewards] = getTotalSplitRewards(poolsToFinalize, poolRewards);
// Assert the `RewardsPaid` logs.
const rewardsPaidEvents = getRewardsPaidEvents(finalizationLogs);
@@ -203,13 +196,13 @@ blockchainTests.resets('Finalizer unit tests', env => {
async function calculatePoolRewardsAsync(
rewardsAvailable: Numberish,
activePools: ActivePoolOpts[],
poolsToFinalize: ActivePoolOpts[],
): Promise<BigNumber[]> {
const totalFees = BigNumber.sum(...activePools.map(p => p.feesCollected));
const totalStake = BigNumber.sum(...activePools.map(p => p.weightedStake));
const poolRewards = _.times(activePools.length, () => constants.ZERO_AMOUNT);
for (const i of _.times(activePools.length)) {
const pool = activePools[i];
const totalFees = BigNumber.sum(...poolsToFinalize.map(p => p.feesCollected));
const totalStake = BigNumber.sum(...poolsToFinalize.map(p => p.weightedStake));
const poolRewards = _.times(poolsToFinalize.length, () => constants.ZERO_AMOUNT);
for (const i of _.times(poolsToFinalize.length)) {
const pool = poolsToFinalize[i];
const feesCollected = new BigNumber(pool.feesCollected);
if (feesCollected.isZero()) {
continue;
@@ -307,32 +300,48 @@ blockchainTests.resets('Finalizer unit tests', env => {
expect(events).to.deep.eq([]);
});
it("clears the next epoch's finalization state", async () => {
// Add a pool so there is state to clear.
await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
const epoch = await testContract.currentEpoch.callAsync();
expect(epoch).to.bignumber.eq(stakingConstants.INITIAL_EPOCH.plus(1));
const numActivePools = await testContract.numActivePoolsThisEpoch.callAsync();
const totalFees = await testContract.totalFeesCollectedThisEpoch.callAsync();
const totalStake = await testContract.totalWeightedStakeThisEpoch.callAsync();
expect(numActivePools).to.bignumber.eq(0);
expect(totalFees).to.bignumber.eq(0);
expect(totalStake).to.bignumber.eq(0);
});
it('prepares unfinalized state', async () => {
// Add a pool so there is state to clear.
const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
return assertUnfinalizedStateAsync({
poolsRemaining: 1,
poolsToFinalize: 1,
rewardsAvailable: INITIAL_BALANCE,
totalFeesCollected: pool.feesCollected,
totalWeightedStake: pool.weightedStake,
});
});
it("correctly stores the epoch's aggregated stats after ending the epoch", async () => {
const pool = await addActivePoolAsync();
const epoch = await testContract.currentEpoch.callAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
const aggregatedStats = await testContract.aggregatedStatsByEpoch.callAsync(epoch);
expect(aggregatedStats).to.be.deep.equal([
INITIAL_BALANCE,
new BigNumber(1), // pools to finalize
pool.feesCollected,
pool.weightedStake,
new BigNumber(0), // rewards finalized
]);
});
it("correctly clear an epoch's aggregated stats after it is finalized", async () => {
const pool = await addActivePoolAsync();
const epoch = await testContract.currentEpoch.callAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId);
await testContract.endEpoch.awaitTransactionSuccessAsync();
const aggregatedStats = await testContract.aggregatedStatsByEpoch.callAsync(epoch);
expect(aggregatedStats).to.be.deep.equal([
new BigNumber(0),
new BigNumber(0),
new BigNumber(0),
new BigNumber(0),
new BigNumber(0),
]);
});
it('reverts if the prior epoch is unfinalized', async () => {
await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
@@ -382,7 +391,7 @@ blockchainTests.resets('Finalizer unit tests', env => {
const pool = _.sample(pools) as ActivePoolOpts;
await testContract.endEpoch.awaitTransactionSuccessAsync();
await finalizePoolsAsync([pool.poolId]);
const poolState = await testContract.getActivePoolFromEpoch.callAsync(
const poolState = await testContract.getPoolStatsFromEpoch.callAsync(
stakingConstants.INITIAL_EPOCH,
pool.poolId,
);

View File

@@ -15,7 +15,7 @@ import * as _ from 'lodash';
import {
artifacts,
IStakingEventsEvents,
IStakingEventsStakingPoolActivatedEventArgs,
IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs,
TestProtocolFeesContract,
TestProtocolFeesERC20ProxyTransferFromEventArgs,
TestProtocolFeesEvents,
@@ -152,7 +152,7 @@ blockchainTests('Protocol Fees unit tests', env => {
});
async function getProtocolFeesAsync(poolId: string): Promise<BigNumber> {
return (await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId)).feesCollected;
return (await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId)).feesCollected;
}
describe('ETH fees', () => {
@@ -369,21 +369,22 @@ blockchainTests('Protocol Fees unit tests', env => {
});
interface FinalizationState {
numActivePools: BigNumber;
poolsToFinalize: BigNumber;
totalFeesCollected: BigNumber;
totalWeightedStake: BigNumber;
}
async function getFinalizationStateAsync(): Promise<FinalizationState> {
const aggregatedStats = await testContract.getAggregatedStatsForCurrentEpoch.callAsync();
return {
numActivePools: await testContract.numActivePoolsThisEpoch.callAsync(),
totalFeesCollected: await testContract.totalFeesCollectedThisEpoch.callAsync(),
totalWeightedStake: await testContract.totalWeightedStakeThisEpoch.callAsync(),
poolsToFinalize: aggregatedStats.poolsToFinalize,
totalFeesCollected: aggregatedStats.totalFeesCollected,
totalWeightedStake: aggregatedStats.totalWeightedStake,
};
}
interface PayToMakerResult {
poolActivatedEvents: IStakingEventsStakingPoolActivatedEventArgs[];
poolEarnedRewardsEvents: IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs[];
fee: BigNumber;
}
@@ -395,13 +396,13 @@ blockchainTests('Protocol Fees unit tests', env => {
new BigNumber(_fee),
{ from: exchangeAddress, value: _fee },
);
const events = filterLogsToArguments<IStakingEventsStakingPoolActivatedEventArgs>(
const events = filterLogsToArguments<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
receipt.logs,
IStakingEventsEvents.StakingPoolActivated,
IStakingEventsEvents.StakingPoolEarnedRewardsInEpoch,
);
return {
fee: new BigNumber(_fee),
poolActivatedEvents: events,
poolEarnedRewardsEvents: events,
};
}
@@ -414,14 +415,14 @@ blockchainTests('Protocol Fees unit tests', env => {
it('no active pools to start', async () => {
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(0);
expect(state.poolsToFinalize).to.bignumber.eq(0);
expect(state.totalFeesCollected).to.bignumber.eq(0);
expect(state.totalWeightedStake).to.bignumber.eq(0);
});
it('pool is not registered to start', async () => {
const { poolId } = await createTestPoolAsync();
const pool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const pool = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
expect(pool.feesCollected).to.bignumber.eq(0);
expect(pool.membersStake).to.bignumber.eq(0);
expect(pool.weightedStake).to.bignumber.eq(0);
@@ -433,16 +434,16 @@ blockchainTests('Protocol Fees unit tests', env => {
poolId,
makers: [poolMaker],
} = pool;
const { fee, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents.length).to.eq(1);
expect(poolActivatedEvents[0].poolId).to.eq(poolId);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const { fee, poolEarnedRewardsEvents } = await payToMakerAsync(poolMaker);
expect(poolEarnedRewardsEvents.length).to.eq(1);
expect(poolEarnedRewardsEvents[0].poolId).to.eq(poolId);
const actualPoolStats = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
expect(actualPool.feesCollected).to.bignumber.eq(fee);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
expect(actualPoolStats.feesCollected).to.bignumber.eq(fee);
expect(actualPoolStats.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPoolStats.weightedStake).to.bignumber.eq(expectedWeightedStake);
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(1);
expect(state.poolsToFinalize).to.bignumber.eq(1);
expect(state.totalFeesCollected).to.bignumber.eq(fee);
expect(state.totalWeightedStake).to.bignumber.eq(expectedWeightedStake);
});
@@ -454,16 +455,16 @@ blockchainTests('Protocol Fees unit tests', env => {
makers: [poolMaker],
} = pool;
const { fee: fee1 } = await payToMakerAsync(poolMaker);
const { fee: fee2, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents).to.deep.eq([]);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const { fee: fee2, poolEarnedRewardsEvents } = await payToMakerAsync(poolMaker);
expect(poolEarnedRewardsEvents).to.deep.eq([]);
const actualPoolStats = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
const fees = BigNumber.sum(fee1, fee2);
expect(actualPool.feesCollected).to.bignumber.eq(fees);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
expect(actualPoolStats.feesCollected).to.bignumber.eq(fees);
expect(actualPoolStats.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPoolStats.weightedStake).to.bignumber.eq(expectedWeightedStake);
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(1);
expect(state.poolsToFinalize).to.bignumber.eq(1);
expect(state.totalFeesCollected).to.bignumber.eq(fees);
expect(state.totalWeightedStake).to.bignumber.eq(expectedWeightedStake);
});
@@ -477,19 +478,19 @@ blockchainTests('Protocol Fees unit tests', env => {
poolId,
makers: [poolMaker],
} = pool;
const { fee, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents.length).to.eq(1);
expect(poolActivatedEvents[0].poolId).to.eq(poolId);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const { fee, poolEarnedRewardsEvents } = await payToMakerAsync(poolMaker);
expect(poolEarnedRewardsEvents.length).to.eq(1);
expect(poolEarnedRewardsEvents[0].poolId).to.eq(poolId);
const actualPoolStats = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
expect(actualPool.feesCollected).to.bignumber.eq(fee);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
expect(actualPoolStats.feesCollected).to.bignumber.eq(fee);
expect(actualPoolStats.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPoolStats.weightedStake).to.bignumber.eq(expectedWeightedStake);
totalFees = totalFees.plus(fee);
totalWeightedStake = totalWeightedStake.plus(expectedWeightedStake);
}
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(pools.length);
expect(state.poolsToFinalize).to.bignumber.eq(pools.length);
expect(state.totalFeesCollected).to.bignumber.eq(totalFees);
expect(state.totalWeightedStake).to.bignumber.eq(totalWeightedStake);
});
@@ -502,10 +503,10 @@ blockchainTests('Protocol Fees unit tests', env => {
} = pool;
await payToMakerAsync(poolMaker);
await testContract.advanceEpoch.awaitTransactionSuccessAsync();
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
expect(actualPool.feesCollected).to.bignumber.eq(0);
expect(actualPool.membersStake).to.bignumber.eq(0);
expect(actualPool.weightedStake).to.bignumber.eq(0);
const actualPoolStats = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
expect(actualPoolStats.feesCollected).to.bignumber.eq(0);
expect(actualPoolStats.membersStake).to.bignumber.eq(0);
expect(actualPoolStats.weightedStake).to.bignumber.eq(0);
});
describe('Multiple makers', () => {

View File

@@ -9,7 +9,7 @@ import * as _ from 'lodash';
import {
artifacts,
IStakingEventsEpochEndedEventArgs,
IStakingEventsStakingPoolActivatedEventArgs,
IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs,
ReadOnlyProxyContract,
StakingProxyContract,
TestCobbDouglasContract,
@@ -76,13 +76,13 @@ export class StakingApiWrapper {
findActivePoolIdsAsync: async (epoch?: number): Promise<string[]> => {
const _epoch = epoch !== undefined ? epoch : await this.stakingContract.currentEpoch.callAsync();
const events = filterLogsToArguments<IStakingEventsStakingPoolActivatedEventArgs>(
const events = filterLogsToArguments<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
await this.stakingContract.getLogsAsync(
TestStakingEvents.StakingPoolActivated,
TestStakingEvents.StakingPoolEarnedRewardsInEpoch,
{ fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest },
{ epoch: new BigNumber(_epoch) },
),
TestStakingEvents.StakingPoolActivated,
TestStakingEvents.StakingPoolEarnedRewardsInEpoch,
);
return events.map(e => e.poolId);
},