// 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/IElection.sol'; /// @dev Election contract /// @author Alexandas contract Election is IElection, ReentrancyGuardUpgradeable { using SignedSafeMath for int256; struct VoterInfo { uint256 amount; int256 rewardDebt; } /// @dev return ERC20 token address IERC20 public override token; /// @dev return `Rewarder` contract address IRewarder public override rewarder; /// @dev return `Stake` contract address IStake public override stake; /// @dev return `MajorCandidates` contract address IMajorCandidates public override majorCandidates; /// @dev return precision for shares uint256 public override ACC_PRECISION; /// @dev vote frozen period uint256 public voteFrozenPeriod; /// @dev return voter inforamtion mapping(address => mapping(address => VoterInfo)) public voters; /// @dev return votes for a specific candidate mapping(address => uint256) public override voteSupply; /// @dev return shares for a specific candidate mapping(address => uint256) public accPerShares; /// @dev last vote timestamp for voter mapping(address => mapping(address => uint256)) public lastVoteTime; modifier onlyStake() { require(msg.sender == address(stake), 'Election: caller must be Stake contract'); _; } /// @dev proxy initialize function /// @param _token ERC20 token address /// @param _rewarder `Rewarder` contract /// @param _stake `Stake` contract /// @param _majorCandidates `MajorCandidates` contract function initialize( IERC20 _token, IRewarder _rewarder, IStake _stake, IMajorCandidates _majorCandidates ) external initializer { _setToken(_token); _setRewarder(_rewarder); _setStake(_stake); _setMajorCandidates(_majorCandidates); _setVoteFrozenPeriod(7 * 24 * 3600); ACC_PRECISION = 1e12; } /// @dev voter vote a specific candidate /// @param candidate candidate address /// @param voter voter address /// @param amount reward amount /// @param anchor anchor candidate address /// @param maxSlippage maximum rank change value for the candidate from the anchor candidate function vote( address candidate, address voter, uint256 amount, address anchor, uint256 maxSlippage ) external override nonReentrant { require(stake.isCandidateRegistered(candidate), 'Election: nonexistent candidate'); token.transferFrom(msg.sender, address(this), amount); stake.voterAllocate(candidate); voters[candidate][voter].amount = voters[candidate][voter].amount + amount; voters[candidate][voter].rewardDebt = voters[candidate][voter].rewardDebt + int256((amount * accPerShares[candidate]) / ACC_PRECISION); voteSupply[candidate] = voteSupply[candidate] + amount; lastVoteTime[candidate][voter] = block.timestamp; majorCandidates.upsetCandidateWithAnchor(candidate, voteSupply[candidate], anchor, maxSlippage); emit Vote(candidate, voter, amount); } /// @dev voter claim reward for a specific candidate /// @param candidate candidate address /// @param to receiver address function claim(address candidate, address to) external override nonReentrant { stake.voterAllocate(candidate); VoterInfo storage voter = voters[candidate][msg.sender]; int256 accumulated = int256((voter.amount * accPerShares[candidate]) / ACC_PRECISION); uint256 pending = (accumulated - voter.rewardDebt).toUInt256(); voter.rewardDebt = accumulated; rewarder.mint(to, pending); emit Claimed(candidate, msg.sender, to, pending); } /// @dev voter withdraw reward for a specific candidate /// @param candidate candidate address /// @param amount reward amount /// @param to receiver address /// @param anchor anchor candidate address /// @param maxSlippage maximum rank change value for the candidate from the anchor candidate function withdraw( address candidate, uint256 amount, address to, address anchor, uint256 maxSlippage ) external override nonReentrant { require(unfrozenAt(candidate, msg.sender) > block.timestamp, 'Election: vote is frozen'); stake.voterAllocate(candidate); VoterInfo storage voter = voters[candidate][msg.sender]; voter.rewardDebt = voter.rewardDebt - int256((amount * accPerShares[candidate]) / ACC_PRECISION); voter.amount = voter.amount - amount; voteSupply[candidate] = voteSupply[candidate] - amount; if (stake.isCandidateRegistered(candidate)) { majorCandidates.upsetCandidateWithAnchor(candidate, voteSupply[candidate], anchor, maxSlippage); } token.transfer(to, amount); emit Withdrawn(candidate, msg.sender, to, amount); } /// @dev voter withdraw votes and claim reward for a specific candidate /// @param candidate candidate address /// @param amount reward amount /// @param to receiver address /// @param anchor anchor candidate address /// @param maxSlippage maximum rank change value for the candidate from the anchor candidate function withdrawAndClaim( address candidate, uint256 amount, address to, address anchor, uint256 maxSlippage ) external override nonReentrant { require(unfrozenAt(candidate, msg.sender) > block.timestamp, 'Election: vote is frozen'); stake.voterAllocate(candidate); VoterInfo storage voter = voters[candidate][msg.sender]; int256 accumulated = int256((voter.amount * accPerShares[candidate]) / ACC_PRECISION); uint256 pending = (accumulated - voter.rewardDebt).toUInt256(); voter.rewardDebt = accumulated - int256((amount * accPerShares[candidate]) / ACC_PRECISION); voter.amount = voter.amount - amount; voteSupply[candidate] = voteSupply[candidate] - amount; if (stake.isCandidateRegistered(candidate)) { majorCandidates.upsetCandidateWithAnchor(candidate, voteSupply[candidate], anchor, maxSlippage); } rewarder.mint(to, pending); token.transfer(to, amount); emit Claimed(candidate, msg.sender, to, pending); emit Withdrawn(candidate, msg.sender, to, amount); } /// @dev pending reward for a voter given a candidate /// @param candidate candidate address /// @param voter voter address /// @return pending pending reward function pendingReward(address candidate, address voter) public view override returns (uint256 pending) { if (voteSupply[candidate] > 0) { uint256 pendingAllocation = stake.pendingVoterAllocation(candidate); int256 rewardDebt = voters[candidate][voter].rewardDebt; uint256 pendingShares = accPerShares[candidate] + ((pendingAllocation * ACC_PRECISION) / voteSupply[candidate]); pending = (int256((voters[candidate][voter].amount * pendingShares) / ACC_PRECISION) - rewardDebt).toUInt256(); } } /// @dev candidate allocate reward for the voters /// @param candidate candidate address /// @param amount reward amount function onAllocate(address candidate, uint256 amount) external override onlyStake { if (voteSupply[candidate] > 0) { accPerShares[candidate] = accPerShares[candidate] + ((amount * ACC_PRECISION) / voteSupply[candidate]); emit ShareUpdated(candidate); } } function unfrozenAt(address candidate, address voter) public view returns(uint256) { require(lastVoteTime[candidate][voter] > 0, 'Election: nonexistent vote'); return lastVoteTime[candidate][voter] + voteFrozenPeriod; } function _setRewarder(IRewarder _rewarder) internal { rewarder = _rewarder; emit RewarderUpdated(_rewarder); } function _setStake(IStake _stake) internal { stake = _stake; emit StakeUpdated(_stake); } function _setMajorCandidates(IMajorCandidates _majorCandidates) internal { majorCandidates = _majorCandidates; emit MajorCandidatesUpdated(_majorCandidates); } function _setToken(IERC20 _token) internal { token = _token; emit TokenUpdated(_token); } function _setVoteFrozenPeriod(uint256 period) internal { voteFrozenPeriod = period; emit VoteFrozenPeriodUpdated(period); } }