Skip to content

Commit

Permalink
Add ERC721Votes for NFT-based governance (#2944)
Browse files Browse the repository at this point in the history
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
Co-authored-by: Hadrien Croubois <hadrien@openzeppelin.com>
  • Loading branch information
3 people committed Dec 10, 2021
1 parent 9a7e4a0 commit b42b053
Show file tree
Hide file tree
Showing 31 changed files with 1,346 additions and 36 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* `Governor`: add a relay function to help recover assets sent to a governor that is not its own executor (e.g. when using a timelock). ([#2926](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2926))
* `GovernorPreventLateQuorum`: add new module to ensure a minimum voting duration is available after the quorum is reached. ([#2973](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2973))
* `ERC721`: improved revert reason when transferring from wrong owner. ([#2975](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2975))
* `Votes`: Added a base contract for vote tracking with delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944))
* `ERC721Votes`: Added an extension of ERC721 enabled with vote tracking and delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944))

## 4.4.1 (2021-12-10)

Expand Down
2 changes: 1 addition & 1 deletion contracts/governance/IGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ abstract contract IGovernor is IERC165 {
function castVote(uint256 proposalId, uint8 support) public virtual returns (uint256 balance);

/**
* @dev Cast a with a reason
* @dev Cast a vote with a reason
*
* Emits a {VoteCast} event.
*/
Expand Down
4 changes: 4 additions & 0 deletions contracts/governance/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ NOTE: Functions of the `Governor` contract do not include access control. If you

{{GovernorProposalThreshold}}

== Utils

{{Votes}}

== Timelock

In a governance system, the {TimelockController} contract is in charge of introducing a delay between a proposal and its execution. It can be used with or without a {Governor}.
Expand Down
9 changes: 4 additions & 5 deletions contracts/governance/extensions/GovernorVotes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@
pragma solidity ^0.8.0;

import "../Governor.sol";
import "../../token/ERC20/extensions/ERC20Votes.sol";
import "../../utils/math/Math.sol";
import "../utils/IVotes.sol";

/**
* @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token.
* @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token.
*
* _Available since v4.3._
*/
abstract contract GovernorVotes is Governor {
ERC20Votes public immutable token;
IVotes public immutable token;

constructor(ERC20Votes tokenAddress) {
constructor(IVotes tokenAddress) {
token = tokenAddress;
}

Expand Down
61 changes: 61 additions & 0 deletions contracts/governance/utils/IVotes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.0 (interfaces/IVotes.sol)
pragma solidity ^0.8.0;

/**
* @dev Common interface for {ERC20Votes}, {ERC721Votes}, and other {Votes}-enabled contracts.
*
* _Available since v4.5._
*/
interface IVotes {
/**
* @dev Emitted when an account changes their delegate.
*/
event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);

/**
* @dev Emitted when a token transfer or delegate change results in changes to a delegate's number of votes.
*/
event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);

/**
* @dev Returns the current amount of votes that `account` has.
*/
function getVotes(address account) external view returns (uint256);

/**
* @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`).
*/
function getPastVotes(address account, uint256 blockNumber) external view returns (uint256);

/**
* @dev Returns the total supply of votes available at the end of a past block (`blockNumber`).
*
* NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes.
* Votes that have not been delegated are still part of total supply, even though they would not participate in a
* vote.
*/
function getPastTotalSupply(uint256 blockNumber) external view returns (uint256);

/**
* @dev Returns the delegate that `account` has chosen.
*/
function delegates(address account) external view returns (address);

/**
* @dev Delegates votes from the sender to `delegatee`.
*/
function delegate(address delegatee) external;

/**
* @dev Delegates votes from signer to `delegatee`.
*/
function delegateBySig(
address delegatee,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external;
}
210 changes: 210 additions & 0 deletions contracts/governance/utils/Votes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../../utils/Context.sol";
import "../../utils/Counters.sol";
import "../../utils/Checkpoints.sol";
import "../../utils/cryptography/draft-EIP712.sol";
import "./IVotes.sol";

/**
* @dev This is a base abstract contract that tracks voting units, which are a measure of voting power that can be
* transferred, and provides a system of vote delegation, where an account can delegate its voting units to a sort of
* "representative" that will pool delegated voting units from different accounts and can then use it to vote in
* decisions. In fact, voting units _must_ be delegated in order to count as actual votes, and an account has to
* delegate those votes to itself if it wishes to participate in decisions and does not have a trusted representative.
*
* This contract is often combined with a token contract such that voting units correspond to token units. For an
* example, see {ERC721Votes}.
*
* The full history of delegate votes is tracked on-chain so that governance protocols can consider votes as distributed
* at a particular block number to protect against flash loans and double voting. The opt-in delegate system makes the
* cost of this history tracking optional.
*
* When using this module the derived contract must implement {_getVotingUnits} (for example, make it return
* {ERC721-balanceOf}), and can use {_transferVotingUnits} to track a change in the distribution of those units (in the
* previous example, it would be included in {ERC721-_beforeTokenTransfer}).
*
* _Available since v4.5._
*/
abstract contract Votes is IVotes, Context, EIP712 {
using Checkpoints for Checkpoints.History;
using Counters for Counters.Counter;

bytes32 private constant _DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

mapping(address => address) private _delegation;
mapping(address => Checkpoints.History) private _delegateCheckpoints;
Checkpoints.History private _totalCheckpoints;

mapping(address => Counters.Counter) private _nonces;

/**
* @dev Returns the current amount of votes that `account` has.
*/
function getVotes(address account) public view virtual override returns (uint256) {
return _delegateCheckpoints[account].latest();
}

/**
* @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`).
*
* Requirements:
*
* - `blockNumber` must have been already mined
*/
function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
return _delegateCheckpoints[account].getAtBlock(blockNumber);
}

/**
* @dev Returns the total supply of votes available at the end of a past block (`blockNumber`).
*
* NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes.
* Votes that have not been delegated are still part of total supply, even though they would not participate in a
* vote.
*
* Requirements:
*
* - `blockNumber` must have been already mined
*/
function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) {
require(blockNumber < block.number, "Votes: block not yet mined");
return _totalCheckpoints.getAtBlock(blockNumber);
}

/**
* @dev Returns the current total supply of votes.
*/
function _getTotalSupply() internal view virtual returns (uint256) {
return _totalCheckpoints.latest();
}

/**
* @dev Returns the delegate that `account` has chosen.
*/
function delegates(address account) public view virtual override returns (address) {
return _delegation[account];
}

/**
* @dev Delegates votes from the sender to `delegatee`.
*/
function delegate(address delegatee) public virtual override {
address account = _msgSender();
_delegate(account, 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, "Votes: signature expired");
address signer = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))),
v,
r,
s
);
require(nonce == _useNonce(signer), "Votes: invalid nonce");
_delegate(signer, delegatee);
}

/**
* @dev Delegate all of `account`'s voting units to `delegatee`.
*
* Emits events {DelegateChanged} and {DelegateVotesChanged}.
*/
function _delegate(address account, address delegatee) internal virtual {
address oldDelegate = delegates(account);
_delegation[account] = delegatee;

emit DelegateChanged(account, oldDelegate, delegatee);
_moveDelegateVotes(oldDelegate, delegatee, _getVotingUnits(account));
}

/**
* @dev Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to`
* should be zero. Total supply of voting units will be adjusted with mints and burns.
*/
function _transferVotingUnits(
address from,
address to,
uint256 amount
) internal virtual {
if (from == address(0)) {
_totalCheckpoints.push(_add, amount);
}
if (to == address(0)) {
_totalCheckpoints.push(_subtract, amount);
}
_moveDelegateVotes(delegates(from), delegates(to), amount);
}

/**
* @dev Moves delegated votes from one delegate to another.
*/
function _moveDelegateVotes(
address from,
address to,
uint256 amount
) private {
if (from != to && amount > 0) {
if (from != address(0)) {
(uint256 oldValue, uint256 newValue) = _delegateCheckpoints[from].push(_subtract, amount);
emit DelegateVotesChanged(from, oldValue, newValue);
}
if (to != address(0)) {
(uint256 oldValue, uint256 newValue) = _delegateCheckpoints[to].push(_add, amount);
emit DelegateVotesChanged(to, oldValue, newValue);
}
}
}

function _add(uint256 a, uint256 b) private pure returns (uint256) {
return a + b;
}

function _subtract(uint256 a, uint256 b) private pure returns (uint256) {
return a - b;
}

/**
* @dev Consumes a nonce.
*
* Returns the current value and increments nonce.
*/
function _useNonce(address owner) internal virtual returns (uint256 current) {
Counters.Counter storage nonce = _nonces[owner];
current = nonce.current();
nonce.increment();
}

/**
* @dev Returns an address nonce.
*/
function nonces(address owner) public view virtual returns (uint256) {
return _nonces[owner].current();
}

/**
* @dev Returns the contract's {EIP712} domain separator.
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _domainSeparatorV4();
}

/**
* @dev Must return the voting units held by an account.
*/
function _getVotingUnits(address) internal virtual returns (uint256);
}
23 changes: 23 additions & 0 deletions contracts/mocks/CheckpointsImpl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../utils/Checkpoints.sol";

contract CheckpointsImpl {
using Checkpoints for Checkpoints.History;

Checkpoints.History private _totalCheckpoints;

function latest() public view returns (uint256) {
return _totalCheckpoints.latest();
}

function getAtBlock(uint256 blockNumber) public view returns (uint256) {
return _totalCheckpoints.getAtBlock(blockNumber);
}

function push(uint256 value) public returns (uint256, uint256) {
return _totalCheckpoints.push(value);
}
}
25 changes: 25 additions & 0 deletions contracts/mocks/ERC721VotesMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../token/ERC721/extensions/draft-ERC721Votes.sol";

contract ERC721VotesMock is ERC721Votes {
constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {}

function getTotalSupply() public view returns (uint256) {
return _getTotalSupply();
}

function mint(address account, uint256 tokenId) public {
_mint(account, tokenId);
}

function burn(uint256 tokenId) public {
_burn(tokenId);
}

function getChainId() external view returns (uint256) {
return block.chainid;
}
}
2 changes: 1 addition & 1 deletion contracts/mocks/GovernorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ contract GovernorMock is
{
constructor(
string memory name_,
ERC20Votes token_,
IVotes token_,
uint256 votingDelay_,
uint256 votingPeriod_,
uint256 quorumNumerator_
Expand Down
2 changes: 1 addition & 1 deletion contracts/mocks/GovernorPreventLateQuorumMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contract GovernorPreventLateQuorumMock is

constructor(
string memory name_,
ERC20Votes token_,
IVotes token_,
uint256 votingDelay_,
uint256 votingPeriod_,
uint256 quorum_,
Expand Down

0 comments on commit b42b053

Please sign in to comment.