// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.8.0; import '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol'; import '../libraries/SignedSafeMath.sol'; import '../interfaces/IStake.sol'; import './CandidateRegistry.sol'; import './Pools.sol'; /// @dev Stake contract /// @author Alexandas contract Stake is IStake, CandidateRegistry, Pools, ReentrancyGuardUpgradeable { using SignedSafeMath for int256; /// @dev return `Election` contract IElection public override election; /// @dev return `MajorCandidates` contract IMajorCandidates public override majorCandidates; /// @dev return `Slasher` contract ISlasher public override slasher; /// @dev return minimum stake for registering a candidate uint256 public minStake; /// @dev return stake frozen period uint256 public stakeFrozenPeriod; /// @dev return stake frozen period uint256 public claimFrozenPeriod; /// @dev return last stake timestamp for a candidate mapping(address => uint256) public lastStakeTimestamp; /// @dev return last claim timestamp for a candidate mapping(address => uint256) public lastClaimTimestamp; /// @dev return vote reward coefficient for a candidate mapping(address => uint256) internal voteRewardCoefs; modifier onlyElection() { require(msg.sender == address(election), 'Stake: caller must be Election contract'); _; } modifier onlyMajorCandidates() { require(msg.sender == address(majorCandidates), 'Stake: caller must be MajorCandidates contract'); _; } modifier onlySlasher() { require(msg.sender == address(slasher), 'Stake: caller must be Slasher contract'); _; } /// @dev proxy initialize function /// @param token ERC20 token address /// @param rewarder `Rewarder` contract /// @param _election `Election` contract /// @param _majorCandidates `MajorCandidates` contract /// @param _slasher `Slasher` contract /// @param _stakeFrozenPeriod stake frozen period /// @param _minStake minimum stake for candidate /// @param rewardPerSecond reward generated per second function initialize( IERC20 token, IRewarder rewarder, IElection _election, IMajorCandidates _majorCandidates, ISlasher _slasher, uint256 _stakeFrozenPeriod, uint256 _minStake, uint256 rewardPerSecond ) external initializer { _setToken(token); _setRewarder(rewarder); _setElection(_election); _setMajorCandidates(_majorCandidates); _setSlasher(_slasher); _setStakeFrozenPeriod(_stakeFrozenPeriod); _setClaimFrozenPeriod(28 * 24 * 3600); _setMinStake(_minStake); _setRewardPerSecond(rewardPerSecond); _setMaxCoef(1e6); _setAccPrecision(1e12); _addPool(Types.Grade.Major, 750); _addPool(Types.Grade.Secondary, 250); } /// @dev candidate stake tokens /// @param amount token amount function stake(uint256 amount) external override { _stake(msg.sender, msg.sender, amount); } /// @dev register a candidate and stake tokens /// @param amount token amount /// @param manifest node manifest function registerAndStake(uint256 amount, string memory manifest) external override { _register(msg.sender, manifest); _stake(msg.sender, msg.sender, amount); } function _stake( address user, address _candidate, uint256 amount ) internal { require(isCandidateRegistered(_candidate), 'Stake: candidate is not registered'); Types.CandidateInfo storage candidate = candidates[_candidate]; Types.PoolInfo memory pool = updatePool(candidate.grade); require(amount + candidate.amount >= minStake, 'Stake: total amount is less than minimum stake'); token.transferFrom(user, address(this), amount); candidate.amount = candidate.amount + amount; candidate.rewardDebt = candidate.rewardDebt + int256(amount * pool.accPerShare / ACC_PRECISION); lastStakeTimestamp[_candidate] = block.timestamp; emit Stake(user, _candidate, amount); } /// @dev candidate quit from the protocol /// @param to token receiver function quit(address to) external override nonReentrant { require(isStakeUnfrozen(msg.sender), 'Stake: stake is frozen currently'); require(!slasher.slashExists(msg.sender), 'Stake: slash exists'); _quit(msg.sender, to); } function _quit(address _candidate, address to) internal { Types.CandidateInfo memory candidate = candidates[_candidate]; _withdrawAndClaim(_candidate, candidate.amount, to); if (majorCandidates.exists(_candidate)) { majorCandidates.remove(_candidate); } delete candidates[_candidate]; delete manifestMap[candidate.manifest]; emit Quit(candidate.grade, _candidate); } /// @dev candidate claim reward from the protocol /// @param to token receiver function claim(address to) external override nonReentrant { require(isClaimUnfrozen(msg.sender), 'Stake: reward is frozen currently'); _allocate(msg.sender); rewarder.mint(to, candidates[msg.sender].allocation); } function isStakeUnfrozen(address candidate) public view returns(bool) { return block.timestamp > stakeFrozenPeriod + lastStakeTimestamp[candidate]; } function isClaimUnfrozen(address candidate) public view returns(bool) { return block.timestamp > claimFrozenPeriod + lastClaimTimestamp[candidate]; } /// @dev candidate withdraw the tokens and claim reward from the protocol /// @param amount token amount /// @param to token receiver // function withdrawAndClaim(uint256 amount, address to) external override nonReentrant { // require(isStakeUnfrozen(msg.sender), 'Stake: stake is frozen currently'); // _withdrawAndClaim(msg.sender, amount, to); // } function _withdrawAndClaim(address _candidate, uint256 amount, address to) internal { Types.CandidateInfo storage candidate = candidates[_candidate]; Types.PoolInfo memory pool = updatePool(candidate.grade); int256 accumulated = int256(candidate.amount * pool.accPerShare / ACC_PRECISION); uint256 _pending = (accumulated - candidate.rewardDebt).toUInt256(); candidate.rewardDebt = accumulated - int256(amount * pool.accPerShare / ACC_PRECISION); candidate.amount = candidate.amount - amount; uint256 currentAllocation = candidate.allocation; uint256 pendingAllocation = _allocateReward(_candidate, _pending); candidate.allocation = 0; rewarder.mint(to, currentAllocation + pendingAllocation); token.transfer(to, amount); emit Claimed(_candidate, to, currentAllocation + pendingAllocation); emit Withdrawn(_candidate, to, amount); } function _setMinStake(uint256 amount) internal { minStake = amount; emit MinStakeUpdated(amount); } function _setStakeFrozenPeriod(uint256 period) internal { stakeFrozenPeriod = period; emit StakeFrozenPeriodUpdated(period); } function _setClaimFrozenPeriod(uint256 period) internal { claimFrozenPeriod = period; emit ClaimFrozenPeriodUpdated(period); } function _setMajorCandidates(IMajorCandidates _majorCandidates) internal { majorCandidates = _majorCandidates; emit IMajorCandidatesUpdated(_majorCandidates); } /// @dev upgrade a candidate /// @param candidate candidate address function upgrade(address candidate) external override onlyMajorCandidates { _onCandidateGradeChanged(candidate, Types.Grade.Major); emit Upgrade(candidate, Types.Grade.Secondary, Types.Grade.Major); } /// @dev downgrade a candidate /// @param candidate candidate address function downgrade(address candidate) external override onlyMajorCandidates { _onCandidateGradeChanged(candidate, Types.Grade.Secondary); emit Downgrade(candidate, Types.Grade.Major, Types.Grade.Secondary); } // make sure pool updated and reward allocated function _onCandidateGradeChanged(address _candidate, Types.Grade grade) internal { _allocate(_candidate); Types.CandidateInfo storage candidate = candidates[_candidate]; candidate.grade = grade; Types.PoolInfo memory pool = updatePool(grade); candidate.rewardDebt = int256(candidate.amount * pool.accPerShare / ACC_PRECISION); } /// @dev set allocate the reward to voters from the candidate /// @param candidate candidate address function voterAllocate(address candidate) external override onlyElection { _allocate(candidate); } function _allocate(address _candidate) internal { if (isCandidateRegistered(_candidate)) { Types.CandidateInfo storage candidate = candidates[_candidate]; Types.PoolInfo memory pool = updatePool(candidate.grade); int256 accumulated = int256(candidate.amount * pool.accPerShare / ACC_PRECISION); uint256 _pending = (accumulated - candidate.rewardDebt).toUInt256(); candidate.rewardDebt = accumulated; uint256 allocation = _allocateReward(_candidate, _pending); if (allocation > 0) { candidate.allocation = candidate.allocation + allocation; emit CandidateAllocated(_candidate, allocation); } } } function _allocateReward( address _candidate, uint256 _pending ) internal returns(uint256 pendingAllocation) { if (_pending > 0) { pendingAllocation = _pending; if (voteRewardCoef(_candidate) > 0 && election.voteSupply(_candidate) > 0) { uint256 allocation = _pending * voteRewardCoef(_candidate) / MAXCOEF; election.onAllocate(_candidate, allocation); pendingAllocation = _pending - allocation; emit VoterAllocated(_candidate, allocation); } } } /// @dev set voter slash reward coefficient /// @param coef voter reward coefficient function setVoteRewardCoef(uint256 coef) external override { _setVoteRewardCoef(msg.sender, coef); } function _setVoteRewardCoef(address candidate, uint256 coef) internal { require(coef > 0 && coef <= MAXCOEF, 'Stake: invalid coef'); require(isCandidateRegistered(candidate), 'Stake: candidate is not registered'); _allocate(candidate); voteRewardCoefs[candidate] = coef; emit VoteRewardCoefUpdated(coef); } function _setElection(IElection _election) internal { election = _election; emit ElectionUpdated(_election); } function _pendingReward(address _candidate) internal view returns (uint256 pending) { Types.CandidateInfo storage candidate = candidates[_candidate]; Types.PoolInfo memory pool = pools[candidate.grade]; uint256 accPerShare = pool.accPerShare; uint256 lpSupply = token.balanceOf(address(this)); if (block.timestamp > pool.lastRewardTime && lpSupply != 0) { uint256 time = block.timestamp - pool.lastRewardTime; uint256 reward = time * rewardPerSecond * pool.allocPoint / totalAllocPoint; accPerShare = accPerShare + (reward * ACC_PRECISION / lpSupply); } pending = int256(int256(candidate.amount * accPerShare / ACC_PRECISION) - candidate.rewardDebt).toUInt256(); } /// @dev return pending candidate allocation /// @param _candidate candidate address /// @return pendingAllocation pending candidate allocation function pendingCandidateAllocation(address _candidate) public view override returns (uint256 pendingAllocation) { if (voteRewardCoef(_candidate) > 0 && election.voteSupply(_candidate) > 0) { uint256 pending = _pendingReward(_candidate); pendingAllocation = pending * (MAXCOEF - voteRewardCoef(_candidate)) / MAXCOEF; } } /// @dev return pending voters allocation for a specific candidate /// @param _candidate candidate address /// @return pendingAllocation pending voters allocation function pendingVoterAllocation(address _candidate) public view override returns (uint256 pendingAllocation) { if (voteRewardCoef(_candidate) > 0 && election.voteSupply(_candidate) > 0) { uint256 pending = _pendingReward(_candidate); pendingAllocation = pending * voteRewardCoef(_candidate) / MAXCOEF; } } /// @dev return pending reward for a specific candidate /// @param _candidate candidate address /// @return pending reward for the candidate function pendingReward(address _candidate) public view override returns (uint256 pending) { pending = pendingCandidateAllocation(_candidate) + candidates[_candidate].allocation; } /// @dev return voter reward coefficient for a specific candidate /// @param candidate candidate address /// @return coef voter reward coefficient function voteRewardCoef(address candidate) public view override returns (uint256) { return voteRewardCoefs[candidate]; } function _setSlasher(ISlasher _slasher) internal { slasher = _slasher; emit SlasherUpdated(_slasher); } /// @dev draft a slash for a candidate /// @param _candidate candidate address /// @param amount slash amount /// @return pendingSlash real slash amount function draftSlash(address _candidate, uint256 amount) external override onlySlasher returns(uint256 pendingSlash) { Types.CandidateInfo storage candidate = candidates[_candidate]; require(candidate.amount > 0, 'Stake: zero stake amount'); if (candidate.amount < amount) { _quit(_candidate, _candidate); } else { pendingSlash = amount; Types.PoolInfo memory pool = updatePool(candidate.grade); candidate.rewardDebt = candidate.rewardDebt - int256(amount * pool.accPerShare / ACC_PRECISION); candidate.amount = candidate.amount - amount; emit DraftSlash(_candidate, pendingSlash); } } /// @dev reject a slash for a candidate /// @param _candidate candidate address /// @param pendingSlash real slash amount function rejectSlash(address _candidate, uint256 pendingSlash) external override onlySlasher { Types.CandidateInfo storage candidate = candidates[_candidate]; Types.PoolInfo memory pool = updatePool(candidate.grade); candidate.rewardDebt = candidate.rewardDebt + int256(pendingSlash * pool.accPerShare / ACC_PRECISION); candidate.amount = candidate.amount + pendingSlash; emit RejectSlash(_candidate, pendingSlash); } /// @dev executed a slash for a candiate /// @param _candidate candidate address /// @param slash slash amount /// @param beneficiaries slash reward beneficiaries /// @param amounts slash reward amounts /// @param burned burned amount function executeSlash( address _candidate, uint256 slash, address[] memory beneficiaries, uint256[] memory amounts, uint256 burned ) external override onlySlasher { require(beneficiaries.length == amounts.length, 'Stake: invalid params'); for(uint8 i = 0; i < beneficiaries.length; i++) { token.transfer(beneficiaries[i], amounts[i]); } token.burn(burned); emit ExcuteSlash(_candidate, slash, beneficiaries, amounts, burned); } function majorCandidateManifests() external view returns(string[] memory manifests) { address[] memory candidates = majorCandidates.majorCandidateList(); manifests = new string[](candidates.length); for (uint256 i = 0; i < candidates.length; i++) { Types.CandidateInfo memory candidate = candidateInfo(candidates[i]); manifests[i] = candidate.manifest; } } }