Skip to content

Commit ad42de7

Browse files
committed
Add ERC20MultiVotes and MultiVotes
1 parent efdc7cd commit ad42de7

File tree

12 files changed

+2246
-2
lines changed

12 files changed

+2246
-2
lines changed

.changeset/silent-terms-beg.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+
Add ERC20MultiVotes with MultiVotes for partial delegations support

contracts/governance/README.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you
110110

111111
{{VotesExtended}}
112112

113+
{{MultiVotes}}
114+
113115
== Timelock
114116

115117
In a governance system, the {TimelockController} contract is in charge of introducing a delay between a proposal and its execution. It can be used with or without a {Governor}.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts (last updated v5.5.0) (governance/utils/IMultiVotes.sol)
3+
pragma solidity ^0.8.26;
4+
5+
import {IVotes} from "./IVotes.sol";
6+
7+
/**
8+
* @dev Common interface for {ERC20MultiVotes} and other {MultiVotes}-enabled contracts.
9+
*/
10+
interface IMultiVotes is IVotes {
11+
/**
12+
* @dev Invalid, start should be equal or smaller than end.
13+
*/
14+
error StartIsBiggerThanEnd(uint256 start, uint256 end);
15+
16+
/**
17+
* @dev Requested more units than actually available.
18+
*/
19+
error MultiVotesExceededAvailableUnits(uint256 requested, uint256 available);
20+
21+
/**
22+
* @dev Mismatch between number of given delegates and correspective units.
23+
*/
24+
error MultiVotesDelegatesAndUnitsMismatch(uint256 delegatesLength, uint256 unitsLength);
25+
26+
/**
27+
* @dev Invalid operation, you should give at least one delegate.
28+
*/
29+
error MultiVotesNoDelegatesGiven();
30+
31+
/**
32+
* @dev Emitted when units assigned to a partial delegate are modified.
33+
*/
34+
event DelegateModified(address indexed delegator, address indexed delegate, uint256 fromUnits, uint256 toUnits);
35+
36+
/**
37+
* @dev Returns `account` partial delegations list starting from `start` to `end`.
38+
*
39+
* NOTE: Order may unexpectedly change if called in different transactions.
40+
* Trust the returned array only if you obtain it within a single transaction.
41+
*/
42+
function multiDelegates(address account, uint256 start, uint256 end) external view returns (address[] memory);
43+
44+
/**
45+
* @dev Set delegates list with units assigned for each one
46+
*/
47+
function multiDelegate(address[] calldata delegatess, uint256[] calldata units) external;
48+
49+
/**
50+
* @dev Multi delegate votes from signer to `delegatess`.
51+
*/
52+
function multiDelegateBySig(
53+
address[] calldata delegatess,
54+
uint256[] calldata units,
55+
uint256 nonce,
56+
uint256 expiry,
57+
uint8 v,
58+
bytes32 r,
59+
bytes32 s
60+
) external;
61+
62+
/**
63+
* @dev Returns number of units a partial delegate of `account` has.
64+
*
65+
* NOTE: This function returns only the partial delegation value, defaulted units are not counted
66+
*/
67+
function getDelegatedUnits(address account, address delegatee) external view returns (uint256);
68+
69+
/**
70+
* @dev Returns number of unassigned units that `account` has. Free units are assigned to the Votes single delegate selected.
71+
*/
72+
function getFreeUnits(address account) external view returns (uint256);
73+
}
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts (last updated v5.5.0) (governance/utils/MultiVotes.sol)
3+
pragma solidity ^0.8.26;
4+
5+
import {Checkpoints} from "../../utils/structs/Checkpoints.sol";
6+
import {Votes} from "./Votes.sol";
7+
import {SafeCast} from "../../utils/math/SafeCast.sol";
8+
import {ECDSA} from "../../utils/cryptography/ECDSA.sol";
9+
import {IMultiVotes} from "./IMultiVotes.sol";
10+
11+
/**
12+
* @dev Extension of {Votes} with support for partial delegations.
13+
* You can give a fixed amount of voting power to each delegate and select one as `defaulted` using {Votes} methods
14+
* `defaulted` takes all of the remaining votes.
15+
*
16+
* NOTE: If inheriting from this contract there are things you should be careful of
17+
* multiDelegates getter is considered possibly failing for out of gas if too many partial delegates are assigned
18+
* If a limit on the number of delegates per delegator is enforced, {multiDelegates} can be considered reliable.
19+
*/
20+
abstract contract MultiVotes is Votes, IMultiVotes {
21+
bytes32 private constant MULTI_DELEGATION_TYPEHASH =
22+
keccak256("MultiDelegation(address[] delegatees,uint256[] units,uint256 nonce,uint256 expiry)");
23+
24+
/**
25+
* NOTE: If you work directly with these mappings be careful.
26+
* Only _delegatesList is assured to have up to date and coherent data.
27+
* Values on _delegatesIndex and _delegatesUnits may be left dangling to save on gas.
28+
* So always use _accountHasDelegate() before giving trust to _delegatesIndex and _delegatesUnits values.
29+
*/
30+
mapping(address account => address[]) private _delegatesList;
31+
mapping(address account => mapping(address delegatee => uint256)) private _delegatesIndex;
32+
mapping(address account => mapping(address delegatee => uint256)) private _delegatesUnits;
33+
34+
mapping(address account => uint256) private _usedUnits;
35+
36+
/**
37+
* @inheritdoc Votes
38+
*/
39+
function _delegate(address account, address delegatee) internal virtual override {
40+
address oldDelegate = delegates(account);
41+
_setDelegate(account, delegatee);
42+
43+
emit DelegateChanged(account, oldDelegate, delegatee);
44+
_moveDelegateVotes(oldDelegate, delegatee, getFreeUnits(account));
45+
}
46+
47+
/**
48+
* @inheritdoc Votes
49+
*/
50+
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual override {
51+
if (from != address(0)) {
52+
uint256 freeUnits = getFreeUnits(from);
53+
require(amount <= freeUnits, MultiVotesExceededAvailableUnits(amount, freeUnits));
54+
}
55+
super._transferVotingUnits(from, to, amount);
56+
}
57+
58+
/**
59+
* @dev Returns `account` partial delegations list starting from `start` to `end`.
60+
*
61+
* NOTE: Order may unexpectedly change if called in different transactions.
62+
* Trust the returned array only if you obtain it within a single transaction.
63+
*/
64+
function multiDelegates(
65+
address account,
66+
uint256 start,
67+
uint256 end
68+
) public view virtual returns (address[] memory) {
69+
uint256 maxLength = _delegatesList[account].length;
70+
require(end >= start, StartIsBiggerThanEnd(start, end));
71+
if (start >= maxLength) {
72+
address[] memory empty = new address[](0);
73+
return empty;
74+
}
75+
76+
if (end >= maxLength) {
77+
end = maxLength - 1;
78+
}
79+
uint256 length = (end + 1) - start;
80+
address[] memory list = new address[](length);
81+
82+
for (uint256 i; i < length; i++) {
83+
list[i] = _delegatesList[account][start + i];
84+
}
85+
86+
return list;
87+
}
88+
89+
/**
90+
* @dev Set delegates list with units assigned for each one
91+
*/
92+
function multiDelegate(address[] calldata delegatees, uint256[] calldata units) public virtual {
93+
address account = _msgSender();
94+
_multiDelegate(account, delegatees, units);
95+
}
96+
97+
/**
98+
* @dev Multi delegate votes from signer to `delegatees`.
99+
*/
100+
function multiDelegateBySig(
101+
address[] calldata delegatees,
102+
uint256[] calldata units,
103+
uint256 nonce,
104+
uint256 expiry,
105+
uint8 v,
106+
bytes32 r,
107+
bytes32 s
108+
) public virtual {
109+
if (block.timestamp > expiry) {
110+
revert VotesExpiredSignature(expiry);
111+
}
112+
113+
bytes32 delegatesHash = keccak256(abi.encodePacked(delegatees));
114+
bytes32 unitsHash = keccak256(abi.encodePacked(units));
115+
bytes32 structHash = keccak256(abi.encode(MULTI_DELEGATION_TYPEHASH, delegatesHash, unitsHash, nonce, expiry));
116+
117+
address signer = ECDSA.recover(_hashTypedDataV4(structHash), v, r, s);
118+
119+
_useCheckedNonce(signer, nonce);
120+
_multiDelegate(signer, delegatees, units);
121+
}
122+
123+
/**
124+
* @dev Add delegates to the multi delegation list or modify units of already existing.
125+
*
126+
* Emits multiple events {IMultiVotes-DelegateAdded} and {IMultiVotes-DelegateModified}.
127+
*/
128+
function _multiDelegate(
129+
address account,
130+
address[] calldata delegatees,
131+
uint256[] calldata unitsList
132+
) internal virtual {
133+
require(
134+
delegatees.length == unitsList.length,
135+
MultiVotesDelegatesAndUnitsMismatch(delegatees.length, unitsList.length)
136+
);
137+
require(delegatees.length > 0, MultiVotesNoDelegatesGiven());
138+
139+
uint256 givenUnits;
140+
uint256 removedUnits;
141+
for (uint256 i; i < delegatees.length; i++) {
142+
address delegatee = delegatees[i];
143+
uint256 units = unitsList[i];
144+
145+
if (units != 0) {
146+
if (_accountHasDelegate(account, delegatee)) {
147+
(uint256 difference, bool refunded) = _modifyDelegate(account, delegatee, units);
148+
refunded ? givenUnits += difference : removedUnits += difference;
149+
continue;
150+
}
151+
152+
_addDelegate(account, delegatee, units);
153+
givenUnits += units;
154+
} else {
155+
removedUnits += _removeDelegate(account, delegatee);
156+
}
157+
}
158+
159+
if (removedUnits >= givenUnits) {
160+
uint256 refundedUnits;
161+
refundedUnits = removedUnits - givenUnits;
162+
/**
163+
* Cannot Underflow: code logic assures that _usedUnits[account] is just a sum of active delegates units
164+
* and that every units change of delegate on `account`, updates coherently _usedUnits
165+
* so refundedUnits cannot be higher than _usedUnits[account]
166+
*/
167+
unchecked {
168+
_usedUnits[account] -= refundedUnits;
169+
}
170+
_moveDelegateVotes(address(0), delegates(account), refundedUnits);
171+
} else {
172+
uint256 addedUnits = givenUnits - removedUnits;
173+
uint256 availableUnits = getFreeUnits(account);
174+
require(availableUnits >= addedUnits, MultiVotesExceededAvailableUnits(addedUnits, availableUnits));
175+
176+
_usedUnits[account] += addedUnits;
177+
_moveDelegateVotes(delegates(account), address(0), addedUnits);
178+
}
179+
}
180+
181+
/**
182+
* @dev Helper for _multiDelegate that adds a delegate to multi delegations.
183+
*
184+
* Emits event {IMultiVotes-DelegateModified}.
185+
*
186+
* NOTE: this function does not automatically update _usedUnits and should never receive 0 `units` value
187+
*/
188+
function _addDelegate(address account, address delegatee, uint256 units) private {
189+
_delegatesUnits[account][delegatee] = units;
190+
_delegatesIndex[account][delegatee] = _delegatesList[account].length;
191+
_delegatesList[account].push(delegatee);
192+
emit DelegateModified(account, delegatee, 0, units);
193+
194+
_moveDelegateVotes(address(0), delegatee, units);
195+
}
196+
197+
/**
198+
* @dev Helper for _multiDelegate to modify a specific delegate. Returns difference and if it's refunded units.
199+
*
200+
* Emits event {IMultiVotes-DelegateModified}.
201+
*
202+
* NOTE: this function does not automatically update _usedUnits and should never receive 0 `units` value
203+
*/
204+
function _modifyDelegate(
205+
address account,
206+
address delegatee,
207+
uint256 units
208+
) private returns (uint256 difference, bool refunded) {
209+
uint256 oldUnits = _delegatesUnits[account][delegatee];
210+
211+
if (oldUnits == units) return (0, false);
212+
213+
if (oldUnits > units) {
214+
difference = oldUnits - units;
215+
_moveDelegateVotes(delegatee, address(0), difference);
216+
} else {
217+
difference = units - oldUnits;
218+
_moveDelegateVotes(address(0), delegatee, difference);
219+
refunded = true;
220+
}
221+
222+
_delegatesUnits[account][delegatee] = units;
223+
emit DelegateModified(account, delegatee, oldUnits, units);
224+
return (difference, refunded);
225+
}
226+
227+
/**
228+
* @dev Helper for _multiDelegate to remove a delegate from multi delegations list. Returns removed units.
229+
*
230+
* Emits event {IMultiVotes-DelegateModified}.
231+
*
232+
* NOTE: this function does not automatically update _usedUnits
233+
*/
234+
function _removeDelegate(address account, address delegatee) private returns (uint256) {
235+
if (!_accountHasDelegate(account, delegatee)) return 0;
236+
237+
uint256 delegateIndex = _delegatesIndex[account][delegatee];
238+
uint256 lastDelegateIndex = _delegatesList[account].length - 1;
239+
address lastDelegate = _delegatesList[account][lastDelegateIndex];
240+
uint256 refundedUnits = _delegatesUnits[account][delegatee];
241+
242+
_delegatesList[account][delegateIndex] = lastDelegate;
243+
_delegatesIndex[account][lastDelegate] = delegateIndex;
244+
_delegatesList[account].pop();
245+
emit DelegateModified(account, delegatee, refundedUnits, 0);
246+
247+
_moveDelegateVotes(delegatee, address(0), refundedUnits);
248+
return refundedUnits;
249+
}
250+
251+
/**
252+
* @dev Returns number of units a partial delegate of `account` has.
253+
*
254+
* NOTE: This function returns only the partial delegation value, defaulted units are not counted
255+
*/
256+
function getDelegatedUnits(address account, address delegatee) public view virtual returns (uint256) {
257+
if (!_accountHasDelegate(account, delegatee)) {
258+
return 0;
259+
}
260+
return _delegatesUnits[account][delegatee];
261+
}
262+
263+
/**
264+
* @dev Returns number of unassigned units that `account` has. Free units are assigned to the Votes single delegate selected.
265+
*/
266+
function getFreeUnits(address account) public view virtual returns (uint256) {
267+
return _getVotingUnits(account) - _usedUnits[account];
268+
}
269+
270+
/**
271+
* @dev Returns true if account has a specific delegate.
272+
*
273+
* NOTE: This works only assuming that every time a value is added to _delegatesList
274+
* the corresponding entries in _delegatesUnits and _delegatesIndex are updated.
275+
*/
276+
function _accountHasDelegate(address account, address delegatee) internal view virtual returns (bool) {
277+
uint256 delegateIndex = _delegatesIndex[account][delegatee];
278+
279+
if (_delegatesList[account].length <= delegateIndex) {
280+
return false;
281+
}
282+
283+
if (delegatee == _delegatesList[account][delegateIndex]) {
284+
return true;
285+
} else {
286+
return false;
287+
}
288+
}
289+
}

0 commit comments

Comments
 (0)