-
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 1 commit
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,41 +107,81 @@ | |
* | ||
* - `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 { | ||
address account = _msgSender(); | ||
_delegate(account, proposalType, delegatees); | ||
} | ||
|
||
/** | ||
* @dev See {IWonderVotes-delegate}. | ||
*/ | ||
function delegate(address delegatee, uint8 proposalType) public virtual { | ||
address account = _msgSender(); | ||
Delegate[] memory _singleDelegate = new Delegate[](1); | ||
_singleDelegate[0] = Delegate({account: delegatee, weight: _totalWeight()}); | ||
_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: _totalWeight()}); | ||
|
||
uint8[] memory proposalTypes = _getProposalTypes(); | ||
|
||
for (uint256 i = 0; i < proposalTypes.length; i++) { | ||
_delegate(account, proposalTypes[i], _singleDelegate); | ||
} | ||
} | ||
|
||
/** | ||
* @dev See {IWonderVotes-totalWeight}. | ||
*/ | ||
function totalWeight() external view virtual returns (uint8) { | ||
return _totalWeight(); | ||
} | ||
|
||
/** | ||
* @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, | ||
|
@@ -149,67 +191,142 @@ | |
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 { | ||
Delegate[] memory _singleDelegate = new Delegate[](1); | ||
_singleDelegate[0] = Delegate({account: delegatee, weight: _totalWeight()}); | ||
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: _totalWeight()}); | ||
|
||
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 |
||
|
||
uint8 _weightSum; | ||
for (uint256 i = 0; i < delegatees.length; i++) { | ||
if (delegatees[i].weight == 0) revert ZeroWeight(); | ||
_weightSum += delegatees[i].weight; | ||
} | ||
if (_weightSum != _totalWeight()) revert InvalidWeightSum(_weightSum); | ||
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. an alternative approach (but don't think its worth) is normalizing, i.e. considering the proportion and translating to the real weights 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. that would even allow users to input percentages for example instead of precise uints 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. mmm not sure if I get this one, could you provide an example?. If I translate to real weight wouldn't that one be a float?. In this approach, the protocol implements the Eg. Let's suppose that a user delegates his votes (let's abstract the token decimals since it's transparent for us, and also the proposalTypes since we know that it will apply to all of them):
The user delegates to 3 different delegates, knowing that he has to distribute a total 255 in weights units
The formula is: The vote power distribution will be:
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. this block of code only controls that the 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. Really appreciate the details! What I meant was maybe not reverting if the sum of the weights doesnt add up to Example: suppose
and then you can compute I'm not sure if this is better, I think its a little bit more user friendly, but I might be mistaken 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. I considered this approach using relative weights; it's more flexible. The problem here might be that, in some conditions, it could overflow; let's suppose that a user delegates to two delegates with a considerable weight number but with a low balance. Then, if the user's balance significantly increments, for example, it could overflow when transferring the tokens and give the protocol less control over that. 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. ok, so end of the day, using 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. yes we could do this control, we would still have the downside that when transferring tokens for example we will always have to sum up all the weights of the arrays instead of using a straight calculation which will introduce more overhead 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. why does it need to be also, what happens when you transfer tokens out? 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. This is done for optimization every time you decide to distribute your voting power for proposal you will have to define the entire distribution. If we don't delegate the 100% of the weight, then we will have to allow the user to add more delegations to the same proposals, controlling the percentages that we already assigned which will introduce overhead. Another way if you mean this would be if delegating a percentage of weight that is not the 100% means that the user always keeps the rest of the voting power. And a new delegation will override the previous one for a proposalType that would make sense. But kinda could be the same that the current approach if the user delegates himself in the array with the weight desired to keep. when transferring tokens Could you expand on the ordered list of delegates solution?. Since all the delegates from the sender and all the delegates of the destination for all 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.
yes, i'm trying to figure out a way to avoid having to loop over all proposalTypes * all delegatees. but it might not be a desirable UX. |
||
|
||
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 { | ||
uint8 _weightSum = _totalWeight(); | ||
uint8 _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,19 @@ | |
* @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 _totalWeight() internal view virtual returns (uint8); | ||
|
||
/** | ||
* @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); | ||
} |
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.