ERC-5805 Delegation-based voting power tracking standard (Voting with delegation)

Review · Tokens · created 2022-07-04

A standard that lets token holders delegate voting power to another account without transferring the tokens themselves, and lets anyone query the voting power held at a specific point in the past.

Why it is needed

Many decentralized autonomous organizations (DAOs) determine voting power by token quantity. However, handling voting purely through the balance query (balanceOf) that most tokens provide creates two problems. First, because balances are not recorded per point in time, a person could vote and then move their tokens to another account to vote again with the same tokens. Second, there is no way to hand off only voting power to someone else without transferring ownership of the tokens outright. As a result, several projects, including Compound, have each built their own voting tokens with delegation and timepoint recording (checkpoints), but no standard existed. Existing implementations also only support recording by block number, which is a problem on chains like layer 2s where block intervals are not constant. This standard unifies the delegation and voting power query interface and allows any monotonically increasing time reference, whether block number or timestamp, to be used.

What to implement

In depth

Delegation mechanism

Each account can designate itself, a third party, or address(0) as its delegation target. Held assets cannot be used as voting power until they are delegated. When a delegator delegates, that balance is added to the delegate's voting power; if the delegation is changed, it is subtracted from the previous delegate and added to the new one. When tokens are transferred, the voting power must be subtracted from the sending delegate and added to the receiving delegate. Tokens delegated to address(0) are not tracked, so users can skip updating delegate checkpoints to save gas on transfers. Since a token is delegated to at most one party at a time, double voting is prevented.

Clock and ERC-6372

Contracts conforming to this standard SHOULD implement ERC-6372 (contract clock) to signal the time reference used for vote tracking. If ERC-6372 is not implemented, the contract MUST behave as if CLOCK_MODE returned mode=blocknumber&from=default, operating with a block number clock. On mainnet, block numbers offer better compatibility with existing apps, while timestamps are more meaningful for users to understand and fit better for expressing durations in seconds. External contracts such as governors can read the clock() value to stay consistent with the token's checkpoints.

Rules for getVotes and getPastVotes

getVotes(account) returns the total voting power delegated to that account at the time of the call. getPastVotes(account, timepoint) returns the voting power at a past point in time, and timepoint MUST match the contract's operating mode. Calling with a timepoint at or beyond the current clock SHOULD revert, calling with a timepoint earlier than the current clock SHOULD NOT revert, and a past value once returned SHOULD remain the same whenever queried again later. Tokens delegated to address(0) are not tallied, so getVotes(0) and getPastVotes(0, x) MUST always return 0. The ERC-6372 clock is uint48, but getPastVotes' argument is uint256 for backward compatibility, and timepoints of 2**48 or greater represent future queries and MUST revert.

delegateBySig and EIP-712 signatures

delegateBySig(delegatee, nonce, expiry, v, r, s) changes the signer's delegation using an EIP-712 signature of type Delegation(address delegatee, uint256 nonce, uint256 expiry). It only executes when the current timestamp is at or before expiry and nonces(signer) equals nonce; it must revert if these conditions are not met. On success, it increments the signer's nonce by 1 and emits a DelegateChanged event (and DelegateVotesChanged if applicable). The function can be called by anyone and does not reference msg.sender. This lets holders who do not want to pay gas delegate with just a signature, and the nonces mapping prevents replay. DOMAIN_SEPARATOR must be unique per contract and chain to prevent replay attacks across domains.

Conditions for emitting events

DelegateChanged MUST be emitted whenever an account's delegation target is modified via delegate(address) or delegateBySig. DelegateVotesChanged MUST be emitted in two cases: when an account holding a nonzero amount of assets changes its delegation to or from a given delegate, and when assets are transferred to or from an account delegated to that delegate. The event carries the previous voting power (previousBalance) and the new voting power (newBalance).

Invariants to maintain

Letting clock denote the current clock, getVotes(a) SHOULD equal the sum of the balances of all accounts delegated to a, for every nonzero account a. getPastVotes(a, t) for a past point in time t MUST equal the sum of the balances of accounts that were delegated to a as of the clock passing t, and this value MUST NOT change once t has become the past. Changing delegation from b to c MUST NOT increase b's current voting power and MUST NOT decrease c's current voting power.

Compatibility with existing implementations

Compound and OpenZeppelin already provide voting token implementations, and the delegation-related methods are the same across both implementations and this standard. Voting power queries follow the OpenZeppelin approach (uint256 return type), since Compound's implementation returns uint96, which imposes significant constraints on allowed values. Both implementations use block number checkpoints and do not implement ERC-6372, which is compatible with this standard. Existing governors compatible with the OpenZeppelin implementation are compatible with this standard's block number mode. Note that this proposal is still in Review status and its content may change.

Things to watch out for

Original ERC · ← ERCs