Skip to content

Commit 005c2cc

Browse files
Amxxfrangio
authored andcommitted
Use Trace208 in Votes to support ERC6372 clocks (#4539)
Co-authored-by: Francisco <[email protected]>
1 parent 28037d5 commit 005c2cc

File tree

9 files changed

+339
-31
lines changed

9 files changed

+339
-31
lines changed

.changeset/wild-peas-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': major
3+
---
4+
5+
`Votes`: Use Trace208 for checkpoints. This enables EIP-6372 clock support for keys but reduces the max supported voting power to uint208.

.github/workflows/checks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ jobs:
6363
run: npm run test:inheritance
6464
- name: Check storage layout
6565
uses: ./.github/actions/storage-layout
66+
continue-on-error: ${{ contains(github.event.pull_request.labels.*.name, 'breaking change') }}
6667
with:
6768
token: ${{ github.token }}
6869

contracts/governance/extensions/GovernorVotesQuorumFraction.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import {Checkpoints} from "../../utils/structs/Checkpoints.sol";
1212
* fraction of the total supply.
1313
*/
1414
abstract contract GovernorVotesQuorumFraction is GovernorVotes {
15-
using Checkpoints for Checkpoints.Trace224;
15+
using Checkpoints for Checkpoints.Trace208;
1616

17-
Checkpoints.Trace224 private _quorumNumeratorHistory;
17+
Checkpoints.Trace208 private _quorumNumeratorHistory;
1818

1919
event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator);
2020

@@ -49,15 +49,15 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes {
4949
uint256 length = _quorumNumeratorHistory._checkpoints.length;
5050

5151
// Optimistic search, check the latest checkpoint
52-
Checkpoints.Checkpoint224 storage latest = _quorumNumeratorHistory._checkpoints[length - 1];
53-
uint32 latestKey = latest._key;
54-
uint224 latestValue = latest._value;
52+
Checkpoints.Checkpoint208 memory latest = _quorumNumeratorHistory._checkpoints[length - 1];
53+
uint48 latestKey = latest._key;
54+
uint208 latestValue = latest._value;
5555
if (latestKey <= timepoint) {
5656
return latestValue;
5757
}
5858

5959
// Otherwise, do the binary search
60-
return _quorumNumeratorHistory.upperLookupRecent(SafeCast.toUint32(timepoint));
60+
return _quorumNumeratorHistory.upperLookupRecent(SafeCast.toUint48(timepoint));
6161
}
6262

6363
/**
@@ -104,7 +104,7 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes {
104104
}
105105

106106
uint256 oldQuorumNumerator = quorumNumerator();
107-
_quorumNumeratorHistory.push(SafeCast.toUint32(clock()), SafeCast.toUint224(newQuorumNumerator));
107+
_quorumNumeratorHistory.push(clock(), SafeCast.toUint208(newQuorumNumerator));
108108

109109
emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator);
110110
}

contracts/governance/utils/Votes.sol

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ import {ECDSA} from "../../utils/cryptography/ECDSA.sol";
2929
* previous example, it would be included in {ERC721-_beforeTokenTransfer}).
3030
*/
3131
abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
32-
using Checkpoints for Checkpoints.Trace224;
32+
using Checkpoints for Checkpoints.Trace208;
3333

3434
bytes32 private constant DELEGATION_TYPEHASH =
3535
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
3636

3737
mapping(address account => address) private _delegatee;
3838

39-
mapping(address delegatee => Checkpoints.Trace224) private _delegateCheckpoints;
39+
mapping(address delegatee => Checkpoints.Trace208) private _delegateCheckpoints;
4040

41-
Checkpoints.Trace224 private _totalCheckpoints;
41+
Checkpoints.Trace208 private _totalCheckpoints;
4242

4343
/**
4444
* @dev The clock was incorrectly modified.
@@ -90,7 +90,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
9090
if (timepoint >= currentTimepoint) {
9191
revert ERC5805FutureLookup(timepoint, currentTimepoint);
9292
}
93-
return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint32(timepoint));
93+
return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint48(timepoint));
9494
}
9595

9696
/**
@@ -110,7 +110,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
110110
if (timepoint >= currentTimepoint) {
111111
revert ERC5805FutureLookup(timepoint, currentTimepoint);
112112
}
113-
return _totalCheckpoints.upperLookupRecent(SafeCast.toUint32(timepoint));
113+
return _totalCheckpoints.upperLookupRecent(SafeCast.toUint48(timepoint));
114114
}
115115

116116
/**
@@ -178,10 +178,10 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
178178
*/
179179
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual {
180180
if (from == address(0)) {
181-
_push(_totalCheckpoints, _add, SafeCast.toUint224(amount));
181+
_push(_totalCheckpoints, _add, SafeCast.toUint208(amount));
182182
}
183183
if (to == address(0)) {
184-
_push(_totalCheckpoints, _subtract, SafeCast.toUint224(amount));
184+
_push(_totalCheckpoints, _subtract, SafeCast.toUint208(amount));
185185
}
186186
_moveDelegateVotes(delegates(from), delegates(to), amount);
187187
}
@@ -195,15 +195,15 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
195195
(uint256 oldValue, uint256 newValue) = _push(
196196
_delegateCheckpoints[from],
197197
_subtract,
198-
SafeCast.toUint224(amount)
198+
SafeCast.toUint208(amount)
199199
);
200200
emit DelegateVotesChanged(from, oldValue, newValue);
201201
}
202202
if (to != address(0)) {
203203
(uint256 oldValue, uint256 newValue) = _push(
204204
_delegateCheckpoints[to],
205205
_add,
206-
SafeCast.toUint224(amount)
206+
SafeCast.toUint208(amount)
207207
);
208208
emit DelegateVotesChanged(to, oldValue, newValue);
209209
}
@@ -223,23 +223,23 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
223223
function _checkpoints(
224224
address account,
225225
uint32 pos
226-
) internal view virtual returns (Checkpoints.Checkpoint224 memory) {
226+
) internal view virtual returns (Checkpoints.Checkpoint208 memory) {
227227
return _delegateCheckpoints[account].at(pos);
228228
}
229229

230230
function _push(
231-
Checkpoints.Trace224 storage store,
232-
function(uint224, uint224) view returns (uint224) op,
233-
uint224 delta
234-
) private returns (uint224, uint224) {
235-
return store.push(SafeCast.toUint32(clock()), op(store.latest(), delta));
231+
Checkpoints.Trace208 storage store,
232+
function(uint208, uint208) view returns (uint208) op,
233+
uint208 delta
234+
) private returns (uint208, uint208) {
235+
return store.push(clock(), op(store.latest(), delta));
236236
}
237237

238-
function _add(uint224 a, uint224 b) private pure returns (uint224) {
238+
function _add(uint208 a, uint208 b) private pure returns (uint208) {
239239
return a + b;
240240
}
241241

242-
function _subtract(uint224 a, uint224 b) private pure returns (uint224) {
242+
function _subtract(uint208 a, uint208 b) private pure returns (uint208) {
243243
return a - b;
244244
}
245245

contracts/token/ERC20/extensions/ERC20Votes.sol

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {Checkpoints} from "../../../utils/structs/Checkpoints.sol";
99

1010
/**
1111
* @dev Extension of ERC20 to support Compound-like voting and delegation. This version is more generic than Compound's,
12-
* and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1.
12+
* and supports token supply up to 2^208^ - 1, while COMP is limited to 2^96^ - 1.
1313
*
1414
* NOTE: This contract does not provide interface compatibility with Compound's COMP token.
1515
*
@@ -27,10 +27,17 @@ abstract contract ERC20Votes is ERC20, Votes {
2727
error ERC20ExceededSafeSupply(uint256 increasedSupply, uint256 cap);
2828

2929
/**
30-
* @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1).
30+
* @dev Maximum token supply. Defaults to `type(uint208).max` (2^208^ - 1).
31+
*
32+
* This maximum is enforced in {_update}. It limits the total supply of the token, which is otherwise a uint256,
33+
* so that checkpoints can be stored in the Trace208 structure used by {{Votes}}. Increasing this value will not
34+
* remove the underlying limitation, and will cause {_update} to fail because of a math overflow in
35+
* {_transferVotingUnits}. An override could be used to further restrict the total supply (to a lower value) if
36+
* additional logic requires it. When resolving override conflicts on this function, the minimum should be
37+
* returned.
3138
*/
32-
function _maxSupply() internal view virtual returns (uint224) {
33-
return type(uint224).max;
39+
function _maxSupply() internal view virtual returns (uint256) {
40+
return type(uint208).max;
3441
}
3542

3643
/**
@@ -70,7 +77,7 @@ abstract contract ERC20Votes is ERC20, Votes {
7077
/**
7178
* @dev Get the `pos`-th checkpoint for `account`.
7279
*/
73-
function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint224 memory) {
80+
function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint208 memory) {
7481
return _checkpoints(account, pos);
7582
}
7683
}

contracts/utils/structs/Checkpoints.sol

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,193 @@ library Checkpoints {
206206
}
207207
}
208208

209+
struct Trace208 {
210+
Checkpoint208[] _checkpoints;
211+
}
212+
213+
struct Checkpoint208 {
214+
uint48 _key;
215+
uint208 _value;
216+
}
217+
218+
/**
219+
* @dev Pushes a (`key`, `value`) pair into a Trace208 so that it is stored as the checkpoint.
220+
*
221+
* Returns previous value and new value.
222+
*
223+
* IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint48).max` key set will disable the library.
224+
*/
225+
function push(Trace208 storage self, uint48 key, uint208 value) internal returns (uint208, uint208) {
226+
return _insert(self._checkpoints, key, value);
227+
}
228+
229+
/**
230+
* @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none.
231+
*/
232+
function lowerLookup(Trace208 storage self, uint48 key) internal view returns (uint208) {
233+
uint256 len = self._checkpoints.length;
234+
uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len);
235+
return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value;
236+
}
237+
238+
/**
239+
* @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none.
240+
*/
241+
function upperLookup(Trace208 storage self, uint48 key) internal view returns (uint208) {
242+
uint256 len = self._checkpoints.length;
243+
uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len);
244+
return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
245+
}
246+
247+
/**
248+
* @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none.
249+
*
250+
* NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys).
251+
*/
252+
function upperLookupRecent(Trace208 storage self, uint48 key) internal view returns (uint208) {
253+
uint256 len = self._checkpoints.length;
254+
255+
uint256 low = 0;
256+
uint256 high = len;
257+
258+
if (len > 5) {
259+
uint256 mid = len - Math.sqrt(len);
260+
if (key < _unsafeAccess(self._checkpoints, mid)._key) {
261+
high = mid;
262+
} else {
263+
low = mid + 1;
264+
}
265+
}
266+
267+
uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high);
268+
269+
return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
270+
}
271+
272+
/**
273+
* @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
274+
*/
275+
function latest(Trace208 storage self) internal view returns (uint208) {
276+
uint256 pos = self._checkpoints.length;
277+
return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
278+
}
279+
280+
/**
281+
* @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value
282+
* in the most recent checkpoint.
283+
*/
284+
function latestCheckpoint(Trace208 storage self) internal view returns (bool exists, uint48 _key, uint208 _value) {
285+
uint256 pos = self._checkpoints.length;
286+
if (pos == 0) {
287+
return (false, 0, 0);
288+
} else {
289+
Checkpoint208 memory ckpt = _unsafeAccess(self._checkpoints, pos - 1);
290+
return (true, ckpt._key, ckpt._value);
291+
}
292+
}
293+
294+
/**
295+
* @dev Returns the number of checkpoint.
296+
*/
297+
function length(Trace208 storage self) internal view returns (uint256) {
298+
return self._checkpoints.length;
299+
}
300+
301+
/**
302+
* @dev Returns checkpoint at given position.
303+
*/
304+
function at(Trace208 storage self, uint32 pos) internal view returns (Checkpoint208 memory) {
305+
return self._checkpoints[pos];
306+
}
307+
308+
/**
309+
* @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint,
310+
* or by updating the last one.
311+
*/
312+
function _insert(Checkpoint208[] storage self, uint48 key, uint208 value) private returns (uint208, uint208) {
313+
uint256 pos = self.length;
314+
315+
if (pos > 0) {
316+
// Copying to memory is important here.
317+
Checkpoint208 memory last = _unsafeAccess(self, pos - 1);
318+
319+
// Checkpoint keys must be non-decreasing.
320+
if (last._key > key) {
321+
revert CheckpointUnorderedInsertion();
322+
}
323+
324+
// Update or push new checkpoint
325+
if (last._key == key) {
326+
_unsafeAccess(self, pos - 1)._value = value;
327+
} else {
328+
self.push(Checkpoint208({_key: key, _value: value}));
329+
}
330+
return (last._value, value);
331+
} else {
332+
self.push(Checkpoint208({_key: key, _value: value}));
333+
return (0, value);
334+
}
335+
}
336+
337+
/**
338+
* @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` if there is none.
339+
* `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
340+
*
341+
* WARNING: `high` should not be greater than the array's length.
342+
*/
343+
function _upperBinaryLookup(
344+
Checkpoint208[] storage self,
345+
uint48 key,
346+
uint256 low,
347+
uint256 high
348+
) private view returns (uint256) {
349+
while (low < high) {
350+
uint256 mid = Math.average(low, high);
351+
if (_unsafeAccess(self, mid)._key > key) {
352+
high = mid;
353+
} else {
354+
low = mid + 1;
355+
}
356+
}
357+
return high;
358+
}
359+
360+
/**
361+
* @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or `high` if there is none.
362+
* `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
363+
*
364+
* WARNING: `high` should not be greater than the array's length.
365+
*/
366+
function _lowerBinaryLookup(
367+
Checkpoint208[] storage self,
368+
uint48 key,
369+
uint256 low,
370+
uint256 high
371+
) private view returns (uint256) {
372+
while (low < high) {
373+
uint256 mid = Math.average(low, high);
374+
if (_unsafeAccess(self, mid)._key < key) {
375+
low = mid + 1;
376+
} else {
377+
high = mid;
378+
}
379+
}
380+
return high;
381+
}
382+
383+
/**
384+
* @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
385+
*/
386+
function _unsafeAccess(
387+
Checkpoint208[] storage self,
388+
uint256 pos
389+
) private pure returns (Checkpoint208 storage result) {
390+
assembly {
391+
mstore(0, self.slot)
392+
result.slot := add(keccak256(0, 0x20), pos)
393+
}
394+
}
395+
209396
struct Trace160 {
210397
Checkpoint160[] _checkpoints;
211398
}

scripts/generate/templates/Checkpoints.opts.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// OPTIONS
2-
const VALUE_SIZES = [224, 160];
2+
const VALUE_SIZES = [224, 208, 160];
33

44
const defaultOpts = size => ({
55
historyTypeName: `Trace${size}`,

0 commit comments

Comments
 (0)