@0x/contracts-test-utils: Finish off finalizer tests... for now.

This commit is contained in:
Lawrence Forman
2019-09-14 19:35:49 -04:00
committed by Lawrence Forman
parent b4929df1e6
commit da0f6b5e8f
6 changed files with 387 additions and 264 deletions

View File

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

View File

@@ -1,30 +1,30 @@
{
"artifactsDir": "./generated-artifacts",
"contractsDir": "./contracts",
"useDockerisedSolc": false,
"isOfflineMode": false,
"compilerSettings": {
"evmVersion": "constantinople",
"optimizer": {
"enabled": true,
"runs": 1000000,
"details": {
"yul": true,
"deduplicate": true,
"cse": true,
"constantOptimizer": true
}
},
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
"artifactsDir": "./generated-artifacts",
"contractsDir": "./contracts",
"useDockerisedSolc": false,
"isOfflineMode": false,
"compilerSettings": {
"evmVersion": "constantinople",
"optimizer": {
"enabled": true,
"runs": 1000000,
"details": {
"yul": true,
"deduplicate": true,
"cse": true,
"constantOptimizer": true
}
},
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
}
}
}
}

View File

@@ -154,12 +154,8 @@ contract MixinFinalizer is
continue;
}
// Clear the pool state so we don't finalize it again, and to recoup
// some gas.
delete activePools[poolId];
IStructs.PoolRewards memory poolRewards =
_finalizePool(epoch, poolId, pool, true);
_creditRewardsToPool(epoch, poolId, pool);
rewardsPaid = rewardsPaid.safeAdd(
poolRewards.operatorReward + poolRewards.membersReward
@@ -194,6 +190,7 @@ contract MixinFinalizer is
/// epoch, crediting it rewards and sending those rewards to the reward
/// vault. This can be called by internal functions that need to
/// finalize a pool immediately. Does nothing if the pool is already
/// finalized. Does nothing if the pool was not active or was already
/// finalized.
/// @param poolId The pool ID to finalize.
/// @return rewards Rewards.
@@ -207,12 +204,37 @@ contract MixinFinalizer is
if (epoch == 0) {
return rewards;
}
rewards = _finalizePool(
epoch,
poolId,
_getActivePoolFromEpoch(epoch - 1, poolId),
false
);
IStructs.ActivePool memory pool =
_getActivePoolFromEpoch(epoch - 1, poolId);
// Do nothing if the pool was not active (has no fees).
if (pool.feesCollected == 0) {
return rewards;
}
rewards = _creditRewardsToPool(epoch, poolId, pool);
uint256 totalReward =
rewards.membersReward.safeAdd(rewards.operatorReward);
if (totalReward > 0) {
totalRewardsPaidLastEpoch =
totalRewardsPaidLastEpoch.safeAdd(totalReward);
_depositIntoStakingPoolRewardVault(totalReward);
}
// Decrease the number of unfinalized pools left.
uint256 poolsRemaining = unfinalizedPoolsRemaining;
unfinalizedPoolsRemaining = poolsRemaining = poolsRemaining.safeSub(1);
// If there are no more unfinalized pools remaining, the epoch is
// finalized.
if (poolsRemaining == 0) {
emit EpochFinalized(
epoch - 1,
totalRewardsPaidLastEpoch,
unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch)
);
}
}
/// @dev Computes the reward owed to a pool during finalization.
@@ -358,33 +380,24 @@ contract MixinFinalizer is
rewards.membersStake = pool.membersStake;
}
/// @dev Either fully or partially finalizes a single pool that was active
/// in the previous epoch. If `batchedMode` is `true`, this function
/// will NOT:
/// - transfer ether into the reward vault
/// - update `poolsRemaining`
/// - update `totalRewardsPaidLastEpoch`
/// - clear the pool from `activePoolsByEpoch`
/// - emit an `EpochFinalized` event.
/// @dev Credits finalization rewards to a pool that was active in the
/// last epoch.
/// @param epoch The current epoch.
/// @param poolId The pool ID to finalize.
/// @param pool The active pool to finalize.
/// @param batchedMode Only calculate and credit rewards.
/// @return rewards Rewards.
/// @return rewards The rewards credited to the pool.
function _finalizePool(
function _creditRewardsToPool(
uint256 epoch,
bytes32 poolId,
IStructs.ActivePool memory pool,
bool batchedMode
IStructs.ActivePool memory pool
)
private
returns (IStructs.PoolRewards memory rewards)
{
// Ignore pools that weren't active.
if (pool.feesCollected == 0) {
return rewards;
}
// Clear the pool state so we don't finalize it again, and to recoup
// some gas.
delete _getActivePoolsFromEpoch(epoch - 1)[poolId];
// Compute the rewards.
rewards = _getUnfinalizedPoolRewards(poolId, pool);
@@ -415,33 +428,5 @@ contract MixinFinalizer is
rewards.operatorReward,
rewards.membersReward
);
if (batchedMode) {
return rewards;
}
// Clear the pool state so we don't finalize it again, and to recoup
// some gas.
delete _getActivePoolsFromEpoch(epoch)[poolId];
if (totalReward > 0) {
totalRewardsPaidLastEpoch =
totalRewardsPaidLastEpoch.safeAdd(totalReward);
_depositIntoStakingPoolRewardVault(totalReward);
}
// Decrease the number of unfinalized pools left.
uint256 poolsRemaining = unfinalizedPoolsRemaining;
unfinalizedPoolsRemaining = poolsRemaining = poolsRemaining.safeSub(1);
// If there are no more unfinalized pools remaining, the epoch is
// finalized.
if (poolsRemaining == 0) {
emit EpochFinalized(
epoch - 1,
totalRewardsPaidLastEpoch,
unfinalizedRewardsAvailable.safeSub(totalRewardsPaidLastEpoch)
);
}
}
}

View File

@@ -43,9 +43,13 @@ contract TestFinalizer is
uint256 amount
);
mapping (bytes32 => uint32) internal _operatorSharesByPool;
address payable private _rewardReceiver;
mapping (bytes32 => uint32) private _operatorSharesByPool;
constructor() public {
/// @param rewardReceiver The address to transfer rewards into when
/// a pool is finalized.
constructor(address payable rewardReceiver) public {
_rewardReceiver = rewardReceiver;
init();
}
@@ -91,7 +95,8 @@ contract TestFinalizer is
require(feesCollected > 0, "FEES_MUST_BE_NONZERO");
mapping (bytes32 => IStructs.ActivePool) storage activePools =
_getActivePoolsFromEpoch(currentEpoch);
require(feesCollected > 0, "POOL_ALREADY_ADDED");
IStructs.ActivePool memory pool = activePools[poolId];
require(pool.feesCollected == 0, "POOL_ALREADY_ADDED");
_operatorSharesByPool[poolId] = operatorShare;
activePools[poolId] = IStructs.ActivePool({
feesCollected: feesCollected,
@@ -172,6 +177,7 @@ contract TestFinalizer is
/// @dev Overridden to store inputs and do some really basic math.
function _depositIntoStakingPoolRewardVault(uint256 amount) internal {
emit DepositIntoStakingPoolRewardVaultCall(amount);
_rewardReceiver.transfer(amount);
}
/// @dev Overridden to store inputs and do some really basic math.

View File

@@ -23,20 +23,23 @@ import {
} from '../../src';
import { getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
blockchainTests.resets.only('finalization tests', env => {
const { ONE_ETHER, ZERO_AMOUNT } = constants;
blockchainTests.resets('finalizer tests', env => {
const { ZERO_AMOUNT } = constants;
const INITIAL_EPOCH = 0;
const INITIAL_BALANCE = toBaseUnitAmount(32);
let senderAddress: string;
let rewardReceiverAddress: string;
let testContract: TestFinalizerContract;
before(async () => {
[senderAddress] = await env.getAccountAddressesAsync();
rewardReceiverAddress = hexRandom(constants.ADDRESS_LENGTH);
testContract = await TestFinalizerContract.deployFrom0xArtifactAsync(
artifacts.TestFinalizer,
env.provider,
env.txDefaults,
artifacts,
rewardReceiverAddress,
);
// Give the contract a balance.
await sendEtherAsync(testContract.address, INITIAL_BALANCE);
@@ -44,7 +47,7 @@ blockchainTests.resets.only('finalization tests', env => {
async function sendEtherAsync(to: string, amount: Numberish): Promise<void> {
await env.web3Wrapper.awaitTransactionSuccessAsync(
await env.web3Wrapper.sendTransactionAsync({
await env.web3Wrapper.sendTransactionAsync({
from: senderAddress,
to,
value: new BigNumber(amount),
@@ -61,22 +64,23 @@ blockchainTests.resets.only('finalization tests', env => {
}
async function addActivePoolAsync(opts?: Partial<ActivePoolOpts>): Promise<ActivePoolOpts> {
const _opts = {
poolId: hexRandom(),
operatorShare: Math.floor(Math.random() * constants.PPM_DENOMINATOR) / constants.PPM_DENOMINATOR,
feesCollected: getRandomInteger(0, ONE_ETHER),
membersStake: getRandomInteger(0, ONE_ETHER),
weightedStake: getRandomInteger(0, ONE_ETHER),
...opts,
};
await testContract.addActivePool.awaitTransactionSuccessAsync(
_opts.poolId,
const maxAmount = toBaseUnitAmount(1e9);
const _opts = {
poolId: hexRandom(),
operatorShare: Math.floor(Math.random() * constants.PPM_DENOMINATOR) / constants.PPM_DENOMINATOR,
feesCollected: getRandomInteger(0, maxAmount),
membersStake: getRandomInteger(0, maxAmount),
weightedStake: getRandomInteger(0, maxAmount),
...opts,
};
await testContract.addActivePool.awaitTransactionSuccessAsync(
_opts.poolId,
new BigNumber(_opts.operatorShare * constants.PPM_DENOMINATOR).integerValue(),
new BigNumber(_opts.feesCollected),
new BigNumber(_opts.membersStake),
new BigNumber(_opts.weightedStake),
);
return _opts;
);
return _opts;
}
interface FinalizationState {
@@ -108,9 +112,7 @@ blockchainTests.resets.only('finalization tests', env => {
};
}
async function assertFinalizationStateAsync(
expected: Partial<FinalizationState>,
): Promise<void> {
async function assertFinalizationStateAsync(expected: Partial<FinalizationState>): Promise<void> {
const actual = await getFinalizationStateAsync();
if (expected.balance !== undefined) {
expect(actual.balance).to.bignumber.eq(expected.balance);
@@ -122,43 +124,30 @@ blockchainTests.resets.only('finalization tests', env => {
expect(actual.closingEpoch).to.eq(expected.closingEpoch);
}
if (expected.numActivePoolsThisEpoch !== undefined) {
expect(actual.numActivePoolsThisEpoch)
.to.eq(expected.numActivePoolsThisEpoch);
expect(actual.numActivePoolsThisEpoch).to.eq(expected.numActivePoolsThisEpoch);
}
if (expected.totalFeesCollectedThisEpoch !== undefined) {
expect(actual.totalFeesCollectedThisEpoch)
.to.bignumber.eq(expected.totalFeesCollectedThisEpoch);
expect(actual.totalFeesCollectedThisEpoch).to.bignumber.eq(expected.totalFeesCollectedThisEpoch);
}
if (expected.totalWeightedStakeThisEpoch !== undefined) {
expect(actual.totalWeightedStakeThisEpoch)
.to.bignumber.eq(expected.totalWeightedStakeThisEpoch);
expect(actual.totalWeightedStakeThisEpoch).to.bignumber.eq(expected.totalWeightedStakeThisEpoch);
}
if (expected.unfinalizedPoolsRemaining !== undefined) {
expect(actual.unfinalizedPoolsRemaining)
.to.eq(expected.unfinalizedPoolsRemaining);
expect(actual.unfinalizedPoolsRemaining).to.eq(expected.unfinalizedPoolsRemaining);
}
if (expected.unfinalizedRewardsAvailable !== undefined) {
expect(actual.unfinalizedRewardsAvailable)
.to.bignumber.eq(expected.unfinalizedRewardsAvailable);
expect(actual.unfinalizedRewardsAvailable).to.bignumber.eq(expected.unfinalizedRewardsAvailable);
}
if (expected.unfinalizedTotalFeesCollected !== undefined) {
expect(actual.unfinalizedTotalFeesCollected)
.to.bignumber.eq(expected.unfinalizedTotalFeesCollected);
expect(actual.unfinalizedTotalFeesCollected).to.bignumber.eq(expected.unfinalizedTotalFeesCollected);
}
if (expected.unfinalizedTotalFeesCollected !== undefined) {
expect(actual.unfinalizedTotalFeesCollected)
.to.bignumber.eq(expected.unfinalizedTotalFeesCollected);
expect(actual.unfinalizedTotalFeesCollected).to.bignumber.eq(expected.unfinalizedTotalFeesCollected);
}
}
function assertEpochEndedEvent(
logs: LogEntry[],
args: Partial<IStakingEventsEpochEndedEventArgs>,
): void {
const events = filterLogsToArguments<IStakingEventsEpochEndedEventArgs>(
logs,
IStakingEventsEvents.EpochEnded,
);
function assertEpochEndedEvent(logs: LogEntry[], args: Partial<IStakingEventsEpochEndedEventArgs>): void {
const events = getEpochEndedEvents(logs);
expect(events.length).to.eq(1);
if (args.epoch !== undefined) {
expect(events[0].epoch).to.bignumber.eq(INITIAL_EPOCH);
@@ -177,10 +166,7 @@ blockchainTests.resets.only('finalization tests', env => {
}
}
function assertEpochFinalizedEvent(
logs: LogEntry[],
args: Partial<IStakingEventsEpochFinalizedEventArgs>,
): void {
function assertEpochFinalizedEvent(logs: LogEntry[], args: Partial<IStakingEventsEpochFinalizedEventArgs>): void {
const events = getEpochFinalizedEvents(logs);
expect(events.length).to.eq(1);
if (args.epoch !== undefined) {
@@ -194,10 +180,7 @@ blockchainTests.resets.only('finalization tests', env => {
}
}
function assertDepositIntoStakingPoolRewardVaultCallEvent(
logs: LogEntry[],
amount?: Numberish,
): void {
function assertDepositIntoStakingPoolRewardVaultCallEvent(logs: LogEntry[], amount?: Numberish): void {
const events = filterLogsToArguments<TestFinalizerDepositIntoStakingPoolRewardVaultCallEventArgs>(
logs,
TestFinalizerEvents.DepositIntoStakingPoolRewardVaultCall,
@@ -208,24 +191,31 @@ blockchainTests.resets.only('finalization tests', env => {
}
}
async function assertRewarReceiverBalanceAsync(expectedAmount: Numberish): Promise<void> {
const balance = await getBalanceOfAsync(rewardReceiverAddress);
expect(balance).to.be.bignumber.eq(expectedAmount);
}
function getEpochEndedEvents(logs: LogEntry[]): IStakingEventsEpochEndedEventArgs[] {
return filterLogsToArguments<IStakingEventsEpochEndedEventArgs>(logs, IStakingEventsEvents.EpochEnded);
}
function getEpochFinalizedEvents(logs: LogEntry[]): IStakingEventsEpochFinalizedEventArgs[] {
return filterLogsToArguments<IStakingEventsEpochFinalizedEventArgs>(
logs,
IStakingEventsEvents.EpochFinalized,
);
return filterLogsToArguments<IStakingEventsEpochFinalizedEventArgs>(logs, IStakingEventsEvents.EpochFinalized);
}
function getRewardsPaidEvents(logs: LogEntry[]): IStakingEventsRewardsPaidEventArgs[] {
return filterLogsToArguments<IStakingEventsRewardsPaidEventArgs>(
logs,
IStakingEventsEvents.RewardsPaid,
);
return filterLogsToArguments<IStakingEventsRewardsPaidEventArgs>(logs, IStakingEventsEvents.RewardsPaid);
}
async function getCurrentEpochAsync(): Promise<number> {
return (await testContract.getCurrentEpoch.callAsync()).toNumber();
}
async function getBalanceOfAsync(whom: string): Promise<BigNumber> {
return env.web3Wrapper.getBalanceInWeiAsync(whom);
}
describe('endEpoch()', () => {
it('advances the epoch', async () => {
await testContract.endEpoch.awaitTransactionSuccessAsync();
@@ -235,28 +225,22 @@ blockchainTests.resets.only('finalization tests', env => {
it('emits an `EpochEnded` event', async () => {
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
assertEpochEndedEvent(
receipt.logs,
{
epoch: new BigNumber(INITIAL_EPOCH),
numActivePools: ZERO_AMOUNT,
rewardsAvailable: INITIAL_BALANCE,
totalFeesCollected: ZERO_AMOUNT,
totalWeightedStake: ZERO_AMOUNT,
},
);
assertEpochEndedEvent(receipt.logs, {
epoch: new BigNumber(INITIAL_EPOCH),
numActivePools: ZERO_AMOUNT,
rewardsAvailable: INITIAL_BALANCE,
totalFeesCollected: ZERO_AMOUNT,
totalWeightedStake: ZERO_AMOUNT,
});
});
it('immediately finalizes if there are no active pools', async () => {
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
assertEpochFinalizedEvent(
receipt.logs,
{
epoch: new BigNumber(INITIAL_EPOCH),
rewardsPaid: ZERO_AMOUNT,
rewardsRemaining: INITIAL_BALANCE,
},
);
assertEpochFinalizedEvent(receipt.logs, {
epoch: new BigNumber(INITIAL_EPOCH),
rewardsPaid: ZERO_AMOUNT,
rewardsRemaining: INITIAL_BALANCE,
});
});
it('does not immediately finalize if there is an active pool', async () => {
@@ -269,7 +253,7 @@ blockchainTests.resets.only('finalization tests', env => {
expect(events).to.deep.eq([]);
});
it('clears the next epoch\'s finalization state', async () => {
it("clears the next epoch's finalization state", async () => {
// Add a pool so there is state to clear.
await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
@@ -298,10 +282,7 @@ blockchainTests.resets.only('finalization tests', env => {
await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
const tx = testContract.endEpoch.awaitTransactionSuccessAsync();
const expectedError = new StakingRevertErrors.PreviousEpochNotFinalizedError(
INITIAL_EPOCH,
1,
);
const expectedError = new StakingRevertErrors.PreviousEpochNotFinalizedError(INITIAL_EPOCH, 1);
return expect(tx).to.revertWith(expectedError);
});
});
@@ -328,17 +309,13 @@ blockchainTests.resets.only('finalization tests', env => {
expect(rewardsPaidEvents.length).to.eq(1);
expect(rewardsPaidEvents[0].epoch).to.bignumber.eq(INITIAL_EPOCH + 1);
expect(rewardsPaidEvents[0].poolId).to.eq(pool.poolId);
assertEpochFinalizedEvent(
receipt.logs,
{
epoch: new BigNumber(INITIAL_EPOCH),
rewardsPaid: INITIAL_BALANCE,
},
);
assertDepositIntoStakingPoolRewardVaultCallEvent(
receipt.logs,
INITIAL_BALANCE,
);
assertEpochFinalizedEvent(receipt.logs, {
epoch: new BigNumber(INITIAL_EPOCH),
rewardsPaid: INITIAL_BALANCE,
});
assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs, INITIAL_BALANCE);
const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0];
await assertRewarReceiverBalanceAsync(totalRewardsPaid);
});
it('can finalize multiple pools', async () => {
@@ -349,16 +326,37 @@ blockchainTests.resets.only('finalization tests', env => {
const receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs);
expect(rewardsPaidEvents.length).to.eq(pools.length);
for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as
Array<[ActivePoolOpts, IStakingEventsRewardsPaidEventArgs]>) {
for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array<
[ActivePoolOpts, IStakingEventsRewardsPaidEventArgs]
>) {
expect(event.epoch).to.bignumber.eq(nextEpoch);
expect(event.poolId).to.eq(pool.poolId);
}
assertEpochFinalizedEvent(
receipt.logs,
{ epoch: new BigNumber(INITIAL_EPOCH) },
);
assertEpochFinalizedEvent(receipt.logs, { epoch: new BigNumber(INITIAL_EPOCH) });
assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs);
const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0];
await assertRewarReceiverBalanceAsync(totalRewardsPaid);
});
it('can finalize multiple pools over multiple transactions', async () => {
const nextEpoch = INITIAL_EPOCH + 1;
const pools = await Promise.all(_.times(2, () => 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));
const rewardsPaidEvents = getRewardsPaidEvents(allLogs);
expect(rewardsPaidEvents.length).to.eq(pools.length);
for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array<
[ActivePoolOpts, IStakingEventsRewardsPaidEventArgs]
>) {
expect(event.epoch).to.bignumber.eq(nextEpoch);
expect(event.poolId).to.eq(pool.poolId);
}
assertEpochFinalizedEvent(allLogs, { epoch: new BigNumber(INITIAL_EPOCH) });
const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(allLogs)[0];
await assertRewarReceiverBalanceAsync(totalRewardsPaid);
});
it('ignores a non-active pool', async () => {
@@ -393,13 +391,180 @@ blockchainTests.resets.only('finalization tests', env => {
const pool = _.sample(pools) as ActivePoolOpts;
await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]);
const poolState = await testContract
.internalGetActivePoolFromEpoch
.callAsync(new BigNumber(INITIAL_EPOCH), pool.poolId);
const poolState = await testContract.internalGetActivePoolFromEpoch.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, () => 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, () => 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, () => 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()', () => {
it('does nothing if there were no active pools', async () => {
await testContract.endEpoch.awaitTransactionSuccessAsync();
const poolId = hexRandom();
const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(poolId);
expect(receipt.logs).to.deep.eq([]);
});
it('can finalize a pool', async () => {
const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs);
expect(rewardsPaidEvents.length).to.eq(1);
expect(rewardsPaidEvents[0].epoch).to.bignumber.eq(INITIAL_EPOCH + 1);
expect(rewardsPaidEvents[0].poolId).to.eq(pool.poolId);
assertEpochFinalizedEvent(receipt.logs, {
epoch: new BigNumber(INITIAL_EPOCH),
rewardsPaid: INITIAL_BALANCE,
});
assertDepositIntoStakingPoolRewardVaultCallEvent(receipt.logs, INITIAL_BALANCE);
const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(receipt.logs)[0];
await assertRewarReceiverBalanceAsync(totalRewardsPaid);
});
it('can finalize multiple pools over multiple transactions', async () => {
const nextEpoch = INITIAL_EPOCH + 1;
const pools = await Promise.all(_.times(2, () => addActivePoolAsync()));
await testContract.endEpoch.awaitTransactionSuccessAsync();
const receipts = await Promise.all(
pools.map(pool => testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId)),
);
const allLogs = _.flatten(receipts.map(r => r.logs));
const rewardsPaidEvents = getRewardsPaidEvents(allLogs);
expect(rewardsPaidEvents.length).to.eq(pools.length);
for (const [pool, event] of _.zip(pools, rewardsPaidEvents) as Array<
[ActivePoolOpts, IStakingEventsRewardsPaidEventArgs]
>) {
expect(event.epoch).to.bignumber.eq(nextEpoch);
expect(event.poolId).to.eq(pool.poolId);
}
assertEpochFinalizedEvent(allLogs, { epoch: new BigNumber(INITIAL_EPOCH) });
const { rewardsPaid: totalRewardsPaid } = getEpochFinalizedEvents(allLogs)[0];
await assertRewarReceiverBalanceAsync(totalRewardsPaid);
});
it('ignores a finalized pool', async () => {
const pools = await Promise.all(_.times(3, () => addActivePoolAsync()));
await testContract.endEpoch.awaitTransactionSuccessAsync();
const [finalizedPool] = _.sampleSize(pools, 1);
await testContract.internalFinalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId);
const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(finalizedPool.poolId);
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs);
expect(rewardsPaidEvents).to.deep.eq([]);
});
it('resets pool state after finalizing it', async () => {
const pools = await Promise.all(_.times(3, () => addActivePoolAsync()));
const pool = _.sample(pools) as ActivePoolOpts;
await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.internalFinalizePool.awaitTransactionSuccessAsync(pool.poolId);
const poolState = await testContract.internalGetActivePoolFromEpoch.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` <= `rewardsAvailable` <= contract balance at the end of the epoch', async () => {
const pools = await Promise.all(_.times(3, () => addActivePoolAsync()));
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE);
const receipts = await Promise.all(
pools.map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)),
);
const allLogs = _.flatten(receipts.map(r => r.logs));
const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[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 receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
const receipts = await Promise.all(
[pool1, pool2].map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)),
);
const allLogs = _.flatten(receipts.map(r => r.logs));
const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[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, () => addActivePoolAsync()));
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
const receipts = await Promise.all(
pools.map(p => testContract.internalFinalizePool.awaitTransactionSuccessAsync(p.poolId)),
);
const allLogs = _.flatten(receipts.map(r => r.logs));
const { rewardsPaid } = getEpochFinalizedEvents(allLogs)[0];
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
});
}
});
});
describe('lifecycle', () => {
@@ -435,6 +600,18 @@ blockchainTests.resets.only('finalization tests', env => {
const rewardsPaidEvents = getRewardsPaidEvents(receipt.logs);
expect(rewardsPaidEvents).to.deep.eq([]);
});
it('rolls over leftover rewards into th next epoch', async () => {
const poolIds = _.times(3, () => hexRandom());
await Promise.all(poolIds.map(id => addActivePoolAsync({ poolId: id })));
await testContract.endEpoch.awaitTransactionSuccessAsync();
let receipt = await testContract.finalizePools.awaitTransactionSuccessAsync(poolIds);
const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(receipt.logs)[0];
await Promise.all(poolIds.map(id => addActivePoolAsync({ poolId: id })));
receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards);
});
});
interface PoolRewards {
@@ -475,8 +652,7 @@ blockchainTests.resets.only('finalization tests', env => {
if (new BigNumber(pool.membersStake).isZero()) {
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_DOWN);
const membersShare = new BigNumber(totalReward).minus(operatorShare);
return [operatorShare, membersShare];
}
@@ -490,23 +666,20 @@ blockchainTests.resets.only('finalization tests', env => {
it('returns empty if epoch is 0', async () => {
const poolId = hexRandom();
const rewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(poolId);
const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(poolId);
assertPoolRewards(rewards, ZERO_REWARDS);
});
it('returns empty if pool was not active', async () => {
await testContract.endEpoch.awaitTransactionSuccessAsync();
const poolId = hexRandom();
const rewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(poolId);
const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(poolId);
assertPoolRewards(rewards, ZERO_REWARDS);
});
it('returns empty if pool is active only in the current epoch', async () => {
const pool = await addActivePoolAsync();
const rewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(rewards, ZERO_REWARDS);
});
@@ -514,8 +687,7 @@ blockchainTests.resets.only('finalization tests', env => {
const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]);
const rewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(rewards, ZERO_REWARDS);
});
@@ -524,8 +696,7 @@ blockchainTests.resets.only('finalization tests', env => {
const [pool] = _.sampleSize(pools, 1);
await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePools.awaitTransactionSuccessAsync([pool.poolId]);
const rewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
const rewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(rewards, ZERO_REWARDS);
});
@@ -533,18 +704,13 @@ blockchainTests.resets.only('finalization tests', env => {
const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
const expectedTotalRewards = INITIAL_BALANCE;
const [expectedOperatorReward, expectedMembersReward] =
splitRewards(pool, expectedTotalRewards);
const actualRewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(
actualRewards,
{
operatorReward: expectedOperatorReward,
membersReward: expectedMembersReward,
membersStake: pool.membersStake,
},
);
const [expectedOperatorReward, expectedMembersReward] = splitRewards(pool, expectedTotalRewards);
const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(actualRewards, {
operatorReward: expectedOperatorReward,
membersReward: expectedMembersReward,
membersStake: pool.membersStake,
});
});
it('computes one reward among multiple pools', async () => {
@@ -560,48 +726,35 @@ blockchainTests.resets.only('finalization tests', env => {
pool.weightedStake,
totalWeightedStake,
);
const [expectedOperatorReward, expectedMembersReward] =
splitRewards(pool, expectedTotalRewards);
const actualRewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(
actualRewards,
{
operatorReward: expectedOperatorReward,
membersReward: expectedMembersReward,
membersStake: pool.membersStake,
},
);
const [expectedOperatorReward, expectedMembersReward] = splitRewards(pool, expectedTotalRewards);
const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(actualRewards, {
operatorReward: expectedOperatorReward,
membersReward: expectedMembersReward,
membersStake: pool.membersStake,
});
});
it('computes a reward with 0% operatorShare', async () => {
const pool = await addActivePoolAsync({ operatorShare: 0 });
await testContract.endEpoch.awaitTransactionSuccessAsync();
const actualRewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(
actualRewards,
{
operatorReward: 0,
membersReward: INITIAL_BALANCE,
membersStake: pool.membersStake,
},
);
const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(actualRewards, {
operatorReward: 0,
membersReward: INITIAL_BALANCE,
membersStake: pool.membersStake,
});
});
it('computes a reward with 100% operatorShare', async () => {
const pool = await addActivePoolAsync({ operatorShare: 1 });
await testContract.endEpoch.awaitTransactionSuccessAsync();
const actualRewards = await testContract
.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(
actualRewards,
{
operatorReward: INITIAL_BALANCE,
membersReward: 0,
membersStake: pool.membersStake,
},
);
const actualRewards = await testContract.internalGetUnfinalizedPoolRewards.callAsync(pool.poolId);
assertPoolRewards(actualRewards, {
operatorReward: INITIAL_BALANCE,
membersReward: 0,
membersStake: pool.membersStake,
});
});
});
});

View File

@@ -198,11 +198,7 @@ blockchainTests.resets('LibProxy', env => {
describe('Combinatorial Tests', () => {
// Combinatorial Scenarios for `proxyCall()`.
function getCombinatorialTestDescription(params: [RevertRule, boolean, string, string]): string {
const REVERT_RULE_NAMES = [
'RevertOnError',
'AlwaysRevert',
'NeverRevert',
];
const REVERT_RULE_NAMES = ['RevertOnError', 'AlwaysRevert', 'NeverRevert'];
return [
`revertRule: ${REVERT_RULE_NAMES[params[0]]}`,
`ignoreIngressSelector: ${params[1]}`,
@@ -218,11 +214,7 @@ blockchainTests.resets('LibProxy', env => {
const scenarios = [
// revertRule
[
RevertRule.RevertOnError,
RevertRule.AlwaysRevert,
RevertRule.NeverRevert,
],
[RevertRule.RevertOnError, RevertRule.AlwaysRevert, RevertRule.NeverRevert],
// ignoreIngressSelector
[false, true],
// customEgressSelector
@@ -233,10 +225,7 @@ blockchainTests.resets('LibProxy', env => {
constructRandomFailureCalldata(),
],
// calldata
[
constructRandomFailureCalldata(),
constructRandomSuccessCalldata(),
],
[constructRandomFailureCalldata(), constructRandomSuccessCalldata()],
] as [RevertRule[], boolean[], string[], string[]];
for (const params of cartesianProduct(...scenarios).toArray()) {