ERC20 extension for governance tokens (vote delegation and snapshots) (#2632)
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
This commit is contained in:
@ -21,6 +21,7 @@ Additionally there are multiple custom extensions, including:
|
||||
* {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time.
|
||||
* {ERC20Permit}: gasless approval of tokens (standardized as ERC2612).
|
||||
* {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156).
|
||||
* {ERC20Votes}: support for voting and vote delegation (compatible with Compound's token).
|
||||
|
||||
Finally, there are some utilities to interact with ERC20 contracts in various ways.
|
||||
|
||||
@ -31,6 +32,7 @@ The following related EIPs are in draft status.
|
||||
|
||||
- {ERC20Permit}
|
||||
- {ERC20FlashMint}
|
||||
- {ERC20Votes}
|
||||
|
||||
NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <<ERC20-_mint-address-uint256-,`_mint`>>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc20.adoc#Presets[ERC20 Presets] (such as {ERC20PresetMinterPauser}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts.
|
||||
|
||||
@ -60,6 +62,8 @@ The following EIPs are still in Draft status. Due to their nature as drafts, the
|
||||
|
||||
{{ERC20FlashMint}}
|
||||
|
||||
{{ERC20Votes}}
|
||||
|
||||
== Presets
|
||||
|
||||
These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code.
|
||||
|
||||
@ -20,6 +20,13 @@ import "../../../utils/Counters.sol";
|
||||
* id. To get the balance of an account at the time of a snapshot, call the {balanceOfAt} function with the snapshot id
|
||||
* and the account address.
|
||||
*
|
||||
* NOTE: Snapshot policy can be customized by overriding the {_getCurrentSnapshotId} method. For example, having it
|
||||
* return `block.number` will trigger the creation of snapshot at the begining of each new block. When overridding this
|
||||
* function, be careful about the monotonicity of its result. Non-monotonic snapshot ids will break the contract.
|
||||
*
|
||||
* Implementing snapshots for every block using this method will incur significant gas costs. For a gas-efficient
|
||||
* alternative consider {ERC20Votes}.
|
||||
*
|
||||
* ==== Gas Costs
|
||||
*
|
||||
* Snapshots are efficient. Snapshot creation is _O(1)_. Retrieval of balances or total supply from a snapshot is _O(log
|
||||
@ -30,6 +37,7 @@ import "../../../utils/Counters.sol";
|
||||
* only significant for the first transfer that immediately follows a snapshot for a particular account. Subsequent
|
||||
* transfers will have normal cost until the next snapshot, and so on.
|
||||
*/
|
||||
|
||||
abstract contract ERC20Snapshot is ERC20 {
|
||||
// Inspired by Jordi Baylina's MiniMeToken to record historical balances:
|
||||
// https://github.com/Giveth/minimd/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol
|
||||
@ -79,11 +87,18 @@ abstract contract ERC20Snapshot is ERC20 {
|
||||
function _snapshot() internal virtual returns (uint256) {
|
||||
_currentSnapshotId.increment();
|
||||
|
||||
uint256 currentId = _currentSnapshotId.current();
|
||||
uint256 currentId = _getCurrentSnapshotId();
|
||||
emit Snapshot(currentId);
|
||||
return currentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the current snapshotId
|
||||
*/
|
||||
function _getCurrentSnapshotId() internal view virtual returns (uint256) {
|
||||
return _currentSnapshotId.current();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Retrieves the balance of `account` at the time `snapshotId` was created.
|
||||
*/
|
||||
@ -102,7 +117,6 @@ abstract contract ERC20Snapshot is ERC20 {
|
||||
return snapshotted ? value : totalSupply();
|
||||
}
|
||||
|
||||
|
||||
// Update balance and/or total supply snapshots before the values are modified. This is implemented
|
||||
// in the _beforeTokenTransfer hook, which is executed for _mint, _burn, and _transfer operations.
|
||||
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
|
||||
@ -127,8 +141,7 @@ abstract contract ERC20Snapshot is ERC20 {
|
||||
private view returns (bool, uint256)
|
||||
{
|
||||
require(snapshotId > 0, "ERC20Snapshot: id is 0");
|
||||
// solhint-disable-next-line max-line-length
|
||||
require(snapshotId <= _currentSnapshotId.current(), "ERC20Snapshot: nonexistent id");
|
||||
require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id");
|
||||
|
||||
// When a valid snapshot is queried, there are three possibilities:
|
||||
// a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never
|
||||
@ -162,7 +175,7 @@ abstract contract ERC20Snapshot is ERC20 {
|
||||
}
|
||||
|
||||
function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private {
|
||||
uint256 currentId = _currentSnapshotId.current();
|
||||
uint256 currentId = _getCurrentSnapshotId();
|
||||
if (_lastSnapshotId(snapshots.ids) < currentId) {
|
||||
snapshots.ids.push(currentId);
|
||||
snapshots.values.push(currentValue);
|
||||
|
||||
172
contracts/token/ERC20/extensions/draft-ERC20Votes.sol
Normal file
172
contracts/token/ERC20/extensions/draft-ERC20Votes.sol
Normal file
@ -0,0 +1,172 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./draft-ERC20Permit.sol";
|
||||
import "./draft-IERC20Votes.sol";
|
||||
import "../../../utils/math/Math.sol";
|
||||
import "../../../utils/math/SafeCast.sol";
|
||||
import "../../../utils/cryptography/ECDSA.sol";
|
||||
|
||||
/**
|
||||
* @dev Extension of the ERC20 token contract to support Compound's voting and delegation.
|
||||
*
|
||||
* This extensions keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either
|
||||
* by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting
|
||||
* power can be queried through the public accessors {getCurrentVotes} and {getPriorVotes}.
|
||||
*
|
||||
* By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it
|
||||
* requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked.
|
||||
* Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this
|
||||
* will significantly increase the base gas cost of transfers.
|
||||
*
|
||||
* _Available since v4.2._
|
||||
*/
|
||||
abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
|
||||
bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
|
||||
|
||||
mapping (address => address) private _delegates;
|
||||
mapping (address => Checkpoint[]) private _checkpoints;
|
||||
|
||||
/**
|
||||
* @dev Get the `pos`-th checkpoint for `account`.
|
||||
*/
|
||||
function checkpoints(address account, uint32 pos) external view virtual override returns (Checkpoint memory) {
|
||||
return _checkpoints[account][pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get number of checkpoints for `account`.
|
||||
*/
|
||||
function numCheckpoints(address account) external view virtual override returns (uint32) {
|
||||
return SafeCast.toUint32(_checkpoints[account].length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the address `account` is currently delegating to.
|
||||
*/
|
||||
function delegates(address account) public view virtual override returns (address) {
|
||||
return _delegates[account];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Gets the current votes balance for `account`
|
||||
*/
|
||||
function getCurrentVotes(address account) external view override returns (uint256) {
|
||||
uint256 pos = _checkpoints[account].length;
|
||||
return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Determine the number of votes for `account` at the begining of `blockNumber`.
|
||||
*/
|
||||
function getPriorVotes(address account, uint256 blockNumber) external view override returns (uint256) {
|
||||
require(blockNumber < block.number, "ERC20Votes::getPriorVotes: not yet determined");
|
||||
|
||||
Checkpoint[] storage ckpts = _checkpoints[account];
|
||||
|
||||
// We run a binary search to look for the earliest checkpoint taken after `blockNumber`.
|
||||
//
|
||||
// During the loop, the index of the wanted checkpoint remains in the range [low, high).
|
||||
// With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant.
|
||||
// - If the middle checkpoint is after `blockNumber`, we look in [low, mid)
|
||||
// - If the middle checkpoint is before `blockNumber`, we look in [mid+1, high)
|
||||
// Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not
|
||||
// out of bounds (in which case we're looking too far in the past and the result is 0).
|
||||
// Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is
|
||||
// past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out
|
||||
// the same.
|
||||
uint256 high = ckpts.length;
|
||||
uint256 low = 0;
|
||||
while (low < high) {
|
||||
uint256 mid = Math.average(low, high);
|
||||
if (ckpts[mid].fromBlock > blockNumber) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return high == 0 ? 0 : ckpts[high - 1].votes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Delegate votes from the sender to `delegatee`.
|
||||
*/
|
||||
function delegate(address delegatee) public virtual override {
|
||||
return _delegate(_msgSender(), delegatee);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Delegates votes from signer to `delegatee`
|
||||
*/
|
||||
function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)
|
||||
public virtual override
|
||||
{
|
||||
require(block.timestamp <= expiry, "ERC20Votes::delegateBySig: signature expired");
|
||||
address signer = ECDSA.recover(
|
||||
_hashTypedDataV4(keccak256(abi.encode(
|
||||
_DELEGATION_TYPEHASH,
|
||||
delegatee,
|
||||
nonce,
|
||||
expiry
|
||||
))),
|
||||
v, r, s
|
||||
);
|
||||
require(nonce == _useNonce(signer), "ERC20Votes::delegateBySig: invalid nonce");
|
||||
return _delegate(signer, delegatee);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change delegation for `delegator` to `delegatee`.
|
||||
*/
|
||||
function _delegate(address delegator, address delegatee) internal virtual {
|
||||
address currentDelegate = delegates(delegator);
|
||||
uint256 delegatorBalance = balanceOf(delegator);
|
||||
_delegates[delegator] = delegatee;
|
||||
|
||||
emit DelegateChanged(delegator, currentDelegate, delegatee);
|
||||
|
||||
_moveVotingPower(currentDelegate, delegatee, delegatorBalance);
|
||||
}
|
||||
|
||||
function _moveVotingPower(address src, address dst, uint256 amount) private {
|
||||
if (src != dst && amount > 0) {
|
||||
if (src != address(0)) {
|
||||
uint256 srcCkptLen = _checkpoints[src].length;
|
||||
uint256 srcCkptOld = srcCkptLen == 0 ? 0 : _checkpoints[src][srcCkptLen - 1].votes;
|
||||
uint256 srcCkptNew = srcCkptOld - amount;
|
||||
_writeCheckpoint(src, srcCkptLen, srcCkptOld, srcCkptNew);
|
||||
}
|
||||
|
||||
if (dst != address(0)) {
|
||||
uint256 dstCkptLen = _checkpoints[dst].length;
|
||||
uint256 dstCkptOld = dstCkptLen == 0 ? 0 : _checkpoints[dst][dstCkptLen - 1].votes;
|
||||
uint256 dstCkptNew = dstCkptOld + amount;
|
||||
_writeCheckpoint(dst, dstCkptLen, dstCkptOld, dstCkptNew);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _writeCheckpoint(address delegatee, uint256 pos, uint256 oldWeight, uint256 newWeight) private {
|
||||
if (pos > 0 && _checkpoints[delegatee][pos - 1].fromBlock == block.number) {
|
||||
_checkpoints[delegatee][pos - 1].votes = SafeCast.toUint224(newWeight);
|
||||
} else {
|
||||
_checkpoints[delegatee].push(Checkpoint({
|
||||
fromBlock: SafeCast.toUint32(block.number),
|
||||
votes: SafeCast.toUint224(newWeight)
|
||||
}));
|
||||
}
|
||||
|
||||
emit DelegateVotesChanged(delegatee, oldWeight, newWeight);
|
||||
}
|
||||
|
||||
function _mint(address account, uint256 amount) internal virtual override {
|
||||
super._mint(account, amount);
|
||||
require(totalSupply() <= type(uint224).max, "ERC20Votes: total supply exceeds 2**224");
|
||||
}
|
||||
|
||||
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
|
||||
_moveVotingPower(delegates(from), delegates(to), amount);
|
||||
}
|
||||
}
|
||||
23
contracts/token/ERC20/extensions/draft-IERC20Votes.sol
Normal file
23
contracts/token/ERC20/extensions/draft-IERC20Votes.sol
Normal file
@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "../IERC20.sol";
|
||||
|
||||
interface IERC20Votes is IERC20 {
|
||||
struct Checkpoint {
|
||||
uint32 fromBlock;
|
||||
uint224 votes;
|
||||
}
|
||||
|
||||
event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);
|
||||
event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);
|
||||
|
||||
function delegates(address owner) external view returns (address);
|
||||
function checkpoints(address account, uint32 pos) external view returns (Checkpoint memory);
|
||||
function numCheckpoints(address account) external view returns (uint32);
|
||||
function getCurrentVotes(address account) external view returns (uint256);
|
||||
function getPriorVotes(address account, uint256 blockNumber) external view returns (uint256);
|
||||
function delegate(address delegatee) external;
|
||||
function delegateBySig(address delegatee, uint nonce, uint expiry, uint8 v, bytes32 r, bytes32 s) external;
|
||||
}
|
||||
Reference in New Issue
Block a user