Skip to content

Commit 548c859

Browse files
committed
feat: create a private implementation of Votes
1 parent 4b4bbf1 commit 548c859

File tree

1 file changed

+390
-0
lines changed

1 file changed

+390
-0
lines changed
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol";
5+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
6+
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
7+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
8+
import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
9+
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
10+
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
11+
import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
12+
13+
/**
14+
* @title PrivateVotes
15+
* @dev This is a base abstract contract that tracks voting units with enhanced privacy controls.
16+
* It provides a system of vote delegation where events can be emitted privately to authorized viewers.
17+
*
18+
* Based on OpenZeppelin Contracts (last updated v5.2.0) (governance/utils/Votes.sol)
19+
* with modifications for private event functionality.
20+
*
21+
* This contract provides the same voting and delegation functionality as OpenZeppelin's Votes contract,
22+
* but enables customization of event emission through virtual functions. The base functionality includes:
23+
* - Vote delegation system where accounts can delegate voting power to representatives
24+
* - Historical tracking of voting power through checkpoints
25+
* - Protection against flash loans and double voting through on-chain history
26+
* - EIP-712 compatible signature-based delegation
27+
*
28+
* Key differences from OpenZeppelin's Votes:
29+
* - Uses Private Events by default for selective visibility of voting activities
30+
* - Virtual event emission functions (_emitDelegateChanged, _emitDelegateVotesChanged) for further customization
31+
* - Virtual viewer functions (_getDelegateChangedEventViewers, _getDelegateVotesChangedEventViewers) for access control
32+
* - Default privacy model: delegators and delegates can view relevant events
33+
* - Maintains full interface compatibility with existing governance protocols
34+
*
35+
* Security considerations:
36+
* - Event emission customization should preserve governance transparency requirements
37+
* - Private event implementations must ensure authorized viewers include relevant governance participants
38+
* - Delegation and voting power changes should remain auditable by appropriate parties
39+
* - The checkpoint system integrity must be maintained regardless of event privacy settings
40+
*
41+
* This contract is often combined with a token contract such that voting units correspond to token units.
42+
* The full history of delegate votes is tracked on-chain so that governance protocols can consider votes
43+
* as distributed at a particular block number to protect against flash loans and double voting.
44+
*
45+
* When using this module the derived contract must implement {_getVotingUnits} and can use
46+
* {_transferVotingUnits} to track changes in the distribution of voting units.
47+
*/
48+
abstract contract PrivateVotes is Context, EIP712, Nonces, IERC5805 {
49+
using Checkpoints for Checkpoints.Trace208;
50+
51+
// Event type constants for Private Events
52+
/**
53+
* @notice DelegateChanged event parameter mapping:
54+
* - address param0: delegator - Account that changed its delegation
55+
* - address param1: fromDelegate - Previous delegate (address(0) if first delegation)
56+
* - address param2: toDelegate - New delegate (address(0) if removing delegation)
57+
* @custom:signature DelegateChanged(address delegator, address fromDelegate, address toDelegate)
58+
*/
59+
bytes32 public constant EVENT_TYPE_DELEGATE_CHANGED = keccak256("DelegateChanged(address,address,address)");
60+
61+
/**
62+
* @notice DelegateVotesChanged event parameter mapping:
63+
* - address param0: delegate - Delegate whose vote weight changed
64+
* - uint256 param1: previousVotes - Previous vote weight
65+
* - uint256 param2: newVotes - New vote weight
66+
* @custom:signature DelegateVotesChanged(address delegate, uint256 previousVotes, uint256 newVotes)
67+
*/
68+
bytes32 public constant EVENT_TYPE_DELEGATE_VOTES_CHANGED = keccak256("DelegateVotesChanged(address,uint256,uint256)");
69+
70+
bytes32 private constant DELEGATION_TYPEHASH =
71+
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
72+
73+
mapping(address account => address) private _delegatee;
74+
75+
mapping(address delegatee => Checkpoints.Trace208) private _delegateCheckpoints;
76+
77+
Checkpoints.Trace208 private _totalCheckpoints;
78+
79+
/**
80+
* @dev Private Event for selective visibility of voting-related events
81+
* @param allowedViewers List of addresses authorized to view the event
82+
* @param eventType The keccak256 hash of the original event signature
83+
* @param payload The ABI-encoded event arguments
84+
*/
85+
event PrivateEvent(
86+
address[] allowedViewers,
87+
bytes32 indexed eventType,
88+
bytes payload
89+
);
90+
91+
/**
92+
* @dev The clock was incorrectly modified.
93+
*/
94+
error ERC6372InconsistentClock();
95+
96+
/**
97+
* @dev Lookup to future votes is not available.
98+
*/
99+
error ERC5805FutureLookup(uint256 timepoint, uint48 clock);
100+
101+
/**
102+
* @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based
103+
* checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match.
104+
*/
105+
function clock() public view virtual returns (uint48) {
106+
return Time.blockNumber();
107+
}
108+
109+
/**
110+
* @dev Machine-readable description of the clock as specified in ERC-6372.
111+
*/
112+
// solhint-disable-next-line func-name-mixedcase
113+
function CLOCK_MODE() public view virtual returns (string memory) {
114+
// Check that the clock was not modified
115+
if (clock() != Time.blockNumber()) {
116+
revert ERC6372InconsistentClock();
117+
}
118+
return "mode=blocknumber&from=default";
119+
}
120+
121+
/**
122+
* @dev Validate that a timepoint is in the past, and return it as a uint48.
123+
*/
124+
function _validateTimepoint(uint256 timepoint) internal view returns (uint48) {
125+
uint48 currentTimepoint = clock();
126+
if (timepoint >= currentTimepoint) revert ERC5805FutureLookup(timepoint, currentTimepoint);
127+
return SafeCast.toUint48(timepoint);
128+
}
129+
130+
/**
131+
* @dev Returns the current amount of votes that `account` has.
132+
*/
133+
function getVotes(address account) public view virtual returns (uint256) {
134+
return _delegateCheckpoints[account].latest();
135+
}
136+
137+
/**
138+
* @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is
139+
* configured to use block numbers, this will return the value at the end of the corresponding block.
140+
*
141+
* Requirements:
142+
*
143+
* - `timepoint` must be in the past. If operating using block numbers, the block must be already mined.
144+
*/
145+
function getPastVotes(address account, uint256 timepoint) public view virtual returns (uint256) {
146+
return _delegateCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint));
147+
}
148+
149+
/**
150+
* @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is
151+
* configured to use block numbers, this will return the value at the end of the corresponding block.
152+
*
153+
* NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes.
154+
* Votes that have not been delegated are still part of total supply, even though they would not participate in a
155+
* vote.
156+
*
157+
* Requirements:
158+
*
159+
* - `timepoint` must be in the past. If operating using block numbers, the block must be already mined.
160+
*/
161+
function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) {
162+
return _totalCheckpoints.upperLookupRecent(_validateTimepoint(timepoint));
163+
}
164+
165+
/**
166+
* @dev Returns the current total supply of votes.
167+
*/
168+
function _getTotalSupply() internal view virtual returns (uint256) {
169+
return _totalCheckpoints.latest();
170+
}
171+
172+
/**
173+
* @dev Returns the delegate that `account` has chosen.
174+
*/
175+
function delegates(address account) public view virtual returns (address) {
176+
return _delegatee[account];
177+
}
178+
179+
/**
180+
* @dev Delegates votes from the sender to `delegatee`.
181+
*/
182+
function delegate(address delegatee) public virtual {
183+
address account = _msgSender();
184+
_delegate(account, delegatee);
185+
}
186+
187+
/**
188+
* @dev Delegates votes from signer to `delegatee`.
189+
*/
190+
function delegateBySig(
191+
address delegatee,
192+
uint256 nonce,
193+
uint256 expiry,
194+
uint8 v,
195+
bytes32 r,
196+
bytes32 s
197+
) public virtual {
198+
if (block.timestamp > expiry) {
199+
revert VotesExpiredSignature(expiry);
200+
}
201+
address signer = ECDSA.recover(
202+
_hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry))),
203+
v,
204+
r,
205+
s
206+
);
207+
_useCheckedNonce(signer, nonce);
208+
_delegate(signer, delegatee);
209+
}
210+
211+
/**
212+
* @dev Delegate all of `account`'s voting units to `delegatee`.
213+
*
214+
* Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}.
215+
*/
216+
function _delegate(address account, address delegatee) internal virtual {
217+
address oldDelegate = delegates(account);
218+
_delegatee[account] = delegatee;
219+
220+
_emitDelegateChanged(account, oldDelegate, delegatee);
221+
_moveDelegateVotes(oldDelegate, delegatee, _getVotingUnits(account));
222+
}
223+
224+
/**
225+
* @dev Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to`
226+
* should be zero. Total supply of voting units will be adjusted with mints and burns.
227+
*/
228+
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual {
229+
if (from == address(0)) {
230+
_push(_totalCheckpoints, _add, SafeCast.toUint208(amount));
231+
}
232+
if (to == address(0)) {
233+
_push(_totalCheckpoints, _subtract, SafeCast.toUint208(amount));
234+
}
235+
_moveDelegateVotes(delegates(from), delegates(to), amount);
236+
}
237+
238+
/**
239+
* @dev Moves delegated votes from one delegate to another.
240+
*/
241+
function _moveDelegateVotes(address from, address to, uint256 amount) internal virtual {
242+
if (from != to && amount > 0) {
243+
if (from != address(0)) {
244+
(uint256 oldValue, uint256 newValue) = _push(
245+
_delegateCheckpoints[from],
246+
_subtract,
247+
SafeCast.toUint208(amount)
248+
);
249+
_emitDelegateVotesChanged(from, oldValue, newValue);
250+
}
251+
if (to != address(0)) {
252+
(uint256 oldValue, uint256 newValue) = _push(
253+
_delegateCheckpoints[to],
254+
_add,
255+
SafeCast.toUint208(amount)
256+
);
257+
_emitDelegateVotesChanged(to, oldValue, newValue);
258+
}
259+
}
260+
}
261+
262+
/**
263+
* @dev Get number of checkpoints for `account`.
264+
*/
265+
function _numCheckpoints(address account) internal view virtual returns (uint32) {
266+
return SafeCast.toUint32(_delegateCheckpoints[account].length());
267+
}
268+
269+
/**
270+
* @dev Get the `pos`-th checkpoint for `account`.
271+
*/
272+
function _checkpoints(
273+
address account,
274+
uint32 pos
275+
) internal view virtual returns (Checkpoints.Checkpoint208 memory) {
276+
return _delegateCheckpoints[account].at(pos);
277+
}
278+
279+
function _push(
280+
Checkpoints.Trace208 storage store,
281+
function(uint208, uint208) view returns (uint208) op,
282+
uint208 delta
283+
) private returns (uint208 oldValue, uint208 newValue) {
284+
return store.push(clock(), op(store.latest(), delta));
285+
}
286+
287+
function _add(uint208 a, uint208 b) private pure returns (uint208) {
288+
return a + b;
289+
}
290+
291+
function _subtract(uint208 a, uint208 b) private pure returns (uint208) {
292+
return a - b;
293+
}
294+
295+
/**
296+
* @dev Must return the voting units held by an account.
297+
*/
298+
function _getVotingUnits(address) internal view virtual returns (uint256);
299+
300+
/**
301+
* @dev Internal function to emit DelegateChanged events
302+
* Can be overridden by derived contracts to implement custom emission logic
303+
* Default implementation emits private events with selective visibility
304+
* @param delegator The account that changed its delegation
305+
* @param fromDelegate The previous delegate (address(0) if this is the first delegation)
306+
* @param toDelegate The new delegate (address(0) if delegation is being removed)
307+
*/
308+
function _emitDelegateChanged(address delegator, address fromDelegate, address toDelegate) internal virtual {
309+
address[] memory allowedViewers = _getDelegateChangedEventViewers(delegator, fromDelegate, toDelegate);
310+
bytes memory payload = abi.encode(delegator, fromDelegate, toDelegate);
311+
312+
emit PrivateEvent(allowedViewers, EVENT_TYPE_DELEGATE_CHANGED, payload);
313+
}
314+
315+
/**
316+
* @dev Internal function to determine who can view DelegateChanged events
317+
* Can be overridden by derived contracts to implement custom viewer logic
318+
* Default implementation: delegator, old delegate, and new delegate can view
319+
* @param delegator The account that changed its delegation
320+
* @param fromDelegate The previous delegate
321+
* @param toDelegate The new delegate
322+
* @return allowedViewers Array of addresses authorized to view this delegation change
323+
*/
324+
function _getDelegateChangedEventViewers(
325+
address delegator,
326+
address fromDelegate,
327+
address toDelegate
328+
) internal view virtual returns (address[] memory allowedViewers) {
329+
// Count unique non-zero addresses
330+
uint256 viewerCount = 0;
331+
if (delegator != address(0)) viewerCount++;
332+
if (fromDelegate != address(0) && fromDelegate != delegator) viewerCount++;
333+
if (toDelegate != address(0) && toDelegate != delegator && toDelegate != fromDelegate) viewerCount++;
334+
335+
allowedViewers = new address[](viewerCount);
336+
uint256 index = 0;
337+
338+
// Populate viewer list with unique addresses
339+
if (delegator != address(0)) {
340+
allowedViewers[index++] = delegator;
341+
}
342+
if (fromDelegate != address(0) && fromDelegate != delegator) {
343+
allowedViewers[index++] = fromDelegate;
344+
}
345+
if (toDelegate != address(0) && toDelegate != delegator && toDelegate != fromDelegate) {
346+
allowedViewers[index] = toDelegate;
347+
}
348+
}
349+
350+
/**
351+
* @dev Internal function to emit DelegateVotesChanged events
352+
* Can be overridden by derived contracts to implement custom emission logic
353+
* Default implementation emits private events with selective visibility
354+
* @param delegateAddress The delegate whose vote weight changed
355+
* @param previousVotes The previous vote weight
356+
* @param newVotes The new vote weight
357+
*/
358+
function _emitDelegateVotesChanged(address delegateAddress, uint256 previousVotes, uint256 newVotes) internal virtual {
359+
address[] memory allowedViewers = _getDelegateVotesChangedEventViewers(delegateAddress, previousVotes, newVotes);
360+
bytes memory payload = abi.encode(delegateAddress, previousVotes, newVotes);
361+
362+
emit PrivateEvent(allowedViewers, EVENT_TYPE_DELEGATE_VOTES_CHANGED, payload);
363+
}
364+
365+
/**
366+
* @dev Internal function to determine who can view DelegateVotesChanged events
367+
* Can be overridden by derived contracts to implement custom viewer logic
368+
* Default implementation: only the delegate whose vote weight changed can view
369+
* @param delegateAddress The delegate whose vote weight changed
370+
* @param previousVotes The previous vote weight (available for derived contracts)
371+
* @param newVotes The new vote weight (available for derived contracts)
372+
* @return allowedViewers Array of addresses authorized to view this vote weight change
373+
*/
374+
function _getDelegateVotesChangedEventViewers(
375+
address delegateAddress,
376+
uint256 previousVotes,
377+
uint256 newVotes
378+
) internal view virtual returns (address[] memory allowedViewers) {
379+
previousVotes; // Available for derived contracts
380+
newVotes; // Available for derived contracts
381+
382+
// Only the delegate can view their vote weight changes by default
383+
if (delegateAddress != address(0)) {
384+
allowedViewers = new address[](1);
385+
allowedViewers[0] = delegateAddress;
386+
} else {
387+
allowedViewers = new address[](0);
388+
}
389+
}
390+
}

0 commit comments

Comments
 (0)