-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add smart and partial delegation #3
Changes from all commits
a7719cd
d7d9aa0
a041f0c
c58841a
80af104
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,14 +2,15 @@ | |
// OpenZeppelin Contracts (last updated v5.0.0) (governance/utils/Votes.sol) | ||
pragma solidity ^0.8.20; | ||
|
||
import {IERC5805} from '@openzeppelin/contracts/interfaces/IERC5805.sol'; | ||
import {IERC6372} from '@openzeppelin/contracts/interfaces/IERC6372.sol'; | ||
import {Context} from '@openzeppelin/contracts/utils/Context.sol'; | ||
import {Nonces} from '@openzeppelin/contracts/utils/Nonces.sol'; | ||
import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; | ||
import {Checkpoints} from '@openzeppelin/contracts/utils/structs/Checkpoints.sol'; | ||
import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; | ||
import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; | ||
import {Time} from '@openzeppelin/contracts/utils/types/Time.sol'; | ||
import {IWonderVotes} from 'interfaces/governance/utils/IWonderVotes.sol'; | ||
|
||
/** | ||
* @dev This is a base abstract contract that tracks voting units, which are a measure of voting power that can be | ||
|
@@ -29,16 +30,17 @@ | |
* {ERC721-balanceOf}), and can use {_transferVotingUnits} to track a change in the distribution of those units (in the | ||
* previous example, it would be included in {ERC721-_update}). | ||
*/ | ||
abstract contract WonderVotes is Context, EIP712, Nonces, IERC5805 { | ||
abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes { | ||
using Checkpoints for Checkpoints.Trace208; | ||
|
||
bytes32 private constant DELEGATION_TYPEHASH = keccak256('Delegation(address delegatee,uint256 nonce,uint256 expiry)'); | ||
bytes32 private constant DELEGATION_TYPEHASH = | ||
keccak256('Delegation(uint8 proposalType, Delegate[] delegatees,uint256 nonce,uint256 expiry)'); | ||
|
||
mapping(address account => address) private _delegatee; | ||
mapping(address account => mapping(uint8 proposalType => Delegate[])) private _delegatees; | ||
|
||
mapping(address delegatee => Checkpoints.Trace208) private _delegateCheckpoints; | ||
mapping(address delegatee => mapping(uint8 proposalType => Checkpoints.Trace208)) private _delegateCheckpoints; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here |
||
|
||
Checkpoints.Trace208 private _totalCheckpoints; | ||
mapping(uint8 proposalType => Checkpoints.Trace208) private _totalCheckpoints; | ||
|
||
/** | ||
* @dev The clock was incorrectly modified. | ||
|
@@ -48,13 +50,13 @@ | |
/** | ||
* @dev Lookup to future votes is not available. | ||
*/ | ||
error ERC5805FutureLookup(uint256 timepoint, uint48 clock); | ||
Check warning on line 53 in solidity/contracts/governance/utils/WonderVotes.sol GitHub Actions / Run Linters (18.x)
|
||
|
||
/** | ||
* @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based | ||
* checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match. | ||
*/ | ||
function clock() public view virtual returns (uint48) { | ||
return Time.blockNumber(); | ||
} | ||
|
||
|
@@ -62,7 +64,7 @@ | |
* @dev Machine-readable description of the clock as specified in EIP-6372. | ||
*/ | ||
// solhint-disable-next-line func-name-mixedcase | ||
function CLOCK_MODE() public view virtual returns (string memory) { | ||
// Check that the clock was not modified | ||
if (clock() != Time.blockNumber()) { | ||
revert ERC6372InconsistentClock(); | ||
|
@@ -71,30 +73,30 @@ | |
} | ||
|
||
/** | ||
* @dev Returns the current amount of votes that `account` has. | ||
* @dev Returns the current amount of votes that `account` has for the given `proposalType`. | ||
*/ | ||
function getVotes(address account) public view virtual returns (uint256) { | ||
return _delegateCheckpoints[account].latest(); | ||
function getVotes(address account, uint8 proposalType) public view virtual returns (uint256) { | ||
Check warning on line 78 in solidity/contracts/governance/utils/WonderVotes.sol GitHub Actions / Run Linters (18.x)
Check warning on line 78 in solidity/contracts/governance/utils/WonderVotes.sol GitHub Actions / Run Linters (18.x)
|
||
return _delegateCheckpoints[account][proposalType].latest(); | ||
} | ||
|
||
/** | ||
* @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is | ||
* @dev Returns the amount of votes that `account` had at a specific moment in the past for a given proposalType. If the `clock()` is | ||
* configured to use block numbers, this will return the value at the end of the corresponding block. | ||
* | ||
* Requirements: | ||
* | ||
* - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. | ||
*/ | ||
function getPastVotes(address account, uint256 timepoint) public view virtual returns (uint256) { | ||
function getPastVotes(address account, uint8 proposalType, uint256 timepoint) public view virtual returns (uint256) { | ||
Check warning on line 90 in solidity/contracts/governance/utils/WonderVotes.sol GitHub Actions / Run Linters (18.x)
|
||
uint48 currentTimepoint = clock(); | ||
if (timepoint >= currentTimepoint) { | ||
revert ERC5805FutureLookup(timepoint, currentTimepoint); | ||
} | ||
return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint48(timepoint)); | ||
return _delegateCheckpoints[account][proposalType].upperLookupRecent(SafeCast.toUint48(timepoint)); | ||
} | ||
|
||
/** | ||
* @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is | ||
* @dev Returns for a given `proposalType` the total supply of votes available at a specific moment in the past. If the `clock()` is | ||
* configured to use block numbers, this will return the value at the end of the corresponding block. | ||
* | ||
* NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. | ||
|
@@ -105,111 +107,226 @@ | |
* | ||
* - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. | ||
*/ | ||
function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) { | ||
function getPastTotalSupply(uint8 proposalType, uint256 timepoint) public view virtual returns (uint256) { | ||
uint48 currentTimepoint = clock(); | ||
if (timepoint >= currentTimepoint) { | ||
revert ERC5805FutureLookup(timepoint, currentTimepoint); | ||
} | ||
return _totalCheckpoints.upperLookupRecent(SafeCast.toUint48(timepoint)); | ||
return _totalCheckpoints[proposalType].upperLookupRecent(SafeCast.toUint48(timepoint)); | ||
} | ||
|
||
/** | ||
* @dev Returns the current total supply of votes. | ||
* @dev Returns the current total supply of votes for a given `proposalType`. | ||
*/ | ||
function _getTotalSupply() internal view virtual returns (uint256) { | ||
return _totalCheckpoints.latest(); | ||
function _getTotalSupply(uint8 proposalType) internal view virtual returns (uint256) { | ||
return _totalCheckpoints[proposalType].latest(); | ||
} | ||
|
||
/** | ||
* @dev Returns the delegate that `account` has chosen. | ||
* @dev Returns the delegates that `account` has chosen. | ||
*/ | ||
function delegates(address account) public view virtual returns (address) { | ||
return _delegatee[account]; | ||
function delegates(address account, uint8 proposalType) public view virtual returns (Delegate[] memory) { | ||
return _delegatees[account][proposalType]; | ||
} | ||
|
||
/** | ||
* @dev Delegates votes from the sender to `delegatee`. | ||
*/ | ||
function delegate(Delegate[] calldata delegatees, uint8 proposalType) public virtual validProposalType(proposalType) { | ||
address account = _msgSender(); | ||
_delegate(account, proposalType, delegatees); | ||
} | ||
|
||
/** | ||
* @dev See {IWonderVotes-delegate}. | ||
*/ | ||
function delegate(address delegatee, uint8 proposalType) public virtual validProposalType(proposalType) { | ||
address account = _msgSender(); | ||
Delegate[] memory _singleDelegate = new Delegate[](1); | ||
_singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); | ||
_delegate(account, proposalType, _singleDelegate); | ||
} | ||
|
||
/** | ||
* @dev See {IWonderVotes-delegate}. | ||
*/ | ||
function delegate(address delegatee) public virtual { | ||
address account = _msgSender(); | ||
_delegate(account, delegatee); | ||
Delegate[] memory _singleDelegate = new Delegate[](1); | ||
_singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); | ||
|
||
uint8[] memory proposalTypes = _getProposalTypes(); | ||
|
||
for (uint256 i = 0; i < proposalTypes.length; i++) { | ||
_delegate(account, proposalTypes[i], _singleDelegate); | ||
} | ||
} | ||
|
||
/** | ||
* @dev See {IWonderVotes-weightNormalizer}. | ||
*/ | ||
function weightNormalizer() external view virtual returns (uint256) { | ||
return _weightNormalizer(); | ||
} | ||
|
||
/** | ||
* @dev See {IWonderVotes-maxDelegates}. | ||
*/ | ||
function maxDelegates() external view returns (uint8) { | ||
return _maxDelegates(); | ||
} | ||
|
||
/** | ||
* @dev Delegates votes from signer to `delegatee`. | ||
*/ | ||
function delegateBySig( | ||
address delegatee, | ||
Delegate[] memory delegatees, | ||
uint8 proposalType, | ||
uint256 nonce, | ||
uint256 expiry, | ||
uint8 v, | ||
bytes32 r, | ||
bytes32 s | ||
) public virtual { | ||
) public virtual validProposalType(proposalType) { | ||
if (block.timestamp > expiry) { | ||
revert VotesExpiredSignature(expiry); | ||
} | ||
address signer = | ||
ECDSA.recover(_hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry))), v, r, s); | ||
address signer = ECDSA.recover( | ||
_hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, proposalType, delegatees, nonce, expiry))), v, r, s | ||
); | ||
_useCheckedNonce(signer, nonce); | ||
_delegate(signer, delegatee); | ||
_delegate(signer, proposalType, delegatees); | ||
} | ||
|
||
/** | ||
* @dev See {IWonderVotes-delegateBySig}. | ||
*/ | ||
function delegateBySig( | ||
address delegatee, | ||
uint8 proposalType, | ||
uint256 nonce, | ||
uint256 expiry, | ||
uint8 v, | ||
bytes32 r, | ||
bytes32 s | ||
) public virtual validProposalType(proposalType) { | ||
Delegate[] memory _singleDelegate = new Delegate[](1); | ||
_singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); | ||
delegateBySig(_singleDelegate, proposalType, nonce, expiry, v, r, s); | ||
} | ||
|
||
/** | ||
* @dev See {IWonderVotes-delegateBySig}. | ||
*/ | ||
function delegateBySig( | ||
address delegatee, | ||
uint256 nonce, | ||
uint256 expiry, | ||
uint8 v, | ||
bytes32 r, | ||
bytes32 s | ||
) public virtual { | ||
Delegate[] memory _singleDelegate = new Delegate[](1); | ||
_singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()}); | ||
|
||
uint8[] memory proposalTypes = _getProposalTypes(); | ||
|
||
for (uint256 i = 0; i < proposalTypes.length; i++) { | ||
delegateBySig(_singleDelegate, proposalTypes[i], nonce, expiry, v, r, s); | ||
} | ||
} | ||
|
||
/** | ||
* @dev Delegate all of `account`'s voting units to `delegatee`. | ||
* | ||
* Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}. | ||
*/ | ||
function _delegate(address account, address delegatee) internal virtual { | ||
address oldDelegate = delegates(account); | ||
_delegatee[account] = delegatee; | ||
function _delegate(address account, uint8 proposalType, Delegate[] memory delegatees) internal virtual { | ||
if (delegatees.length > _maxDelegates()) revert DelegatesMaxNumberExceeded(delegatees.length); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the max number of delegates that a user can delegate for a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got it, so on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exactly |
||
|
||
uint256 _weightSum; | ||
for (uint256 i = 0; i < delegatees.length; i++) { | ||
if (delegatees[i].weight == 0) revert ZeroWeight(); | ||
_weightSum += delegatees[i].weight; | ||
} | ||
if (_weightSum != _weightNormalizer()) revert InvalidWeightSum(_weightSum); | ||
|
||
Delegate[] memory _oldDelegates = delegates(account, proposalType); | ||
_delegatees[account][proposalType] = delegatees; | ||
|
||
emit DelegateChanged(account, proposalType, _oldDelegates, delegatees); | ||
_moveDelegateVotes(proposalType, _oldDelegates, delegatees, _getVotingUnits(account)); | ||
} | ||
|
||
/** | ||
* @dev Loops the proposalTypes implemented and calls the `_transferVotingUnits` helper method. | ||
*/ | ||
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual { | ||
uint8[] memory _proposalTypes = _getProposalTypes(); | ||
|
||
emit DelegateChanged(account, oldDelegate, delegatee); | ||
_moveDelegateVotes(oldDelegate, delegatee, _getVotingUnits(account)); | ||
for (uint256 i = 0; i < _proposalTypes.length; i++) { | ||
_transferVotingUnits(_proposalTypes[i], from, to, amount); | ||
} | ||
} | ||
|
||
/** | ||
* @dev Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to` | ||
* should be zero. Total supply of voting units will be adjusted with mints and burns. | ||
*/ | ||
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual { | ||
function _transferVotingUnits(uint8 proposalType, address from, address to, uint256 amount) private { | ||
if (from == address(0)) { | ||
_push(_totalCheckpoints, _add, SafeCast.toUint208(amount)); | ||
_push(_totalCheckpoints[proposalType], _add, SafeCast.toUint208(amount)); | ||
} | ||
if (to == address(0)) { | ||
_push(_totalCheckpoints, _subtract, SafeCast.toUint208(amount)); | ||
_push(_totalCheckpoints[proposalType], _subtract, SafeCast.toUint208(amount)); | ||
} | ||
_moveDelegateVotes(delegates(from), delegates(to), amount); | ||
_moveDelegateVotes(proposalType, delegates(from, proposalType), delegates(to, proposalType), amount); | ||
} | ||
|
||
/** | ||
* @dev Moves delegated votes from one delegate to another. | ||
*/ | ||
function _moveDelegateVotes(address from, address to, uint256 amount) private { | ||
if (from != to && amount > 0) { | ||
if (from != address(0)) { | ||
(uint256 oldValue, uint256 newValue) = _push(_delegateCheckpoints[from], _subtract, SafeCast.toUint208(amount)); | ||
emit DelegateVotesChanged(from, oldValue, newValue); | ||
function _moveDelegateVotes(uint8 proposalType, Delegate[] memory from, Delegate[] memory to, uint256 amount) private { | ||
uint256 _weightSum = _weightNormalizer(); | ||
uint256 _weight; | ||
|
||
for (uint256 i = 0; i < from.length; i++) { | ||
if (from[i].account != address(0)) { | ||
_weight = from[i].weight; | ||
uint256 _votingUnits = amount * _weight / _weightSum; | ||
(uint256 oldValue, uint256 newValue) = | ||
_push(_delegateCheckpoints[from[i].account][proposalType], _subtract, SafeCast.toUint208(_votingUnits)); | ||
emit DelegateVotesChanged(from[i].account, proposalType, oldValue, newValue); | ||
} | ||
if (to != address(0)) { | ||
(uint256 oldValue, uint256 newValue) = _push(_delegateCheckpoints[to], _add, SafeCast.toUint208(amount)); | ||
emit DelegateVotesChanged(to, oldValue, newValue); | ||
} | ||
|
||
for (uint256 i = 0; i < to.length; i++) { | ||
if (to[i].account != address(0)) { | ||
_weight = to[i].weight; | ||
uint256 _votingUnits = amount * _weight / _weightSum; | ||
(uint256 oldValue, uint256 newValue) = | ||
_push(_delegateCheckpoints[to[i].account][proposalType], _add, SafeCast.toUint208(_votingUnits)); | ||
emit DelegateVotesChanged(to[i].account, proposalType, oldValue, newValue); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @dev Get number of checkpoints for `account`. | ||
* @dev Get number of checkpoints for `account` given a `proposalType`. | ||
*/ | ||
function _numCheckpoints(address account) internal view virtual returns (uint32) { | ||
return SafeCast.toUint32(_delegateCheckpoints[account].length()); | ||
function _numCheckpoints(address account, uint8 proposalType) internal view virtual returns (uint32) { | ||
return SafeCast.toUint32(_delegateCheckpoints[account][proposalType].length()); | ||
} | ||
|
||
/** | ||
* @dev Get the `pos`-th checkpoint for `account`. | ||
* @dev Get the `pos`-th checkpoint for `account` given a `proposalType`. | ||
*/ | ||
function _checkpoints(address account, uint32 pos) internal view virtual returns (Checkpoints.Checkpoint208 memory) { | ||
return _delegateCheckpoints[account].at(pos); | ||
function _checkpoints( | ||
address account, | ||
uint8 proposalType, | ||
uint32 pos | ||
) internal view virtual returns (Checkpoints.Checkpoint208 memory) { | ||
return _delegateCheckpoints[account][proposalType].at(pos); | ||
} | ||
|
||
function _push( | ||
|
@@ -232,4 +349,32 @@ | |
* @dev Must return the voting units held by an account. | ||
*/ | ||
function _getVotingUnits(address) internal view virtual returns (uint256); | ||
|
||
/** | ||
* @dev Returns the total weight that each delegation should sum. | ||
*/ | ||
function _weightNormalizer() internal view virtual returns (uint256); | ||
|
||
/** | ||
* @dev Returns the types of proposals that are supported by the implementation. | ||
*/ | ||
function _getProposalTypes() internal view virtual returns (uint8[] memory); | ||
|
||
/** | ||
* @dev Returns the maximum number of delegates that `proposalType` can delegate to. | ||
*/ | ||
function _maxDelegates() internal view virtual returns (uint8); | ||
|
||
/** | ||
* @dev Returns true if the `proposalType` is valid, false otherwise. | ||
*/ | ||
function _validProposalType(uint8 proposalType) internal view virtual returns (bool); | ||
|
||
/** | ||
* @dev checks the `proposalType` validity | ||
*/ | ||
modifier validProposalType(uint8 proposalType) { | ||
if (!_validProposalType(proposalType)) revert InvalidProposalType(proposalType); | ||
_; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have you considered to use a single mapping with the hash(account, proposalType)? I guess it would be cheaper
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I considered it. We could discuss it. The main tradeoff that I see with the hash approach is that we would have to perform a hashing operation when the user votes and when delegates (in this case 1, hashing operation per account-proposalType) combination.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@skeletor-spaceman any thoughts about this one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hashing is done either way on the mapping, so it does not really optimizes anything, nested mappings are easier and cleaner to read.