diff --git a/contracts/interfaces/IRewardPool.sol b/contracts/interfaces/IRewardPool.sol index d0196601c..4c36ba3dd 100644 --- a/contracts/interfaces/IRewardPool.sol +++ b/contracts/interfaces/IRewardPool.sol @@ -2,84 +2,66 @@ pragma solidity ^0.8.9; -interface IRewardPool { - /// @dev Emitted when the settled pool is updated. - event SettledPoolsUpdated(address[] poolAddress, uint256[] accumulatedRps); - /// @dev Emitted when the pending pool is updated. - event PendingPoolUpdated(address poolAddress, uint256 accumulatedRps); - /// @dev Emitted when the fields to calculate settled reward for the user is updated. - event SettledRewardUpdated(address poolAddress, address user, uint256 debited, uint256 accumulatedRps); +import "../interfaces/consumers/PeriodWrapperConsumer.sol"; + +interface IRewardPool is PeriodWrapperConsumer { /// @dev Emitted when the fields to calculate pending reward for the user is updated. - event PendingRewardUpdated(address poolAddress, address user, uint256 debited, uint256 credited); + event UserRewardUpdated(address indexed poolAddr, address indexed user, uint256 debited); /// @dev Emitted when the user claimed their reward - event RewardClaimed(address poolAddress, address user, uint256 amount); + event RewardClaimed(address indexed poolAddr, address indexed user, uint256 amount); - struct PendingRewardFields { - // Recorded reward amount. - uint256 debited; - // The amount rewards that user have already earned. - uint256 credited; - // Last period number that the info updated. - uint256 lastSyncedPeriod; - } + /// @dev Emitted when the pool shares are updated + event PoolSharesUpdated(uint256 indexed period, address indexed poolAddr, uint256 shares); + /// @dev Emitted when the pools are updated + event PoolsUpdated(uint256 indexed period, address[] poolAddrs, uint256[] aRps, uint256[] shares); + /// @dev Emitted when the contract fails when updating the pools + event PoolsUpdateFailed(uint256 indexed period, address[] poolAddrs, uint256[] rewards); + /// @dev Emitted when the contract fails when updating a pool that already set + event PoolUpdateConflicted(uint256 indexed period, address indexed poolAddr); - struct SettledRewardFields { + struct UserRewardFields { // Recorded reward amount. uint256 debited; - // Accumulated of the amount rewards per share (one unit staking). - uint256 accumulatedRps; - } - - struct PendingPool { - // Accumulated of the amount rewards per share (one unit staking). - uint256 accumulatedRps; + // The last accumulated of the amount rewards per share (one unit staking) that the info updated. + uint256 aRps; + // Min staking amount in the period. + uint256 minAmount; + // Last period number that the info updated. + uint256 lastPeriod; } - struct SettledPool { - // Last period number that the info updated. - uint256 lastSyncedPeriod; + struct PoolFields { // Accumulated of the amount rewards per share (one unit staking). - uint256 accumulatedRps; + uint256 aRps; + // The staking total to share reward of the current period. + PeriodWrapper shares; } - /** - * @dev Returns total rewards from scratch including pending reward and claimable reward except the claimed amount. - * - * Note: Do not use this function to get claimable reward, consider using the method `getClaimableReward` instead. - * - */ - function getTotalReward(address _poolAddr, address _user) external view returns (uint256); - /** * @dev Returns the reward amount that user claimable. */ - function getClaimableReward(address _poolAddr, address _user) external view returns (uint256); - - /** - * @dev Returns the pending reward. - */ - function getPendingReward(address _poolAddr, address _user) external view returns (uint256 _amount); + function getReward(address _poolAddr, address _user) external view returns (uint256); /** - * @dev Returns the staked amount of the user. + * @dev Returns the staking amount of an user. */ - function balanceOf(address _poolAddr, address _user) external view returns (uint256); + function stakingAmountOf(address _poolAddr, address _user) external view returns (uint256); /** - * @dev Returns the staked amounts of the users. + * @dev Returns the staking amounts of the users. */ - function bulkBalanceOf(address[] calldata _poolAddrs, address[] calldata _userList) + function bulkStakingAmountOf(address[] calldata _poolAddrs, address[] calldata _userList) external view returns (uint256[] memory); /** - * @dev Returns the total staked amount of all users. + * @dev Returns the total staking amount of all users for a pool. */ - function totalBalance(address _poolAddr) external view returns (uint256); + function stakingTotal(address _poolAddr) external view returns (uint256); /** - * @dev Returns the total staked amount of all users. + * @dev Returns the total staking amounts of all users for the pools `_poolAddrs`. */ - function totalBalances(address[] calldata _poolAddr) external view returns (uint256[] memory); + function bulkStakingTotal(address[] calldata _poolAddrs) external view returns (uint256[] memory); } diff --git a/contracts/interfaces/IStaking.sol b/contracts/interfaces/IStaking.sol index 69e0eccfd..cc90c9d87 100644 --- a/contracts/interfaces/IStaking.sol +++ b/contracts/interfaces/IStaking.sol @@ -10,20 +10,20 @@ interface IStaking is IRewardPool { address addr; // Pool admin address address admin; - // Self-staked amount - uint256 stakedAmount; - // Total balance of the pool - uint256 totalBalance; - // Mapping from delegator => delegated amount - mapping(address => uint256) delegatedAmount; + // Self-staking amount + uint256 stakingAmount; + // Total number of RON staking for the pool + uint256 stakingTotal; + // Mapping from delegator => delegating amount + mapping(address => uint256) delegatingAmount; } /// @dev Emitted when the validator pool is approved. event PoolApproved(address indexed validator, address indexed admin); /// @dev Emitted when the validator pool is deprecated. event PoolsDeprecated(address[] validator); - /// @dev Emitted when the staked amount is deprecated. - event StakedAmountDeprecated(address indexed validator, address indexed admin, uint256 amount); + /// @dev Emitted when the staking amount is deprecated. + event StakingAmountDeprecated(address indexed validator, address indexed admin, uint256 amount); /// @dev Emitted when the pool admin staked for themself. event Staked(address indexed consensuAddr, uint256 amount); /// @dev Emitted when the pool admin unstaked the amount of RON from themself. @@ -32,8 +32,8 @@ interface IStaking is IRewardPool { event Delegated(address indexed delegator, address indexed consensuAddr, uint256 amount); /// @dev Emitted when the delegator unstaked from a validator candidate. event Undelegated(address indexed delegator, address indexed consensuAddr, uint256 amount); - /// @dev Emitted when the minimum balance for being a validator is updated. - event MinValidatorBalanceUpdated(uint256 threshold); + /// @dev Emitted when the minimum staking amount for being a validator is updated. + event MinValidatorStakingAmountUpdated(uint256 threshold); /////////////////////////////////////////////////////////////////////////////////////// // FUNCTIONS FOR GOVERNANCE // @@ -42,7 +42,7 @@ interface IStaking is IRewardPool { /** * @dev Returns the minimum threshold for being a validator candidate. */ - function minValidatorBalance() external view returns (uint256); + function minValidatorStakingAmount() external view returns (uint256); /** * @dev Sets the minimum threshold for being a validator candidate. @@ -50,52 +50,36 @@ interface IStaking is IRewardPool { * Requirements: * - The method caller is admin. * - * Emits the `MinValidatorBalanceUpdated` event. + * Emits the `MinValidatorStakingAmountUpdated` event. * */ - function setMinValidatorBalance(uint256) external; + function setMinValidatorStakingAmount(uint256) external; /////////////////////////////////////////////////////////////////////////////////////// // FUNCTIONS FOR VALIDATOR CONTRACT // /////////////////////////////////////////////////////////////////////////////////////// /** - * @dev Records the amount of reward `_reward` for the pending pool `_poolAddr`. + * @dev Records the amount of rewards `_rewards` for the pools `_poolAddrs`. * * Requirements: * - The method caller is validator contract. * - * Emits the `PendingPoolUpdated` event. + * Emits the event `PoolsUpdated` once the contract recorded the rewards successfully. + * Emits the event `PoolsUpdateFailed` once the input array lengths are not equal. + * Emits the event `PoolUpdateConflicted` when the pool is already updated in the period. * - * Note: This method should not be called after the pending pool is sinked. + * Note: This method should be called once at the period ending. * */ - function recordReward(address _consensusAddr, uint256 _reward) external payable; - - /** - * @dev Settles the pending pool and allocates rewards for the pool `_consensusAddr`. - * - * Requirements: - * - The method caller is validator contract. - * - * Emits the `SettledPoolsUpdated` event. - * - */ - function settleRewardPools(address[] calldata _consensusAddrs) external; - - /** - * @dev Handles when the pending reward pool of the validator is sinked. - * - * Requirements: - * - The method caller is validator contract. - * - * Emits the `PendingPoolUpdated` event. - * - */ - function sinkPendingReward(address _consensusAddr) external; + function recordRewards( + uint256 _period, + address[] calldata _consensusAddrs, + uint256[] calldata _rewards + ) external payable; /** - * @dev Deducts from staked amount of the validator `_consensusAddr` for `_amount`. + * @dev Deducts from staking amount of the validator `_consensusAddr` for `_amount`. * * Requirements: * - The method caller is validator contract. @@ -103,7 +87,7 @@ interface IStaking is IRewardPool { * Emits the event `Unstaked`. * */ - function deductStakedAmount(address _consensusAddr, uint256 _amount) external; + function deductStakingAmount(address _consensusAddr, uint256 _amount) external; /** * @dev Deprecates the pool. @@ -112,7 +96,7 @@ interface IStaking is IRewardPool { * - The method caller is validator contract. * * Emits the event `PoolsDeprecated` and `Unstaked` events. - * Emits the event `StakedAmountDeprecated` if the contract cannot transfer RON back to the pool admin. + * Emits the event `StakingAmountDeprecated` if the contract cannot transfer RON back to the pool admin. * */ function deprecatePools(address[] calldata _pools) external; @@ -127,7 +111,7 @@ interface IStaking is IRewardPool { * Requirements: * - The method caller is able to receive RON. * - The treasury is able to receive RON. - * - The amount is larger than or equal to the minimum validator balance `minValidatorBalance()`. + * - The amount is larger than or equal to the minimum validator staking amount `minValidatorStakingAmount()`. * * Emits the event `PoolApproved`. * @@ -169,7 +153,7 @@ interface IStaking is IRewardPool { function unstake(address _consensusAddr, uint256 _amount) external; /** - * @dev Renounces being a validator candidate and takes back the delegated/staked amount. + * @dev Renounces being a validator candidate and takes back the delegating/staking amount. * * Requirements: * - The consensus address is a validator candidate. @@ -233,12 +217,12 @@ interface IStaking is IRewardPool { ) external; /** - * @dev Returns the pending reward and the claimable reward of the user `_user`. + * @dev Returns the claimable reward of the user `_user`. */ function getRewards(address _user, address[] calldata _poolAddrList) external view - returns (uint256[] memory _pendings, uint256[] memory _claimables); + returns (uint256[] memory _rewards); /** * @dev Claims the reward of method caller. @@ -270,7 +254,7 @@ interface IStaking is IRewardPool { view returns ( address _admin, - uint256 _stakedAmount, - uint256 _totalBalance + uint256 _stakingAmount, + uint256 _stakingTotal ); } diff --git a/contracts/interfaces/consumers/PeriodWrapperConsumer.sol b/contracts/interfaces/consumers/PeriodWrapperConsumer.sol new file mode 100644 index 000000000..1f8b5d6f6 --- /dev/null +++ b/contracts/interfaces/consumers/PeriodWrapperConsumer.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface PeriodWrapperConsumer { + struct PeriodWrapper { + // Inner value. + uint256 inner; + // Last period number that the info updated. + uint256 lastPeriod; + } +} diff --git a/contracts/mocks/MockPrecompile.sol b/contracts/mocks/MockPrecompile.sol index 8932be402..1fe464e5e 100644 --- a/contracts/mocks/MockPrecompile.sol +++ b/contracts/mocks/MockPrecompile.sol @@ -23,12 +23,12 @@ contract MockPrecompile { function pickValidatorSet( address[] memory _candidates, - uint256[] memory _balanceWeights, + uint256[] memory _weights, uint256[] memory _trustedWeights, uint256 _maxValidatorNumber, uint256 _maxPrioritizedValidatorNumber ) public pure returns (address[] memory _result) { - (_result, _trustedWeights) = Sorting.sortWithExternalKeys(_candidates, _balanceWeights, _trustedWeights); + (_result, _trustedWeights) = Sorting.sortWithExternalKeys(_candidates, _weights, _trustedWeights); uint256 _newValidatorCount = Math.min(_maxValidatorNumber, _result.length); _arrangeValidatorCandidates(_result, _trustedWeights, _newValidatorCount, _maxPrioritizedValidatorNumber); } diff --git a/contracts/mocks/MockRoninValidatorSetOverridePrecompile.sol b/contracts/mocks/MockRoninValidatorSetOverridePrecompile.sol index 4c34bc092..11a3af629 100644 --- a/contracts/mocks/MockRoninValidatorSetOverridePrecompile.sol +++ b/contracts/mocks/MockRoninValidatorSetOverridePrecompile.sol @@ -30,14 +30,14 @@ contract MockRoninValidatorSetOverridePrecompile is RoninValidatorSet, MockPreco function _pcPickValidatorSet( address[] memory _candidates, - uint256[] memory _balanceWeights, + uint256[] memory _weights, uint256[] memory _trustedWeights, uint256 _maxValidatorNumber, uint256 _maxPrioritizedValidatorNumber ) internal pure override returns (address[] memory _result, uint256 _newValidatorCount) { _result = pickValidatorSet( _candidates, - _balanceWeights, + _weights, _trustedWeights, _maxValidatorNumber, _maxPrioritizedValidatorNumber diff --git a/contracts/mocks/MockStaking.sol b/contracts/mocks/MockStaking.sol index bd74aa05e..e6afa5471 100644 --- a/contracts/mocks/MockStaking.sol +++ b/contracts/mocks/MockStaking.sol @@ -6,55 +6,66 @@ import "../ronin/staking/RewardCalculation.sol"; contract MockStaking is RewardCalculation { /// @dev Mapping from user => staking balance - mapping(address => uint256) internal _stakingBalance; + mapping(address => uint256) internal _stakingAmount; /// @dev Mapping from period number => slashed mapping(uint256 => bool) internal _periodSlashed; - uint256 internal _lastUpdatedPeriod; - uint256 internal _totalBalance; + uint256 internal _stakingTotal; + + uint256 public lastUpdatedPeriod; + uint256 public pendingReward; address public poolAddr; constructor(address _poolAddr) { - _lastUpdatedPeriod++; poolAddr = _poolAddr; } + function firstEverWrapup() external { + delete pendingReward; + lastUpdatedPeriod = block.timestamp / 1 days + 1; + } + function endPeriod() external { - _lastUpdatedPeriod++; + address[] memory _addrs = new address[](1); + uint256[] memory _rewards = new uint256[](1); + _addrs[0] = poolAddr; + _rewards[0] = pendingReward; + this.recordRewards(_addrs, _rewards); + + pendingReward = 0; + lastUpdatedPeriod++; } - function stake(address _user, uint256 _amount) external { - uint256 _balance = _stakingBalance[_user]; - uint256 _newBalance = _balance + _amount; - _syncUserReward(poolAddr, _user, _newBalance); - _stakingBalance[_user] = _newBalance; - _totalBalance += _amount; + function increasePeriod() external { + lastUpdatedPeriod++; } - function unstake(address _user, uint256 _amount) external { - uint256 _balance = _stakingBalance[_user]; - uint256 _newBalance = _balance - _amount; - _syncUserReward(poolAddr, _user, _newBalance); - _stakingBalance[_user] = _newBalance; - _totalBalance -= _amount; + function stake(address _user, uint256 _amount) external { + uint256 _lastStakingAmount = _stakingAmount[_user]; + uint256 _newStakingAmount = _lastStakingAmount + _amount; + _syncUserReward(poolAddr, _user, _newStakingAmount); + _stakingAmount[_user] = _newStakingAmount; + _stakingTotal += _amount; } - function slash() external { - uint256 _period = getPeriod(); - _periodSlashed[_period] = true; - _sinkPendingReward(poolAddr); + function unstake(address _user, uint256 _amount) external { + uint256 _lastStakingAmount = _stakingAmount[_user]; + uint256 _newStakingAmount = _lastStakingAmount - _amount; + _syncUserReward(poolAddr, _user, _newStakingAmount); + _stakingAmount[_user] = _newStakingAmount; + _stakingTotal -= _amount; } - function recordReward(uint256 _rewardAmount) external { - _recordReward(poolAddr, _rewardAmount); + function increaseReward(uint256 _amount) external { + pendingReward += _amount; } - function settledPools(address[] calldata _addrList) external { - _onPoolsSettled(_addrList); + function decreaseReward(uint256 _amount) external { + pendingReward -= _amount; } - function increaseAccumulatedRps(uint256 _amount) external { - _recordReward(poolAddr, _amount); + function recordRewards(address[] calldata _addrList, uint256[] calldata _rewards) external { + _recordRewards(_currentPeriod(), _addrList, _rewards); } function getPeriod() public view returns (uint256) { @@ -65,28 +76,24 @@ contract MockStaking is RewardCalculation { _amount = _claimReward(poolAddr, _user); } - function balanceOf(address, address _user) public view override returns (uint256) { - return _stakingBalance[_user]; + function stakingAmountOf(address, address _user) public view override returns (uint256) { + return _stakingAmount[_user]; } - function bulkBalanceOf(address[] calldata _poolAddrs, address[] calldata _userList) + function bulkStakingAmountOf(address[] calldata _poolAddrs, address[] calldata _userList) external view override returns (uint256[] memory) {} - function totalBalance(address) public view virtual override returns (uint256) { - return _totalBalance; - } - - function _rewardSinked(address, uint256 _period) internal view override returns (bool) { - return _periodSlashed[_period]; + function stakingTotal(address) public view virtual override returns (uint256) { + return _stakingTotal; } function _currentPeriod() internal view override returns (uint256 _period) { - return _lastUpdatedPeriod; + return lastUpdatedPeriod; } - function totalBalances(address[] calldata _poolAddr) external view override returns (uint256[] memory) {} + function bulkStakingTotal(address[] calldata _poolAddr) external view override returns (uint256[] memory) {} } diff --git a/contracts/mocks/MockValidatorSet.sol b/contracts/mocks/MockValidatorSet.sol index 8b30f07c5..6a98ef43c 100644 --- a/contracts/mocks/MockValidatorSet.sol +++ b/contracts/mocks/MockValidatorSet.sol @@ -30,31 +30,10 @@ contract MockValidatorSet is IRoninValidatorSet, CandidateManager { _numberOfBlocksInEpoch = __numberOfBlocksInEpoch; } - function depositReward() external payable { - _stakingContract.recordReward{ value: msg.value }(msg.sender, msg.value); - } - - function settledReward(address[] calldata _validatorList) external { - _stakingContract.settleRewardPools(_validatorList); - } - - function slashMisdemeanor(address _validator) external { - _stakingContract.sinkPendingReward(_validator); - } - - function slashFelony(address _validator) external { - _stakingContract.sinkPendingReward(_validator); - _stakingContract.deductStakedAmount(_validator, 1); - } - - function slashDoubleSign(address _validator) external { - _stakingContract.sinkPendingReward(_validator); - } - function submitBlockReward() external payable override {} function wrapUpEpoch() external payable override { - _filterUnsatisfiedCandidates(0); + _filterUnsatisfiedCandidates(); _lastUpdatedPeriod = currentPeriod(); } diff --git a/contracts/mocks/precompile-usages/MockPrecompileUsagePickValidatorSet.sol b/contracts/mocks/precompile-usages/MockPrecompileUsagePickValidatorSet.sol index 8b742ed18..bd2e47108 100644 --- a/contracts/mocks/precompile-usages/MockPrecompileUsagePickValidatorSet.sol +++ b/contracts/mocks/precompile-usages/MockPrecompileUsagePickValidatorSet.sol @@ -21,14 +21,14 @@ contract MockPrecompileUsagePickValidatorSet is PrecompileUsagePickValidatorSet function callPrecompile( address[] memory _candidates, - uint256[] memory _balanceWeights, + uint256[] memory _weights, uint256[] memory _trustedWeights, uint256 _maxValidatorNumber, uint256 _maxPrioritizedValidatorNumber ) public view returns (address[] memory _result) { (_result, ) = _pcPickValidatorSet( _candidates, - _balanceWeights, + _weights, _trustedWeights, _maxValidatorNumber, _maxPrioritizedValidatorNumber diff --git a/contracts/precompile-usages/PrecompileUsagePickValidatorSet.sol b/contracts/precompile-usages/PrecompileUsagePickValidatorSet.sol index 4b87a96ce..bee39ff08 100644 --- a/contracts/precompile-usages/PrecompileUsagePickValidatorSet.sol +++ b/contracts/precompile-usages/PrecompileUsagePickValidatorSet.sol @@ -16,7 +16,7 @@ abstract contract PrecompileUsagePickValidatorSet { */ function _pcPickValidatorSet( address[] memory _candidates, - uint256[] memory _balanceWeights, + uint256[] memory _weights, uint256[] memory _trustedWeights, uint256 _maxValidatorNumber, uint256 _maxPrioritizedValidatorNumber @@ -25,7 +25,7 @@ abstract contract PrecompileUsagePickValidatorSet { bytes memory _payload = abi.encodeWithSignature( "pickValidatorSet(address[],uint256[],uint256[],uint256,uint256)", _candidates, - _balanceWeights, + _weights, _trustedWeights, _maxValidatorNumber, _maxPrioritizedValidatorNumber diff --git a/contracts/ronin/staking/RewardCalculation.sol b/contracts/ronin/staking/RewardCalculation.sol index 2a3f6557d..c32e0c659 100644 --- a/contracts/ronin/staking/RewardCalculation.sol +++ b/contracts/ronin/staking/RewardCalculation.sol @@ -3,200 +3,200 @@ pragma solidity ^0.8.9; import "../../interfaces/IRewardPool.sol"; +import "../../libraries/Math.sol"; /** * @title RewardCalculation contract * @dev This contract mainly contains the methods to calculate reward for staking contract. */ abstract contract RewardCalculation is IRewardPool { - /// @dev Mapping from the pool address => user address => settled reward info of the user - mapping(address => mapping(address => SettledRewardFields)) internal _sUserReward; - /// @dev Mapping from the pool address => user address => pending reward info of the user - mapping(address => mapping(address => PendingRewardFields)) internal _pUserReward; - - /// @dev Mapping from the pool address => pending pool data - mapping(address => PendingPool) internal _pendingPool; - /// @dev Mapping from the pool address => settled pool data - mapping(address => SettledPool) internal _settledPool; + /// @dev Mapping from period number => accumulated rewards per share (one unit staking) + mapping(uint256 => PeriodWrapper) private _accumulatedRps; + /// @dev Mapping from the pool address => user address => the reward info of the user + mapping(address => mapping(address => UserRewardFields)) private _userReward; + /// @dev Mapping from the pool address => reward pool fields + mapping(address => PoolFields) private _stakingPool; /** * @inheritdoc IRewardPool */ - function getTotalReward(address _poolAddr, address _user) public view returns (uint256) { - PendingRewardFields memory _reward = _pUserReward[_poolAddr][_user]; - PendingPool memory _pool = _pendingPool[_poolAddr]; - - uint256 _balance = balanceOf(_poolAddr, _user); - if (_rewardSinked(_poolAddr, _reward.lastSyncedPeriod)) { - SettledRewardFields memory _sReward = _sUserReward[_poolAddr][_user]; - uint256 _diffRps = _pool.accumulatedRps - _sReward.accumulatedRps; - return (_balance * _diffRps) / 1e18 + _sReward.debited; - } - - return (_balance * _pool.accumulatedRps) / 1e18 + _reward.debited - _reward.credited; + function getReward(address _poolAddr, address _user) external view returns (uint256) { + return _getReward(_poolAddr, _user, _currentPeriod(), stakingAmountOf(_poolAddr, _user)); } /** * @inheritdoc IRewardPool */ - function getClaimableReward(address _poolAddr, address _user) public view returns (uint256) { - PendingRewardFields memory _reward = _pUserReward[_poolAddr][_user]; - SettledRewardFields memory _sReward = _sUserReward[_poolAddr][_user]; - SettledPool memory _sPool = _settledPool[_poolAddr]; - - uint256 _diffRps = _sPool.accumulatedRps - _sReward.accumulatedRps; - if (_reward.lastSyncedPeriod <= _sPool.lastSyncedPeriod) { - uint256 _currentBalance = balanceOf(_poolAddr, _user); - - if (_rewardSinked(_poolAddr, _reward.lastSyncedPeriod)) { - return (_currentBalance * _diffRps) / 1e18 + _sReward.debited; - } - - return (_currentBalance * _sPool.accumulatedRps) / 1e18 + _reward.debited - _reward.credited; - } - - return _sReward.debited; - } + function stakingAmountOf(address _poolAddr, address _user) public view virtual returns (uint256); /** * @inheritdoc IRewardPool */ - function getPendingReward(address _poolAddr, address _user) external view returns (uint256 _amount) { - _amount = getTotalReward(_poolAddr, _user) - getClaimableReward(_poolAddr, _user); - } + function stakingTotal(address _poolAddr) public view virtual returns (uint256); /** - * @inheritdoc IRewardPool + * @dev Returns the reward amount that user claimable. */ - function balanceOf(address _poolAddr, address _user) public view virtual returns (uint256); + function _getReward( + address _poolAddr, + address _user, + uint256 _latestPeriod, + uint256 _latestStakingAmount + ) internal view returns (uint256) { + UserRewardFields storage _reward = _userReward[_poolAddr][_user]; - /** - * @inheritdoc IRewardPool - */ - function totalBalance(address _poolAddr) public view virtual returns (uint256); + if (_reward.lastPeriod == _latestPeriod) { + return _reward.debited; + } + + PoolFields storage _pool = _stakingPool[_poolAddr]; + uint256 _minAmount = _reward.minAmount; + uint256 _aRps = _accumulatedRps[_reward.lastPeriod].inner; + uint256 _lastPeriodReward = _minAmount * (_aRps - _reward.aRps); + uint256 _newPeriodsReward = _latestStakingAmount * (_pool.aRps - _aRps); + return _reward.debited + (_lastPeriodReward + _newPeriodsReward) / 1e18; + } /** * @dev Syncs the user reward. * - * Emits the `SettledRewardUpdated` event if the last block user made changes is recorded in the settled period. - * Emits the `PendingRewardUpdated` event. + * Emits the event `UserRewardUpdated` once the debit amount is updated. + * Emits the event `PoolSharesUpdated` once the pool share is updated. * - * Note: The method should be called whenever the user's balance changes. + * Note: The method should be called whenever the user's staking amount changes. * */ function _syncUserReward( address _poolAddr, address _user, - uint256 _newBalance + uint256 _newStakingAmount ) internal { - PendingRewardFields storage _reward = _pUserReward[_poolAddr][_user]; - SettledPool memory _sPool = _settledPool[_poolAddr]; + uint256 _period = _currentPeriod(); + PoolFields storage _pool = _stakingPool[_poolAddr]; + uint256 _lastShares = _pool.shares.inner; + + // Updates the pool shares if it is outdated + if (_pool.shares.lastPeriod < _period) { + _pool.shares = PeriodWrapper(stakingTotal(_poolAddr), _period); + } - // Syncs the reward once the last sync is settled. - if (_reward.lastSyncedPeriod <= _sPool.lastSyncedPeriod) { - uint256 _claimableReward = getClaimableReward(_poolAddr, _user); + UserRewardFields storage _reward = _userReward[_poolAddr][_user]; + uint256 _currentStakingAmount = stakingAmountOf(_poolAddr, _user); + uint256 _debited = _getReward(_poolAddr, _user, _period, _currentStakingAmount); - SettledRewardFields storage _sReward = _sUserReward[_poolAddr][_user]; - _sReward.debited = _claimableReward; - _sReward.accumulatedRps = _sPool.accumulatedRps; - emit SettledRewardUpdated(_poolAddr, _user, _claimableReward, _sPool.accumulatedRps); + if (_reward.debited != _debited) { + _reward.debited = _debited; + emit UserRewardUpdated(_poolAddr, _user, _debited); } - PendingPool memory _pool = _pendingPool[_poolAddr]; - uint256 _debited = getTotalReward(_poolAddr, _user); - uint256 _credited = (_newBalance * _pool.accumulatedRps) / 1e18; + _syncMinStakingAmount(_pool, _reward, _period, _newStakingAmount, _currentStakingAmount); + _reward.aRps = _pool.aRps; + _reward.lastPeriod = _period; - _reward.debited = _debited; - _reward.credited = _credited; - _reward.lastSyncedPeriod = _currentPeriod(); - emit PendingRewardUpdated(_poolAddr, _user, _debited, _credited); + if (_pool.shares.inner != _lastShares) { + emit PoolSharesUpdated(_period, _poolAddr, _pool.shares.inner); + } } /** - * @dev Claims the settled reward for a specific user. - * - * Emits the `PendingRewardUpdated` event and the `SettledRewardUpdated` event. - * - * Note: This method should be called before transferring rewards for the user. - * + * @dev Syncs the minimum staking amount of an user in the current period. */ - function _claimReward(address _poolAddr, address _user) internal returns (uint256 _amount) { - _amount = getClaimableReward(_poolAddr, _user); - emit RewardClaimed(_poolAddr, _user, _amount); - - SettledPool memory _sPool = _settledPool[_poolAddr]; - PendingRewardFields storage _reward = _pUserReward[_poolAddr][_user]; - SettledRewardFields storage _sReward = _sUserReward[_poolAddr][_user]; - - _sReward.debited = 0; - if (_reward.lastSyncedPeriod <= _sPool.lastSyncedPeriod) { - _sReward.accumulatedRps = _sPool.accumulatedRps; + function _syncMinStakingAmount( + PoolFields storage _pool, + UserRewardFields storage _reward, + uint256 _latestPeriod, + uint256 _newStakingAmount, + uint256 _currentStakingAmount + ) internal { + if (_reward.lastPeriod < _latestPeriod) { + _reward.minAmount = _currentStakingAmount; } - emit SettledRewardUpdated(_poolAddr, _user, 0, _sReward.accumulatedRps); - _reward.credited += _amount; - _reward.lastSyncedPeriod = _currentPeriod(); - emit PendingRewardUpdated(_poolAddr, _user, _reward.debited, _reward.credited); + uint256 _minAmount = Math.min(_reward.minAmount, _newStakingAmount); + uint256 _diffAmount = _reward.minAmount - _minAmount; + if (_diffAmount > 0) { + _reward.minAmount = _minAmount; + require(_pool.shares.inner >= _diffAmount, "RewardCalculation: invalid pool shares"); + _pool.shares.inner -= _diffAmount; + } } /** - * @dev Records the amount of reward `_reward` for the pending pool `_poolAddr`. + * @dev Claims the settled reward for a specific user. * - * Emits the `PendingPoolUpdated` event. + * Emits the `PendingRewardUpdated` event and the `SettledRewardUpdated` event. * - * Note: This method should not be called after the pending pool is sinked. + * Note: This method should be called before transferring rewards for the user. * */ - function _recordReward(address _poolAddr, uint256 _reward) internal { - PendingPool storage _pool = _pendingPool[_poolAddr]; - uint256 _accumulatedRps = _pool.accumulatedRps + (_reward * 1e18) / totalBalance(_poolAddr); - _pool.accumulatedRps = _accumulatedRps; - emit PendingPoolUpdated(_poolAddr, _accumulatedRps); - } + function _claimReward(address _poolAddr, address _user) internal returns (uint256 _amount) { + uint256 _latestPeriod = _currentPeriod(); + _amount = _getReward(_poolAddr, _user, _latestPeriod, stakingAmountOf(_poolAddr, _user)); + emit RewardClaimed(_poolAddr, _user, _amount); - /** - * @dev Handles when the pool `_poolAddr` is sinked. - * - * Emits the `PendingPoolUpdated` event. - * - * Note: This method should be called when the pool is sinked. - * - */ - function _sinkPendingReward(address _poolAddr) internal { - uint256 _accumulatedRps = _settledPool[_poolAddr].accumulatedRps; - PendingPool storage _pool = _pendingPool[_poolAddr]; - _pool.accumulatedRps = _accumulatedRps; - emit PendingPoolUpdated(_poolAddr, _accumulatedRps); + UserRewardFields storage _reward = _userReward[_poolAddr][_user]; + _reward.debited = 0; + _reward.lastPeriod = _latestPeriod; + _reward.aRps = _stakingPool[_poolAddr].aRps; + emit UserRewardUpdated(_poolAddr, _user, 0); } /** - * @dev Handles when the pool `_poolAddr` is settled. + * @dev Records the amount of rewards `_rewards` for the pools `_poolAddrs`. * - * Emits the `SettledPoolsUpdated` event. + * Emits the event `PoolsUpdated` once the contract recorded the rewards successfully. + * Emits the event `PoolsUpdateFailed` once the input array lengths are not equal. + * Emits the event `PoolUpdateConflicted` when the pool is already updated in the period. * - * Note: This method should be called once in the end of each period. + * Note: This method should be called once at the period ending. * */ - function _onPoolsSettled(address[] calldata _poolList) internal { - uint256[] memory _accumulatedRpsList = new uint256[](_poolList.length); + function _recordRewards( + uint256 _period, + address[] calldata _poolAddrs, + uint256[] calldata _rewards + ) internal { + if (_poolAddrs.length != _rewards.length) { + emit PoolsUpdateFailed(_period, _poolAddrs, _rewards); + return; + } + + uint256 _rps; address _poolAddr; - for (uint256 _i; _i < _poolList.length; _i++) { - _poolAddr = _poolList[_i]; - _accumulatedRpsList[_i] = _pendingPool[_poolAddr].accumulatedRps; - - SettledPool storage _sPool = _settledPool[_poolAddr]; - if (_accumulatedRpsList[_i] != _sPool.accumulatedRps) { - _sPool.accumulatedRps = _accumulatedRpsList[_i]; - _sPool.lastSyncedPeriod = _currentPeriod(); + uint256 _stakingTotal; + uint256[] memory _aRps = new uint256[](_poolAddrs.length); + uint256[] memory _shares = new uint256[](_poolAddrs.length); + + for (uint _i = 0; _i < _poolAddrs.length; _i++) { + _poolAddr = _poolAddrs[_i]; + PoolFields storage _pool = _stakingPool[_poolAddr]; + _stakingTotal = stakingTotal(_poolAddr); + + if (_accumulatedRps[_period].lastPeriod == _period) { + _aRps[_i] = _pool.aRps; + _shares[_i] = _pool.shares.inner; + emit PoolUpdateConflicted(_period, _poolAddr); + continue; + } + + // Updates the pool shares if it is outdated + if (_pool.shares.lastPeriod < _period) { + _pool.shares = PeriodWrapper(_stakingTotal, _period); } + + // The rps is 0 if no one stakes for the pool + _rps = _pool.shares.inner == 0 ? 0 : (_rewards[_i] * 1e18) / _pool.shares.inner; + + _aRps[_i] = _pool.aRps += _rps; + _accumulatedRps[_period] = PeriodWrapper(_aRps[_i], _period); + if (_pool.shares.inner != _stakingTotal) { + _pool.shares.inner = _stakingTotal; + } + _shares[_i] = _pool.shares.inner; } - emit SettledPoolsUpdated(_poolList, _accumulatedRpsList); - } - /** - * @dev Returns whether the pool is slashed in the period `_period`. - */ - function _rewardSinked(address _poolAddr, uint256 _period) internal view virtual returns (bool); + emit PoolsUpdated(_period, _poolAddrs, _aRps, _shares); + } /** * @dev Returns the current period. diff --git a/contracts/ronin/staking/Staking.sol b/contracts/ronin/staking/Staking.sol index 6ec6a2b5e..963356031 100644 --- a/contracts/ronin/staking/Staking.sol +++ b/contracts/ronin/staking/Staking.sol @@ -9,9 +9,7 @@ import "./StakingManager.sol"; contract Staking is IStaking, StakingManager, Initializable { /// @dev The minimum threshold for being a validator candidate. - uint256 internal _minValidatorBalance; - /// @dev Mapping from pool address => period index => indicating the pending reward in the period is sinked or not. - mapping(address => mapping(uint256 => bool)) internal _pRewardSinked; + uint256 internal _minValidatorStakingAmount; constructor() { _disableInitializers(); @@ -24,9 +22,9 @@ contract Staking is IStaking, StakingManager, Initializable { /** * @dev Initializes the contract storage. */ - function initialize(address __validatorContract, uint256 __minValidatorBalance) external initializer { + function initialize(address __validatorContract, uint256 __minValidatorStakingAmount) external initializer { _setValidatorContract(__validatorContract); - _setMinValidatorBalance(__minValidatorBalance); + _setMinValidatorStakingAmount(__minValidatorStakingAmount); } /** @@ -38,63 +36,44 @@ contract Staking is IStaking, StakingManager, Initializable { poolExists(_poolAddr) returns ( address _admin, - uint256 _stakedAmount, - uint256 _totalBalance + uint256 _stakingAmount, + uint256 _stakingTotal ) { PoolDetail storage _pool = _stakingPool[_poolAddr]; - return (_pool.admin, _pool.stakedAmount, _pool.totalBalance); + return (_pool.admin, _pool.stakingAmount, _pool.stakingTotal); } /** * @inheritdoc IStaking */ - function minValidatorBalance() public view override(IStaking, StakingManager) returns (uint256) { - return _minValidatorBalance; + function minValidatorStakingAmount() public view override(IStaking, StakingManager) returns (uint256) { + return _minValidatorStakingAmount; } /** * @inheritdoc IStaking */ - function setMinValidatorBalance(uint256 _threshold) external override onlyAdmin { - _setMinValidatorBalance(_threshold); - } - - /////////////////////////////////////////////////////////////////////////////////////// - // FUNCTIONS FOR VALIDATOR // - /////////////////////////////////////////////////////////////////////////////////////// - - /** - * @inheritdoc IStaking - */ - function recordReward(address _consensusAddr, uint256 _reward) external payable onlyValidatorContract { - _recordReward(_consensusAddr, _reward); - } - - /** - * @inheritdoc IStaking - */ - function settleRewardPools(address[] calldata _consensusAddrs) external onlyValidatorContract { - if (_consensusAddrs.length == 0) { - return; - } - _onPoolsSettled(_consensusAddrs); + function setMinValidatorStakingAmount(uint256 _threshold) external override onlyAdmin { + _setMinValidatorStakingAmount(_threshold); } /** * @inheritdoc IStaking */ - function sinkPendingReward(address _consensusAddr) external onlyValidatorContract { - uint256 _period = _currentPeriod(); - _pRewardSinked[_consensusAddr][_period] = true; - _sinkPendingReward(_consensusAddr); + function recordRewards( + uint256 _period, + address[] calldata _consensusAddrs, + uint256[] calldata _rewards + ) external payable onlyValidatorContract { + _recordRewards(_period, _consensusAddrs, _rewards); } /** * @inheritdoc IStaking */ - function deductStakedAmount(address _consensusAddr, uint256 _amount) public onlyValidatorContract { - return _deductStakedAmount(_stakingPool[_consensusAddr], _amount); + function deductStakingAmount(address _consensusAddr, uint256 _amount) external onlyValidatorContract { + return _deductStakingAmount(_stakingPool[_consensusAddr], _amount); } /** @@ -108,40 +87,27 @@ contract Staking is IStaking, StakingManager, Initializable { uint256 _amount; for (uint _i = 0; _i < _pools.length; _i++) { PoolDetail storage _pool = _stakingPool[_pools[_i]]; - _amount = _pool.stakedAmount; - _deductStakedAmount(_pool, _pool.stakedAmount); + _amount = _pool.stakingAmount; if (_amount > 0) { + _deductStakingAmount(_pool, _amount); if (!_sendRON(payable(_pool.admin), _amount)) { - emit StakedAmountDeprecated(_pool.addr, _pool.admin, _amount); + emit StakingAmountDeprecated(_pool.addr, _pool.admin, _amount); } } - - delete _stakingPool[_pool.addr].stakedAmount; } emit PoolsDeprecated(_pools); } - /////////////////////////////////////////////////////////////////////////////////////// - // HELPER FUNCTIONS // - /////////////////////////////////////////////////////////////////////////////////////// - /** * @dev Sets the minimum threshold for being a validator candidate. * - * Emits the `MinValidatorBalanceUpdated` event. + * Emits the `MinValidatorStakingAmountUpdated` event. * */ - function _setMinValidatorBalance(uint256 _threshold) internal { - _minValidatorBalance = _threshold; - emit MinValidatorBalanceUpdated(_threshold); - } - - /** - * @inheritdoc RewardCalculation - */ - function _rewardSinked(address _poolAddr, uint256 _period) internal view virtual override returns (bool) { - return _pRewardSinked[_poolAddr][_period]; + function _setMinValidatorStakingAmount(uint256 _threshold) internal { + _minValidatorStakingAmount = _threshold; + emit MinValidatorStakingAmountUpdated(_threshold); } /** @@ -152,16 +118,16 @@ contract Staking is IStaking, StakingManager, Initializable { } /** - * @dev Deducts from staked amount of the validator `_consensusAddr` for `_amount`. + * @dev Deducts from staking amount of the validator `_consensusAddr` for `_amount`. * * Emits the event `Unstaked`. * */ - function _deductStakedAmount(PoolDetail storage _pool, uint256 _amount) internal { - _amount = Math.min(_pool.stakedAmount, _amount); + function _deductStakingAmount(PoolDetail storage _pool, uint256 _amount) internal { + _amount = Math.min(_pool.stakingAmount, _amount); - _pool.stakedAmount -= _amount; - _changeDelegatedAmount(_pool, _pool.admin, _pool.stakedAmount, _pool.totalBalance - _amount); + _pool.stakingAmount -= _amount; + _changeDelegatingAmount(_pool, _pool.admin, _pool.stakingAmount, _pool.stakingTotal - _amount); emit Unstaked(_pool.addr, _amount); } } diff --git a/contracts/ronin/staking/StakingManager.sol b/contracts/ronin/staking/StakingManager.sol index 55a276bd5..9f60e3886 100644 --- a/contracts/ronin/staking/StakingManager.sol +++ b/contracts/ronin/staking/StakingManager.sol @@ -42,52 +42,52 @@ abstract contract StakingManager is /** * @inheritdoc IRewardPool */ - function balanceOf(address _poolAddr, address _user) + function stakingAmountOf(address _poolAddr, address _user) public view override(IRewardPool, RewardCalculation) returns (uint256) { - return _stakingPool[_poolAddr].delegatedAmount[_user]; + return _stakingPool[_poolAddr].delegatingAmount[_user]; } /** * @inheritdoc IRewardPool */ - function bulkBalanceOf(address[] calldata _poolAddrs, address[] calldata _userList) + function bulkStakingAmountOf(address[] calldata _poolAddrs, address[] calldata _userList) external view override - returns (uint256[] memory _balances) + returns (uint256[] memory _stakingAmounts) { require(_poolAddrs.length > 0 && _poolAddrs.length == _userList.length, "StakingManager: invalid input array"); - _balances = new uint256[](_poolAddrs.length); - for (uint _i = 0; _i < _balances.length; _i++) { - _balances[_i] = _stakingPool[_poolAddrs[_i]].delegatedAmount[_userList[_i]]; + _stakingAmounts = new uint256[](_poolAddrs.length); + for (uint _i = 0; _i < _stakingAmounts.length; _i++) { + _stakingAmounts[_i] = _stakingPool[_poolAddrs[_i]].delegatingAmount[_userList[_i]]; } } /** * @inheritdoc IRewardPool */ - function totalBalance(address _poolAddr) public view override(IRewardPool, RewardCalculation) returns (uint256) { - return _stakingPool[_poolAddr].totalBalance; + function stakingTotal(address _poolAddr) public view override(IRewardPool, RewardCalculation) returns (uint256) { + return _stakingPool[_poolAddr].stakingTotal; } /** * @inheritdoc IRewardPool */ - function totalBalances(address[] calldata _poolList) public view override returns (uint256[] memory _balances) { - _balances = new uint256[](_poolList.length); + function bulkStakingTotal(address[] calldata _poolList) public view override returns (uint256[] memory _stakingAmounts) { + _stakingAmounts = new uint256[](_poolList.length); for (uint _i = 0; _i < _poolList.length; _i++) { - _balances[_i] = totalBalance(_poolList[_i]); + _stakingAmounts[_i] = stakingTotal(_poolList[_i]); } } /** * @inheritdoc IStaking */ - function minValidatorBalance() public view virtual returns (uint256); + function minValidatorStakingAmount() public view virtual returns (uint256); /////////////////////////////////////////////////////////////////////////////////////// // FUNCTIONS FOR VALIDATOR CANDIDATE // @@ -136,8 +136,8 @@ abstract contract StakingManager is require(_amount > 0, "StakingManager: invalid amount"); address _delegator = msg.sender; PoolDetail storage _pool = _stakingPool[_consensusAddr]; - uint256 _remainAmount = _pool.stakedAmount - _amount; - require(_remainAmount >= minValidatorBalance(), "StakingManager: invalid staked amount left"); + uint256 _remainAmount = _pool.stakingAmount - _amount; + require(_remainAmount >= minValidatorStakingAmount(), "StakingManager: invalid staking amount left"); _unstake(_pool, _delegator, _amount); require(_sendRON(payable(_delegator), _amount), "StakingManager: could not transfer RON"); @@ -160,7 +160,7 @@ abstract contract StakingManager is * Requirements: * - The pool admin is able to receive RON. * - The treasury is able to receive RON. - * - The amount is larger than or equal to the minimum validator balance `minValidatorBalance()`. + * - The amount is larger than or equal to the minimum validator staking amount `minValidatorStakingAmount()`. * * @param _candidateAdmin the candidate admin will be stored in the validator contract, used for calling function that affects * to its candidate. IE: scheduling maintenance. @@ -177,7 +177,7 @@ abstract contract StakingManager is ) internal { require(_sendRON(_poolAdmin, 0), "StakingManager: pool admin cannot receive RON"); require(_sendRON(_treasuryAddr, 0), "StakingManager: treasury cannot receive RON"); - require(_amount >= minValidatorBalance(), "StakingManager: insufficient amount"); + require(_amount >= minValidatorStakingAmount(), "StakingManager: insufficient amount"); _validatorContract.grantValidatorCandidate( _candidateAdmin, @@ -202,13 +202,13 @@ abstract contract StakingManager is address _requester, uint256 _amount ) internal onlyPoolAdmin(_pool, _requester) { - _pool.stakedAmount += _amount; - _changeDelegatedAmount(_pool, _requester, _pool.stakedAmount, _pool.totalBalance + _amount); + _pool.stakingAmount += _amount; + _changeDelegatingAmount(_pool, _requester, _pool.stakingAmount, _pool.stakingTotal + _amount); emit Staked(_pool.addr, _amount); } /** - * @dev Withdraws the staked amount `_amount` for the validator candidate. + * @dev Withdraws the staking amount `_amount` for the validator candidate. * * Requirements: * - The address `_requester` must be the pool admin. @@ -221,10 +221,10 @@ abstract contract StakingManager is address _requester, uint256 _amount ) internal onlyPoolAdmin(_pool, _requester) { - require(_amount <= _pool.stakedAmount, "StakingManager: insufficient staked amount"); + require(_amount <= _pool.stakingAmount, "StakingManager: insufficient staking amount"); - _pool.stakedAmount -= _amount; - _changeDelegatedAmount(_pool, _requester, _pool.stakedAmount, _pool.totalBalance - _amount); + _pool.stakingAmount -= _amount; + _changeDelegatingAmount(_pool, _requester, _pool.stakingAmount, _pool.stakingTotal - _amount); emit Unstaked(_pool.addr, _amount); } @@ -287,19 +287,15 @@ abstract contract StakingManager is function getRewards(address _user, address[] calldata _poolAddrList) external view - returns (uint256[] memory _pendings, uint256[] memory _claimables) + returns (uint256[] memory _rewards) { address _consensusAddr; - _pendings = new uint256[](_poolAddrList.length); - _claimables = new uint256[](_poolAddrList.length); + uint256 _period = _validatorContract.currentPeriod(); + _rewards = new uint256[](_poolAddrList.length); for (uint256 _i = 0; _i < _poolAddrList.length; _i++) { _consensusAddr = _poolAddrList[_i]; - - uint256 _totalReward = getTotalReward(_consensusAddr, _user); - uint256 _claimableReward = getClaimableReward(_consensusAddr, _user); - _pendings[_i] = _totalReward - _claimableReward; - _claimables[_i] = _claimableReward; + _rewards[_i] = _getReward(_consensusAddr, _user, _period, stakingAmountOf(_consensusAddr, _user)); } } @@ -308,7 +304,7 @@ abstract contract StakingManager is */ function claimRewards(address[] calldata _consensusAddrList) external nonReentrant returns (uint256 _amount) { _amount = _claimRewards(msg.sender, _consensusAddrList); - require(_sendRON(payable(msg.sender), _amount), "StakingManager: could not transfer RON"); + _transferRON(payable(msg.sender), _amount); } /** @@ -340,11 +336,11 @@ abstract contract StakingManager is address _delegator, uint256 _amount ) internal notPoolAdmin(_pool, _delegator) { - _changeDelegatedAmount( + _changeDelegatingAmount( _pool, _delegator, - _pool.delegatedAmount[_delegator] + _amount, - _pool.totalBalance + _amount + _pool.delegatingAmount[_delegator] + _amount, + _pool.stakingTotal + _amount ); emit Delegated(_delegator, _pool.addr, _amount); } @@ -355,7 +351,7 @@ abstract contract StakingManager is * Requirements: * - The delegator is not the pool admin. * - The amount is larger than 0. - * - The delegated amount is larger than or equal to the undelegated amount. + * - The delegating amount is larger than or equal to the undelegating amount. * * Emits the `Undelegated` event. * @@ -368,12 +364,12 @@ abstract contract StakingManager is uint256 _amount ) private notPoolAdmin(_pool, _delegator) { require(_amount > 0, "StakingManager: invalid amount"); - require(_pool.delegatedAmount[_delegator] >= _amount, "StakingManager: insufficient amount to undelegate"); - _changeDelegatedAmount( + require(_pool.delegatingAmount[_delegator] >= _amount, "StakingManager: insufficient amount to undelegate"); + _changeDelegatingAmount( _pool, _delegator, - _pool.delegatedAmount[_delegator] - _amount, - _pool.totalBalance - _amount + _pool.delegatingAmount[_delegator] - _amount, + _pool.stakingTotal - _amount ); emit Undelegated(_delegator, _pool.addr, _amount); } @@ -381,15 +377,15 @@ abstract contract StakingManager is /** * @dev Changes the delelgate amount. */ - function _changeDelegatedAmount( + function _changeDelegatingAmount( PoolDetail storage _pool, address _delegator, - uint256 _newDelegateBalance, - uint256 _newTotalBalance + uint256 _newDelegatingAmount, + uint256 _newStakingTotal ) internal { - _syncUserReward(_pool.addr, _delegator, _newDelegateBalance); - _pool.totalBalance = _newTotalBalance; - _pool.delegatedAmount[_delegator] = _newDelegateBalance; + _syncUserReward(_pool.addr, _delegator, _newDelegatingAmount); + _pool.stakingTotal = _newStakingTotal; + _pool.delegatingAmount[_delegator] = _newDelegatingAmount; } /** diff --git a/contracts/ronin/validator/CandidateManager.sol b/contracts/ronin/validator/CandidateManager.sol index 5214983ef..9caedff08 100644 --- a/contracts/ronin/validator/CandidateManager.sol +++ b/contracts/ronin/validator/CandidateManager.sol @@ -3,10 +3,11 @@ pragma solidity ^0.8.9; import "../../extensions/collections/HasStakingContract.sol"; +import "../../extensions/consumers/PercentageConsumer.sol"; import "../../interfaces/ICandidateManager.sol"; import "../../interfaces/IStaking.sol"; -abstract contract CandidateManager is ICandidateManager, HasStakingContract { +abstract contract CandidateManager is ICandidateManager, HasStakingContract, PercentageConsumer { /// @dev Maximum number of validator candidate uint256 private _maxValidatorCandidate; @@ -50,6 +51,7 @@ abstract contract CandidateManager is ICandidateManager, HasStakingContract { uint256 _length = _candidates.length; require(_length < maxValidatorCandidate(), "CandidateManager: exceeds maximum number of candidates"); require(!isValidatorCandidate(_consensusAddr), "CandidateManager: query for already existent candidate"); + require(_commissionRate <= _MAX_PERCENTAGE, "CandidateManager: invalid comission rate"); _candidateIndex[_consensusAddr] = ~_length; _candidates.push(_consensusAddr); @@ -119,29 +121,38 @@ abstract contract CandidateManager is ICandidateManager, HasStakingContract { function numberOfBlocksInEpoch() public view virtual returns (uint256); /** - * @dev Removes unsastisfied candidates, the ones who have insufficient minimum candidate balance, + * @dev Removes unsastisfied candidates, the ones who have insufficient minimum candidate staking amount, * or the ones who revoked their candidate role. * * Emits the event `CandidatesRevoked` when a candidate is revoked. * - * @return _balances a list of balances of the new candidates. + * @return _stakingTotals a list of staking totals of the new candidates. * */ - function _filterUnsatisfiedCandidates(uint256 _minBalance) internal returns (uint256[] memory _balances) { + function _filterUnsatisfiedCandidates() internal returns (uint256[] memory _stakingTotals) { IStaking _staking = _stakingContract; - _balances = _staking.totalBalances(_candidates); + // NOTE: we should filter the ones who does not keep the minium candidate staking amount here + // IE: _staking.bulkSelftStakingAmount(_candidates); + _stakingTotals = _staking.bulkStakingTotal(_candidates); + uint256 _minStakingAmount = _stakingContract.minValidatorStakingAmount(); uint256 _length = _candidates.length; uint256 _period = currentPeriod(); address[] memory _unsatisfiedCandidates = new address[](_length); uint256 _unsatisfiedCount; address _addr; - for (uint _i = 0; _i < _length; _i++) { - _addr = _candidates[_i]; - if (_balances[_i] < _minBalance || _candidateInfo[_addr].revokedPeriod <= _period) { - _balances[_i] = _balances[--_length]; - _unsatisfiedCandidates[_unsatisfiedCount++] = _addr; - _removeCandidate(_addr); + + { + uint256 _i; + while (_i < _length) { + _addr = _candidates[_i]; + if (_stakingTotals[_i] < _minStakingAmount || _candidateInfo[_addr].revokedPeriod <= _period) { + _stakingTotals[_i] = _stakingTotals[--_length]; + _unsatisfiedCandidates[_unsatisfiedCount++] = _addr; + _removeCandidate(_addr); + continue; + } + _i++; } } @@ -154,7 +165,7 @@ abstract contract CandidateManager is ICandidateManager, HasStakingContract { } assembly { - mstore(_balances, _length) + mstore(_stakingTotals, _length) } } diff --git a/contracts/ronin/validator/RoninValidatorSet.sol b/contracts/ronin/validator/RoninValidatorSet.sol index 1bda14b7f..19be96835 100644 --- a/contracts/ronin/validator/RoninValidatorSet.sol +++ b/contracts/ronin/validator/RoninValidatorSet.sol @@ -10,7 +10,6 @@ import "../../extensions/collections/HasSlashIndicatorContract.sol"; import "../../extensions/collections/HasMaintenanceContract.sol"; import "../../extensions/collections/HasRoninTrustedOrganizationContract.sol"; import "../../extensions/collections/HasBridgeTrackingContract.sol"; -import "../../extensions/consumers/PercentageConsumer.sol"; import "../../interfaces/IRoninValidatorSet.sol"; import "../../libraries/Math.sol"; import "../../libraries/EnumFlags.sol"; @@ -30,7 +29,6 @@ contract RoninValidatorSet is HasRoninTrustedOrganizationContract, HasBridgeTrackingContract, CandidateManager, - PercentageConsumer, Initializable { using EnumFlags for EnumFlags.ValidatorFlag; @@ -153,20 +151,20 @@ contract RoninValidatorSet is _totalBridgeReward += _bridgeOperatorBonus; // Deprecates reward for non-validator or slashed validator - if (_requestForBlockProducer) { - uint256 _reward = _submittedReward + _blockProducerBonus; - uint256 _rate = _candidateInfo[_coinbaseAddr].commissionRate; - - uint256 _miningAmount = (_rate * _reward) / 100_00; - _miningReward[_coinbaseAddr] += _miningAmount; - - uint256 _delegatingAmount = _reward - _miningAmount; - _delegatingReward[_coinbaseAddr] += _delegatingAmount; - _stakingContract.recordReward(_coinbaseAddr, _delegatingAmount); - emit BlockRewardSubmitted(_coinbaseAddr, _submittedReward, _blockProducerBonus); - } else { + if (!_requestForBlockProducer) { emit BlockRewardRewardDeprecated(_coinbaseAddr, _submittedReward); + return; } + + uint256 _reward = _submittedReward + _blockProducerBonus; + uint256 _rate = _candidateInfo[_coinbaseAddr].commissionRate; + + uint256 _miningAmount = (_rate * _reward) / _MAX_PERCENTAGE; + _miningReward[_coinbaseAddr] += _miningAmount; + + uint256 _delegatingAmount = _reward - _miningAmount; + _delegatingReward[_coinbaseAddr] += _delegatingAmount; + emit BlockRewardSubmitted(_coinbaseAddr, _submittedReward, _blockProducerBonus); } /** @@ -182,11 +180,11 @@ contract RoninValidatorSet is uint256 _lastPeriod = currentPeriod(); if (_periodEnding) { - uint256 _totalDelegatingReward = _distributeRewardToTreasuriesAndCalculateTotalDelegatingReward( - _lastPeriod, - _currentValidators - ); - _settleAndTransferDelegatingRewards(_currentValidators, _totalDelegatingReward); + ( + uint256 _totalDelegatingReward, + uint256[] memory _delegatingRewards + ) = _distributeRewardToTreasuriesAndCalculateTotalDelegatingReward(_lastPeriod, _currentValidators); + _settleAndTransferDelegatingRewards(_lastPeriod, _currentValidators, _totalDelegatingReward, _delegatingRewards); _slashIndicatorContract.updateCreditScore(_currentValidators, _lastPeriod); _currentValidators = _syncValidatorSet(_newPeriod); } @@ -212,14 +210,13 @@ contract RoninValidatorSet is _miningRewardDeprecatedAtPeriod[_validatorAddr][_period] = true; delete _miningReward[_validatorAddr]; delete _delegatingReward[_validatorAddr]; - IStaking(_stakingContract).sinkPendingReward(_validatorAddr); if (_newJailedUntil > 0) { _jailedUntil[_validatorAddr] = Math.max(_newJailedUntil, _jailedUntil[_validatorAddr]); } if (_slashAmount > 0) { - IStaking(_stakingContract).deductStakedAmount(_validatorAddr, _slashAmount); + IStaking(_stakingContract).deductStakingAmount(_validatorAddr, _slashAmount); } emit ValidatorPunished(_validatorAddr, _period, _jailedUntil[_validatorAddr], _slashAmount, true, false); @@ -516,7 +513,8 @@ contract RoninValidatorSet is function _distributeRewardToTreasuriesAndCalculateTotalDelegatingReward( uint256 _lastPeriod, address[] memory _currentValidators - ) private returns (uint256 _totalDelegatingReward) { + ) private returns (uint256 _totalDelegatingReward, uint256[] memory _delegatingRewards) { + _delegatingRewards = new uint256[](_currentValidators.length); address _consensusAddr; address payable _treasury; IBridgeTracking _bridgeTracking = _bridgeTrackingContract; @@ -550,6 +548,7 @@ contract RoninValidatorSet is if (!_jailed(_consensusAddr) && !_miningRewardDeprecated(_consensusAddr, _lastPeriod)) { _totalDelegatingReward += _delegatingReward[_consensusAddr]; + _delegatingRewards[_i] = _delegatingReward[_consensusAddr]; _distributeMiningReward(_consensusAddr, _treasury); } @@ -658,14 +657,16 @@ contract RoninValidatorSet is * Note: This method should be called once in the end of each period. * */ - function _settleAndTransferDelegatingRewards(address[] memory _currentValidators, uint256 _totalDelegatingReward) - private - { + function _settleAndTransferDelegatingRewards( + uint256 _period, + address[] memory _currentValidators, + uint256 _totalDelegatingReward, + uint256[] memory _delegatingRewards + ) private { IStaking _staking = _stakingContract; - _staking.settleRewardPools(_currentValidators); - if (_totalDelegatingReward > 0) { if (_sendRON(payable(address(_staking)), _totalDelegatingReward)) { + _staking.recordRewards(_period, _currentValidators, _delegatingRewards); emit StakingRewardDistributed(_totalDelegatingReward); return; } @@ -684,16 +685,14 @@ contract RoninValidatorSet is * */ function _syncValidatorSet(uint256 _newPeriod) private returns (address[] memory _newValidators) { - uint256[] memory _balanceWeights; - // This is a temporary approach since the slashing issue is still not finalized. + // NOTE: This is a temporary approach since the slashing issue is still not finalized. // Read more about slashing issue at: https://www.notion.so/skymavis/Slashing-Issue-9610ae1452434faca1213ab2e1d7d944 - uint256 _minBalance = _stakingContract.minValidatorBalance(); - _balanceWeights = _filterUnsatisfiedCandidates(_minBalance); + uint256[] memory _weights = _filterUnsatisfiedCandidates(); uint256[] memory _trustedWeights = _roninTrustedOrganizationContract.getConsensusWeights(_candidates); uint256 _newValidatorCount; (_newValidators, _newValidatorCount) = _pcPickValidatorSet( _candidates, - _balanceWeights, + _weights, _trustedWeights, _maxValidatorNumber, _maxPrioritizedValidatorNumber diff --git a/src/config.ts b/src/config.ts index 2127201af..8d3a6f74a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -63,7 +63,7 @@ export const maintenanceConf: MaintenanceConfig = { export const stakingConfig: StakingConfig = { [Network.Hardhat]: undefined, [Network.Devnet]: { - minValidatorBalance: BigNumber.from(10).pow(18).mul(BigNumber.from(10).pow(5)), // 100.000 RON + minValidatorStakingAmount: BigNumber.from(10).pow(18).mul(BigNumber.from(10).pow(5)), // 100.000 RON }, [Network.Testnet]: undefined, [Network.Mainnet]: undefined, @@ -147,7 +147,6 @@ export const mainchainGovernanceAdminConf: MainchainGovernanceAdminConfig = { [Network.Hardhat]: undefined, [Network.Devnet]: { roleSetter: '0x93b8eed0a1e082ae2f478fd7f8c14b1fc0261bb1', - // bridgeContract: ethers.constants.AddressZero, relayers: ['0x93b8eed0a1e082ae2f478fd7f8c14b1fc0261bb1'], }, [Network.Goerli]: undefined, diff --git a/src/deploy/proxy/staking-proxy.ts b/src/deploy/proxy/staking-proxy.ts index 38cb14b2d..82d030148 100644 --- a/src/deploy/proxy/staking-proxy.ts +++ b/src/deploy/proxy/staking-proxy.ts @@ -17,7 +17,7 @@ const deploy = async ({ getNamedAccounts, deployments }: HardhatRuntimeEnvironme const data = new Staking__factory().interface.encodeFunctionData('initialize', [ generalRoninConf[network.name]!.validatorContract?.address, - stakingConfig[network.name]!.minValidatorBalance, + stakingConfig[network.name]!.minValidatorStakingAmount, ]); const deployment = await deploy('StakingProxy', { diff --git a/src/utils.ts b/src/utils.ts index 1558ff0e0..75381d9dd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -71,7 +71,7 @@ export interface MaintenanceConfig { } export interface StakingArguments { - minValidatorBalance?: BigNumberish; + minValidatorStakingAmount?: BigNumberish; } export interface StakingConfig { diff --git a/test/governance-admin/GovernanceAdmin.test.ts b/test/governance-admin/GovernanceAdmin.test.ts index 456b6c0f5..06cc4ee71 100644 --- a/test/governance-admin/GovernanceAdmin.test.ts +++ b/test/governance-admin/GovernanceAdmin.test.ts @@ -74,7 +74,7 @@ describe('Governance Admin test', () => { stakingContract.address, 0, governanceAdminInterface.interface.encodeFunctionData('functionDelegateCall', [ - stakingContract.interface.encodeFunctionData('setMinValidatorBalance', [555]), + stakingContract.interface.encodeFunctionData('setMinValidatorStakingAmount', [555]), ]), 500_000 ); @@ -82,7 +82,7 @@ describe('Governance Admin test', () => { supports = signatures.map(() => VoteType.For); await governanceAdmin.connect(governors[0]).proposeProposalStructAndCastVotes(proposal, supports, signatures); - expect(await stakingContract.minValidatorBalance()).eq(555); + expect(await stakingContract.minValidatorStakingAmount()).eq(555); }); it('Should not be able to reuse already voted signatures or proposals', async () => { diff --git a/test/helpers/fixture.ts b/test/helpers/fixture.ts index 37f85b264..b2a0532db 100644 --- a/test/helpers/fixture.ts +++ b/test/helpers/fixture.ts @@ -62,7 +62,7 @@ export const defaultTestConfig: InitTestInput = { }, stakingArguments: { - minValidatorBalance: BigNumber.from(100), + minValidatorStakingAmount: BigNumber.from(100), }, stakingVestingArguments: { diff --git a/test/helpers/reward-calculation.ts b/test/helpers/reward-calculation.ts deleted file mode 100644 index 107fe3825..000000000 --- a/test/helpers/reward-calculation.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { expect } from 'chai'; -import { BigNumberish, ContractTransaction } from 'ethers'; - -import { expectEvent } from './utils'; -import { RewardCalculation__factory } from '../../src/types'; - -const contractInterface = RewardCalculation__factory.createInterface(); - -export const expects = { - emitSettledPoolsUpdatedEvent: async function ( - tx: ContractTransaction, - expectingPoolList?: string[], - expectingAccumulatedRpsList?: BigNumberish[] - ) { - await expectEvent( - contractInterface, - 'SettledPoolsUpdated', - tx, - (event) => { - if (expectingPoolList) { - expect(event.args[0], 'invalid pool list').eql(expectingPoolList); - } - if (expectingAccumulatedRpsList) { - expect(event.args[1], 'invalid accumulated rps list').eql(expectingAccumulatedRpsList); - } - }, - 1 - ); - }, -}; diff --git a/test/helpers/staking.ts b/test/helpers/staking.ts index 112814bf5..6383bd541 100644 --- a/test/helpers/staking.ts +++ b/test/helpers/staking.ts @@ -7,18 +7,26 @@ import { Staking__factory } from '../../src/types'; const contractInterface = Staking__factory.createInterface(); export const expects = { - emitSettledPoolsUpdatedEvent: async function ( + emitPoolsUpdatedEvent: async function ( tx: ContractTransaction, - expectingPoolAddressList: string[], - expectingAccumulatedRpsList: BigNumberish[] + expectingPeriod?: BigNumberish, + expectingPoolAddressList?: string[], + expectingAccumulatedRpsList?: BigNumberish[] ) { await expectEvent( contractInterface, - 'SettledPoolsUpdated', + 'PoolsUpdated', tx, (event) => { - expect(event.args[0], 'invalid pool address list').eql(expectingPoolAddressList); - expect(event.args[1], 'invalid accumulated rps list').eql(expectingAccumulatedRpsList); + if (!!expectingPeriod) { + expect(event.args[0], 'invalid period').eql(expectingPeriod); + } + if (!!expectingPoolAddressList) { + expect(event.args[1], 'invalid pool address list').eql(expectingPoolAddressList); + } + if (!!expectingAccumulatedRpsList) { + expect(event.args[2], 'invalid accumulated rps list').eql(expectingAccumulatedRpsList); + } }, 1 ); diff --git a/test/integration/ActionSlashValidators.test.ts b/test/integration/ActionSlashValidators.test.ts index ae490cd82..be70a6d06 100644 --- a/test/integration/ActionSlashValidators.test.ts +++ b/test/integration/ActionSlashValidators.test.ts @@ -38,7 +38,7 @@ const unavailabilityTier1Threshold = 10; const unavailabilityTier2Threshold = 20; const slashAmountForUnavailabilityTier2Threshold = BigNumber.from(1); const slashDoubleSignAmount = 1000; -const minValidatorBalance = BigNumber.from(100); +const minValidatorStakingAmount = BigNumber.from(100); describe('[Integration] Slash validators', () => { before(async () => { @@ -59,7 +59,7 @@ describe('[Integration] Slash validators', () => { }, }, stakingArguments: { - minValidatorBalance, + minValidatorStakingAmount, }, roninTrustedOrganizationArguments: { trustedOrganizations: [ @@ -131,14 +131,14 @@ describe('[Integration] Slash validators', () => { before(async () => { slasheeIdx = 2; slashee = validatorCandidates[slasheeIdx]; - slasheeInitStakingAmount = minValidatorBalance.add(slashAmountForUnavailabilityTier2Threshold.mul(10)); + slasheeInitStakingAmount = minValidatorStakingAmount.add(slashAmountForUnavailabilityTier2Threshold.mul(10)); await stakingContract .connect(slashee) .applyValidatorCandidate(slashee.address, slashee.address, slashee.address, slashee.address, 2_00, { value: slasheeInitStakingAmount, }); - expect(await stakingContract.balanceOf(slashee.address, slashee.address)).eq(slasheeInitStakingAmount); + expect(await stakingContract.stakingAmountOf(slashee.address, slashee.address)).eq(slasheeInitStakingAmount); await EpochController.setTimestampToPeriodEnding(); await mineBatchTxs(async () => { @@ -192,9 +192,11 @@ describe('[Integration] Slash validators', () => { .withArgs(slashee.address, slashAmountForUnavailabilityTier2Threshold); }); - it('Should the Staking contract subtract staked amount from validator', async () => { + it('Should the Staking contract subtract staking amount from validator', async () => { let _expectingSlasheeStakingAmount = slasheeInitStakingAmount.sub(slashAmountForUnavailabilityTier2Threshold); - expect(await stakingContract.balanceOf(slashee.address, slashee.address)).eq(_expectingSlasheeStakingAmount); + expect(await stakingContract.stakingAmountOf(slashee.address, slashee.address)).eq( + _expectingSlasheeStakingAmount + ); }); it('Should the block producer set exclude the jailed validator in the next epoch', async () => { @@ -258,7 +260,7 @@ describe('[Integration] Slash validators', () => { before(async () => { slasheeIdx = 3; slashee = validatorCandidates[slasheeIdx]; - slasheeInitStakingAmount = minValidatorBalance; + slasheeInitStakingAmount = minValidatorStakingAmount; await stakingContract .connect(slashee) @@ -266,7 +268,7 @@ describe('[Integration] Slash validators', () => { value: slasheeInitStakingAmount, }); - expect(await stakingContract.balanceOf(slashee.address, slashee.address)).eq(slasheeInitStakingAmount); + expect(await stakingContract.stakingAmountOf(slashee.address, slashee.address)).eq(slasheeInitStakingAmount); await EpochController.setTimestampToPeriodEnding(); await mineBatchTxs(async () => { @@ -281,7 +283,7 @@ describe('[Integration] Slash validators', () => { expect(await validatorContract.getValidators()).eql(expectingValidatorSet); }); - describe('Check effects on indicator and staked amount', async () => { + describe('Check effects on indicator and staking amount', async () => { it('Should the ValidatorSet contract emit event', async () => { for (let i = 0; i < unavailabilityTier2Threshold - 1; i++) { await slashContract.connect(coinbase).slashUnavailability(slashee.address); @@ -319,9 +321,11 @@ describe('[Integration] Slash validators', () => { .withArgs(slashee.address, slashAmountForUnavailabilityTier2Threshold); }); - it('Should the Staking contract subtract staked amount from validator', async () => { + it('Should the Staking contract subtract staking amount from validator', async () => { let _expectingSlasheeStakingAmount = slasheeInitStakingAmount.sub(slashAmountForUnavailabilityTier2Threshold); - expect(await stakingContract.balanceOf(slashee.address, slashee.address)).eq(_expectingSlasheeStakingAmount); + expect(await stakingContract.stakingAmountOf(slashee.address, slashee.address)).eq( + _expectingSlasheeStakingAmount + ); }); }); diff --git a/test/integration/ActionSubmitReward.test.ts b/test/integration/ActionSubmitReward.test.ts index f47c853a4..4fa68f3a3 100644 --- a/test/integration/ActionSubmitReward.test.ts +++ b/test/integration/ActionSubmitReward.test.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { network, ethers } from 'hardhat'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; import { BigNumber, ContractTransaction } from 'ethers'; import { @@ -33,7 +32,7 @@ let validatorCandidates: SignerWithAddress[]; const unavailabilityTier2Threshold = 10; const slashAmountForUnavailabilityTier2Threshold = BigNumber.from(1); const slashDoubleSignAmount = 1000; -const minValidatorBalance = BigNumber.from(100); +const minValidatorStakingAmount = BigNumber.from(100); const blockProducerBonusPerBlock = BigNumber.from(1); describe('[Integration] Submit Block Reward', () => { @@ -55,7 +54,7 @@ describe('[Integration] Submit Block Reward', () => { }, }, stakingArguments: { - minValidatorBalance, + minValidatorStakingAmount, }, stakingVestingArguments: { blockProducerBonusPerBlock, @@ -108,7 +107,7 @@ describe('[Integration] Submit Block Reward', () => { let submitRewardTx: ContractTransaction; before(async () => { - let initStakingAmount = minValidatorBalance.mul(2); + let initStakingAmount = minValidatorStakingAmount.mul(2); validator = validatorCandidates[0]; await stakingContract .connect(validator) @@ -144,10 +143,6 @@ describe('[Integration] Submit Block Reward', () => { it.skip('Should the ValidatorSetContract update mining reward', async () => {}); - it('Should the StakingContract emit event of recording reward', async () => { - await expect(submitRewardTx).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(validator.address, anyValue); - }); - it.skip('Should the StakingContract record update for new block reward', async () => {}); }); @@ -156,7 +151,7 @@ describe('[Integration] Submit Block Reward', () => { let submitRewardTx: ContractTransaction; before(async () => { - let initStakingAmount = minValidatorBalance.mul(2); + let initStakingAmount = minValidatorStakingAmount.mul(2); validator = validatorCandidates[1]; await stakingContract diff --git a/test/integration/ActionWrapUpEpoch.test.ts b/test/integration/ActionWrapUpEpoch.test.ts index 4938f4370..2309d51ed 100644 --- a/test/integration/ActionWrapUpEpoch.test.ts +++ b/test/integration/ActionWrapUpEpoch.test.ts @@ -13,7 +13,7 @@ import { RoninGovernanceAdmin, RoninGovernanceAdmin__factory, } from '../../src/types'; -import { expects as StakingExpects } from '../helpers/reward-calculation'; +import { expects as StakingExpects } from '../helpers/staking'; import { EpochController, expects as ValidatorSetExpects } from '../helpers/ronin-validator-set'; import { mineBatchTxs } from '../helpers/utils'; import { initTest } from '../helpers/fixture'; @@ -34,7 +34,7 @@ let validatorCandidates: SignerWithAddress[]; const unavailabilityTier2Threshold = 10; const slashAmountForUnavailabilityTier2Threshold = BigNumber.from(1); const slashDoubleSignAmount = 1000; -const minValidatorBalance = BigNumber.from(100); +const minValidatorStakingAmount = BigNumber.from(100); const maxValidatorNumber = 3; describe('[Integration] Wrap up epoch', () => { @@ -56,7 +56,7 @@ describe('[Integration] Wrap up epoch', () => { }, }, stakingArguments: { - minValidatorBalance, + minValidatorStakingAmount, }, roninTrustedOrganizationArguments: { trustedOrganizations: [governor].map((v) => ({ @@ -131,7 +131,7 @@ describe('[Integration] Wrap up epoch', () => { validatorCandidates[i].address, 2_00, { - value: minValidatorBalance.mul(2).add(i), + value: minValidatorStakingAmount.mul(2).add(i), } ); } @@ -158,7 +158,6 @@ describe('[Integration] Wrap up epoch', () => { await validatorContract.endEpoch(); await validatorContract.wrapUpEpoch(); let duplicatedWrapUpTx = validatorContract.wrapUpEpoch(); - await expect(duplicatedWrapUpTx).to.be.revertedWith('RoninValidatorSet: query for already wrapped up epoch'); }); }); @@ -175,8 +174,9 @@ describe('[Integration] Wrap up epoch', () => { describe('StakingContract internal actions: settle reward pool', async () => { it('Should the StakingContract emit event of settling reward', async () => { - await StakingExpects.emitSettledPoolsUpdatedEvent( + await StakingExpects.emitPoolsUpdatedEvent( wrapUpTx, + undefined, validators .slice(1, 4) .map((_) => _.address) @@ -232,7 +232,7 @@ describe('[Integration] Wrap up epoch', () => { validators[i].address, 2_00, { - value: minValidatorBalance.mul(3).add(i), + value: minValidatorStakingAmount.mul(3).add(i), } ); } diff --git a/test/integration/Configuration.test.ts b/test/integration/Configuration.test.ts index 87df09058..5646eb472 100644 --- a/test/integration/Configuration.test.ts +++ b/test/integration/Configuration.test.ts @@ -50,7 +50,7 @@ const config: InitTestInput = { }, stakingArguments: { - minValidatorBalance: BigNumber.from(100), + minValidatorStakingAmount: BigNumber.from(100), }, stakingVestingArguments: { blockProducerBonusPerBlock: 1, @@ -191,7 +191,7 @@ describe('[Integration] Configuration check', () => { it('Should the StakingContract contract set configs correctly', async () => { expect(await stakingContract.validatorContract()).to.eq(validatorContract.address); - expect(await stakingContract.minValidatorBalance()).to.eq(config.stakingArguments?.minValidatorBalance); + expect(await stakingContract.minValidatorStakingAmount()).to.eq(config.stakingArguments?.minValidatorStakingAmount); }); it('Should the StakingVestingContract contract set configs correctly', async () => { diff --git a/test/maintainance/Maintenance.test.ts b/test/maintainance/Maintenance.test.ts index 67749c752..202f43752 100644 --- a/test/maintainance/Maintenance.test.ts +++ b/test/maintainance/Maintenance.test.ts @@ -37,7 +37,7 @@ const unavailabilityTier1Threshold = 50; const unavailabilityTier2Threshold = 150; const maxValidatorNumber = 4; const numberOfBlocksInEpoch = 600; -const minValidatorBalance = BigNumber.from(100); +const minValidatorStakingAmount = BigNumber.from(100); const minMaintenanceBlockPeriod = 100; const maxMaintenanceBlockPeriod = 1000; const minOffset = 200; @@ -63,7 +63,7 @@ describe('Maintenance test', () => { }, }, stakingArguments: { - minValidatorBalance, + minValidatorStakingAmount, }, roninValidatorSetArguments: { maxValidatorNumber, @@ -105,7 +105,7 @@ describe('Maintenance test', () => { validatorCandidates[i].address, validatorCandidates[i].address, 1, - { value: minValidatorBalance.add(maxValidatorNumber).sub(i) } + { value: minValidatorStakingAmount.add(maxValidatorNumber).sub(i) } ); } diff --git a/test/slash/CreditScore.test.ts b/test/slash/CreditScore.test.ts index 9e5458916..938fb40d7 100644 --- a/test/slash/CreditScore.test.ts +++ b/test/slash/CreditScore.test.ts @@ -45,7 +45,7 @@ const unavailabilityTier1Threshold = 5; const unavailabilityTier2Threshold = 15; const slashAmountForUnavailabilityTier2Threshold = 2; -const minValidatorBalance = BigNumber.from(100); +const minValidatorStakingAmount = BigNumber.from(100); const maxValidatorCandidate = 3; const maxValidatorNumber = 2; const numberOfBlocksInEpoch = 600; @@ -126,7 +126,7 @@ describe('Credit score and bail out test', () => { }, }, stakingArguments: { - minValidatorBalance, + minValidatorStakingAmount, }, roninValidatorSetArguments: { maxValidatorNumber, @@ -170,7 +170,7 @@ describe('Credit score and bail out test', () => { validatorCandidates[i].address, validatorCandidates[i].address, 1, - { value: minValidatorBalance.mul(2).sub(i) } + { value: minValidatorStakingAmount.mul(2).sub(i) } ); } diff --git a/test/slash/SlashIndicator.test.ts b/test/slash/SlashIndicator.test.ts index e227e7e15..ae220b7af 100644 --- a/test/slash/SlashIndicator.test.ts +++ b/test/slash/SlashIndicator.test.ts @@ -39,7 +39,7 @@ const unavailabilityTier2Threshold = 10; const maxValidatorNumber = 21; const maxValidatorCandidate = 50; const numberOfBlocksInEpoch = 600; -const minValidatorBalance = BigNumber.from(100); +const minValidatorStakingAmount = BigNumber.from(100); const slashAmountForUnavailabilityTier2Threshold = BigNumber.from(2); const slashDoubleSignAmount = BigNumber.from(5); @@ -69,7 +69,7 @@ describe('Slash indicator test', () => { }, }, stakingArguments: { - minValidatorBalance, + minValidatorStakingAmount, }, roninValidatorSetArguments: { maxValidatorNumber, @@ -114,7 +114,7 @@ describe('Slash indicator test', () => { validatorCandidates[i].address, validatorCandidates[i].address, 1, - { value: minValidatorBalance.mul(2).add(maxValidatorNumber).sub(i) } + { value: minValidatorStakingAmount.mul(2).add(maxValidatorNumber).sub(i) } ); } diff --git a/test/staking/AdvancedCalculation.test.ts b/test/staking/AdvancedCalculation.test.ts deleted file mode 100644 index 102317633..000000000 --- a/test/staking/AdvancedCalculation.test.ts +++ /dev/null @@ -1,586 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { BigNumber, BigNumberish, ContractTransaction } from 'ethers'; -import { ethers, network } from 'hardhat'; - -import { MockStaking, MockStaking__factory } from '../../src/types'; -import * as StakingContract from '../helpers/staking'; - -const EPS = 1; -const MASK = BigNumber.from(10).pow(18); -const poolAddr = ethers.constants.AddressZero; - -let deployer: SignerWithAddress; -let userA: SignerWithAddress; -let userB: SignerWithAddress; -let stakingContract: MockStaking; - -const local = { - balanceA: BigNumber.from(0), - balanceB: BigNumber.from(0), - accumulatedRewardForA: BigNumber.from(0), - accumulatedRewardForB: BigNumber.from(0), - claimableRewardForA: BigNumber.from(0), - claimableRewardForB: BigNumber.from(0), - aRps: BigNumber.from(0), - settledARps: BigNumber.from(0), - syncBalance: async function () { - this.balanceA = await stakingContract.balanceOf(poolAddr, userA.address); - this.balanceB = await stakingContract.balanceOf(poolAddr, userB.address); - }, - recordReward: async function (reward: BigNumberish) { - const totalStaked = await stakingContract.totalBalance(poolAddr); - await this.syncBalance(); - this.accumulatedRewardForA = this.accumulatedRewardForA.add( - BigNumber.from(reward).mul(this.balanceA).div(totalStaked) - ); - this.accumulatedRewardForB = this.accumulatedRewardForB.add( - BigNumber.from(reward).mul(this.balanceB).div(totalStaked) - ); - this.aRps = this.aRps.add(BigNumber.from(reward).mul(MASK).div(totalStaked)); - }, - settledPools: function () { - this.claimableRewardForA = this.accumulatedRewardForA; - this.claimableRewardForB = this.accumulatedRewardForB; - this.settledARps = this.aRps; - }, - slash: function () { - this.accumulatedRewardForA = this.claimableRewardForA; - this.accumulatedRewardForB = this.claimableRewardForB; - this.aRps = this.settledARps; - }, - reset: function () { - this.claimableRewardForA = BigNumber.from(0); - this.claimableRewardForB = BigNumber.from(0); - this.accumulatedRewardForA = BigNumber.from(0); - this.accumulatedRewardForB = BigNumber.from(0); - this.aRps = BigNumber.from(0); - this.settledARps = BigNumber.from(0); - this.balanceA = BigNumber.from(0); - this.balanceB = BigNumber.from(0); - }, - claimRewardForA: function () { - this.accumulatedRewardForA = this.accumulatedRewardForA.sub(this.claimableRewardForA); - this.claimableRewardForA = BigNumber.from(0); - }, - claimRewardForB: function () { - this.accumulatedRewardForB = this.accumulatedRewardForB.sub(this.claimableRewardForB); - this.claimableRewardForB = BigNumber.from(0); - }, -}; - -const expectLocalCalculationRight = async () => { - { - const userReward = await stakingContract.getTotalReward(poolAddr, userA.address); - expect( - userReward.sub(local.accumulatedRewardForA).abs().lte(EPS), - `invalid user reward for A expected=${local.accumulatedRewardForA.toString()} actual=${userReward}` - ).to.be.true; - const claimableReward = await stakingContract.getClaimableReward(poolAddr, userA.address); - expect( - claimableReward.sub(local.claimableRewardForA).abs().lte(EPS), - `invalid claimable reward for A expected=${local.claimableRewardForA.toString()} actual=${claimableReward}` - ).to.be.true; - } - { - const userReward = await stakingContract.getTotalReward(poolAddr, userB.address); - expect( - userReward.sub(local.accumulatedRewardForB).abs().lte(EPS), - `invalid user reward for B expected=${local.accumulatedRewardForB.toString()} actual=${userReward}` - ).to.be.true; - const claimableReward = await stakingContract.getClaimableReward(poolAddr, userB.address); - expect( - claimableReward.sub(local.claimableRewardForB).abs().lte(EPS), - `invalid claimable reward for B expected=${local.claimableRewardForB.toString()} actual=${claimableReward}` - ).to.be.true; - } -}; - -describe('Advanced Calculation test', () => { - let tx: ContractTransaction; - const txs: ContractTransaction[] = []; - - before(async () => { - [deployer, userA, userB] = await ethers.getSigners(); - stakingContract = await new MockStaking__factory(deployer).deploy(poolAddr); - await network.provider.send('evm_setAutomine', [false]); - local.reset(); - }); - - after(async () => { - await network.provider.send('evm_setAutomine', [true]); - }); - - it('Should work properly with staking actions occurring sequentially for a period that will be settled', async () => { - txs[0] = await stakingContract.stake(userA.address, 100); - txs[1] = await stakingContract.stake(userB.address, 100); - await network.provider.send('evm_mine'); - await expect(txs[0]!).emit(stakingContract, 'PendingRewardUpdated').withArgs(poolAddr, userA.address, 0, 0); - await expect(txs[1]!).emit(stakingContract, 'PendingRewardUpdated').withArgs(poolAddr, userB.address, 0, 0); - - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await network.provider.send('evm_mine'); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.stake(userA.address, 200); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 1000, local.aRps.mul(local.balanceA).div(MASK)); - await expectLocalCalculationRight(); - - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.unstake(userA.address, 200); - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 1750, local.aRps.mul(local.balanceA).div(MASK)); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.stake(userA.address, 200); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 2250, local.aRps.mul(local.balanceA).div(MASK)); - await expectLocalCalculationRight(); - - tx = await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await StakingContract.expects.emitSettledPoolsUpdatedEvent(tx!, [poolAddr], [local.aRps]); - local.settledPools(); - await expectLocalCalculationRight(); - }); - - it('Should work properly with staking actions occurring sequentially for a period that will be slashed', async () => { - txs[0] = await stakingContract.stake(userA.address, 100); - await network.provider.send('evm_mine'); - await expectLocalCalculationRight(); - await expect(txs[0]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userA.address, 2250, local.aRps); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 2250, local.aRps.mul(local.balanceA).div(MASK)); - - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expectLocalCalculationRight(); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - - txs[0] = await stakingContract.stake(userA.address, 300); - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 3850, local.aRps.mul(local.balanceA).div(MASK)); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.slash(); - await network.provider.send('evm_mine'); - local.slash(); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.recordReward(0); - await network.provider.send('evm_mine'); - await local.recordReward(0); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - await network.provider.send('evm_mine'); - await network.provider.send('evm_mine'); - await network.provider.send('evm_mine'); - await network.provider.send('evm_mine'); - - txs[0] = await stakingContract.unstake(userA.address, 300); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 2250, local.aRps.mul(local.balanceA).div(MASK)); - txs[0] = await stakingContract.unstake(userA.address, 50); - await network.provider.send('evm_mine'); - await expectLocalCalculationRight(); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 2250, local.aRps.mul(local.balanceA).div(MASK)); - - tx = await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await expectLocalCalculationRight(); - await StakingContract.expects.emitSettledPoolsUpdatedEvent(tx!, [poolAddr], [local.aRps]); - }); - - it('Should work properly with staking actions occurring sequentially for a period that will be slashed again', async () => { - txs[0] = await stakingContract.stake(userA.address, 50); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 2250, local.aRps.mul(local.balanceA).div(MASK)); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.claimReward(userA.address); - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await expect(txs[0]!) - .emit(stakingContract, 'RewardClaimed') - .withArgs(poolAddr, userA.address, local.claimableRewardForA); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 2250, local.aRps.mul(local.balanceA).div(MASK).add(local.claimableRewardForA)); - await expect(txs[0]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userA.address, 0, local.settledARps); - local.claimRewardForA(); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.stake(userA.address, 300); - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 1600, local.aRps.mul(local.balanceA).div(MASK)); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.slash(); - await network.provider.send('evm_mine'); - local.slash(); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.recordReward(0); - await network.provider.send('evm_mine'); - await local.recordReward(0); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - await network.provider.send('evm_mine'); - await network.provider.send('evm_mine'); - await network.provider.send('evm_mine'); - await network.provider.send('evm_mine'); - - txs[0] = await stakingContract.unstake(userA.address, 300); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 0, local.aRps.mul(local.balanceA).div(MASK)); - - txs[0] = await stakingContract.unstake(userA.address, 100); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 0, local.aRps.mul(local.balanceA).div(MASK)); - await expectLocalCalculationRight(); - - txs[1] = await stakingContract.claimReward(userB.address); - tx = await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await expect(txs[1]!) - .emit(stakingContract, 'RewardClaimed') - .withArgs(poolAddr, userB.address, local.claimableRewardForB); - await expect(txs[1]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userB.address, 0, local.aRps.mul(local.balanceB).div(MASK)); - await expect(txs[1]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userB.address, 0, local.settledARps); - local.claimRewardForB(); - await StakingContract.expects.emitSettledPoolsUpdatedEvent(tx!, [poolAddr], [local.aRps]); - local.settledPools(); - await expectLocalCalculationRight(); - }); - - it('Should be able to calculate right reward after claiming', async () => { - const lastCredited = local.aRps.mul(300).div(MASK); - - txs[0] = await stakingContract.recordReward(1000); - txs[1] = await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expect(txs[0]!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await StakingContract.expects.emitSettledPoolsUpdatedEvent(txs[1]!, [poolAddr], [local.aRps]); - local.settledPools(); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.claimReward(userA.address); - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await expect(txs[0]!) - .emit(stakingContract, 'RewardClaimed') - .withArgs(poolAddr, userA.address, local.claimableRewardForA); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 0, lastCredited.add(local.claimableRewardForA)); - await expect(txs[0]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userA.address, 0, local.aRps); - local.claimRewardForA(); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.claimReward(userA.address); - await network.provider.send('evm_mine'); - local.claimRewardForA(); - await expectLocalCalculationRight(); - await expect(txs[0]!).emit(stakingContract, 'RewardClaimed').withArgs(poolAddr, userA.address, 0); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 0, lastCredited.add(750)); - await expect(txs[0]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userA.address, 0, local.settledARps); - - txs[1] = await stakingContract.claimReward(userB.address); - tx = await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await expect(txs[1]!).emit(stakingContract, 'RewardClaimed').withArgs(poolAddr, userB.address, 250); - await expect(txs[1]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userB.address, 0, 1750 + 250); - await expect(txs[1]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userB.address, 0, local.settledARps); - local.claimRewardForB(); - local.settledPools(); - await StakingContract.expects.emitSettledPoolsUpdatedEvent(tx!, [poolAddr], [local.settledARps]); - await expectLocalCalculationRight(); - }); - - it('Should work properly with staking actions from multi-users occurring in the same block', async () => { - txs[0] = await stakingContract.stake(userA.address, 100); - await network.provider.send('evm_mine'); - await expect(txs[0]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userA.address, local.claimableRewardForA, local.settledARps); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, local.claimableRewardForA, local.aRps.mul(local.balanceA).div(MASK)); - - txs[0] = await stakingContract.stake(userA.address, 300); - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.syncBalance(); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 750, local.aRps.mul(local.balanceA).div(MASK)); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - txs[1] = await stakingContract.stake(userB.address, 200); - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await expect(txs[1]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userB.address, local.claimableRewardForB, local.settledARps); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - txs[1] = await stakingContract.unstake(userB.address, 200); - txs[0] = await stakingContract.unstake(userA.address, 400); - tx = await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.syncBalance(); - let lastCreditedB = local.aRps.mul(local.balanceB).div(MASK); - let lastCreditedA = local.aRps.mul(local.balanceA).div(MASK); - await expect(txs[1]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userB.address, local.accumulatedRewardForB, lastCreditedB); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 3725, lastCreditedA); - await local.recordReward(1000); - await expect(tx!).to.emit(stakingContract, 'PendingPoolUpdated').withArgs(poolAddr, local.aRps); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.claimReward(userA.address); - txs[1] = await stakingContract.claimReward(userB.address); - await network.provider.send('evm_mine'); - lastCreditedA = lastCreditedA.add(local.claimableRewardForA); - await expect(txs[0]!) - .emit(stakingContract, 'RewardClaimed') - .withArgs(poolAddr, userA.address, local.claimableRewardForA); - await expect(txs[0]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userA.address, 0, local.settledARps); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 3725, lastCreditedA); - lastCreditedB = lastCreditedB.add(local.claimableRewardForB); - await expect(txs[1]!) - .emit(stakingContract, 'RewardClaimed') - .withArgs(poolAddr, userB.address, local.claimableRewardForB); - await expect(txs[1]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userB.address, 0, local.settledARps); - await expect(txs[1]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userB.address, 1275, lastCreditedB); - local.claimRewardForA(); - local.claimRewardForB(); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.unstake(userA.address, 200); - tx = await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await local.syncBalance(); - lastCreditedA = local.balanceA.mul(local.aRps).div(MASK); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, local.accumulatedRewardForA, lastCreditedA); - local.settledPools(); - await StakingContract.expects.emitSettledPoolsUpdatedEvent(tx!, [poolAddr], [local.settledARps]); - await expectLocalCalculationRight(); - - txs[0] = await stakingContract.claimReward(userA.address); - txs[1] = await stakingContract.claimReward(userB.address); - await network.provider.send('evm_mine'); - lastCreditedA = lastCreditedA.add(local.claimableRewardForA); - await expect(txs[0]!) - .emit(stakingContract, 'RewardClaimed') - .withArgs(poolAddr, userA.address, local.claimableRewardForA); - await expect(txs[0]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userA.address, 0, local.settledARps); - await expect(txs[0]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userA.address, 3725, lastCreditedA); - lastCreditedB = lastCreditedB.add(local.claimableRewardForB); - await expect(txs[1]!) - .emit(stakingContract, 'RewardClaimed') - .withArgs(poolAddr, userB.address, local.claimableRewardForB); - await expect(txs[1]!) - .emit(stakingContract, 'SettledRewardUpdated') - .withArgs(poolAddr, userB.address, 0, local.settledARps); - await expect(txs[1]!) - .emit(stakingContract, 'PendingRewardUpdated') - .withArgs(poolAddr, userB.address, 1275, lastCreditedB); - local.claimRewardForA(); - local.claimRewardForB(); - await expectLocalCalculationRight(); - }); - - it('Should work properly with staking actions occurring in the same block', async () => { - await stakingContract.stake(userA.address, 100); - await stakingContract.unstake(userA.address, 100); - await stakingContract.stake(userA.address, 100); - await stakingContract.stake(userB.address, 200); - await stakingContract.unstake(userB.address, 200); - await stakingContract.recordReward(1000); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - local.settledPools(); - await expectLocalCalculationRight(); - - await stakingContract.recordReward(1000); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - await expectLocalCalculationRight(); - - await stakingContract.slash(); - await network.provider.send('evm_mine'); - local.slash(); - await expectLocalCalculationRight(); - - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await expectLocalCalculationRight(); - - await stakingContract.recordReward(1000); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - local.settledPools(); - await expectLocalCalculationRight(); - - await stakingContract.recordReward(1000); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await network.provider.send('evm_mine'); - await local.recordReward(1000); - local.settledPools(); - await expectLocalCalculationRight(); - - await stakingContract.claimReward(userA.address); - await stakingContract.claimReward(userA.address); - await stakingContract.claimReward(userB.address); - await stakingContract.claimReward(userB.address); - await stakingContract.claimReward(userB.address); - await network.provider.send('evm_mine'); - local.claimRewardForA(); - local.claimRewardForB(); - await expectLocalCalculationRight(); - }); -}); diff --git a/test/staking/RewardAmountCalculation.test.ts b/test/staking/RewardAmountCalculation.test.ts deleted file mode 100644 index 04bddebae..000000000 --- a/test/staking/RewardAmountCalculation.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { ethers } from 'hardhat'; - -import { MockStaking, MockStaking__factory } from '../../src/types'; - -const poolAddr = ethers.constants.AddressZero; - -let deployer: SignerWithAddress; -let userA: SignerWithAddress; -let userB: SignerWithAddress; -let stakingContract: MockStaking; - -const setupNormalCase = async (stakingContract: MockStaking) => { - await stakingContract.stake(userA.address, 100); - await stakingContract.stake(userB.address, 100); - await stakingContract.recordReward(1000); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); -}; - -const expectPendingRewards = async (expectingA: number, expectingB: number) => { - expect(await stakingContract.getPendingReward(poolAddr, userA.address)).eq(expectingA); - expect(await stakingContract.getPendingReward(poolAddr, userB.address)).eq(expectingB); -}; - -const expectClaimableRewards = async (expectingA: number, expectingB: number) => { - expect(await stakingContract.getClaimableReward(poolAddr, userA.address)).eq(expectingA); - expect(await stakingContract.getClaimableReward(poolAddr, userB.address)).eq(expectingB); -}; - -describe('Claimable/Pending Reward Calculation test', () => { - before(async () => { - [deployer, userA, userB] = await ethers.getSigners(); - stakingContract = await new MockStaking__factory(deployer).deploy(poolAddr); - }); - - it('Should calculate correctly the claimable reward in the normal case', async () => { - await setupNormalCase(stakingContract); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - }); - - describe('Interaction with a pool that will be settled', async () => { - describe('One interaction per period', async () => { - before(async () => { - stakingContract = await new MockStaking__factory(deployer).deploy(poolAddr); - await setupNormalCase(stakingContract); - }); - - it('Should the claimable reward not change when the user interacts in the pending period', async () => { - await stakingContract.stake(userA.address, 200); - await stakingContract.recordReward(1000); - await expectClaimableRewards(500, 500); - await expectPendingRewards(750, 250); - }); - - it('Should the claimable reward increase when the pool is settled', async () => { - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750, 500 + 250); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward be still, no matter whether the pool is slashed or settled', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750, 500 + 250); - await expectPendingRewards(0, 0); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750, 500 + 250); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750, 500 + 250); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750, 500 + 250); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward increase when the pool is settled', async () => { - await stakingContract.recordReward(1000); - await expectPendingRewards(750, 250); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750 + 750, 500 + 250 + 250); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward be still, no matter whether the pool is slashed or settled', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750 + 750, 500 + 250 + 250); - await expectPendingRewards(0, 0); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750 + 750, 500 + 250 + 250); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750 + 750, 500 + 250 + 250); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 750 + 750, 500 + 250 + 250); - await expectPendingRewards(0, 0); - }); - }); - - describe('Many interactions per period', async () => { - before(async () => { - stakingContract = await new MockStaking__factory(deployer).deploy(poolAddr); - await setupNormalCase(stakingContract); - }); - - it('Should the claimable reward not change when the user interacts in the pending period', async () => { - await stakingContract.stake(userA.address, 200); - await stakingContract.stake(userA.address, 500); - await stakingContract.stake(userA.address, 100); - await stakingContract.recordReward(1000); - await stakingContract.stake(userA.address, 800); - await expectClaimableRewards(500, 500); - await expectPendingRewards(900, 100); - }); - - it('Should the claimable reward increase when the pool is settled', async () => { - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - }); - - it('Should the claimable reward be still, no matter whether the pool is slashed or settled', async () => { - await stakingContract.slash(); - await stakingContract.unstake(userA.address, 800); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward increase when the pool is settled', async () => { - await stakingContract.recordReward(1000); - await expectPendingRewards(900, 100); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900 + 900, 500 + 100 + 100); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward be still, no matter whether the pool is slashed or settled', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900 + 900, 500 + 100 + 100); - await expectPendingRewards(0, 0); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900 + 900, 500 + 100 + 100); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900 + 900, 500 + 100 + 100); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900 + 900, 500 + 100 + 100); - await expectPendingRewards(0, 0); - }); - }); - }); - - describe('Interaction with a pool that will be slashed', async () => { - describe('One interaction per period', async () => { - before(async () => { - stakingContract = await new MockStaking__factory(deployer).deploy(poolAddr); - await setupNormalCase(stakingContract); - }); - - it('Should the claimable reward not change when the user interacts in the pending period', async () => { - await stakingContract.stake(userA.address, 200); - await stakingContract.recordReward(1000); - await stakingContract.stake(userA.address, 600); - await expectClaimableRewards(500, 500); - await expectPendingRewards(750, 250); - }); - - it('Should the claimable reward not change when the pool is slashed', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward be still, no matter whether the pool is slashed or settled', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward increase when the pool records reward', async () => { - await stakingContract.recordReward(1000); - await expectPendingRewards(900, 100); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward be still, no matter whether the pool is slashed or settled', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - }); - }); - - describe('Many interactions per period', async () => { - before(async () => { - stakingContract = await new MockStaking__factory(deployer).deploy(poolAddr); - await setupNormalCase(stakingContract); - }); - - it('Should the claimable reward not change when the user interacts in the pending period', async () => { - await stakingContract.stake(userA.address, 200); - await stakingContract.unstake(userA.address, 150); - await stakingContract.unstake(userA.address, 50); - await stakingContract.recordReward(1000); - await stakingContract.stake(userA.address, 500); - await stakingContract.stake(userA.address, 300); - await expectClaimableRewards(500, 500); - await expectPendingRewards(500, 500); - }); - - it('Should the claimable reward not change when the pool is slashed', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward be still, no matter whether the pool is slashed or settled', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500, 500); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward increase when the pool records reward', async () => { - await stakingContract.recordReward(1000); - await expectPendingRewards(900, 100); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - }); - - it('Should the claimable reward be still, no matter whether the pool is slashed or settled', async () => { - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.settledPools([poolAddr]); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - await stakingContract.slash(); - await stakingContract.endPeriod(); - await expectClaimableRewards(500 + 900, 500 + 100); - await expectPendingRewards(0, 0); - }); - }); - }); -}); diff --git a/test/staking/RewardCalculation.test.ts b/test/staking/RewardCalculation.test.ts new file mode 100644 index 000000000..041e404e0 --- /dev/null +++ b/test/staking/RewardCalculation.test.ts @@ -0,0 +1,221 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { BigNumber, ContractTransaction } from 'ethers'; +import { ethers } from 'hardhat'; +import { expect } from 'chai'; + +import { MockStaking, MockStaking__factory } from '../../src/types'; + +const MASK = BigNumber.from(10).pow(18); +const poolAddr = ethers.constants.AddressZero; + +let period = 1; +let deployer: SignerWithAddress; +let userA: SignerWithAddress; +let userB: SignerWithAddress; +let stakingContract: MockStaking; +let aRps: BigNumber; + +describe('Reward Calculation test', () => { + let tx: ContractTransaction; + const txs: ContractTransaction[] = []; + + before(async () => { + [deployer, userA, userB] = await ethers.getSigners(); + stakingContract = await new MockStaking__factory(deployer).deploy(poolAddr); + }); + + describe('Before the first wrap up', () => { + it('Should be able to stake/unstake before the first period', async () => { + txs[0] = await stakingContract.stake(userA.address, 500); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + txs[0] = await stakingContract.unstake(userA.address, 450); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + txs[0] = await stakingContract.stake(userA.address, 50); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + expect(await stakingContract.stakingAmountOf(poolAddr, userA.address)).eq(100); + }); + + it('Should be able to wrap up period for the first period', async () => { + await stakingContract.firstEverWrapup(); + period = (await stakingContract.lastUpdatedPeriod()).toNumber(); + }); + }); + + describe('Period: x+0 -> x+1', () => { + it('Should be able to unstake/stake at the first period', async () => { + txs[0] = await stakingContract.unstake(userA.address, 50); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + await expect(txs[0]).emit(stakingContract, 'PoolSharesUpdated').withArgs(period, poolAddr, 50); + txs[0] = await stakingContract.stake(userA.address, 50); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + expect(await stakingContract.stakingAmountOf(poolAddr, userA.address)).eq(100); + }); + + it('Should be able to record reward for the pool', async () => { + await stakingContract.increaseReward(1000); + await stakingContract.decreaseReward(500); + aRps = MASK.mul(500 / 50); + tx = await stakingContract.endPeriod(); // period = 1 + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [100]); + expect(await stakingContract.getReward(poolAddr, userA.address)).eq(500); + }); + }); + + describe('Period: x+1 -> x+2', () => { + it('Should not be able to record reward with invalid arguments', async () => { + tx = await stakingContract.recordRewards([poolAddr], [100, 100]); + await expect(tx).emit(stakingContract, 'PoolsUpdateFailed').withArgs(period, [poolAddr], [100, 100]); + }); + + it('Should not be able to record reward more than once for a pool', async () => { + aRps = aRps.add(MASK.mul(1000 / 100)); + tx = await stakingContract.recordRewards([poolAddr], [1000]); + await expect(tx).emit(stakingContract, 'PoolsUpdated').withArgs(period, [poolAddr], [aRps], [100]); + tx = await stakingContract.recordRewards([poolAddr], [1000]); + await expect(tx).emit(stakingContract, 'PoolUpdateConflicted').withArgs(period, poolAddr); + await stakingContract.increasePeriod(); // period = 2 + period++; + expect(await stakingContract.getReward(poolAddr, userA.address)).eq(1500); // 1000 + 500 from the last period + }); + }); + + describe('Period: x+2 -> x+3', () => { + it('Should be able to change the staking amount and the reward moved into the debited part', async () => { + txs[0] = await stakingContract.stake(userA.address, 200); + await expect(txs[0]).emit(stakingContract, 'UserRewardUpdated').withArgs(poolAddr, userA.address, 1500); + txs[0] = await stakingContract.unstake(userA.address, 100); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + }); + + it('Should be able to claim the earned reward', async () => { + txs[0] = await stakingContract.claimReward(userA.address); + await expect(txs[0]).emit(stakingContract, 'RewardClaimed').withArgs(poolAddr, userA.address, 1500); + await expect(txs[0]).emit(stakingContract, 'UserRewardUpdated').withArgs(poolAddr, userA.address, 0); + }); + + it('Should be able to change the staking amount and the debited part is empty', async () => { + txs[0] = await stakingContract.stake(userA.address, 200); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + txs[0] = await stakingContract.unstake(userA.address, 350); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + await expect(txs[0]).emit(stakingContract, 'PoolSharesUpdated').withArgs(period, poolAddr, 50); + expect(await stakingContract.stakingAmountOf(poolAddr, userA.address)).eq(50); + txs[0] = await stakingContract.stake(userA.address, 250); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + + txs[1] = await stakingContract.stake(userB.address, 200); + await expect(txs[1]).not.emit(stakingContract, 'UserRewardUpdated'); + expect(await stakingContract.stakingAmountOf(poolAddr, userB.address)).eq(200); + }); + + it('Should be able to distribute reward based on the smallest amount in the last period', async () => { + aRps = aRps.add(MASK.mul(1000 / 50)); + await stakingContract.increaseReward(1000); + tx = await stakingContract.endPeriod(); // period = 3 + expect(await stakingContract.getReward(poolAddr, userA.address)).eq(1000); + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [await stakingContract.stakingTotal(poolAddr)]); + }); + }); + + describe('Period: x+3 -> x+10', () => { + it('Should be able to get right reward', async () => { + aRps = aRps.add(MASK.mul(1000 / 500)); + await stakingContract.increaseReward(1000); + tx = await stakingContract.endPeriod(); // period 4 + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [500]); + + expect(await stakingContract.getReward(poolAddr, userA.address)).eq(1600); // 3/5 of 1000 + 1000 from the last period + expect(await stakingContract.getReward(poolAddr, userB.address)).eq(400); // 2/5 of 1000 + }); + + it('Should be able to unstake and receives reward based on the smallest amount in the last period', async () => { + txs[0] = await stakingContract.unstake(userA.address, 250); + await expect(txs[0]).emit(stakingContract, 'UserRewardUpdated').withArgs(poolAddr, userA.address, 1600); + await expect(txs[0]).emit(stakingContract, 'PoolSharesUpdated').withArgs(period, poolAddr, 250); + expect(await stakingContract.stakingAmountOf(poolAddr, userA.address)).eq(50); + + txs[1] = await stakingContract.unstake(userB.address, 150); + await expect(txs[1]).emit(stakingContract, 'UserRewardUpdated').withArgs(poolAddr, userB.address, 400); + await expect(txs[1]).emit(stakingContract, 'PoolSharesUpdated').withArgs(period, poolAddr, 100); + expect(await stakingContract.stakingAmountOf(poolAddr, userB.address)).eq(50); + + aRps = aRps.add(MASK.mul(1000 / 100)); + await stakingContract.increaseReward(1000); + tx = await stakingContract.endPeriod(); // period 5 + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [await stakingContract.stakingTotal(poolAddr)]); + expect(await stakingContract.getReward(poolAddr, userA.address)).eq(2100); // 50% of 1000 + 1600 from the last period + expect(await stakingContract.getReward(poolAddr, userB.address)).eq(900); // 50% of 1000 + 400 from the last period + }); + + it('Should not distribute reward for the ones who unstake all in the period', async () => { + txs[1] = await stakingContract.unstake(userB.address, 50); + await expect(txs[1]).emit(stakingContract, 'PoolSharesUpdated').withArgs(period, poolAddr, 50); + await expect(txs[1]).emit(stakingContract, 'UserRewardUpdated').withArgs(poolAddr, userB.address, 900); + expect(await stakingContract.stakingAmountOf(poolAddr, userB.address)).eq(0); + + aRps = aRps.add(MASK.mul(1000 / 50)); + await stakingContract.increaseReward(1000); + tx = await stakingContract.endPeriod(); // period 6 + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [await stakingContract.stakingTotal(poolAddr)]); + expect(await stakingContract.getReward(poolAddr, userA.address)).eq(3100); // 1000 + 2100 from the last period + }); + + it('The pool should be fine when no one stakes', async () => { + txs[0] = await stakingContract.unstake(userA.address, 50); + await expect(txs[0]).emit(stakingContract, 'PoolSharesUpdated').withArgs(period, poolAddr, 0); + await expect(txs[0]).emit(stakingContract, 'UserRewardUpdated').withArgs(poolAddr, userA.address, 3100); + expect(await stakingContract.stakingAmountOf(poolAddr, userA.address)).eq(0); + expect(await stakingContract.stakingAmountOf(poolAddr, userB.address)).eq(0); + + aRps = aRps.add(0); + await stakingContract.increaseReward(1000); + tx = await stakingContract.endPeriod(); // period 7 + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [0]); + expect(await stakingContract.getReward(poolAddr, userA.address)).eq(3100); + expect(await stakingContract.getReward(poolAddr, userB.address)).eq(900); + }); + + it('The rewards should be still when the pool has no reward for multi periods', async () => { + tx = await stakingContract.endPeriod(); // period 8 + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [0]); + tx = await stakingContract.endPeriod(); // period 9 + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [0]); + tx = await stakingContract.endPeriod(); // period 10 + await expect(tx) + .emit(stakingContract, 'PoolsUpdated') + .withArgs(period++, [poolAddr], [aRps], [0]); + expect(await stakingContract.getReward(poolAddr, userA.address)).eq(3100); + expect(await stakingContract.getReward(poolAddr, userB.address)).eq(900); + }); + + it('Should be able to claim reward after all', async () => { + txs[0] = await stakingContract.unstake(userA.address, 0); + await expect(txs[0]).not.emit(stakingContract, 'UserRewardUpdated'); + txs[0] = await stakingContract.claimReward(userA.address); + await expect(txs[0]).emit(stakingContract, 'RewardClaimed').withArgs(poolAddr, userA.address, 3100); + await expect(txs[0]).emit(stakingContract, 'UserRewardUpdated').withArgs(poolAddr, userA.address, 0); + + txs[1] = await stakingContract.claimReward(userB.address); + await expect(txs[1]).emit(stakingContract, 'RewardClaimed').withArgs(poolAddr, userB.address, 900); + await expect(txs[1]).emit(stakingContract, 'UserRewardUpdated').withArgs(poolAddr, userB.address, 0); + txs[1] = await stakingContract.unstake(userA.address, 0); + await expect(txs[1]).not.emit(stakingContract, 'UserRewardUpdated'); + }); + }); +}); diff --git a/test/staking/Staking.test.ts b/test/staking/Staking.test.ts index 4285f9101..0af2b3f8c 100644 --- a/test/staking/Staking.test.ts +++ b/test/staking/Staking.test.ts @@ -18,7 +18,7 @@ let validatorContract: MockValidatorSet; let stakingContract: Staking; let validatorCandidates: SignerWithAddress[]; -const minValidatorBalance = BigNumber.from(20); +const minValidatorStakingAmount = BigNumber.from(20); const maxValidatorCandidate = 50; const numberOfBlocksInEpoch = 2; @@ -42,7 +42,7 @@ describe('Staking test', () => { const proxyContract = await new TransparentUpgradeableProxyV2__factory(deployer).deploy( logicContract.address, proxyAdmin.address, - logicContract.interface.encodeFunctionData('initialize', [validatorContract.address, minValidatorBalance]) + logicContract.interface.encodeFunctionData('initialize', [validatorContract.address, minValidatorStakingAmount]) ); await proxyContract.deployed(); stakingContract = Staking__factory.connect(proxyContract.address, deployer); @@ -67,13 +67,13 @@ describe('Staking test', () => { candidate.address, candidate.address, 1, - /* 0.01% */ { value: minValidatorBalance.mul(2) } + /* 0.01% */ { value: minValidatorStakingAmount.mul(2) } ); await expect(tx).emit(stakingContract, 'PoolApproved').withArgs(candidate.address, candidate.address); } poolAddr = validatorCandidates[1]; - expect(await stakingContract.totalBalance(poolAddr.address)).eq(minValidatorBalance.mul(2)); + expect(await stakingContract.stakingTotal(poolAddr.address)).eq(minValidatorStakingAmount.mul(2)); }); it('Should not be able to propose validator again', async () => { @@ -81,7 +81,7 @@ describe('Staking test', () => { stakingContract .connect(poolAddr) .applyValidatorCandidate(poolAddr.address, poolAddr.address, poolAddr.address, poolAddr.address, 0, { - value: minValidatorBalance, + value: minValidatorStakingAmount, }) ).revertedWith('CandidateManager: query for already existent candidate'); }); @@ -106,25 +106,25 @@ describe('Staking test', () => { let tx: ContractTransaction; tx = await stakingContract.connect(poolAddr).stake(poolAddr.address, { value: 1 }); await expect(tx!).emit(stakingContract, 'Staked').withArgs(poolAddr.address, 1); - expect(await stakingContract.totalBalance(poolAddr.address)).eq(minValidatorBalance.mul(2).add(1)); + expect(await stakingContract.stakingTotal(poolAddr.address)).eq(minValidatorStakingAmount.mul(2).add(1)); tx = await stakingContract.connect(poolAddr).unstake(poolAddr.address, 1); await expect(tx!).emit(stakingContract, 'Unstaked').withArgs(poolAddr.address, 1); - expect(await stakingContract.totalBalance(poolAddr.address)).eq(minValidatorBalance.mul(2)); - expect(await stakingContract.balanceOf(poolAddr.address, poolAddr.address)).eq(minValidatorBalance.mul(2)); + expect(await stakingContract.stakingTotal(poolAddr.address)).eq(minValidatorStakingAmount.mul(2)); + expect(await stakingContract.stakingAmountOf(poolAddr.address, poolAddr.address)).eq(minValidatorStakingAmount.mul(2)); }); it('[Delegator] Should be able to delegate/undelegate to a validator candidate', async () => { await stakingContract.delegate(poolAddr.address, { value: 10 }); - expect(await stakingContract.balanceOf(poolAddr.address, deployer.address)).eq(10); + expect(await stakingContract.stakingAmountOf(poolAddr.address, deployer.address)).eq(10); await stakingContract.undelegate(poolAddr.address, 1); - expect(await stakingContract.balanceOf(poolAddr.address, deployer.address)).eq(9); + expect(await stakingContract.stakingAmountOf(poolAddr.address, deployer.address)).eq(9); }); it('Should be not able to unstake with the balance left is not larger than the minimum balance threshold', async () => { await expect( - stakingContract.connect(poolAddr).unstake(poolAddr.address, minValidatorBalance.add(1)) - ).revertedWith('StakingManager: invalid staked amount left'); + stakingContract.connect(poolAddr).unstake(poolAddr.address, minValidatorStakingAmount.add(1)) + ).revertedWith('StakingManager: invalid staking amount left'); }); it('Should not be able to request renounce using unauthorized account', async () => { @@ -149,13 +149,14 @@ describe('Staking test', () => { ethers.utils.hexStripZeros(BigNumber.from(numberOfBlocksInEpoch).toHexString()), '0x0', ]); - const stakedAmount = minValidatorBalance.mul(2); + const stakingAmount = minValidatorStakingAmount.mul(2); expect(await stakingContract.getStakingPool(poolAddr.address)).eql([ poolAddr.address, - stakedAmount, - stakedAmount.add(9), + stakingAmount, + stakingAmount.add(9), ]); - await expect(() => validatorContract.wrapUpEpoch()).changeEtherBalance(poolAddr, stakedAmount); + + await expect(() => validatorContract.wrapUpEpoch()).changeEtherBalance(poolAddr, stakingAmount); await expect(stakingContract.getStakingPool(poolAddr.address)).revertedWith( 'StakingManager: query for non-existent pool' ); @@ -169,7 +170,7 @@ describe('Staking test', () => { it('Should be able to undelegate from a deprecated validator candidate', async () => { await stakingContract.undelegate(poolAddr.address, 1); - expect(await stakingContract.balanceOf(poolAddr.address, deployer.address)).eq(8); + expect(await stakingContract.stakingAmountOf(poolAddr.address, deployer.address)).eq(8); }); it('Should not be able to delegate to a deprecated pool', async () => { @@ -201,18 +202,18 @@ describe('Staking test', () => { tx = await stakingContract.connect(userB).delegate(otherPoolAddr.address, { value: 1 }); await expect(tx!).emit(stakingContract, 'Delegated').withArgs(userB.address, otherPoolAddr.address, 1); - expect(await stakingContract.totalBalance(otherPoolAddr.address)).eq(minValidatorBalance.mul(2).add(2)); + expect(await stakingContract.stakingTotal(otherPoolAddr.address)).eq(minValidatorStakingAmount.mul(2).add(2)); tx = await stakingContract.connect(userA).undelegate(otherPoolAddr.address, 1); await expect(tx!).emit(stakingContract, 'Undelegated').withArgs(userA.address, otherPoolAddr.address, 1); - expect(await stakingContract.totalBalance(otherPoolAddr.address)).eq(minValidatorBalance.mul(2).add(1)); + expect(await stakingContract.stakingTotal(otherPoolAddr.address)).eq(minValidatorStakingAmount.mul(2).add(1)); }); it('Should not be able to undelegate with empty amount', async () => { await expect(stakingContract.undelegate(otherPoolAddr.address, 0)).revertedWith('StakingManager: invalid amount'); }); - it('Should not be able to undelegate more than the delegated amount', async () => { + it('Should not be able to undelegate more than the delegating amount', async () => { await expect(stakingContract.undelegate(otherPoolAddr.address, 1000)).revertedWith( 'StakingManager: insufficient amount to undelegate' ); @@ -227,30 +228,30 @@ describe('Staking test', () => { poolAddr.address, poolAddr.address, 2, - /* 0.02% */ { value: minValidatorBalance } + /* 0.02% */ { value: minValidatorStakingAmount } ); expect(await stakingContract.getStakingPool(poolAddr.address)).eql([ poolAddr.address, - minValidatorBalance, - minValidatorBalance.add(8), + minValidatorStakingAmount, + minValidatorStakingAmount.add(8), ]); - expect(await stakingContract.balanceOf(poolAddr.address, deployer.address)).eq(8); + expect(await stakingContract.stakingAmountOf(poolAddr.address, deployer.address)).eq(8); }); it('Should be able to delegate/undelegate for the rejoined candidate', async () => { await stakingContract.delegate(poolAddr.address, { value: 2 }); - expect(await stakingContract.balanceOf(poolAddr.address, deployer.address)).eq(10); + expect(await stakingContract.stakingAmountOf(poolAddr.address, deployer.address)).eq(10); await stakingContract.connect(userA).delegate(poolAddr.address, { value: 2 }); await stakingContract.connect(userB).delegate(poolAddr.address, { value: 2 }); expect( - await stakingContract.bulkBalanceOf([poolAddr.address, poolAddr.address], [userA.address, userB.address]) + await stakingContract.bulkStakingAmountOf([poolAddr.address, poolAddr.address], [userA.address, userB.address]) ).eql([2, 2].map(BigNumber.from)); await stakingContract.connect(userA).undelegate(poolAddr.address, 2); await stakingContract.connect(userB).undelegate(poolAddr.address, 1); expect( - await stakingContract.bulkBalanceOf([poolAddr.address, poolAddr.address], [userA.address, userB.address]) + await stakingContract.bulkStakingAmountOf([poolAddr.address, poolAddr.address], [userA.address, userB.address]) ).eql([0, 1].map(BigNumber.from)); }); }); diff --git a/test/validator/RoninValidatorSet.test.ts b/test/validator/RoninValidatorSet.test.ts index 310617b8b..3043942b7 100644 --- a/test/validator/RoninValidatorSet.test.ts +++ b/test/validator/RoninValidatorSet.test.ts @@ -44,7 +44,7 @@ const localValidatorCandidatesLength = 5; const slashAmountForUnavailabilityTier2Threshold = 100; const maxValidatorNumber = 4; const maxValidatorCandidate = 100; -const minValidatorBalance = BigNumber.from(20000); +const minValidatorStakingAmount = BigNumber.from(20000); const blockProducerBonusPerBlock = BigNumber.from(5000); const bridgeOperatorBonusPerBlock = BigNumber.from(37); const zeroTopUpAmount = 0; @@ -69,7 +69,7 @@ describe('Ronin Validator Set test', () => { }, }, stakingArguments: { - minValidatorBalance, + minValidatorStakingAmount, }, stakingVestingArguments: { blockProducerBonusPerBlock, @@ -151,7 +151,7 @@ describe('Ronin Validator Set test', () => { validatorCandidates[i].address, 2_00, { - value: minValidatorBalance.add(i), + value: minValidatorStakingAmount.add(i), } ); } @@ -213,7 +213,7 @@ describe('Ronin Validator Set test', () => { bridgeOperator.address, 1_00 /* 1% */, { - value: minValidatorBalance.mul(100), + value: minValidatorStakingAmount.mul(100), } ); for (let i = 4; i < localValidatorCandidatesLength; i++) { @@ -226,7 +226,7 @@ describe('Ronin Validator Set test', () => { validatorCandidates[i].address, 2_00, { - value: minValidatorBalance.add(i), + value: minValidatorStakingAmount.add(i), } ); } @@ -339,7 +339,7 @@ describe('Ronin Validator Set test', () => { ); const balanceDiff = (await treasury.getBalance()).sub(balance); expect(balanceDiff).eq(61); // = (5000 + 100 + 100) * 1% + 9 = (52 + 9) - expect(await stakingContract.getClaimableReward(coinbase.address, coinbase.address)).eq( + expect(await stakingContract.getReward(coinbase.address, coinbase.address)).eq( 5148 // (5000 + 100 + 100) * 99% = 99% of the reward, since the pool is only staked by the coinbase ); }); @@ -361,7 +361,7 @@ describe('Ronin Validator Set test', () => { const balanceDiff = (await treasury.getBalance()).sub(balance); expect(balanceDiff).eq(0); // The delegators don't receives the new rewards until the period is ended - expect(await stakingContract.getClaimableReward(coinbase.address, coinbase.address)).eq( + expect(await stakingContract.getReward(coinbase.address, coinbase.address)).eq( 5148 // (5000 + 100 + 100) * 99% = 99% of the reward, since the pool is only staked by the coinbase ); await expect(tx!).emit(roninValidatorSet, 'WrappedUpEpoch').withArgs(lastPeriod, epoch, false); @@ -383,7 +383,7 @@ describe('Ronin Validator Set test', () => { const balanceDiff = (await treasury.getBalance()).sub(balance); const totalBridgeReward = bridgeOperatorBonusPerBlock.mul(2); // called submitBlockReward 2 times expect(balanceDiff).eq(totalBridgeReward.div(await roninValidatorSet.totalBlockProducers())); - expect(await stakingContract.getClaimableReward(coinbase.address, coinbase.address)).eq( + expect(await stakingContract.getReward(coinbase.address, coinbase.address)).eq( 5148 // (5000 + 100 + 100) * 99% = 99% of the reward, since the pool is only staked by the coinbase ); await expect(tx!).emit(roninValidatorSet, 'WrappedUpEpoch').withArgs(lastPeriod, epoch, true); @@ -422,7 +422,7 @@ describe('Ronin Validator Set test', () => { let _rewardFromBonus = blockProducerBonusPerBlock.div(100).mul(99).mul(2); let _rewardFromSubmission = BigNumber.from(100).div(100).mul(99).mul(3); - expect(await stakingContract.getClaimableReward(coinbase.address, coinbase.address)).eq( + expect(await stakingContract.getReward(coinbase.address, coinbase.address)).eq( _rewardFromBonus.add(_rewardFromSubmission) ); }); @@ -447,7 +447,7 @@ describe('Ronin Validator Set test', () => { let _rewardFromBonus = blockProducerBonusPerBlock.div(100).mul(99).mul(2); let _rewardFromSubmission = BigNumber.from(100).div(100).mul(99).mul(3); - expect(await stakingContract.getClaimableReward(coinbase.address, coinbase.address)).eq( + expect(await stakingContract.getReward(coinbase.address, coinbase.address)).eq( _rewardFromBonus.add(_rewardFromSubmission) );