Merge pull request #2312 from 0xProject/feat/contracts/staking/MixinStakingPoolRewards-unit-tests

MixinStakingPoolRewards unit tests
This commit is contained in:
Lawrence Forman 2019-11-04 17:19:03 -05:00 committed by GitHub
commit 47e050cbaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 783 additions and 1 deletions

View File

@ -0,0 +1,281 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "../src/interfaces/IStructs.sol";
import "./TestStakingNoWETH.sol";
contract TestMixinStakingPoolRewards is
TestStakingNoWETH
{
// solhint-disable no-simple-event-func-name
event UpdateCumulativeReward(
bytes32 poolId
);
event WithdrawAndSyncDelegatorRewards(
bytes32 poolId,
address delegator
);
struct UnfinalizedPoolReward {
uint256 reward;
uint256 membersStake;
}
constructor() public {
_addAuthorizedAddress(msg.sender);
init();
_removeAuthorizedAddressAtIndex(msg.sender, 0);
}
// Rewards returned by `_computeMemberRewardOverInterval()`, indexed
// by `_getMemberRewardOverIntervalHash()`.
mapping (bytes32 => uint256) private _memberRewardsOverInterval;
// Rewards returned by `_getUnfinalizedPoolRewards()`, indexed by pool ID.
mapping (bytes32 => UnfinalizedPoolReward) private _unfinalizedPoolRewards;
// Set pool `rewardsByPoolId`.
function setPoolRewards(
bytes32 poolId,
uint256 _rewardsByPoolId
)
external
{
rewardsByPoolId[poolId] = _rewardsByPoolId;
}
// Set `wethReservedForPoolRewards`.
function setWethReservedForPoolRewards(
uint256 _wethReservedForPoolRewards
)
external
{
wethReservedForPoolRewards = _wethReservedForPoolRewards;
}
// Set the rewards returned by a call to `_computeMemberRewardOverInterval()`.
function setMemberRewardsOverInterval(
bytes32 poolId,
uint256 memberStakeOverInterval,
uint256 beginEpoch,
uint256 endEpoch,
uint256 reward
)
external
{
bytes32 rewardHash = _getMemberRewardOverIntervalHash(
poolId,
memberStakeOverInterval,
beginEpoch,
endEpoch
);
_memberRewardsOverInterval[rewardHash] = reward;
}
// Set the rewards returned by `_getUnfinalizedPoolRewards()`.
function setUnfinalizedPoolRewards(
bytes32 poolId,
uint256 reward,
uint256 membersStake
)
external
{
_unfinalizedPoolRewards[poolId] = UnfinalizedPoolReward(
reward,
membersStake
);
}
// Set `currentEpoch`.
function setCurrentEpoch(uint256 epoch) external {
currentEpoch = epoch;
}
// Expose `_syncPoolRewards()` for testing.
function syncPoolRewards(
bytes32 poolId,
uint256 reward,
uint256 membersStake
)
external
returns (uint256 operatorReward, uint256 membersReward)
{
return _syncPoolRewards(poolId, reward, membersStake);
}
// Expose `_withdrawAndSyncDelegatorRewards()` for testing.
function withdrawAndSyncDelegatorRewards(
bytes32 poolId,
address member
)
external
{
return _withdrawAndSyncDelegatorRewards(
poolId,
member
);
}
// Expose `_computePoolRewardsSplit()` for testing.
function computePoolRewardsSplit(
uint32 operatorShare,
uint256 totalReward,
uint256 membersStake
)
external
pure
returns (uint256 operatorReward, uint256 membersReward)
{
return _computePoolRewardsSplit(
operatorShare,
totalReward,
membersStake
);
}
// Access `_delegatedStakeToPoolByOwner`
function delegatedStakeToPoolByOwner(address member, bytes32 poolId)
external
view
returns (IStructs.StoredBalance memory balance)
{
return _delegatedStakeToPoolByOwner[member][poolId];
}
// Set `_delegatedStakeToPoolByOwner`
function setDelegatedStakeToPoolByOwner(
address member,
bytes32 poolId,
IStructs.StoredBalance memory balance
)
public
{
_delegatedStakeToPoolByOwner[member][poolId] = balance;
}
// Set `_poolById`.
function setPool(
bytes32 poolId,
IStructs.Pool memory pool
)
public
{
_poolById[poolId] = pool;
}
// Overridden to emit an event.
function _withdrawAndSyncDelegatorRewards(
bytes32 poolId,
address member
)
internal
{
emit WithdrawAndSyncDelegatorRewards(poolId, member);
return MixinStakingPoolRewards._withdrawAndSyncDelegatorRewards(
poolId,
member
);
}
// Overridden to use `_memberRewardsOverInterval`
function _computeMemberRewardOverInterval(
bytes32 poolId,
uint256 memberStakeOverInterval,
uint256 beginEpoch,
uint256 endEpoch
)
internal
view
returns (uint256 reward)
{
bytes32 rewardHash = _getMemberRewardOverIntervalHash(
poolId,
memberStakeOverInterval,
beginEpoch,
endEpoch
);
return _memberRewardsOverInterval[rewardHash];
}
// Overridden to use `_unfinalizedPoolRewards`
function _getUnfinalizedPoolRewards(
bytes32 poolId
)
internal
view
returns (uint256 reward, uint256 membersStake)
{
(reward, membersStake) = (
_unfinalizedPoolRewards[poolId].reward,
_unfinalizedPoolRewards[poolId].membersStake
);
}
// Overridden to just increase `currentEpoch`.
function _loadCurrentBalance(IStructs.StoredBalance storage balancePtr)
internal
view
returns (IStructs.StoredBalance memory balance)
{
balance = balancePtr;
balance.currentEpoch += 1;
}
// Overridden to revert if a pool has unfinalized rewards.
function _assertPoolFinalizedLastEpoch(bytes32 poolId)
internal
view
{
require(
_unfinalizedPoolRewards[poolId].membersStake == 0,
"POOL_NOT_FINALIZED"
);
}
// Overridden to just emit an event.
function _updateCumulativeReward(bytes32 poolId)
internal
{
emit UpdateCumulativeReward(poolId);
}
// Compute a hash to index `_memberRewardsOverInterval`
function _getMemberRewardOverIntervalHash(
bytes32 poolId,
uint256 memberStakeOverInterval,
uint256 beginEpoch,
uint256 endEpoch
)
private
pure
returns (bytes32 rewardHash)
{
return keccak256(
abi.encode(
poolId,
memberStakeOverInterval,
beginEpoch,
endEpoch
)
);
}
}

View File

@ -37,7 +37,7 @@
}, },
"config": { "config": {
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|TestStorageLayoutAndConstants|ZrxVault).json" "abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestMixinStakingPoolRewards|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|TestStorageLayoutAndConstants|ZrxVault).json"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -50,6 +50,7 @@
"homepage": "https://github.com/0xProject/0x-monorepo/contracts/tokens/README.md", "homepage": "https://github.com/0xProject/0x-monorepo/contracts/tokens/README.md",
"devDependencies": { "devDependencies": {
"@0x/abi-gen": "^4.3.0-beta.0", "@0x/abi-gen": "^4.3.0-beta.0",
"@0x/contracts-exchange-libs": "^3.1.0-beta.0",
"@0x/contracts-gen": "^1.1.0-beta.0", "@0x/contracts-gen": "^1.1.0-beta.0",
"@0x/contracts-test-utils": "^3.2.0-beta.0", "@0x/contracts-test-utils": "^3.2.0-beta.0",
"@0x/dev-utils": "^2.4.0-beta.0", "@0x/dev-utils": "^2.4.0-beta.0",

View File

@ -48,6 +48,7 @@ import * as TestMixinStake from '../generated-artifacts/TestMixinStake.json';
import * as TestMixinStakeBalances from '../generated-artifacts/TestMixinStakeBalances.json'; import * as TestMixinStakeBalances from '../generated-artifacts/TestMixinStakeBalances.json';
import * as TestMixinStakeStorage from '../generated-artifacts/TestMixinStakeStorage.json'; import * as TestMixinStakeStorage from '../generated-artifacts/TestMixinStakeStorage.json';
import * as TestMixinStakingPool from '../generated-artifacts/TestMixinStakingPool.json'; import * as TestMixinStakingPool from '../generated-artifacts/TestMixinStakingPool.json';
import * as TestMixinStakingPoolRewards from '../generated-artifacts/TestMixinStakingPoolRewards.json';
import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json'; import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json';
import * as TestProxyDestination from '../generated-artifacts/TestProxyDestination.json'; import * as TestProxyDestination from '../generated-artifacts/TestProxyDestination.json';
import * as TestStaking from '../generated-artifacts/TestStaking.json'; import * as TestStaking from '../generated-artifacts/TestStaking.json';
@ -101,6 +102,7 @@ export const artifacts = {
TestMixinStakeBalances: TestMixinStakeBalances as ContractArtifact, TestMixinStakeBalances: TestMixinStakeBalances as ContractArtifact,
TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact, TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact,
TestMixinStakingPool: TestMixinStakingPool as ContractArtifact, TestMixinStakingPool: TestMixinStakingPool as ContractArtifact,
TestMixinStakingPoolRewards: TestMixinStakingPoolRewards as ContractArtifact,
TestProtocolFees: TestProtocolFees as ContractArtifact, TestProtocolFees: TestProtocolFees as ContractArtifact,
TestProxyDestination: TestProxyDestination as ContractArtifact, TestProxyDestination: TestProxyDestination as ContractArtifact,
TestStaking: TestStaking as ContractArtifact, TestStaking: TestStaking as ContractArtifact,

View File

@ -46,6 +46,7 @@ export * from '../generated-wrappers/test_mixin_stake';
export * from '../generated-wrappers/test_mixin_stake_balances'; export * from '../generated-wrappers/test_mixin_stake_balances';
export * from '../generated-wrappers/test_mixin_stake_storage'; export * from '../generated-wrappers/test_mixin_stake_storage';
export * from '../generated-wrappers/test_mixin_staking_pool'; export * from '../generated-wrappers/test_mixin_staking_pool';
export * from '../generated-wrappers/test_mixin_staking_pool_rewards';
export * from '../generated-wrappers/test_protocol_fees'; export * from '../generated-wrappers/test_protocol_fees';
export * from '../generated-wrappers/test_proxy_destination'; export * from '../generated-wrappers/test_proxy_destination';
export * from '../generated-wrappers/test_staking'; export * from '../generated-wrappers/test_staking';

View File

@ -0,0 +1,496 @@
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import {
blockchainTests,
constants,
expect,
getRandomInteger,
getRandomPortion,
hexRandom,
Numberish,
randomAddress,
TransactionHelper,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { LogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { StoredBalance } from '../utils/types';
import { artifacts, TestMixinStakingPoolRewardsContract, TestMixinStakingPoolRewardsEvents as Events } from '../../src';
blockchainTests.resets('MixinStakingPoolRewards unit tests', env => {
let testContract: TestMixinStakingPoolRewardsContract;
let txHelper: TransactionHelper;
const POOL_ID = hexRandom();
const OPERATOR = randomAddress();
const OPERATOR_SHARE = getRandomInteger(1, constants.PPM_100_PERCENT);
let caller: string;
before(async () => {
testContract = await TestMixinStakingPoolRewardsContract.deployFrom0xArtifactAsync(
artifacts.TestMixinStakingPoolRewards,
env.provider,
env.txDefaults,
artifacts,
);
await testContract.setPool.awaitTransactionSuccessAsync(POOL_ID, {
operator: OPERATOR,
operatorShare: OPERATOR_SHARE,
});
[caller] = await env.getAccountAddressesAsync();
txHelper = new TransactionHelper(env.web3Wrapper, artifacts);
});
async function setUnfinalizedPoolRewardsAsync(
poolId: string,
reward: Numberish,
membersStake: Numberish,
): Promise<void> {
await testContract.setUnfinalizedPoolRewards.awaitTransactionSuccessAsync(
poolId,
new BigNumber(reward),
new BigNumber(membersStake),
);
}
// Set the delegated stake of a delegator in a pool.
// Omitted fields will be randomly generated.
async function setStakeAsync(
poolId: string,
delegator: string,
stake?: Partial<StoredBalance>,
): Promise<StoredBalance> {
const _stake = {
currentEpoch: getRandomInteger(1, 4e9),
currentEpochBalance: getRandomInteger(1, 1e18),
nextEpochBalance: getRandomInteger(1, 1e18),
...stake,
};
await testContract.setDelegatedStakeToPoolByOwner.awaitTransactionSuccessAsync(delegator, poolId, {
currentEpoch: _stake.currentEpoch,
currentEpochBalance: _stake.currentEpochBalance,
nextEpochBalance: _stake.nextEpochBalance,
});
return _stake;
}
// Sets up state for a call to `_computeDelegatorReward()` and return the
// finalized rewards it will compute.
async function setComputeDelegatorRewardStateAsync(
poolId: string,
delegator: string,
finalizedReward?: Numberish,
): Promise<BigNumber> {
const stake = await testContract.delegatedStakeToPoolByOwner.callAsync(delegator, poolId);
// Split the rewards up across the two calls to `_computeMemberRewardOverInterval()`
const reward = finalizedReward === undefined ? getRandomInteger(1, 1e18) : new BigNumber(finalizedReward);
const oldRewards = getRandomPortion(reward);
await testContract.setMemberRewardsOverInterval.awaitTransactionSuccessAsync(
poolId,
stake.currentEpochBalance,
stake.currentEpoch,
stake.currentEpoch.plus(1),
oldRewards,
);
const newRewards = reward.minus(oldRewards);
await testContract.setMemberRewardsOverInterval.awaitTransactionSuccessAsync(
poolId,
stake.nextEpochBalance,
stake.currentEpoch.plus(1),
await testContract.currentEpoch.callAsync(),
newRewards,
);
return reward;
}
function toOperatorPortion(operatorShare: Numberish, reward: Numberish): BigNumber {
return ReferenceFunctions.getPartialAmountCeil(
new BigNumber(operatorShare),
new BigNumber(constants.PPM_DENOMINATOR),
new BigNumber(reward),
);
}
function toMembersPortion(operatorShare: Numberish, reward: Numberish): BigNumber {
return new BigNumber(reward).minus(toOperatorPortion(operatorShare, reward));
}
describe('withdrawDelegatorRewards()', () => {
it('calls `_withdrawAndSyncDelegatorRewards()` with the sender as the member', async () => {
const { logs } = await testContract.withdrawDelegatorRewards.awaitTransactionSuccessAsync(POOL_ID);
verifyEventsFromLogs(
logs,
[{ poolId: POOL_ID, delegator: caller }],
Events.WithdrawAndSyncDelegatorRewards,
);
});
});
describe('_withdrawAndSyncDelegatorRewards()', () => {
const POOL_REWARD = getRandomInteger(1, 100e18);
const WETH_RESERVED_FOR_POOL_REWARDS = POOL_REWARD.plus(getRandomInteger(1, 100e18));
const DELEGATOR = randomAddress();
let stake: StoredBalance;
before(async () => {
stake = await setStakeAsync(POOL_ID, DELEGATOR);
await testContract.setPoolRewards.awaitTransactionSuccessAsync(POOL_ID, POOL_REWARD);
await testContract.setWethReservedForPoolRewards.awaitTransactionSuccessAsync(
WETH_RESERVED_FOR_POOL_REWARDS,
);
});
async function withdrawAndSyncDelegatorRewardsAsync(): Promise<TransactionReceiptWithDecodedLogs> {
return testContract.withdrawAndSyncDelegatorRewards.awaitTransactionSuccessAsync(POOL_ID, DELEGATOR);
}
it('reverts if the pool is not finalized', async () => {
await setUnfinalizedPoolRewardsAsync(POOL_ID, 0, 1);
const tx = withdrawAndSyncDelegatorRewardsAsync();
return expect(tx).to.revertWith('POOL_NOT_FINALIZED');
});
it('calls `_updateCumulativeReward()`', async () => {
const { logs } = await withdrawAndSyncDelegatorRewardsAsync();
verifyEventsFromLogs(logs, [{ poolId: POOL_ID }], Events.UpdateCumulativeReward);
});
it('transfers finalized rewards to the sender', async () => {
const finalizedReward = getRandomPortion(POOL_REWARD);
await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward);
const { logs } = await withdrawAndSyncDelegatorRewardsAsync();
verifyEventsFromLogs(
logs,
[{ _from: testContract.address, _to: DELEGATOR, _value: finalizedReward }],
Events.Transfer,
);
});
it('reduces `rewardsByPoolId` for the pool', async () => {
const finalizedReward = getRandomPortion(POOL_REWARD);
await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward);
await withdrawAndSyncDelegatorRewardsAsync();
const poolReward = await testContract.rewardsByPoolId.callAsync(POOL_ID);
expect(poolReward).to.bignumber.eq(POOL_REWARD.minus(finalizedReward));
});
it('reduces `wethReservedForPoolRewards` for the pool', async () => {
const finalizedReward = getRandomPortion(POOL_REWARD);
await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward);
await withdrawAndSyncDelegatorRewardsAsync();
const wethReserved = await testContract.wethReservedForPoolRewards.callAsync();
expect(wethReserved).to.bignumber.eq(WETH_RESERVED_FOR_POOL_REWARDS.minus(finalizedReward));
});
it('syncs `_delegatedStakeToPoolByOwner`', async () => {
await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, getRandomPortion(POOL_REWARD));
await withdrawAndSyncDelegatorRewardsAsync();
const stakeAfter = await testContract.delegatedStakeToPoolByOwner.callAsync(DELEGATOR, POOL_ID);
// `_loadCurrentBalance` is overridden to just increment `currentEpoch`.
expect(stakeAfter).to.deep.eq({
currentEpoch: stake.currentEpoch.plus(1),
currentEpochBalance: stake.currentEpochBalance,
nextEpochBalance: stake.nextEpochBalance,
});
});
it('does not transfer zero rewards', async () => {
await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, 0);
const { logs } = await withdrawAndSyncDelegatorRewardsAsync();
verifyEventsFromLogs(logs, [], Events.Transfer);
});
it('no rewards if the delegated stake epoch == current epoch', async () => {
// Set some finalized rewards that should be ignored.
await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, getRandomInteger(1, POOL_REWARD));
await testContract.setCurrentEpoch.awaitTransactionSuccessAsync(stake.currentEpoch);
const { logs } = await withdrawAndSyncDelegatorRewardsAsync();
// There will be no Transfer events if computed rewards are zero.
verifyEventsFromLogs(logs, [], Events.Transfer);
});
});
describe('computeRewardBalanceOfOperator()', () => {
async function computeRewardBalanceOfOperatorAsync(): Promise<BigNumber> {
return testContract.computeRewardBalanceOfOperator.callAsync(POOL_ID);
}
it('returns only unfinalized rewards', async () => {
const unfinalizedReward = getRandomInteger(1, 1e18);
await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, getRandomInteger(1, 1e18));
// Set some unfinalized state for a call to `_computeDelegatorReward()`,
// which should not be called.
await setComputeDelegatorRewardStateAsync(POOL_ID, OPERATOR, getRandomInteger(1, 1e18));
const reward = await computeRewardBalanceOfOperatorAsync();
const expectedReward = toOperatorPortion(OPERATOR_SHARE, unfinalizedReward);
expect(reward).to.bignumber.eq(expectedReward);
});
it('returns operator portion of unfinalized rewards', async () => {
const unfinalizedReward = getRandomInteger(1, 1e18);
await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, getRandomInteger(1, 1e18));
const reward = await computeRewardBalanceOfOperatorAsync();
const expectedReward = toOperatorPortion(OPERATOR_SHARE, unfinalizedReward);
expect(reward).to.bignumber.eq(expectedReward);
});
it('returns zero if no unfinalized rewards', async () => {
await setUnfinalizedPoolRewardsAsync(POOL_ID, 0, getRandomInteger(1, 1e18));
const reward = await computeRewardBalanceOfOperatorAsync();
expect(reward).to.bignumber.eq(0);
});
it('returns all unfinalized reward if member stake is zero', async () => {
const unfinalizedReward = getRandomInteger(1, 1e18);
await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, 0);
const reward = await computeRewardBalanceOfOperatorAsync();
expect(reward).to.bignumber.eq(unfinalizedReward);
});
it('returns no reward if operator share is zero', async () => {
await testContract.setPool.awaitTransactionSuccessAsync(POOL_ID, {
operator: OPERATOR,
operatorShare: constants.ZERO_AMOUNT,
});
await setUnfinalizedPoolRewardsAsync(POOL_ID, getRandomInteger(1, 1e18), getRandomInteger(1, 1e18));
const reward = await computeRewardBalanceOfOperatorAsync();
expect(reward).to.bignumber.eq(0);
});
it('returns all unfinalized reward if operator share is 100%', async () => {
await testContract.setPool.awaitTransactionSuccessAsync(POOL_ID, {
operator: OPERATOR,
operatorShare: constants.PPM_100_PERCENT,
});
const unfinalizedReward = getRandomInteger(1, 1e18);
await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, getRandomInteger(1, 1e18));
const reward = await computeRewardBalanceOfOperatorAsync();
expect(reward).to.bignumber.eq(unfinalizedReward);
});
});
describe('computeRewardBalanceOfDelegator()', () => {
const DELEGATOR = randomAddress();
let currentEpoch: BigNumber;
let stake: StoredBalance;
before(async () => {
currentEpoch = await testContract.currentEpoch.callAsync();
stake = await setStakeAsync(POOL_ID, DELEGATOR);
});
async function computeRewardBalanceOfDelegatorAsync(): Promise<BigNumber> {
return testContract.computeRewardBalanceOfDelegator.callAsync(POOL_ID, DELEGATOR);
}
function getDelegatorPortionOfUnfinalizedReward(
unfinalizedReward: Numberish,
unfinalizedMembersStake: Numberish,
): BigNumber {
const unfinalizedStakeBalance = stake.currentEpoch.gte(currentEpoch)
? stake.currentEpochBalance
: stake.nextEpochBalance;
return ReferenceFunctions.getPartialAmountFloor(
unfinalizedStakeBalance,
new BigNumber(unfinalizedMembersStake),
toMembersPortion(OPERATOR_SHARE, unfinalizedReward),
);
}
it('returns zero when no finalized or unfinalized rewards', async () => {
const reward = await computeRewardBalanceOfDelegatorAsync();
expect(reward).to.bignumber.eq(0);
});
it('returns only unfinalized rewards when no finalized rewards', async () => {
const unfinalizedReward = getRandomInteger(1, 1e18);
const unfinalizedMembersStake = getRandomInteger(1, 1e18);
await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, unfinalizedMembersStake);
const expectedReward = getDelegatorPortionOfUnfinalizedReward(unfinalizedReward, unfinalizedMembersStake);
const reward = await computeRewardBalanceOfDelegatorAsync();
expect(reward).to.bignumber.eq(expectedReward);
});
it("returns zero when delegator's synced stake was zero in the last epoch and no finalized rewards", async () => {
await setStakeAsync(POOL_ID, DELEGATOR, {
...stake,
currentEpoch: currentEpoch.minus(1),
currentEpochBalance: constants.ZERO_AMOUNT,
});
await setUnfinalizedPoolRewardsAsync(POOL_ID, getRandomInteger(1, 1e18), getRandomInteger(1, 1e18));
const reward = await computeRewardBalanceOfDelegatorAsync();
expect(reward).to.bignumber.eq(0);
});
it("returns zero when delegator's unsynced stake was zero in the last epoch and no finalized rewards", async () => {
const epoch = 2;
await setStakeAsync(POOL_ID, DELEGATOR, {
...stake,
currentEpoch: new BigNumber(epoch - 2),
nextEpochBalance: constants.ZERO_AMOUNT,
});
await testContract.setCurrentEpoch.awaitTransactionSuccessAsync(new BigNumber(epoch));
await setUnfinalizedPoolRewardsAsync(POOL_ID, getRandomInteger(1, 1e18), getRandomInteger(1, 1e18));
const reward = await computeRewardBalanceOfDelegatorAsync();
expect(reward).to.bignumber.eq(0);
});
it('returns only finalized rewards when no unfinalized rewards', async () => {
const finalizedReward = getRandomInteger(1, 1e18);
await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward);
const reward = await computeRewardBalanceOfDelegatorAsync();
expect(reward).to.bignumber.eq(finalizedReward);
});
it('returns both unfinalized and finalized rewards', async () => {
const unfinalizedReward = getRandomInteger(1, 1e18);
const unfinalizedMembersStake = getRandomInteger(1, 1e18);
await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, unfinalizedMembersStake);
const finalizedReward = getRandomInteger(1, 1e18);
await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward);
const delegatorUnfinalizedReward = getDelegatorPortionOfUnfinalizedReward(
unfinalizedReward,
unfinalizedMembersStake,
);
const expectedReward = delegatorUnfinalizedReward.plus(finalizedReward);
const reward = await computeRewardBalanceOfDelegatorAsync();
expect(reward).to.bignumber.eq(expectedReward);
});
});
describe('_syncPoolRewards()', async () => {
const POOL_REWARD = getRandomInteger(1, 100e18);
const WETH_RESERVED_FOR_POOL_REWARDS = POOL_REWARD.plus(getRandomInteger(1, 100e18));
before(async () => {
await testContract.setPoolRewards.awaitTransactionSuccessAsync(POOL_ID, POOL_REWARD);
await testContract.setWethReservedForPoolRewards.awaitTransactionSuccessAsync(
WETH_RESERVED_FOR_POOL_REWARDS,
);
});
async function syncPoolRewardsAsync(
reward: Numberish,
membersStake: Numberish,
): Promise<[[BigNumber, BigNumber], LogEntry[]]> {
const [result, receipt] = await txHelper.getResultAndReceiptAsync(
testContract.syncPoolRewards,
POOL_ID,
new BigNumber(reward),
new BigNumber(membersStake),
);
return [result, receipt.logs];
}
it("transfers operator's portion of the reward to the operator", async () => {
const totalReward = getRandomInteger(1, 1e18);
const membersStake = getRandomInteger(1, 1e18);
const [, logs] = await syncPoolRewardsAsync(totalReward, membersStake);
const expectedOperatorReward = toOperatorPortion(OPERATOR_SHARE, totalReward);
verifyEventsFromLogs(
logs,
[{ _from: testContract.address, _to: OPERATOR, _value: expectedOperatorReward }],
Events.Transfer,
);
});
it("increases `rewardsByPoolId` with members' portion of rewards", async () => {
const totalReward = getRandomInteger(1, 1e18);
const membersStake = getRandomInteger(1, 1e18);
await syncPoolRewardsAsync(totalReward, membersStake);
const expectedMembersReward = toMembersPortion(OPERATOR_SHARE, totalReward);
const poolReward = await testContract.rewardsByPoolId.callAsync(POOL_ID);
expect(poolReward).to.bignumber.eq(POOL_REWARD.plus(expectedMembersReward));
});
it("increases `wethReservedForPoolRewards` with members' portion of rewards", async () => {
const totalReward = getRandomInteger(1, 1e18);
const membersStake = getRandomInteger(1, 1e18);
await syncPoolRewardsAsync(totalReward, membersStake);
const expectedMembersReward = toMembersPortion(OPERATOR_SHARE, totalReward);
const wethReserved = await testContract.wethReservedForPoolRewards.callAsync();
expect(wethReserved).to.bignumber.eq(WETH_RESERVED_FOR_POOL_REWARDS.plus(expectedMembersReward));
});
it("returns operator and members' portion of the reward", async () => {
const totalReward = getRandomInteger(1, 1e18);
const membersStake = getRandomInteger(1, 1e18);
const [[operatorReward, membersReward]] = await syncPoolRewardsAsync(totalReward, membersStake);
const expectedOperatorReward = toOperatorPortion(OPERATOR_SHARE, totalReward);
const expectedMembersReward = toMembersPortion(OPERATOR_SHARE, totalReward);
expect(operatorReward).to.bignumber.eq(expectedOperatorReward);
expect(membersReward).to.bignumber.eq(expectedMembersReward);
});
it("gives all rewards to operator if members' stake is zero", async () => {
const totalReward = getRandomInteger(1, 1e18);
const [[operatorReward, membersReward], logs] = await syncPoolRewardsAsync(totalReward, 0);
expect(operatorReward).to.bignumber.eq(totalReward);
expect(membersReward).to.bignumber.eq(0);
verifyEventsFromLogs(
logs,
[{ _from: testContract.address, _to: OPERATOR, _value: totalReward }],
Events.Transfer,
);
});
it("gives all rewards to members if operator's share is zero", async () => {
const totalReward = getRandomInteger(1, 1e18);
await testContract.setPool.awaitTransactionSuccessAsync(POOL_ID, {
operator: OPERATOR,
operatorShare: constants.ZERO_AMOUNT,
});
const [[operatorReward, membersReward], logs] = await syncPoolRewardsAsync(
totalReward,
getRandomInteger(1, 1e18),
);
expect(operatorReward).to.bignumber.eq(0);
expect(membersReward).to.bignumber.eq(totalReward);
// Should be no transfer to the operator.
verifyEventsFromLogs(logs, [], Events.Transfer);
});
});
describe('_computePoolRewardsSplit', () => {
it("gives all rewards to operator if members' stake is zero", async () => {
const operatorShare = getRandomPortion(constants.PPM_100_PERCENT);
const totalReward = getRandomInteger(1, 1e18);
const membersStake = constants.ZERO_AMOUNT;
const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync(
operatorShare,
totalReward,
membersStake,
);
expect(operatorReward).to.bignumber.eq(totalReward);
expect(membersReward).to.bignumber.eq(0);
});
it("gives all rewards to operator if members' stake is zero and operator share is zero", async () => {
const operatorShare = constants.ZERO_AMOUNT;
const totalReward = getRandomInteger(1, 1e18);
const membersStake = constants.ZERO_AMOUNT;
const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync(
operatorShare,
totalReward,
membersStake,
);
expect(operatorReward).to.bignumber.eq(totalReward);
expect(membersReward).to.bignumber.eq(0);
});
it('gives all rewards to operator if operator share is 100%', async () => {
const operatorShare = constants.PPM_100_PERCENT;
const totalReward = getRandomInteger(1, 1e18);
const membersStake = getRandomInteger(1, 1e18);
const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync(
operatorShare,
totalReward,
membersStake,
);
expect(operatorReward).to.bignumber.eq(totalReward);
expect(membersReward).to.bignumber.eq(0);
});
it('gives all rewards to members if operator share is 0%', async () => {
const operatorShare = constants.ZERO_AMOUNT;
const totalReward = getRandomInteger(1, 1e18);
const membersStake = getRandomInteger(1, 1e18);
const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync(
operatorShare,
totalReward,
membersStake,
);
expect(operatorReward).to.bignumber.eq(0);
expect(membersReward).to.bignumber.eq(totalReward);
});
it('splits rewards between operator and members based on operator share', async () => {
const operatorShare = getRandomPortion(constants.PPM_100_PERCENT);
const totalReward = getRandomInteger(1, 1e18);
const membersStake = getRandomInteger(1, 1e18);
const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync(
operatorShare,
totalReward,
membersStake,
);
expect(operatorReward).to.bignumber.eq(toOperatorPortion(operatorShare, totalReward));
expect(membersReward).to.bignumber.eq(toMembersPortion(operatorShare, totalReward));
});
});
});
// tslint:disable: max-file-line-count

View File

@ -46,6 +46,7 @@
"generated-artifacts/TestMixinStakeBalances.json", "generated-artifacts/TestMixinStakeBalances.json",
"generated-artifacts/TestMixinStakeStorage.json", "generated-artifacts/TestMixinStakeStorage.json",
"generated-artifacts/TestMixinStakingPool.json", "generated-artifacts/TestMixinStakingPool.json",
"generated-artifacts/TestMixinStakingPoolRewards.json",
"generated-artifacts/TestProtocolFees.json", "generated-artifacts/TestProtocolFees.json",
"generated-artifacts/TestProxyDestination.json", "generated-artifacts/TestProxyDestination.json",
"generated-artifacts/TestStaking.json", "generated-artifacts/TestStaking.json",