Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-terms-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

Add ERC20MultiVotes with MultiVotes for partial delegations support
2 changes: 2 additions & 0 deletions contracts/governance/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you

{{VotesExtended}}

{{MultiVotes}}

== Timelock

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}.
Expand Down
73 changes: 73 additions & 0 deletions contracts/governance/utils/IMultiVotes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.5.0) (governance/utils/IMultiVotes.sol)
pragma solidity ^0.8.26;

import {IVotes} from "./IVotes.sol";

/**
* @dev Common interface for {ERC20MultiVotes} and other {MultiVotes}-enabled contracts.
*/
interface IMultiVotes is IVotes {
/**
* @dev Invalid, start should be equal or smaller than end.
*/
error StartIsBiggerThanEnd(uint256 start, uint256 end);

/**
* @dev Requested more units than actually available.
*/
error MultiVotesExceededAvailableUnits(uint256 requested, uint256 available);

/**
* @dev Mismatch between number of given delegates and correspective units.
*/
error MultiVotesDelegatesAndUnitsMismatch(uint256 delegatesLength, uint256 unitsLength);

/**
* @dev Invalid operation, you should give at least one delegate.
*/
error MultiVotesNoDelegatesGiven();

/**
* @dev Emitted when units assigned to a partial delegate are modified.
*/
event DelegateModified(address indexed delegator, address indexed delegate, uint256 fromUnits, uint256 toUnits);

/**
* @dev Returns `account` partial delegations list starting from `start` to `end`.
*
* NOTE: Order may unexpectedly change if called in different transactions.
* Trust the returned array only if you obtain it within a single transaction.
*/
function multiDelegates(address account, uint256 start, uint256 end) external view returns (address[] memory);

/**
* @dev Set delegates list with units assigned for each one
*/
function multiDelegate(address[] calldata delegatess, uint256[] calldata units) external;

/**
* @dev Multi delegate votes from signer to `delegatess`.
*/
function multiDelegateBySig(
address[] calldata delegatess,
uint256[] calldata units,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external;

/**
* @dev Returns number of units a partial delegate of `account` has.
*
* NOTE: This function returns only the partial delegation value, defaulted units are not counted
*/
function getDelegatedUnits(address account, address delegatee) external view returns (uint256);

/**
* @dev Returns number of unassigned units that `account` has. Free units are assigned to the Votes single delegate selected.
*/
function getFreeUnits(address account) external view returns (uint256);
}
289 changes: 289 additions & 0 deletions contracts/governance/utils/MultiVotes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.5.0) (governance/utils/MultiVotes.sol)
pragma solidity ^0.8.26;

import {Checkpoints} from "../../utils/structs/Checkpoints.sol";
import {Votes} from "./Votes.sol";
import {SafeCast} from "../../utils/math/SafeCast.sol";
import {ECDSA} from "../../utils/cryptography/ECDSA.sol";
import {IMultiVotes} from "./IMultiVotes.sol";

/**
* @dev Extension of {Votes} with support for partial delegations.
* You can give a fixed amount of voting power to each delegate and select one as `defaulted` using {Votes} methods
* `defaulted` takes all of the remaining votes.
*
* NOTE: If inheriting from this contract there are things you should be careful of
* multiDelegates getter is considered possibly failing for out of gas if too many partial delegates are assigned
* If a limit on the number of delegates per delegator is enforced, {multiDelegates} can be considered reliable.
*/
abstract contract MultiVotes is Votes, IMultiVotes {
bytes32 private constant MULTI_DELEGATION_TYPEHASH =
keccak256("MultiDelegation(address[] delegatees,uint256[] units,uint256 nonce,uint256 expiry)");

/**
* NOTE: If you work directly with these mappings be careful.
* Only _delegatesList is assured to have up to date and coherent data.
* Values on _delegatesIndex and _delegatesUnits may be left dangling to save on gas.
* So always use _accountHasDelegate() before giving trust to _delegatesIndex and _delegatesUnits values.
*/
mapping(address account => address[]) private _delegatesList;
mapping(address account => mapping(address delegatee => uint256)) private _delegatesIndex;
mapping(address account => mapping(address delegatee => uint256)) private _delegatesUnits;

mapping(address account => uint256) private _usedUnits;

/**
* @inheritdoc Votes
*/
function _delegate(address account, address delegatee) internal virtual override {
address oldDelegate = delegates(account);
_setDelegate(account, delegatee);

emit DelegateChanged(account, oldDelegate, delegatee);
_moveDelegateVotes(oldDelegate, delegatee, getFreeUnits(account));
}

/**
* @inheritdoc Votes
*/
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual override {
if (from != address(0)) {
uint256 freeUnits = getFreeUnits(from);
require(amount <= freeUnits, MultiVotesExceededAvailableUnits(amount, freeUnits));
}
super._transferVotingUnits(from, to, amount);
}

/**
* @dev Returns `account` partial delegations list starting from `start` to `end`.
*
* NOTE: Order may unexpectedly change if called in different transactions.
* Trust the returned array only if you obtain it within a single transaction.
*/
function multiDelegates(
address account,
uint256 start,
uint256 end
) public view virtual returns (address[] memory) {
uint256 maxLength = _delegatesList[account].length;
require(end >= start, StartIsBiggerThanEnd(start, end));
if (start >= maxLength) {
address[] memory empty = new address[](0);
return empty;
}

if (end >= maxLength) {
end = maxLength - 1;
}
uint256 length = (end + 1) - start;
address[] memory list = new address[](length);

for (uint256 i; i < length; i++) {
list[i] = _delegatesList[account][start + i];
}

return list;
}

/**
* @dev Set delegates list with units assigned for each one
*/
function multiDelegate(address[] calldata delegatees, uint256[] calldata units) public virtual {
address account = _msgSender();
_multiDelegate(account, delegatees, units);
}

/**
* @dev Multi delegate votes from signer to `delegatees`.
*/
function multiDelegateBySig(
address[] calldata delegatees,
uint256[] calldata units,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
if (block.timestamp > expiry) {
revert VotesExpiredSignature(expiry);
}

bytes32 delegatesHash = keccak256(abi.encodePacked(delegatees));
bytes32 unitsHash = keccak256(abi.encodePacked(units));
bytes32 structHash = keccak256(abi.encode(MULTI_DELEGATION_TYPEHASH, delegatesHash, unitsHash, nonce, expiry));

address signer = ECDSA.recover(_hashTypedDataV4(structHash), v, r, s);

_useCheckedNonce(signer, nonce);
_multiDelegate(signer, delegatees, units);
}

/**
* @dev Add delegates to the multi delegation list or modify units of already existing.
*
* Emits multiple events {IMultiVotes-DelegateAdded} and {IMultiVotes-DelegateModified}.
*/
function _multiDelegate(
address account,
address[] calldata delegatees,
uint256[] calldata unitsList
) internal virtual {
require(
delegatees.length == unitsList.length,
MultiVotesDelegatesAndUnitsMismatch(delegatees.length, unitsList.length)
);
require(delegatees.length > 0, MultiVotesNoDelegatesGiven());

uint256 givenUnits;
uint256 removedUnits;
for (uint256 i; i < delegatees.length; i++) {
address delegatee = delegatees[i];
uint256 units = unitsList[i];

if (units != 0) {
if (_accountHasDelegate(account, delegatee)) {
(uint256 difference, bool refunded) = _modifyDelegate(account, delegatee, units);
refunded ? givenUnits += difference : removedUnits += difference;
continue;
}

_addDelegate(account, delegatee, units);
givenUnits += units;
} else {
removedUnits += _removeDelegate(account, delegatee);
}
}

if (removedUnits >= givenUnits) {
uint256 refundedUnits;
refundedUnits = removedUnits - givenUnits;
/**
* Cannot Underflow: code logic assures that _usedUnits[account] is just a sum of active delegates units
* and that every units change of delegate on `account`, updates coherently _usedUnits
* so refundedUnits cannot be higher than _usedUnits[account]
*/
unchecked {
_usedUnits[account] -= refundedUnits;
}
_moveDelegateVotes(address(0), delegates(account), refundedUnits);
} else {
uint256 addedUnits = givenUnits - removedUnits;
uint256 availableUnits = getFreeUnits(account);
require(availableUnits >= addedUnits, MultiVotesExceededAvailableUnits(addedUnits, availableUnits));

_usedUnits[account] += addedUnits;
_moveDelegateVotes(delegates(account), address(0), addedUnits);
}
}

/**
* @dev Helper for _multiDelegate that adds a delegate to multi delegations.
*
* Emits event {IMultiVotes-DelegateModified}.
*
* NOTE: this function does not automatically update _usedUnits and should never receive 0 `units` value
*/
function _addDelegate(address account, address delegatee, uint256 units) private {
_delegatesUnits[account][delegatee] = units;
_delegatesIndex[account][delegatee] = _delegatesList[account].length;
_delegatesList[account].push(delegatee);
emit DelegateModified(account, delegatee, 0, units);

_moveDelegateVotes(address(0), delegatee, units);
}

/**
* @dev Helper for _multiDelegate to modify a specific delegate. Returns difference and if it's refunded units.
*
* Emits event {IMultiVotes-DelegateModified}.
*
* NOTE: this function does not automatically update _usedUnits and should never receive 0 `units` value
*/
function _modifyDelegate(
address account,
address delegatee,
uint256 units
) private returns (uint256 difference, bool refunded) {
uint256 oldUnits = _delegatesUnits[account][delegatee];

if (oldUnits == units) return (0, false);

if (oldUnits > units) {
difference = oldUnits - units;
_moveDelegateVotes(delegatee, address(0), difference);
} else {
difference = units - oldUnits;
_moveDelegateVotes(address(0), delegatee, difference);
refunded = true;
}

_delegatesUnits[account][delegatee] = units;
emit DelegateModified(account, delegatee, oldUnits, units);
return (difference, refunded);
}

/**
* @dev Helper for _multiDelegate to remove a delegate from multi delegations list. Returns removed units.
*
* Emits event {IMultiVotes-DelegateModified}.
*
* NOTE: this function does not automatically update _usedUnits
*/
function _removeDelegate(address account, address delegatee) private returns (uint256) {
if (!_accountHasDelegate(account, delegatee)) return 0;

uint256 delegateIndex = _delegatesIndex[account][delegatee];
uint256 lastDelegateIndex = _delegatesList[account].length - 1;
address lastDelegate = _delegatesList[account][lastDelegateIndex];
uint256 refundedUnits = _delegatesUnits[account][delegatee];

_delegatesList[account][delegateIndex] = lastDelegate;
_delegatesIndex[account][lastDelegate] = delegateIndex;
_delegatesList[account].pop();
emit DelegateModified(account, delegatee, refundedUnits, 0);

_moveDelegateVotes(delegatee, address(0), refundedUnits);
return refundedUnits;
}

/**
* @dev Returns number of units a partial delegate of `account` has.
*
* NOTE: This function returns only the partial delegation value, defaulted units are not counted
*/
function getDelegatedUnits(address account, address delegatee) public view virtual returns (uint256) {
if (!_accountHasDelegate(account, delegatee)) {
return 0;
}
return _delegatesUnits[account][delegatee];
}

/**
* @dev Returns number of unassigned units that `account` has. Free units are assigned to the Votes single delegate selected.
*/
function getFreeUnits(address account) public view virtual returns (uint256) {
return _getVotingUnits(account) - _usedUnits[account];
}

/**
* @dev Returns true if account has a specific delegate.
*
* NOTE: This works only assuming that every time a value is added to _delegatesList
* the corresponding entries in _delegatesUnits and _delegatesIndex are updated.
*/
function _accountHasDelegate(address account, address delegatee) internal view virtual returns (bool) {
uint256 delegateIndex = _delegatesIndex[account][delegatee];

if (_delegatesList[account].length <= delegateIndex) {
return false;
}

if (delegatee == _delegatesList[account][delegateIndex]) {
return true;
} else {
return false;
}
}
}
Loading
Loading