Unverified Commit 100ca0b8 by Hadrien Croubois Committed by GitHub

ERC20 extension for governance tokens (vote delegation and snapshots) (#2632)

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
parent 86694813
...@@ -6,6 +6,10 @@ ...@@ -6,6 +6,10 @@
## 4.1.0 (2021-04-29) ## 4.1.0 (2021-04-29)
* `ERC20Votes`: add a new extension of the `ERC20` token with support for voting snapshots and delegation. This extension is compatible with Compound's `Comp` token interface. ([#2632](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2632))
## Unreleased
* `IERC20Metadata`: add a new extended interface that includes the optional `name()`, `symbol()` and `decimals()` functions. ([#2561](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2561)) * `IERC20Metadata`: add a new extended interface that includes the optional `name()`, `symbol()` and `decimals()` functions. ([#2561](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2561))
* `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552)) * `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552))
* `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565)) * `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565))
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../token/ERC20/extensions/draft-ERC20Votes.sol";
contract ERC20VotesMock is ERC20Votes {
constructor (
string memory name,
string memory symbol,
address initialAccount,
uint256 initialBalance
) payable ERC20(name, symbol) ERC20Permit(name) {
_mint(initialAccount, initialBalance);
}
function getChainId() external view returns (uint256) {
return block.chainid;
}
}
...@@ -21,6 +21,7 @@ Additionally there are multiple custom extensions, including: ...@@ -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. * {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time.
* {ERC20Permit}: gasless approval of tokens (standardized as ERC2612). * {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). * {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. 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. ...@@ -31,6 +32,7 @@ The following related EIPs are in draft status.
- {ERC20Permit} - {ERC20Permit}
- {ERC20FlashMint} - {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. 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 ...@@ -60,6 +62,8 @@ The following EIPs are still in Draft status. Due to their nature as drafts, the
{{ERC20FlashMint}} {{ERC20FlashMint}}
{{ERC20Votes}}
== Presets == 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. 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"; ...@@ -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 * 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. * 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 * ==== Gas Costs
* *
* Snapshots are efficient. Snapshot creation is _O(1)_. Retrieval of balances or total supply from a snapshot is _O(log * 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"; ...@@ -30,6 +37,7 @@ import "../../../utils/Counters.sol";
* only significant for the first transfer that immediately follows a snapshot for a particular account. Subsequent * 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. * transfers will have normal cost until the next snapshot, and so on.
*/ */
abstract contract ERC20Snapshot is ERC20 { abstract contract ERC20Snapshot is ERC20 {
// Inspired by Jordi Baylina's MiniMeToken to record historical balances: // Inspired by Jordi Baylina's MiniMeToken to record historical balances:
// https://github.com/Giveth/minimd/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol // https://github.com/Giveth/minimd/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol
...@@ -79,12 +87,19 @@ abstract contract ERC20Snapshot is ERC20 { ...@@ -79,12 +87,19 @@ abstract contract ERC20Snapshot is ERC20 {
function _snapshot() internal virtual returns (uint256) { function _snapshot() internal virtual returns (uint256) {
_currentSnapshotId.increment(); _currentSnapshotId.increment();
uint256 currentId = _currentSnapshotId.current(); uint256 currentId = _getCurrentSnapshotId();
emit Snapshot(currentId); emit Snapshot(currentId);
return 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. * @dev Retrieves the balance of `account` at the time `snapshotId` was created.
*/ */
function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) { function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) {
...@@ -102,7 +117,6 @@ abstract contract ERC20Snapshot is ERC20 { ...@@ -102,7 +117,6 @@ abstract contract ERC20Snapshot is ERC20 {
return snapshotted ? value : totalSupply(); return snapshotted ? value : totalSupply();
} }
// Update balance and/or total supply snapshots before the values are modified. This is implemented // 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. // in the _beforeTokenTransfer hook, which is executed for _mint, _burn, and _transfer operations.
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
...@@ -127,8 +141,7 @@ abstract contract ERC20Snapshot is ERC20 { ...@@ -127,8 +141,7 @@ abstract contract ERC20Snapshot is ERC20 {
private view returns (bool, uint256) private view returns (bool, uint256)
{ {
require(snapshotId > 0, "ERC20Snapshot: id is 0"); require(snapshotId > 0, "ERC20Snapshot: id is 0");
// solhint-disable-next-line max-line-length require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id");
require(snapshotId <= _currentSnapshotId.current(), "ERC20Snapshot: nonexistent id");
// When a valid snapshot is queried, there are three possibilities: // 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 // 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 { ...@@ -162,7 +175,7 @@ abstract contract ERC20Snapshot is ERC20 {
} }
function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private { function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private {
uint256 currentId = _currentSnapshotId.current(); uint256 currentId = _getCurrentSnapshotId();
if (_lastSnapshotId(snapshots.ids) < currentId) { if (_lastSnapshotId(snapshots.ids) < currentId) {
snapshots.ids.push(currentId); snapshots.ids.push(currentId);
snapshots.values.push(currentValue); snapshots.values.push(currentValue);
......
// 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);
}
}
// 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;
}
...@@ -19,6 +19,21 @@ pragma solidity ^0.8.0; ...@@ -19,6 +19,21 @@ pragma solidity ^0.8.0;
*/ */
library SafeCast { library SafeCast {
/** /**
* @dev Returns the downcasted uint224 from uint256, reverting on
* overflow (when the input is greater than largest uint224).
*
* Counterpart to Solidity's `uint224` operator.
*
* Requirements:
*
* - input must fit into 224 bits
*/
function toUint224(uint256 value) internal pure returns (uint224) {
require(value < 2**224, "SafeCast: value doesn\'t fit in 224 bits");
return uint224(value);
}
/**
* @dev Returns the downcasted uint128 from uint256, reverting on * @dev Returns the downcasted uint128 from uint256, reverting on
* overflow (when the input is greater than largest uint128). * overflow (when the input is greater than largest uint128).
* *
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment