Skip to content

Commit 790cc5b

Browse files
Amxxfrangioernestognw
authored
Add timestamp based governor with EIP-6372 and EIP-5805 (OpenZeppelin#3934)
Co-authored-by: Francisco Giordano <fg@frang.io> Co-authored-by: Ernesto García <ernestognw@gmail.com> Co-authored-by: Francisco <frangio.1@gmail.com>
1 parent 94cd8ef commit 790cc5b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4021
-3149
lines changed

.changeset/four-bats-sniff.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Governor`: Enable timestamp operation for blockchains without a stable block time. This is achieved by connecting a Governor's internal clock to match a voting token's EIP-6372 interface.

.changeset/ninety-hornets-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Votes`, `ERC20Votes`, `ERC721Votes`: support timestamp checkpointing using EIP-6372.

.github/actions/storage-layout/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ runs:
4040
- name: Compare layouts
4141
if: steps.reference.outcome == 'success' && github.event_name == 'pull_request'
4242
run: |
43-
node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }} >> $GITHUB_STEP_SUMMARY
43+
node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }}
4444
shell: bash
4545
- name: Rename artifacts for upload
4646
if: github.event_name != 'pull_request'

contracts/governance/Governor.sol

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import "../utils/math/SafeCast.sol";
1212
import "../utils/structs/DoubleEndedQueue.sol";
1313
import "../utils/Address.sol";
1414
import "../utils/Context.sol";
15-
import "../utils/Timers.sol";
1615
import "./IGovernor.sol";
1716

1817
/**
@@ -29,22 +28,29 @@ import "./IGovernor.sol";
2928
abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver {
3029
using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
3130
using SafeCast for uint256;
32-
using Timers for Timers.BlockNumber;
3331

3432
bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)");
3533
bytes32 public constant EXTENDED_BALLOT_TYPEHASH =
3634
keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)");
3735

36+
// solhint-disable var-name-mixedcase
3837
struct ProposalCore {
39-
Timers.BlockNumber voteStart;
40-
Timers.BlockNumber voteEnd;
38+
// --- start retyped from Timers.BlockNumber at offset 0x00 ---
39+
uint64 voteStart;
40+
address proposer;
41+
bytes4 __gap_unused0;
42+
// --- start retyped from Timers.BlockNumber at offset 0x20 ---
43+
uint64 voteEnd;
44+
bytes24 __gap_unused1;
45+
// --- Remaining fields starting at offset 0x40 ---------------
4146
bool executed;
4247
bool canceled;
43-
address proposer;
4448
}
49+
// solhint-enable var-name-mixedcase
4550

4651
string private _name;
4752

53+
/// @custom:oz-retyped-from mapping(uint256 => Governor.ProposalCore)
4854
mapping(uint256 => ProposalCore) private _proposals;
4955

5056
// This queue keeps track of the governor operating on itself. Calls to functions protected by the
@@ -96,12 +102,13 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
96102
return
97103
interfaceId ==
98104
(type(IGovernor).interfaceId ^
105+
type(IERC6372).interfaceId ^
99106
this.cancel.selector ^
100107
this.castVoteWithReasonAndParams.selector ^
101108
this.castVoteWithReasonAndParamsBySig.selector ^
102109
this.getVotesWithParams.selector) ||
103-
interfaceId == (type(IGovernor).interfaceId ^ this.cancel.selector) ||
104-
interfaceId == type(IGovernor).interfaceId ||
110+
// Previous interface for backwards compatibility
111+
interfaceId == (type(IGovernor).interfaceId ^ type(IERC6372).interfaceId ^ this.cancel.selector) ||
105112
interfaceId == type(IERC1155Receiver).interfaceId ||
106113
super.supportsInterface(interfaceId);
107114
}
@@ -162,13 +169,15 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
162169
revert("Governor: unknown proposal id");
163170
}
164171

165-
if (snapshot >= block.number) {
172+
uint256 currentTimepoint = clock();
173+
174+
if (snapshot >= currentTimepoint) {
166175
return ProposalState.Pending;
167176
}
168177

169178
uint256 deadline = proposalDeadline(proposalId);
170179

171-
if (deadline >= block.number) {
180+
if (deadline >= currentTimepoint) {
172181
return ProposalState.Active;
173182
}
174183

@@ -179,25 +188,32 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
179188
}
180189
}
181190

191+
/**
192+
* @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
193+
*/
194+
function proposalThreshold() public view virtual returns (uint256) {
195+
return 0;
196+
}
197+
182198
/**
183199
* @dev See {IGovernor-proposalSnapshot}.
184200
*/
185201
function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) {
186-
return _proposals[proposalId].voteStart.getDeadline();
202+
return _proposals[proposalId].voteStart;
187203
}
188204

189205
/**
190206
* @dev See {IGovernor-proposalDeadline}.
191207
*/
192208
function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) {
193-
return _proposals[proposalId].voteEnd.getDeadline();
209+
return _proposals[proposalId].voteEnd;
194210
}
195211

196212
/**
197-
* @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
213+
* @dev Address of the proposer
198214
*/
199-
function proposalThreshold() public view virtual returns (uint256) {
200-
return 0;
215+
function _proposalProposer(uint256 proposalId) internal view virtual returns (address) {
216+
return _proposals[proposalId].proposer;
201217
}
202218

203219
/**
@@ -211,13 +227,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
211227
function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool);
212228

213229
/**
214-
* @dev Get the voting weight of `account` at a specific `blockNumber`, for a vote as described by `params`.
230+
* @dev Get the voting weight of `account` at a specific `timepoint`, for a vote as described by `params`.
215231
*/
216-
function _getVotes(
217-
address account,
218-
uint256 blockNumber,
219-
bytes memory params
220-
) internal view virtual returns (uint256);
232+
function _getVotes(address account, uint256 timepoint, bytes memory params) internal view virtual returns (uint256);
221233

222234
/**
223235
* @dev Register a vote for `proposalId` by `account` with a given `support`, voting `weight` and voting `params`.
@@ -252,9 +264,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
252264
string memory description
253265
) public virtual override returns (uint256) {
254266
address proposer = _msgSender();
267+
uint256 currentTimepoint = clock();
255268

256269
require(
257-
getVotes(proposer, block.number - 1) >= proposalThreshold(),
270+
getVotes(proposer, currentTimepoint - 1) >= proposalThreshold(),
258271
"Governor: proposer votes below proposal threshold"
259272
);
260273

@@ -263,16 +276,20 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
263276
require(targets.length == values.length, "Governor: invalid proposal length");
264277
require(targets.length == calldatas.length, "Governor: invalid proposal length");
265278
require(targets.length > 0, "Governor: empty proposal");
279+
require(_proposals[proposalId].proposer == address(0), "Governor: proposal already exists");
266280

267-
ProposalCore storage proposal = _proposals[proposalId];
268-
require(proposal.voteStart.isUnset(), "Governor: proposal already exists");
269-
270-
uint64 snapshot = block.number.toUint64() + votingDelay().toUint64();
271-
uint64 deadline = snapshot + votingPeriod().toUint64();
281+
uint256 snapshot = currentTimepoint + votingDelay();
282+
uint256 deadline = snapshot + votingPeriod();
272283

273-
proposal.voteStart.setDeadline(snapshot);
274-
proposal.voteEnd.setDeadline(deadline);
275-
proposal.proposer = proposer;
284+
_proposals[proposalId] = ProposalCore({
285+
proposer: proposer,
286+
voteStart: snapshot.toUint64(),
287+
voteEnd: deadline.toUint64(),
288+
executed: false,
289+
canceled: false,
290+
__gap_unused0: 0,
291+
__gap_unused1: 0
292+
});
276293

277294
emit ProposalCreated(
278295
proposalId,
@@ -416,19 +433,19 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
416433
/**
417434
* @dev See {IGovernor-getVotes}.
418435
*/
419-
function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
420-
return _getVotes(account, blockNumber, _defaultParams());
436+
function getVotes(address account, uint256 timepoint) public view virtual override returns (uint256) {
437+
return _getVotes(account, timepoint, _defaultParams());
421438
}
422439

423440
/**
424441
* @dev See {IGovernor-getVotesWithParams}.
425442
*/
426443
function getVotesWithParams(
427444
address account,
428-
uint256 blockNumber,
445+
uint256 timepoint,
429446
bytes memory params
430447
) public view virtual override returns (uint256) {
431-
return _getVotes(account, blockNumber, params);
448+
return _getVotes(account, timepoint, params);
432449
}
433450

434451
/**
@@ -546,7 +563,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
546563
ProposalCore storage proposal = _proposals[proposalId];
547564
require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active");
548565

549-
uint256 weight = _getVotes(account, proposal.voteStart.getDeadline(), params);
566+
uint256 weight = _getVotes(account, proposal.voteStart, params);
550567
_countVote(proposalId, account, support, weight, params);
551568

552569
if (params.length == 0) {

contracts/governance/IGovernor.sol

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
pragma solidity ^0.8.0;
55

6-
import "../utils/introspection/ERC165.sol";
6+
import "../interfaces/IERC165.sol";
7+
import "../interfaces/IERC6372.sol";
78

89
/**
910
* @dev Interface of the {Governor} core.
1011
*
1112
* _Available since v4.3._
1213
*/
13-
abstract contract IGovernor is IERC165 {
14+
abstract contract IGovernor is IERC165, IERC6372 {
1415
enum ProposalState {
1516
Pending,
1617
Active,
@@ -32,8 +33,8 @@ abstract contract IGovernor is IERC165 {
3233
uint256[] values,
3334
string[] signatures,
3435
bytes[] calldatas,
35-
uint256 startBlock,
36-
uint256 endBlock,
36+
uint256 voteStart,
37+
uint256 voteEnd,
3738
string description
3839
);
3940

@@ -81,6 +82,19 @@ abstract contract IGovernor is IERC165 {
8182
*/
8283
function version() public view virtual returns (string memory);
8384

85+
/**
86+
* @notice module:core
87+
* @dev See {IERC6372}
88+
*/
89+
function clock() public view virtual override returns (uint48);
90+
91+
/**
92+
* @notice module:core
93+
* @dev See EIP-6372.
94+
*/
95+
// solhint-disable-next-line func-name-mixedcase
96+
function CLOCK_MODE() public view virtual override returns (string memory);
97+
8498
/**
8599
* @notice module:voting
86100
* @dev A description of the possible `support` values for {castVote} and the way these votes are counted, meant to
@@ -104,7 +118,7 @@ abstract contract IGovernor is IERC165 {
104118
* JavaScript class.
105119
*/
106120
// solhint-disable-next-line func-name-mixedcase
107-
function COUNTING_MODE() public pure virtual returns (string memory);
121+
function COUNTING_MODE() public view virtual returns (string memory);
108122

109123
/**
110124
* @notice module:core
@@ -125,29 +139,33 @@ abstract contract IGovernor is IERC165 {
125139

126140
/**
127141
* @notice module:core
128-
* @dev Block number used to retrieve user's votes and quorum. As per Compound's Comp and OpenZeppelin's
129-
* ERC20Votes, the snapshot is performed at the end of this block. Hence, voting for this proposal starts at the
130-
* beginning of the following block.
142+
* @dev Timepoint used to retrieve user's votes and quorum. If using block number (as per Compound's Comp), the
143+
* snapshot is performed at the end of this block. Hence, voting for this proposal starts at the beginning of the
144+
* following block.
131145
*/
132146
function proposalSnapshot(uint256 proposalId) public view virtual returns (uint256);
133147

134148
/**
135149
* @notice module:core
136-
* @dev Block number at which votes close. Votes close at the end of this block, so it is possible to cast a vote
137-
* during this block.
150+
* @dev Timepoint at which votes close. If using block number, votes close at the end of this block, so it is
151+
* possible to cast a vote during this block.
138152
*/
139153
function proposalDeadline(uint256 proposalId) public view virtual returns (uint256);
140154

141155
/**
142156
* @notice module:user-config
143-
* @dev Delay, in number of block, between the proposal is created and the vote starts. This can be increased to
144-
* leave time for users to buy voting power, or delegate it, before the voting of a proposal starts.
157+
* @dev Delay, between the proposal is created and the vote starts. The unit this duration is expressed in depends
158+
* on the clock (see EIP-6372) this contract uses.
159+
*
160+
* This can be increased to leave time for users to buy voting power, or delegate it, before the voting of a
161+
* proposal starts.
145162
*/
146163
function votingDelay() public view virtual returns (uint256);
147164

148165
/**
149166
* @notice module:user-config
150-
* @dev Delay, in number of blocks, between the vote start and vote ends.
167+
* @dev Delay, between the vote start and vote ends. The unit this duration is expressed in depends on the clock
168+
* (see EIP-6372) this contract uses.
151169
*
152170
* NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting
153171
* duration compared to the voting delay.
@@ -158,27 +176,27 @@ abstract contract IGovernor is IERC165 {
158176
* @notice module:user-config
159177
* @dev Minimum number of cast voted required for a proposal to be successful.
160178
*
161-
* Note: The `blockNumber` parameter corresponds to the snapshot used for counting vote. This allows to scale the
162-
* quorum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).
179+
* NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This allows to scale the
180+
* quorum depending on values such as the totalSupply of a token at this timepoint (see {ERC20Votes}).
163181
*/
164-
function quorum(uint256 blockNumber) public view virtual returns (uint256);
182+
function quorum(uint256 timepoint) public view virtual returns (uint256);
165183

166184
/**
167185
* @notice module:reputation
168-
* @dev Voting power of an `account` at a specific `blockNumber`.
186+
* @dev Voting power of an `account` at a specific `timepoint`.
169187
*
170188
* Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or
171189
* multiple), {ERC20Votes} tokens.
172190
*/
173-
function getVotes(address account, uint256 blockNumber) public view virtual returns (uint256);
191+
function getVotes(address account, uint256 timepoint) public view virtual returns (uint256);
174192

175193
/**
176194
* @notice module:reputation
177-
* @dev Voting power of an `account` at a specific `blockNumber` given additional encoded parameters.
195+
* @dev Voting power of an `account` at a specific `timepoint` given additional encoded parameters.
178196
*/
179197
function getVotesWithParams(
180198
address account,
181-
uint256 blockNumber,
199+
uint256 timepoint,
182200
bytes memory params
183201
) public view virtual returns (uint256);
184202

@@ -189,8 +207,8 @@ abstract contract IGovernor is IERC165 {
189207
function hasVoted(uint256 proposalId, address account) public view virtual returns (bool);
190208

191209
/**
192-
* @dev Create a new proposal. Vote start {IGovernor-votingDelay} blocks after the proposal is created and ends
193-
* {IGovernor-votingPeriod} blocks after the voting starts.
210+
* @dev Create a new proposal. Vote start after a delay specified by {IGovernor-votingDelay} and lasts for a
211+
* duration specified by {IGovernor-votingPeriod}.
194212
*
195213
* Emits a {ProposalCreated} event.
196214
*/

contracts/governance/README.adoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ Other extensions can customize the behavior or interface in multiple ways.
4646

4747
In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications:
4848

49-
* <<Governor-votingDelay-,`votingDelay()`>>: Delay (in number of blocks) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes.
50-
* <<Governor-votingPeriod-,`votingPeriod()`>>: Delay (in number of blocks) since the proposal starts until voting ends.
51-
* <<Governor-quorum-uint256-,`quorum(uint256 blockNumber)`>>: Quorum required for a proposal to be successful. This function includes a `blockNumber` argument so the quorum can adapt through time, for example, to follow a token's `totalSupply`.
49+
* <<Governor-votingDelay-,`votingDelay()`>>: Delay (in EIP-6372 clock) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes.
50+
* <<Governor-votingPeriod-,`votingPeriod()`>>: Delay (in EIP-6372 clock) since the proposal starts until voting ends.
51+
* <<Governor-quorum-uint256-,`quorum(uint256 timepoint)`>>: Quorum required for a proposal to be successful. This function includes a `timepoint` argument (see EIP-6372) so the quorum can adapt through time, for example, to follow a token's `totalSupply`.
5252

5353
NOTE: Functions of the `Governor` contract do not include access control. If you want to restrict access, you should add these checks by overloading the particular functions. Among these, {Governor-_cancel} is internal by default, and you will have to expose it (with the right access control mechanism) yourself if this function is needed.
5454

0 commit comments

Comments
 (0)