Skip to content

Commit 23d901c

Browse files
committed
Add ERC20MultiVotes and MultiVotes
1 parent 0950257 commit 23d901c

File tree

12 files changed

+2191
-2
lines changed

12 files changed

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

contracts/governance/utils/Votes.sol

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// OpenZeppelin Contracts (last updated v5.2.0) (governance/utils/Votes.sol)
2+
// OpenZeppelin Contracts (last updated v5.5.0) (governance/utils/Votes.sol)
33
pragma solidity ^0.8.24;
44

55
import {IERC5805} from "../../interfaces/IERC5805.sol";
@@ -162,7 +162,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
162162
}
163163

164164
/**
165-
* @dev Delegate all of `account`'s voting units to `delegatee`.
165+
* @dev Delegate all available `account`'s voting units to `delegatee`.
166166
*
167167
* Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}.
168168
*/
@@ -174,6 +174,13 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 {
174174
_moveDelegateVotes(oldDelegate, delegatee, _getVotingUnits(account));
175175
}
176176

177+
/**
178+
@dev Setter of _delegatee for inheriting contracts
179+
*/
180+
function _setDelegate(address account, address delegatee) internal virtual {
181+
_delegatee[account] = delegatee;
182+
}
183+
177184
/**
178185
* @dev Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to`
179186
* should be zero. Total supply of voting units will be adjusted with mints and burns.

0 commit comments

Comments
 (0)