Merge pull request #2312 from 0xProject/feat/contracts/staking/MixinStakingPoolRewards-unit-tests
MixinStakingPoolRewards unit tests
This commit is contained in:
		
							
								
								
									
										281
									
								
								contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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';
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										496
									
								
								contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										496
									
								
								contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts
									
									
									
									
									
										Normal 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
 | 
				
			||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user