|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.20; |
| 3 | + |
| 4 | +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; |
| 5 | +import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; |
| 6 | +import {IFractionalGovernor} from "src/interfaces/IFractionalGovernor.sol"; |
| 7 | +import {IVotingToken} from "src/interfaces/IVotingToken.sol"; |
| 8 | +import {FlexVotingBase} from "src/FlexVotingBase.sol"; |
| 9 | + |
| 10 | +/// @notice This is an abstract contract designed to make it easy to build clients |
| 11 | +/// for governance systems that inherit from GovernorCountingFractional, a.k.a. |
| 12 | +/// Flexible Voting governors. |
| 13 | +/// |
| 14 | +/// This contract extends FlexVotingBase, adding two features: |
| 15 | +/// (a) the ability for depositors to express voting preferences on |
| 16 | +/// {Governor}'s proposals, and |
| 17 | +/// (b) the ability to cast fractional, rolled up votes on behalf of depositors. |
| 18 | +abstract contract FlexVotingClient is FlexVotingBase { |
| 19 | + using Checkpoints for Checkpoints.Trace208; |
| 20 | + using SafeCast for uint256; |
| 21 | + |
| 22 | + /// @notice The voting options. The order of options should match that of the |
| 23 | + /// voting options in the corresponding {Governor} contract. |
| 24 | + enum VoteType { |
| 25 | + Against, |
| 26 | + For, |
| 27 | + Abstain |
| 28 | + } |
| 29 | + |
| 30 | + /// @notice Data structure to store vote preferences expressed by depositors. |
| 31 | + struct ProposalVote { |
| 32 | + uint128 againstVotes; |
| 33 | + uint128 forVotes; |
| 34 | + uint128 abstainVotes; |
| 35 | + } |
| 36 | + |
| 37 | + /// @dev Map proposalId to an address to whether they have voted on this proposal. |
| 38 | + mapping(uint256 => mapping(address => bool)) private proposalVoterHasVoted; |
| 39 | + |
| 40 | + /// @notice Map proposalId to vote totals expressed on this proposal. |
| 41 | + mapping(uint256 => ProposalVote) public proposalVotes; |
| 42 | + |
| 43 | + /// Constant used by OZ's implementation of {GovernorCountingFractional} to |
| 44 | + /// signal fractional voting. |
| 45 | + /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7b74442c5e87ea51dde41c7f18a209fa5154f1a4/contracts/governance/extensions/GovernorCountingFractional.sol#L37 |
| 46 | + uint8 internal constant VOTE_TYPE_FRACTIONAL = 255; |
| 47 | + |
| 48 | + error FlexVotingClient__NoVotingWeight(); |
| 49 | + error FlexVotingClient__AlreadyVoted(); |
| 50 | + error FlexVotingClient__InvalidSupportValue(); |
| 51 | + error FlexVotingClient__NoVotesExpressed(); |
| 52 | + |
| 53 | + /// @dev Used as the `reason` param when submitting a vote to `GOVERNOR`. |
| 54 | + function _castVoteReasonString() internal virtual returns (string memory) { |
| 55 | + return "rolled-up vote from governance token holders"; |
| 56 | + } |
| 57 | + |
| 58 | + /// @notice Allow the caller to express their voting preference for a given |
| 59 | + /// proposal. Their preference is recorded internally but not moved to the |
| 60 | + /// Governor until `castVote` is called. |
| 61 | + /// @param proposalId The proposalId in the associated Governor |
| 62 | + /// @param support The depositor's vote preferences in accordance with the `VoteType` enum. |
| 63 | + function expressVote(uint256 proposalId, uint8 support) external virtual { |
| 64 | + address voter = msg.sender; |
| 65 | + uint256 weight = getPastVoteWeight(voter, GOVERNOR.proposalSnapshot(proposalId)); |
| 66 | + if (weight == 0) revert FlexVotingClient__NoVotingWeight(); |
| 67 | + |
| 68 | + if (proposalVoterHasVoted[proposalId][voter]) revert FlexVotingClient__AlreadyVoted(); |
| 69 | + proposalVoterHasVoted[proposalId][voter] = true; |
| 70 | + |
| 71 | + if (support == uint8(VoteType.Against)) { |
| 72 | + proposalVotes[proposalId].againstVotes += SafeCast.toUint128(weight); |
| 73 | + } else if (support == uint8(VoteType.For)) { |
| 74 | + proposalVotes[proposalId].forVotes += SafeCast.toUint128(weight); |
| 75 | + } else if (support == uint8(VoteType.Abstain)) { |
| 76 | + proposalVotes[proposalId].abstainVotes += SafeCast.toUint128(weight); |
| 77 | + } else { |
| 78 | + // Support value must be included in VoteType enum. |
| 79 | + revert FlexVotingClient__InvalidSupportValue(); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + /// @notice Causes this contract to cast a vote to the Governor for all of the |
| 84 | + /// accumulated votes expressed by users. Uses the total internal vote weight |
| 85 | + /// to proportionally split weight among expressed votes. Can be called by |
| 86 | + /// anyone. It is idempotent and can be called multiple times during the |
| 87 | + /// lifecycle of a given proposal. |
| 88 | + /// @param proposalId The ID of the proposal which the FlexVotingClient will |
| 89 | + /// now vote on. |
| 90 | + function castVote(uint256 proposalId) external { |
| 91 | + ProposalVote storage _proposalVote = proposalVotes[proposalId]; |
| 92 | + if (_proposalVote.forVotes + _proposalVote.againstVotes + _proposalVote.abstainVotes == 0) { |
| 93 | + revert FlexVotingClient__NoVotesExpressed(); |
| 94 | + } |
| 95 | + |
| 96 | + uint256 _proposalSnapshot = GOVERNOR.proposalSnapshot(proposalId); |
| 97 | + |
| 98 | + // We use the snapshot of total vote weight to determine the weight with |
| 99 | + // which to vote. We do this for two reasons: |
| 100 | + // (1) We cannot use the proposalVote numbers alone, since some people with |
| 101 | + // balances at the snapshot might never express their preferences. If a |
| 102 | + // large holder never expressed a preference, but this contract nevertheless |
| 103 | + // cast votes to the governor with all of its weight, then other users may |
| 104 | + // effectively have *increased* their voting weight because someone else |
| 105 | + // didn't participate, which creates all kinds of bad incentives. |
| 106 | + // (2) Other people might have already expressed their preferences on this |
| 107 | + // proposal and had those preferences submitted to the governor by an |
| 108 | + // earlier call to this function. The weight of those preferences |
| 109 | + // should still be taken into consideration when determining how much |
| 110 | + // weight to vote with this time. |
| 111 | + // Using the total vote weight to proportion votes in this way means that in |
| 112 | + // many circumstances this function will not cast votes with all of its |
| 113 | + // weight. |
| 114 | + uint256 _totalVotesInternal = getPastTotalVoteWeight(_proposalSnapshot); |
| 115 | + |
| 116 | + // We need 256 bits because of the multiplication we're about to do. |
| 117 | + uint256 _totalTokenWeight = |
| 118 | + IVotingToken(address(GOVERNOR.token())).getPastVotes(address(this), _proposalSnapshot); |
| 119 | + |
| 120 | + // userVotesInternal userVoteWeight |
| 121 | + // ------------------------- = -------------------- |
| 122 | + // totalVotesInternal totalTokenWeight |
| 123 | + // |
| 124 | + // userVoteWeight = userVotesInternal * totalTokenWeight / totalVotesInternal |
| 125 | + uint128 _forVotesToCast = |
| 126 | + SafeCast.toUint128((_totalTokenWeight * _proposalVote.forVotes) / _totalVotesInternal); |
| 127 | + uint128 _againstVotesToCast = |
| 128 | + SafeCast.toUint128((_totalTokenWeight * _proposalVote.againstVotes) / _totalVotesInternal); |
| 129 | + uint128 _abstainVotesToCast = |
| 130 | + SafeCast.toUint128((_totalTokenWeight * _proposalVote.abstainVotes) / _totalVotesInternal); |
| 131 | + |
| 132 | + // Clear the stored votes so that we don't double-cast them. |
| 133 | + delete proposalVotes[proposalId]; |
| 134 | + |
| 135 | + bytes memory fractionalizedVotes = |
| 136 | + abi.encodePacked(_againstVotesToCast, _forVotesToCast, _abstainVotesToCast); |
| 137 | + GOVERNOR.castVoteWithReasonAndParams( |
| 138 | + proposalId, VOTE_TYPE_FRACTIONAL, _castVoteReasonString(), fractionalizedVotes |
| 139 | + ); |
| 140 | + } |
| 141 | + |
| 142 | + /// @notice Returns the `_user`'s internal voting weight at `_timepoint`. |
| 143 | + /// @param _user The account that's historical vote weight will be looked up. |
| 144 | + /// @param _timepoint The timepoint at which to lookup the _user's internal |
| 145 | + /// voting weight, either a block number or a timestamp as determined by |
| 146 | + /// {GOVERNOR.token().clock()}. |
| 147 | + function getPastVoteWeight(address _user, uint256 _timepoint) public view returns (uint256) { |
| 148 | + uint48 key = SafeCast.toUint48(_timepoint); |
| 149 | + return voteWeightCheckpoints[_user].upperLookup(key); |
| 150 | + } |
| 151 | + |
| 152 | + /// @notice Returns the total internal voting weight of all users at `_timepoint`. |
| 153 | + /// @param _timepoint The timepoint at which to lookup the total weight, |
| 154 | + /// either a block number or a timestamp as determined by |
| 155 | + /// {GOVERNOR.token().clock()}. |
| 156 | + function getPastTotalVoteWeight(uint256 _timepoint) public view returns (uint256) { |
| 157 | + uint48 key = SafeCast.toUint48(_timepoint); |
| 158 | + return totalVoteWeightCheckpoints.upperLookup(key); |
| 159 | + } |
| 160 | +} |
0 commit comments