-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathERC721Votes.sol
272 lines (221 loc) · 10.6 KB
/
ERC721Votes.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import { IERC721Votes } from "../interfaces/IERC721Votes.sol";
import { ERC721 } from "../token/ERC721.sol";
import { EIP712 } from "../utils/EIP712.sol";
/// @title ERC721Votes
/// @author Rohan Kulkarni
/// @notice Modified from OpenZeppelin Contracts v4.7.3 (token/ERC721/extensions/draft-ERC721Votes.sol) & Nouns DAO ERC721Checkpointable.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license.
/// - Uses custom errors defined in IERC721Votes
/// - Checkpoints are based on timestamps instead of block numbers
/// - Tokens are self-delegated by default
/// - The total number of votes is the token supply itself
abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 {
/// ///
/// CONSTANTS ///
/// ///
/// @dev The EIP-712 typehash to delegate with a signature
bytes32 internal constant DELEGATION_TYPEHASH = keccak256("Delegation(address from,address to,uint256 nonce,uint256 deadline)");
/// ///
/// STORAGE ///
/// ///
/// @notice The delegate for an account
/// @notice Account => Delegate
mapping(address => address) internal delegation;
/// @notice The number of checkpoints for an account
/// @dev Account => Num Checkpoints
mapping(address => uint256) internal numCheckpoints;
/// @notice The checkpoint for an account
/// @dev Account => Checkpoint Id => Checkpoint
mapping(address => mapping(uint256 => Checkpoint)) internal checkpoints;
/// ///
/// VOTING WEIGHT ///
/// ///
/// @notice The current number of votes for an account
/// @param _account The account address
function getVotes(address _account) public view returns (uint256) {
// Get the account's number of checkpoints
uint256 nCheckpoints = numCheckpoints[_account];
// Cannot underflow as `nCheckpoints` is ensured to be greater than 0 if reached
unchecked {
// Return the number of votes at the latest checkpoint if applicable
return nCheckpoints != 0 ? checkpoints[_account][nCheckpoints - 1].votes : 0;
}
}
/// @notice The number of votes for an account at a past timestamp
/// @param _account The account address
/// @param _timestamp The past timestamp
function getPastVotes(address _account, uint256 _timestamp) public view returns (uint256) {
// Ensure the given timestamp is in the past
if (_timestamp >= block.timestamp) revert INVALID_TIMESTAMP();
// Get the account's number of checkpoints
uint256 nCheckpoints = numCheckpoints[_account];
// If there are none return 0
if (nCheckpoints == 0) return 0;
// Get the account's checkpoints
mapping(uint256 => Checkpoint) storage accountCheckpoints = checkpoints[_account];
unchecked {
// Get the latest checkpoint id
// Cannot underflow as `nCheckpoints` is ensured to be greater than 0
uint256 lastCheckpoint = nCheckpoints - 1;
// If the latest checkpoint has a valid timestamp, return its number of votes
if (accountCheckpoints[lastCheckpoint].timestamp <= _timestamp) return accountCheckpoints[lastCheckpoint].votes;
// If the first checkpoint doesn't have a valid timestamp, return 0
if (accountCheckpoints[0].timestamp > _timestamp) return 0;
// Otherwise, find a checkpoint with a valid timestamp
// Use the latest id as the initial upper bound
uint256 high = lastCheckpoint;
uint256 low;
uint256 middle;
// Used to temporarily hold a checkpoint
Checkpoint memory cp;
// While a valid checkpoint is to be found:
while (high > low) {
// Find the id of the middle checkpoint
middle = high - (high - low) / 2;
// Get the middle checkpoint
cp = accountCheckpoints[middle];
// If the timestamp is a match:
if (cp.timestamp == _timestamp) {
// Return the voting weight
return cp.votes;
// Else if the timestamp is before the one looking for:
} else if (cp.timestamp < _timestamp) {
// Update the lower bound
low = middle;
// Else update the upper bound
} else {
high = middle - 1;
}
}
return accountCheckpoints[low].votes;
}
}
/// ///
/// DELEGATION ///
/// ///
/// @notice The delegate for an account
/// @param _account The account address
function delegates(address _account) external view returns (address) {
address current = delegation[_account];
return current == address(0) ? _account : current;
}
/// @notice Delegates votes to an account
/// @param _to The address delegating votes to
function delegate(address _to) external {
_delegate(msg.sender, _to);
}
/// @notice Delegates votes from a signer to an account
/// @param _from The address delegating votes from
/// @param _to The address delegating votes to
/// @param _deadline The signature deadline
/// @param _v The 129th byte and chain id of the signature
/// @param _r The first 64 bytes of the signature
/// @param _s Bytes 64-128 of the signature
function delegateBySig(
address _from,
address _to,
uint256 _deadline,
uint8 _v,
bytes32 _r,
bytes32 _s
) external {
// Ensure the signature has not expired
if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE();
// Used to store the digest
bytes32 digest;
// Cannot realistically overflow
unchecked {
// Compute the hash of the domain seperator with the typed delegation data
digest = keccak256(
abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), keccak256(abi.encode(DELEGATION_TYPEHASH, _from, _to, nonces[_from]++, _deadline)))
);
}
// Recover the message signer
address recoveredAddress = ecrecover(digest, _v, _r, _s);
// Ensure the recovered signer is the voter
if (recoveredAddress == address(0) || recoveredAddress != _from) revert INVALID_SIGNATURE();
// Update the delegate
_delegate(_from, _to);
}
/// @dev Updates delegate addresses
/// @param _from The address delegating votes from
/// @param _to The address delegating votes to
function _delegate(address _from, address _to) internal {
// Get the previous delegate
address prevDelegate = delegation[_from];
// Store the new delegate
delegation[_from] = _to;
emit DelegateChanged(_from, prevDelegate, _to);
// Transfer voting weight from the previous delegate to the new delegate
_moveDelegateVotes(prevDelegate, _to, balanceOf(_from));
}
/// @dev Transfers voting weight
/// @param _from The address delegating votes from
/// @param _to The address delegating votes to
/// @param _amount The number of votes delegating
function _moveDelegateVotes(
address _from,
address _to,
uint256 _amount
) internal {
unchecked {
// If voting weight is being transferred:
if (_from != _to && _amount > 0) {
// If this isn't a token mint:
if (_from != address(0)) {
// Get the sender's number of checkpoints
uint256 nCheckpoints = numCheckpoints[_from]++;
// Used to store the sender's previous voting weight
uint256 prevTotalVotes;
// If this isn't the sender's first checkpoint: Get their previous voting weight
if (nCheckpoints != 0) prevTotalVotes = checkpoints[_from][nCheckpoints - 1].votes;
// Update their voting weight
_writeCheckpoint(_from, nCheckpoints, prevTotalVotes, prevTotalVotes - _amount);
}
// If this isn't a token burn:
if (_to != address(0)) {
// Get the recipients's number of checkpoints
uint256 nCheckpoints = numCheckpoints[_to]++;
// Used to store the recipient's previous voting weight
uint256 prevTotalVotes;
// If this isn't the recipient's first checkpoint: Get their previous voting weight
if (nCheckpoints != 0) prevTotalVotes = checkpoints[_to][nCheckpoints - 1].votes;
// Update their voting weight
_writeCheckpoint(_to, nCheckpoints, prevTotalVotes, prevTotalVotes + _amount);
}
}
}
}
/// @dev Records a checkpoint
/// @param _account The account address
/// @param _id The checkpoint id
/// @param _prevTotalVotes The account's previous voting weight
/// @param _newTotalVotes The account's new voting weight
function _writeCheckpoint(
address _account,
uint256 _id,
uint256 _prevTotalVotes,
uint256 _newTotalVotes
) private {
// Get the pointer to store the checkpoint
Checkpoint storage checkpoint = checkpoints[_account][_id];
// Record the updated voting weight and current time
checkpoint.votes = uint192(_newTotalVotes);
checkpoint.timestamp = uint64(block.timestamp);
emit DelegateVotesChanged(_account, _prevTotalVotes, _newTotalVotes);
}
/// @dev Enables each NFT to equal 1 vote
/// @param _from The token sender
/// @param _to The token recipient
/// @param _tokenId The ERC-721 token id
function _afterTokenTransfer(
address _from,
address _to,
uint256 _tokenId
) internal override {
// Transfer 1 vote from the sender to the recipient
_moveDelegateVotes(_from, _to, 1);
super._afterTokenTransfer(_from, _to, _tokenId);
}
}