import { ReferenceFunctions } from '@0x/contracts-exchange-libs'; import { blockchainTests, constants, expect, getRandomInteger, getRandomPortion, Numberish, randomAddress, verifyEventsFromLogs, } from '@0x/contracts-test-utils'; import { BigNumber, hexUtils } from '@0x/utils'; import { LogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { StoredBalance } from '../../src/types'; import { artifacts } from '../artifacts'; import { TestMixinStakingPoolRewardsContract, TestMixinStakingPoolRewardsEvents as Events } from '../wrappers'; blockchainTests.resets('MixinStakingPoolRewards unit tests', env => { let testContract: TestMixinStakingPoolRewardsContract; const POOL_ID = hexUtils.random(); 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(POOL_ID, { operator: OPERATOR, operatorShare: OPERATOR_SHARE, }) .awaitTransactionSuccessAsync(); [caller] = await env.getAccountAddressesAsync(); }); async function setUnfinalizedPoolRewardsAsync( poolId: string, reward: Numberish, membersStake: Numberish, ): Promise { await testContract .setUnfinalizedPoolRewards(poolId, new BigNumber(reward), new BigNumber(membersStake)) .awaitTransactionSuccessAsync(); } // 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, ): Promise { const _stake = { currentEpoch: getRandomInteger(1, 4e9), currentEpochBalance: getRandomInteger(1, 1e18), nextEpochBalance: getRandomInteger(1, 1e18), ...stake, }; await testContract .setDelegatedStakeToPoolByOwner(delegator, poolId, { currentEpoch: _stake.currentEpoch, currentEpochBalance: _stake.currentEpochBalance, nextEpochBalance: _stake.nextEpochBalance, }) .awaitTransactionSuccessAsync(); 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 { const stake = await testContract.delegatedStakeToPoolByOwner(delegator, poolId).callAsync(); // 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( poolId, stake.currentEpochBalance, stake.currentEpoch, stake.currentEpoch.plus(1), oldRewards, ) .awaitTransactionSuccessAsync(); const newRewards = reward.minus(oldRewards); await testContract .setMemberRewardsOverInterval( poolId, stake.nextEpochBalance, stake.currentEpoch.plus(1), await testContract.currentEpoch().callAsync(), newRewards, ) .awaitTransactionSuccessAsync(); 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(POOL_ID).awaitTransactionSuccessAsync(); 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(POOL_ID, POOL_REWARD).awaitTransactionSuccessAsync(); await testContract .setWethReservedForPoolRewards(WETH_RESERVED_FOR_POOL_REWARDS) .awaitTransactionSuccessAsync(); }); async function withdrawAndSyncDelegatorRewardsAsync(): Promise { return testContract.withdrawAndSyncDelegatorRewards(POOL_ID, DELEGATOR).awaitTransactionSuccessAsync(); } 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(POOL_ID).callAsync(); 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(DELEGATOR, POOL_ID).callAsync(); // `_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(stake.currentEpoch).awaitTransactionSuccessAsync(); 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 { return testContract.computeRewardBalanceOfOperator(POOL_ID).callAsync(); } 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(POOL_ID, { operator: OPERATOR, operatorShare: constants.ZERO_AMOUNT, }) .awaitTransactionSuccessAsync(); 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(POOL_ID, { operator: OPERATOR, operatorShare: constants.PPM_100_PERCENT, }) .awaitTransactionSuccessAsync(); 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 { return testContract.computeRewardBalanceOfDelegator(POOL_ID, DELEGATOR).callAsync(); } 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(new BigNumber(epoch)).awaitTransactionSuccessAsync(); 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(POOL_ID, POOL_REWARD).awaitTransactionSuccessAsync(); await testContract .setWethReservedForPoolRewards(WETH_RESERVED_FOR_POOL_REWARDS) .awaitTransactionSuccessAsync(); }); async function syncPoolRewardsAsync( reward: Numberish, membersStake: Numberish, ): Promise<[[BigNumber, BigNumber], LogEntry[]]> { const contractFn = testContract.syncPoolRewards( POOL_ID, new BigNumber(reward), new BigNumber(membersStake), ); const result = await contractFn.callAsync(); const { logs } = await contractFn.awaitTransactionSuccessAsync(); return [result, 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(POOL_ID).callAsync(); 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(POOL_ID, { operator: OPERATOR, operatorShare: constants.ZERO_AMOUNT, }) .awaitTransactionSuccessAsync(); 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(operatorShare, totalReward, membersStake) .callAsync(); 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(operatorShare, totalReward, membersStake) .callAsync(); 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(operatorShare, totalReward, membersStake) .callAsync(); 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(operatorShare, totalReward, membersStake) .callAsync(); 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(operatorShare, totalReward, membersStake) .callAsync(); expect(operatorReward).to.bignumber.eq(toOperatorPortion(operatorShare, totalReward)); expect(membersReward).to.bignumber.eq(toMembersPortion(operatorShare, totalReward)); }); }); }); // tslint:disable: max-file-line-count