-
Notifications
You must be signed in to change notification settings - Fork 11.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ERC20 extension for governance tokens (vote delegation and snapshots) (…
…#2632) Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
- Loading branch information
Showing
8 changed files
with
715 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
100ca0b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Co śię stało