Skip to content

Commit 37e12d4

Browse files
authored
Make FlexVotingClient clock agnostic (#77)
Contracts can track time in at least two different ways: block numbers or timestamps. [EIP6372](https://eips.ethereum.org/EIPS/eip-6372) introduced a standard specification for contract clocks, giving contracts a canonical way to check how other contracts are keeping time. Since time is fundamental to voting (viz voting periods), it was important to make sure that the `FlexVotingClient` would still work no matter what clock a governor was using. This PR makes `FlexVotingClient` clock agnostic, meaning that it is compatible with any clock system that conforms to EIP6372. Additionally, `FlexVotingClient` now exposes a `_checkpointTotalBalance` function to simplify total balance accounting in child contracts. The new function is analogous to the `_checkpointRawBalanceOf` function already in place for individual account balance checkpointing.
1 parent e435476 commit 37e12d4

File tree

5 files changed

+440
-107
lines changed

5 files changed

+440
-107
lines changed

src/FlexVotingClient.sol

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ pragma solidity ^0.8.20;
33

44
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
55
import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
6-
import {IFractionalGovernor} from "./interfaces/IFractionalGovernor.sol";
7-
import {IVotingToken} from "./interfaces/IVotingToken.sol";
6+
import {IFractionalGovernor} from "src/interfaces/IFractionalGovernor.sol";
7+
import {IVotingToken} from "src/interfaces/IVotingToken.sol";
88

99
/// @notice This is an abstract contract designed to make it easy to build clients
1010
/// for governance systems that inherit from GovernorCountingFractional, a.k.a.
@@ -51,8 +51,8 @@ abstract contract FlexVotingClient {
5151

5252
// @dev Trace208 is used instead of Trace224 because the former allocates 48
5353
// bits to its _key. We need at least 48 bits because the _key is going to be
54-
// a block number. And EIP-6372 specifies that when block numbers are used for
55-
// internal clocks (as they are for ERC20Votes) they need to be uint48s.
54+
// a timepoint. Timepoints in the context of ERC20Votes and ERC721Votes
55+
// conform to the EIP-6372 standard, which specifies they be uint48s.
5656
using Checkpoints for Checkpoints.Trace208;
5757

5858
/// @notice The voting options corresponding to those used in the Governor.
@@ -149,7 +149,7 @@ abstract contract FlexVotingClient {
149149
revert FlexVotingClient__NoVotesExpressed();
150150
}
151151

152-
uint256 _proposalSnapshotBlockNumber = GOVERNOR.proposalSnapshot(proposalId);
152+
uint256 _proposalSnapshot = GOVERNOR.proposalSnapshot(proposalId);
153153

154154
// We use the snapshot of total raw balances to determine the weight with
155155
// which to vote. We do this for two reasons:
@@ -167,12 +167,11 @@ abstract contract FlexVotingClient {
167167
// Using the total raw balance to proportion votes in this way means that in
168168
// many circumstances this function will not cast votes with all of its
169169
// weight.
170-
uint256 _totalRawBalanceAtSnapshot = getPastTotalBalance(_proposalSnapshotBlockNumber);
170+
uint256 _totalRawBalanceAtSnapshot = getPastTotalBalance(_proposalSnapshot);
171171

172172
// We need 256 bits because of the multiplication we're about to do.
173-
uint256 _votingWeightAtSnapshot = IVotingToken(address(GOVERNOR.token())).getPastVotes(
174-
address(this), _proposalSnapshotBlockNumber
175-
);
173+
uint256 _votingWeightAtSnapshot =
174+
IVotingToken(address(GOVERNOR.token())).getPastVotes(address(this), _proposalSnapshot);
176175

177176
// forVotesRaw forVoteWeight
178177
// --------------------- = ------------------
@@ -205,21 +204,52 @@ abstract contract FlexVotingClient {
205204

206205
/// @dev Checkpoints the _user's current raw balance.
207206
function _checkpointRawBalanceOf(address _user) internal {
208-
balanceCheckpoints[_user].push(SafeCast.toUint48(block.number), _rawBalanceOf(_user));
207+
balanceCheckpoints[_user].push(IVotingToken(GOVERNOR.token()).clock(), _rawBalanceOf(_user));
208+
}
209+
210+
/// @dev Checkpoints the total balance after applying `_delta`.
211+
function _checkpointTotalBalance(int256 _delta) internal {
212+
// The casting in this function is safe since:
213+
// - if oldTotal + delta > int256.max it will panic and revert.
214+
// - if |delta| <= oldTotal
215+
// * there is no risk of wrapping
216+
// - if |delta| > oldTotal
217+
// * uint256(oldTotal + delta) will wrap but the wrapped value will
218+
// necessarily be greater than uint208.max, so SafeCast will revert.
219+
// * the lowest that oldTotal + delta can be is int256.min (when
220+
// oldTotal is 0 and delta is int256.min). The wrapped value of a
221+
// negative signed integer is:
222+
// wrapped(integer) = uint256.max + integer
223+
// Substituting:
224+
// wrapped(int256.min) = uint256.max + int256.min
225+
// But:
226+
// uint256.max + int256.min > uint208.max
227+
// Substituting again:
228+
// wrapped(int256.min) > uint208.max, which will revert when safecast.
229+
uint256 _oldTotal = uint256(totalBalanceCheckpoints.latest());
230+
uint256 _newTotal = uint256(int256(_oldTotal) + _delta);
231+
232+
totalBalanceCheckpoints.push(
233+
IVotingToken(GOVERNOR.token()).clock(), SafeCast.toUint208(_newTotal)
234+
);
209235
}
210236

211-
/// @notice Returns the `_user`'s raw balance at `_blockNumber`.
237+
/// @notice Returns the `_user`'s raw balance at `_timepoint`.
212238
/// @param _user The account that's historical raw balance will be looked up.
213-
/// @param _blockNumber The block at which to lookup the _user's raw balance.
214-
function getPastRawBalance(address _user, uint256 _blockNumber) public view returns (uint256) {
215-
uint48 key = SafeCast.toUint48(_blockNumber);
239+
/// @param _timepoint The timepoint at which to lookup the _user's raw
240+
/// balance, either a block number or a timestamp as determined by
241+
/// {GOVERNOR.token().clock()}.
242+
function getPastRawBalance(address _user, uint256 _timepoint) public view returns (uint256) {
243+
uint48 key = SafeCast.toUint48(_timepoint);
216244
return balanceCheckpoints[_user].upperLookup(key);
217245
}
218246

219-
/// @notice Returns the sum total of raw balances of all users at `_blockNumber`.
220-
/// @param _blockNumber The block at which to lookup the total balance.
221-
function getPastTotalBalance(uint256 _blockNumber) public view returns (uint256) {
222-
uint48 key = SafeCast.toUint48(_blockNumber);
247+
/// @notice Returns the sum total of raw balances of all users at `_timepoint`.
248+
/// @param _timepoint The timepoint at which to lookup the total balance,
249+
/// either a block number or a timestamp as determined by
250+
/// {GOVERNOR.token().clock()}.
251+
function getPastTotalBalance(uint256 _timepoint) public view returns (uint256) {
252+
uint48 key = SafeCast.toUint48(_timepoint);
223253
return totalBalanceCheckpoints.upperLookup(key);
224254
}
225255
}

src/interfaces/IVotingToken.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.20;
33

44
/// @dev The interface that flexible voting-compatible voting tokens are expected to support.
55
interface IVotingToken {
6+
function clock() external view returns (uint48);
67
function transfer(address to, uint256 amount) external returns (bool);
78
function transferFrom(address from, address to, uint256 amount) external returns (bool);
89
function delegate(address delegatee) external;

0 commit comments

Comments
 (0)