Skip to content

Commit 35210ef

Browse files
committed
Add ERC20MultiVotes and MultiVotes
draft: update MultiVotes
1 parent efdc7cd commit 35210ef

File tree

12 files changed

+2244
-2
lines changed

12 files changed

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

0 commit comments

Comments
 (0)