forked from DecentralizedClimateFoundation/DCIPs
403 lines
18 KiB
Markdown
403 lines
18 KiB
Markdown
---
|
|
eip: 5805
|
|
title: Voting with delegation
|
|
description: An interface for voting weight tracking, with delegation support
|
|
author: Hadrien Croubois (@Amxx), Francisco Giordano (@frangio)
|
|
discussions-to: https://ethereum-magicians.org/t/eip-5805-voting-with-delegation/11407
|
|
status: Review
|
|
type: Standards Track
|
|
category: ERC
|
|
created: 2022-07-04
|
|
requires: 712, 6372
|
|
---
|
|
|
|
## Abstract
|
|
|
|
Many DAOs (decentralized autonomous organizations) rely on tokens to represent one's voting power. In order to perform this task effectively, the token contracts need to include specific mechanisms such as checkpoints and delegation. The existing implementations are not standardized. This ERC proposes to standardize the way votes are delegated from one account to another, and the way current and past votes are tracked and queried. The corresponding behavior is compatible with many token types, including but not limited to [ERC-20](./eip-20.md) and [ERC-721](./eip-721.md). This ERC also considers the diversity of time tracking functions, allowing the voting tokens (and any contract associated with it) to track the votes based on `block.number`, `block.timestamp`, or any other non-decreasing function.
|
|
|
|
## Motivation
|
|
|
|
Beyond simple monetary transactions, decentralized autonomous organizations are arguably one of the most important use cases of blockchain and smart contract technologies. Today, many communities are organized around a governance contract that allows users to vote. Among these communities, some represent voting power using transferable tokens ([ERC-20](./eip-20.md), [ERC-721](./eip-721.md), other). In this context, the more tokens one owns, the more voting power one has. Governor contracts, such as Compound's `GovernorBravo`, read from these "voting token" contracts to get the voting power of the users.
|
|
|
|
Unfortunately, simply using the `balanceOf(address)` function present in most token standards is not good enough:
|
|
|
|
- The values are not checkpointed, so a user can vote, transfer its tokens to a new account, and vote again with the same tokens.
|
|
- A user cannot delegate their voting power to someone else without transferring full ownership of the tokens.
|
|
|
|
These constraints have led to the emergence of voting tokens with delegation that contain the following logic:
|
|
|
|
- Users can delegate the voting power of their tokens to themselves or a third party. This creates a distinction between balance and voting weight.
|
|
- The voting weights of accounts are checkpointed, allowing lookups for past values at different points in time.
|
|
- The balances are not checkpointed.
|
|
|
|
This ERC is proposing to standardize the interface and behavior of these voting tokens.
|
|
|
|
Additionally, the existing (non-standardized) implementations are limited to `block.number` based checkpoints. This choice causes many issues in a multichain environment, where some chains (particularly L2s) have an inconsistent or unpredictable time between blocks. This ERC also addresses this issue by allowing the voting token to use any time tracking function it wants, and exposing it so that other contracts (such as a Governor) can stay consistent with the token checkpoints.
|
|
|
|
## Specification
|
|
|
|
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
|
|
|
|
Following pre-existing (but not-standardized) implementation, the EIP proposes the following mechanism.
|
|
|
|
Each user account (address) can delegate to an account of its choice. This can be itself, someone else, or no one (represented by `address(0)`). Assets held by the user cannot express their voting power unless they are delegated.
|
|
|
|
When a "delegator" delegates its tokens voting power to a "delegatee", its balance is added to the voting power of the delegatee. If the delegator changes its delegation, the voting power is subtracted from the old delegatee's voting power and added to the new delegate's voting power. The voting power of each account is tracked through time so that it is possible to query its value in the past. With tokens being delegated to at most one delegate at a given point in time, double voting is prevented.
|
|
|
|
Whenever tokens are transferred from one account to another, the associated voting power should be deducted from the sender's delegate and added to the receiver's delegate.
|
|
|
|
Tokens that are delegated to `address(0)` should not be tracked. This allows users to optimize the gas cost of their token transfers by skipping the checkpoint update for their delegate.
|
|
|
|
To accommodate different types of chains, we want the voting checkpoint system to support different forms of time tracking. On the Ethereum mainnet, using block numbers provides backward compatibility with applications that historically use it. On the other hand, using timestamps provides better semantics for end users, and accommodates use cases where the duration is expressed in seconds. Other monotonic functions could also be deemed relevant by developers based on the characteristics of future applications and blockchains.
|
|
|
|
Both timestamps, block numbers, and other possible modes use the same external interfaces. This allows transparent binding of third-party contracts, such as governor systems, to the vote tracking built into the voting contracts. For this to be effective, the voting contracts must, in addition to all the vote-tracking functions, expose the current value used for time-tracking.
|
|
|
|
### Methods
|
|
|
|
#### [ERC-6372](./eip-6372.md): clock and CLOCK_MODE
|
|
|
|
Compliant contracts SHOULD implement ERC-6372 (Contract clock) to announce the clock that is used for vote tracking.
|
|
|
|
If the contract does not implement ERC-6372, it MUST operate according to a block number clock, exactly as if ERC-6372's `CLOCK_MODE` returned `mode=blocknumber&from=default`.
|
|
|
|
In the following specification, "the current clock" refers to either the result of ERC-6372's `clock()`, or the default of `block.number` in its absence.
|
|
|
|
#### getVotes
|
|
|
|
This function returns the current voting weight of an account. This corresponds to all the voting power delegated to it at the moment this function is called.
|
|
|
|
As tokens delegated to `address(0)` should not be counted/snapshotted, `getVotes(0)` SHOULD always return `0`.
|
|
|
|
This function MUST be implemented
|
|
|
|
```yaml
|
|
- name: getVotes
|
|
type: function
|
|
stateMutability: view
|
|
inputs:
|
|
- name: account
|
|
type: address
|
|
outputs:
|
|
- name: votingWeight
|
|
type: uint256
|
|
```
|
|
|
|
#### getPastVotes
|
|
|
|
This function returns the historical voting weight of an account. This corresponds to all the voting power delegated to it at a specific timepoint. The timepoint parameter MUST match the operating mode of the contract. This function SHOULD only serve past checkpoints, which SHOULD be immutable.
|
|
|
|
- Calling this function with a timepoint that is greater or equal to the current clock SHOULD revert.
|
|
- Calling this function with a timepoint strictly smaller than the current clock SHOULD NOT revert.
|
|
- For any integer that is strictly smaller than the current clock, the value returned by `getPastVotes` SHOULD be constant. This means that for any call to this function that returns a value, re-executing the same call (at any time in the future) SHOULD return the same value.
|
|
|
|
As tokens delegated to `address(0)` should not be counted/snapshotted, `getPastVotes(0,x)` SHOULD always return `0` (for all values of `x`).
|
|
|
|
This function MUST be implemented
|
|
|
|
```yaml
|
|
- name: getPastVotes
|
|
type: function
|
|
stateMutability: view
|
|
inputs:
|
|
- name: account
|
|
type: address
|
|
- name: timepoint
|
|
type: uint256
|
|
outputs:
|
|
- name: votingWeight
|
|
type: uint256
|
|
```
|
|
|
|
#### delegates
|
|
|
|
This function returns the address to which the voting power of an account is currently delegated.
|
|
|
|
Note that if the delegate is `address(0)` then the voting power SHOULD NOT be checkpointed, and it should not be possible to vote with it.
|
|
|
|
This function MUST be implemented
|
|
|
|
```yaml
|
|
- name: delegates
|
|
type: function
|
|
stateMutability: view
|
|
inputs:
|
|
- name: account
|
|
type: address
|
|
outputs:
|
|
- name: delegatee
|
|
type: address
|
|
```
|
|
|
|
#### delegate
|
|
|
|
This function changes the caller's delegate, updating the vote delegation in the meantime.
|
|
|
|
This function MUST be implemented
|
|
|
|
```yaml
|
|
- name: delegate
|
|
type: function
|
|
stateMutability: nonpayable
|
|
inputs:
|
|
- name: delegatee
|
|
type: address
|
|
outputs: []
|
|
```
|
|
|
|
#### delegateBySig
|
|
|
|
This function changes an account's delegate using a signature, updating the vote delegation in the meantime.
|
|
|
|
This function MUST be implemented
|
|
|
|
```yaml
|
|
- name: delegateBySig
|
|
type: function
|
|
stateMutability: nonpayable
|
|
inputs:
|
|
- name: delegatee
|
|
type: address
|
|
- name: nonce
|
|
type: uint256
|
|
- name: expiry
|
|
type: uint256
|
|
- name: v
|
|
type: uint8
|
|
- name: r
|
|
type: bytes32
|
|
- name: s
|
|
type: bytes32
|
|
outputs: []
|
|
```
|
|
|
|
This signature should follow the [EIP-712](./eip-712.md) format:
|
|
|
|
A call to `delegateBySig(delegatee, nonce, expiry, v, r, s)` changes the signer's delegate to `delegatee`, increment the signer's nonce by 1, and emits a corresponding `DelegateChanged` event, and possibly `DelegateVotesChanged` events for the old and the new delegate accounts, if and only if the following conditions are met:
|
|
|
|
|
|
- The current timestamp is less than or equal to `expiry`.
|
|
- `nonces(signer)` (before the state update) is equal to `nonce`.
|
|
|
|
If any of these conditions are not met, the `delegateBySig` call must revert. This translates to the following solidity code:
|
|
|
|
```sol
|
|
require(expiry <= block.timestamp)
|
|
bytes signer = ecrecover(
|
|
keccak256(abi.encodePacked(
|
|
hex"1901",
|
|
DOMAIN_SEPARATOR,
|
|
keccak256(abi.encode(
|
|
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"),
|
|
delegatee,
|
|
nonce,
|
|
expiry)),
|
|
v, r, s)
|
|
require(signer != address(0));
|
|
require(nounces[signer] == nonce);
|
|
// increment nonce
|
|
// set delegation of `signer` to `delegatee`
|
|
```
|
|
|
|
where `DOMAIN_SEPARATOR` is defined according to [EIP-712](./eip-712.md). The `DOMAIN_SEPARATOR` should be unique to the contract and chain to prevent replay attacks from other domains,
|
|
and satisfy the requirements of EIP-712, but is otherwise unconstrained.
|
|
|
|
A common choice for `DOMAIN_SEPARATOR` is:
|
|
|
|
```solidity
|
|
DOMAIN_SEPARATOR = keccak256(
|
|
abi.encode(
|
|
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
|
|
keccak256(bytes(name)),
|
|
keccak256(bytes(version)),
|
|
chainid,
|
|
address(this)
|
|
));
|
|
```
|
|
|
|
In other words, the message is the EIP-712 typed structure:
|
|
|
|
```js
|
|
{
|
|
"types": {
|
|
"EIP712Domain": [
|
|
{
|
|
"name": "name",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"name": "version",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"name": "chainId",
|
|
"type": "uint256"
|
|
},
|
|
{
|
|
"name": "verifyingContract",
|
|
"type": "address"
|
|
}
|
|
],
|
|
"Delegation": [{
|
|
"name": "delegatee",
|
|
"type": "address"
|
|
},
|
|
{
|
|
"name": "nonce",
|
|
"type": "uint256"
|
|
},
|
|
{
|
|
"name": "expiry",
|
|
"type": "uint256"
|
|
}
|
|
],
|
|
"primaryType": "Permit",
|
|
"domain": {
|
|
"name": contractName,
|
|
"version": version,
|
|
"chainId": chainid,
|
|
"verifyingContract": contractAddress
|
|
},
|
|
"message": {
|
|
"delegatee": delegatee,
|
|
"nonce": nonce,
|
|
"expiry": expiry
|
|
}
|
|
}}
|
|
```
|
|
|
|
Note that nowhere in this definition do we refer to `msg.sender`. The caller of the `delegateBySig` function can be any address.
|
|
|
|
When this function is successfully executed, the delegator's nonce MUST be incremented to prevent replay attacks.
|
|
|
|
#### nonces
|
|
|
|
This function returns the current nonce for a given account.
|
|
|
|
Signed delegations (see `delegateBySig`) are only accepted if the nonce used in the EIP-712 signature matches the return of this function. This value of `nonce(delegator)` should be incremented whenever a call to `delegateBySig` is performed on behalf of `delegator`.
|
|
|
|
This function MUST be implemented
|
|
|
|
```yaml
|
|
- name: nonces
|
|
type: function
|
|
stateMutability: view
|
|
inputs:
|
|
- name: account
|
|
type: delegator
|
|
outputs:
|
|
- name: nonce
|
|
type: uint256
|
|
```
|
|
|
|
### Events
|
|
|
|
#### DelegateChanged
|
|
|
|
`delegator` changes the delegation of its assets from `fromDelegate` to `toDelegate`.
|
|
|
|
MUST be emitted when the delegate for an account is modified by `delegate(address)` or `delegateBySig(address,uint256,uint256,uint8,bytes32,bytes32)`.
|
|
|
|
```yaml
|
|
- name: DelegateChanged
|
|
type: event
|
|
inputs:
|
|
- name: delegator
|
|
indexed: true
|
|
type: address
|
|
- name: fromDelegate
|
|
indexed: true
|
|
type: address
|
|
- name: toDelegate
|
|
indexed: true
|
|
type: address
|
|
```
|
|
|
|
#### DelegateVotesChanged
|
|
|
|
`delegate` available voting power changes from `previousBalance` to `newBalance`.
|
|
|
|
This MUST be emitted when:
|
|
|
|
- an account (that holds more than 0 assets) updates its delegation from or to `delegate`,
|
|
- an asset transfer from or to an account that is delegated to `delegate`.
|
|
|
|
```yaml
|
|
- name: DelegateVotesChanged
|
|
type: event
|
|
inputs:
|
|
- name: delegate
|
|
indexed: true
|
|
type: address
|
|
- name: previousBalance
|
|
indexed: false
|
|
type: uint256
|
|
- name: newBalance
|
|
indexed: false
|
|
type: uint256
|
|
```
|
|
|
|
### Solidity interface
|
|
|
|
```sol
|
|
interface IERC5805 is IERC6372 /* (optional) */ {
|
|
event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);
|
|
event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);
|
|
|
|
function getVotes(address account) external view returns (uint256);
|
|
function getPastVotes(address account, uint256 timepoint) external view returns (uint256);
|
|
function delegates(address account) external view returns (address);
|
|
function nonces(address owner) public view virtual returns (uint256)
|
|
|
|
function delegate(address delegatee) external;
|
|
function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external;
|
|
}
|
|
```
|
|
|
|
### Expected properties
|
|
|
|
Let `clock` be the current clock.
|
|
|
|
- For all timepoints `t < clock`, `getVotes(address(0))` and `getPastVotes(address(0), t)` SHOULD return 0.
|
|
- For all accounts `a != 0`, `getVotes(a)` SHOULD be the sum of the "balances" of all the accounts that delegate to `a`.
|
|
- For all accounts `a != 0` and all timestamp `t < clock`, `getPastVotes(a, t)` SHOULD be the sum of the "balances" of all the accounts that delegated to `a` when `clock` overtook `t`.
|
|
- For all accounts `a`, `getPastVotes(a, t)` MUST be constant after `t < clock` is reached.
|
|
- For all accounts `a`, the action of changing the delegate from `b` to `c` MUST not increase the current voting power of `b` (`getVotes(b)`) and MUST not decrease the current voting power of `c` (`getVotes(c)`).
|
|
|
|
## Rationale
|
|
|
|
Delegation allows token holders to trust a delegate with their vote while keeping full custody of their token. This means that only a small-ish number of delegates need to pay gas for voting. This leads to better representation of small token holders by allowing their votes to be cast without requiring them to pay expensive gas fees. Users can take over their voting power at any point, and delegate it to someone else, or to themselves.
|
|
|
|
The use of checkpoints prevents double voting. Votes, for example in the context of a governance proposal, should rely on a snapshot defined by a timepoint. Only tokens delegated at that timepoint can be used for voting. This means any token transfer performed after the snapshot will not affect the voting power of the sender/receiver's delegate. This also means that in order to vote, someone must acquire tokens and delegate them before the snapshot is taken. Governors can, and do, include a delay between the proposal is submitted and the snapshot is taken so that users can take the necessary actions (change their delegation, buy more tokens, ...).
|
|
|
|
While timestamps produced by ERC-6372's `clock` are represented as `uint48`, `getPastVotes`'s timepoint argument is `uint256` for backward compatibility. Any timepoint `>=2**48` passed to `getPastVotes` SHOULD cause the function to revert, as it would be a lookup in the future.
|
|
|
|
`delegateBySig` is necessary to offer a gasless workflow to token holders that do not want to pay gas for voting.
|
|
|
|
The `nonces` mapping is given for replay protection.
|
|
|
|
EIP-712 typed messages are included because of their widespread adoption in many wallet providers.
|
|
|
|
## Backwards Compatibility
|
|
|
|
Compound and OpenZeppelin already provide implementations of voting tokens. The delegation-related methods are shared between the two implementations and this ERC. For the vote lookup, this ERC uses OpenZeppelin's implementation (with return type uint256) as Compound's implementation causes significant restrictions of the acceptable values (return type is uint96).
|
|
|
|
Both implementations use `block.number` for their checkpoints and do not implement ERC-6372, which is compatible with this ERC.
|
|
|
|
Existing governors, that are currently compatible with OpenZeppelin's implementation will be compatible with the "block number mode" of this ERC.
|
|
|
|
## Security Considerations
|
|
|
|
Before doing a lookup, one should check the return value of `clock()` and make sure that the parameters of the lookup are consistent. Performing a lookup using a timestamp argument on a contract that uses block numbers will very likely cause a revert. On the other end, performing a lookup using a block number argument on a contract that uses timestamps will likely return 0.
|
|
|
|
Though the signer of a `Delegation` may have a certain party in mind to submit their transaction, another party can always front-run this transaction and call `delegateBySig` before the intended party. The result is the same for the `Delegation` signer, however.
|
|
|
|
Since the ecrecover precompile fails silently and just returns the zero address as `signer` when given malformed messages, it is important to ensure `signer != address(0)` to avoid `delegateBySig` from delegating "zombie funds" belonging to the zero address.
|
|
|
|
Signed `Delegation` messages are censorable. The relaying party can always choose to not submit the `Delegation` after having received it, withholding the option to submit it. The `expiry` parameter is one mitigation to this. If the signing party holds ETH they can also just submit the `Delegation` themselves, which can render previously signed `Delegation`s invalid.
|
|
|
|
If the `DOMAIN_SEPARATOR` contains the `chainId` and is defined at contract deployment instead of reconstructed for every signature, there is a risk of possible replay attacks between chains in the event of a future chain split.
|
|
|
|
## Copyright
|
|
|
|
Copyright and related rights waived via [CC0](../LICENSE.md).
|