Skip to content
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

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

Merged
merged 44 commits into from May 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4494cc8
add ERC20SnapshotEveryBlock
Amxx Mar 12, 2021
53edd89
add changelog entry
Amxx Mar 12, 2021
10a5b63
testing for ERC20SnapshotEveryBlock
Amxx Mar 23, 2021
512324a
improve snapshoting
Amxx Mar 24, 2021
e87de1e
start work on a Comp extension to ERC20
Amxx Apr 18, 2021
a235d0c
delegation query cleanup
Amxx Apr 18, 2021
4c17b9a
minor cleanup of comp test
Amxx Apr 19, 2021
d4b655c
Merge branch 'feature/ERC20SnapshotBlockNumber' into feature/erc20/comp
Amxx Apr 19, 2021
46aa3cc
remove autodelegation
Amxx Apr 20, 2021
fabd802
fix/improve testing for ERC20Comp
Amxx Apr 20, 2021
ba43a54
rename ERC20Comp to ERC20Votes
Amxx Apr 20, 2021
c9a321d
gas optimization + comp compatibility and tests
Amxx Apr 28, 2021
74812f9
function ordering
Amxx Apr 28, 2021
9e49d5e
minor refactor
Amxx Apr 28, 2021
fc83c27
minor gas reduction
Amxx Apr 28, 2021
0678a9f
improve testing
Amxx Apr 28, 2021
321bdf3
remove ERC20SnapsotEveryBlock for now
Amxx Apr 28, 2021
cc8682f
rename IComp to IERC20Votes
Amxx May 3, 2021
9e166bc
storage slot consistency
Amxx May 6, 2021
2667a83
remove comment about delegation overridding
Amxx May 6, 2021
970ea15
Apply suggestions from code review
Amxx May 6, 2021
464b207
improve compound compatibility
Amxx May 6, 2021
96743e9
Merge branch 'feature/erc20/comp' of github.com:Amxx/openzeppelin-con…
Amxx May 6, 2021
a37d5d2
use SafeCast and fix tests
Amxx May 6, 2021
de5c951
update changelog
Amxx May 6, 2021
cdb3484
extend ERC20Votes testing
Amxx May 6, 2021
4158e7f
comment getPriorVotes functionnality
Amxx May 11, 2021
829fdf1
documentation for ERC20Votes
Amxx May 11, 2021
5343989
try to fix batchInBlock
Amxx May 11, 2021
ad792d6
try to fix batchInBlock
Amxx May 11, 2021
7fd286d
remove setTimeout from tests
frangio May 11, 2021
a716d78
reenable automine in finally block
frangio May 11, 2021
72a1d09
use Set to count distinct mined blocks
frangio May 11, 2021
690f43d
use built in provider.send function
frangio May 12, 2021
5060eb9
fix tests
frangio May 12, 2021
da59c90
docs wording and whitespace
frangio May 12, 2021
8fbd9c4
whitespace
frangio May 12, 2021
017fcd9
rename signatory -> signer
frangio May 12, 2021
5cd61c0
edit comment before binary search
frangio May 12, 2021
88aa1bb
typo
frangio May 12, 2021
ea66010
wording
frangio May 12, 2021
63dc247
add uint224 check on ERC20Votes total supply
Amxx May 12, 2021
55ecf72
Rename vars for clarity
Amxx May 12, 2021
861f07f
document batchInBlock
Amxx May 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

* `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))
Expand Down
21 changes: 21 additions & 0 deletions contracts/mocks/ERC20VotesMock.sol
@@ -0,0 +1,21 @@
// 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;
}
}
4 changes: 4 additions & 0 deletions contracts/token/ERC20/README.adoc
Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
23 changes: 18 additions & 5 deletions contracts/token/ERC20/extensions/ERC20Snapshot.sol
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -79,11 +87,18 @@ abstract contract ERC20Snapshot is ERC20 {
function _snapshot() internal virtual returns (uint256) {
_currentSnapshotId.increment();
frangio marked this conversation as resolved.
Show resolved Hide resolved

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.
*/
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
172 changes: 172 additions & 0 deletions contracts/token/ERC20/extensions/draft-ERC20Votes.sol
@@ -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 {
Amxx marked this conversation as resolved.
Show resolved Hide resolved
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 {
frangio marked this conversation as resolved.
Show resolved Hide resolved
_moveVotingPower(delegates(from), delegates(to), amount);
}
}
23 changes: 23 additions & 0 deletions contracts/token/ERC20/extensions/draft-IERC20Votes.sol
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../IERC20.sol";

interface IERC20Votes is IERC20 {
frangio marked this conversation as resolved.
Show resolved Hide resolved
struct Checkpoint {
uint32 fromBlock;
uint224 votes;
}
Amxx marked this conversation as resolved.
Show resolved Hide resolved

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;
}
15 changes: 15 additions & 0 deletions contracts/utils/math/SafeCast.sol
Expand Up @@ -18,6 +18,21 @@ pragma solidity ^0.8.0;
* all math on `uint256` and `int256` and then downcasting.
*/
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
* overflow (when the input is greater than largest uint128).
Expand Down