Commit 9b6c3989 by github-actions

Merge upstream openzeppelin-contracts into upstream-patched

parents 44566ee8 29957d4a
......@@ -137,26 +137,6 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
}
/**
* @dev See {IGovernor-votingDelay}
*/
function votingDelay() public view virtual override returns (uint256);
/**
* @dev See {IGovernor-votingPeriod}
*/
function votingPeriod() public view virtual override returns (uint256);
/**
* @dev See {IGovernor-quorum}
*/
function quorum(uint256 blockNumber) public view virtual override returns (uint256);
/**
* @dev See {IGovernor-getVotes}
*/
function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256);
/**
* @dev Amount of votes already casted passes the threshold limit.
*/
function _quorumReached(uint256 proposalId) internal view virtual returns (bool);
......
......@@ -9,7 +9,7 @@ import "../utils/introspection/ERC165.sol";
*
* _Available since v4.3._
*/
interface IGovernor is IERC165 {
abstract contract IGovernor is IERC165 {
enum ProposalState {
Pending,
Active,
......@@ -57,13 +57,13 @@ interface IGovernor is IERC165 {
* @notice module:core
* @dev Name of the governor instance (used in building the ERC712 domain separator).
*/
function name() external view returns (string memory);
function name() public view virtual returns (string memory);
/**
* @notice module:core
* @dev Version of the governor instance (used in building the ERC712 domain separator). Default: "1"
*/
function version() external view returns (string memory);
function version() public view virtual returns (string memory);
/**
* @notice module:voting
......@@ -82,7 +82,7 @@ interface IGovernor is IERC165 {
* JavaScript class.
*/
// solhint-disable-next-line func-name-mixedcase
function COUNTING_MODE() external pure returns (string memory);
function COUNTING_MODE() public pure virtual returns (string memory);
/**
* @notice module:core
......@@ -93,32 +93,32 @@ interface IGovernor is IERC165 {
uint256[] calldata values,
bytes[] calldata calldatas,
bytes32 descriptionHash
) external pure returns (uint256);
) public pure virtual returns (uint256);
/**
* @notice module:core
* @dev Current state of a proposal, following Compound's convention
*/
function state(uint256 proposalId) external view returns (ProposalState);
function state(uint256 proposalId) public view virtual returns (ProposalState);
/**
* @notice module:core
* @dev block number used to retrieve user's votes and quorum.
*/
function proposalSnapshot(uint256 proposalId) external view returns (uint256);
function proposalSnapshot(uint256 proposalId) public view virtual returns (uint256);
/**
* @notice module:core
* @dev timestamp at which votes close.
*/
function proposalDeadline(uint256 proposalId) external view returns (uint256);
function proposalDeadline(uint256 proposalId) public view virtual returns (uint256);
/**
* @notice module:user-config
* @dev delay, in number of block, between the proposal is created and the vote starts. This can be increassed to
* leave time for users to buy voting power, of delegate it, before the voting of a proposal starts.
*/
function votingDelay() external view returns (uint256);
function votingDelay() public view virtual returns (uint256);
/**
* @notice module:user-config
......@@ -127,7 +127,7 @@ interface IGovernor is IERC165 {
* Note: the {votingDelay} can delay the start of the vote. This must be considered when setting the voting
* duration compared to the voting delay.
*/
function votingPeriod() external view returns (uint256);
function votingPeriod() public view virtual returns (uint256);
/**
* @notice module:user-config
......@@ -136,7 +136,7 @@ interface IGovernor is IERC165 {
* Note: The `blockNumber` parameter corresponds to the snaphot used for counting vote. This allows to scale the
* quroum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).
*/
function quorum(uint256 blockNumber) external view returns (uint256);
function quorum(uint256 blockNumber) public view virtual returns (uint256);
/**
* @notice module:reputation
......@@ -145,13 +145,13 @@ interface IGovernor is IERC165 {
* Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or
* multiple), {ERC20Votes} tokens.
*/
function getVotes(address account, uint256 blockNumber) external view returns (uint256);
function getVotes(address account, uint256 blockNumber) public view virtual returns (uint256);
/**
* @notice module:voting
* @dev Returns weither `account` has casted a vote on `proposalId`.
*/
function hasVoted(uint256 proposalId, address account) external view returns (bool);
function hasVoted(uint256 proposalId, address account) public view virtual returns (bool);
/**
* @dev Create a new proposal. Vote start {IGovernor-votingDelay} blocks after the proposal is created and ends
......@@ -164,7 +164,7 @@ interface IGovernor is IERC165 {
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) external returns (uint256 proposalId);
) public virtual returns (uint256 proposalId);
/**
* @dev Execute a successful proposal. This requiers the quorum to be reached, the vote to be successful, and the
......@@ -179,14 +179,14 @@ interface IGovernor is IERC165 {
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) external payable returns (uint256 proposalId);
) public payable virtual returns (uint256 proposalId);
/**
* @dev Cast a vote
*
* Emits a {VoteCast} event.
*/
function castVote(uint256 proposalId, uint8 support) external returns (uint256 balance);
function castVote(uint256 proposalId, uint8 support) public virtual returns (uint256 balance);
/**
* @dev Cast a with a reason
......@@ -197,7 +197,7 @@ interface IGovernor is IERC165 {
uint256 proposalId,
uint8 support,
string calldata reason
) external returns (uint256 balance);
) public virtual returns (uint256 balance);
/**
* @dev Cast a vote using the user cryptographic signature.
......@@ -210,5 +210,5 @@ interface IGovernor is IERC165 {
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 balance);
) public virtual returns (uint256 balance);
}
......@@ -5,6 +5,7 @@ pragma solidity ^0.8.0;
import "../../utils/Counters.sol";
import "../../utils/math/SafeCast.sol";
import "../extensions/IGovernorTimelock.sol";
import "../extensions/GovernorProposalThreshold.sol";
import "../Governor.sol";
import "./IGovernorCompatibilityBravo.sol";
......@@ -16,7 +17,12 @@ import "./IGovernorCompatibilityBravo.sol";
*
* _Available since v4.3._
*/
abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorCompatibilityBravo, Governor {
abstract contract GovernorCompatibilityBravo is
IGovernorTimelock,
IGovernorCompatibilityBravo,
Governor,
GovernorProposalThreshold
{
using Counters for Counters.Counter;
using Timers for Timers.BlockNumber;
......@@ -41,20 +47,6 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
mapping(uint256 => ProposalDetails) private _proposalDetails;
// public for hooking
function proposalThreshold() public view virtual override returns (uint256);
// public for hooking
function proposalEta(uint256 proposalId) public view virtual override returns (uint256);
// public for hooking
function queue(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public virtual override returns (uint256);
// solhint-disable-next-line func-name-mixedcase
function COUNTING_MODE() public pure virtual override returns (string memory) {
return "support=bravo&quorum=bravo";
......@@ -69,8 +61,9 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) public virtual override(IGovernor, Governor) returns (uint256) {
return propose(targets, values, new string[](calldatas.length), calldatas, description);
) public virtual override(IGovernor, Governor, GovernorProposalThreshold) returns (uint256) {
_storeProposal(_msgSender(), targets, values, new string[](calldatas.length), calldatas, description);
return super.propose(targets, values, calldatas, description);
}
/**
......@@ -83,14 +76,8 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
bytes[] memory calldatas,
string memory description
) public virtual override returns (uint256) {
require(
getVotes(msg.sender, block.number - 1) >= proposalThreshold(),
"GovernorCompatibilityBravo: proposer votes below proposal threshold"
);
uint256 proposalId = super.propose(targets, values, _encodeCalldata(signatures, calldatas), description);
_storeProposal(proposalId, _msgSender(), targets, values, signatures, calldatas, description);
return proposalId;
_storeProposal(_msgSender(), targets, values, signatures, calldatas, description);
return propose(targets, values, _encodeCalldata(signatures, calldatas), description);
}
/**
......@@ -119,6 +106,22 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
);
}
function cancel(uint256 proposalId) public virtual override {
ProposalDetails storage details = _proposalDetails[proposalId];
require(
_msgSender() == details.proposer || getVotes(details.proposer, block.number - 1) < proposalThreshold(),
"GovernorBravo: proposer above threshold"
);
_cancel(
details.targets,
details.values,
_encodeCalldata(details.signatures, details.calldatas),
details.descriptionHash
);
}
/**
* @dev Encodes calldatas with optional function signature.
*/
......@@ -132,7 +135,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
for (uint256 i = 0; i < signatures.length; ++i) {
fullcalldatas[i] = bytes(signatures[i]).length == 0
? calldatas[i]
: abi.encodePacked(bytes4(keccak256(bytes(signatures[i]))), calldatas[i]);
: abi.encodeWithSignature(signatures[i], calldatas[i]);
}
return fullcalldatas;
......@@ -142,7 +145,6 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
* @dev Store proposal metadata for later lookup
*/
function _storeProposal(
uint256 proposalId,
address proposer,
address[] memory targets,
uint256[] memory values,
......@@ -150,18 +152,32 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
bytes[] memory calldatas,
string memory description
) private {
ProposalDetails storage details = _proposalDetails[proposalId];
bytes32 descriptionHash = keccak256(bytes(description));
uint256 proposalId = hashProposal(targets, values, _encodeCalldata(signatures, calldatas), descriptionHash);
details.proposer = proposer;
details.targets = targets;
details.values = values;
details.signatures = signatures;
details.calldatas = calldatas;
details.descriptionHash = keccak256(bytes(description));
ProposalDetails storage details = _proposalDetails[proposalId];
if (details.descriptionHash == bytes32(0)) {
details.proposer = proposer;
details.targets = targets;
details.values = values;
details.signatures = signatures;
details.calldatas = calldatas;
details.descriptionHash = descriptionHash;
}
}
// ==================================================== Views =====================================================
/**
* @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
*/
function proposalThreshold()
public
view
virtual
override(IGovernorCompatibilityBravo, GovernorProposalThreshold)
returns (uint256);
/**
* @dev See {IGovernorCompatibilityBravo-proposals}.
*/
function proposals(uint256 proposalId)
......
......@@ -9,7 +9,7 @@ import "../IGovernor.sol";
*
* _Available since v4.3._
*/
interface IGovernorCompatibilityBravo is IGovernor {
abstract contract IGovernorCompatibilityBravo is IGovernor {
/**
* @dev Proposal structure from Compound Governor Bravo. Not actually used by the compatibility layer, as
* {{proposal}} returns a very different structure.
......@@ -44,14 +44,15 @@ interface IGovernorCompatibilityBravo is IGovernor {
/**
* @dev Part of the Governor Bravo's interface.
*/
function quorumVotes() external view returns (uint256);
function quorumVotes() public view virtual returns (uint256);
/**
* @dev Part of the Governor Bravo's interface: _"The official record of all proposals ever proposed"_.
*/
function proposals(uint256)
external
public
view
virtual
returns (
uint256 id,
address proposer,
......@@ -74,24 +75,30 @@ interface IGovernorCompatibilityBravo is IGovernor {
string[] memory signatures,
bytes[] memory calldatas,
string memory description
) external returns (uint256);
) public virtual returns (uint256);
/**
* @dev Part of the Governor Bravo's interface: _"Queues a proposal of state succeeded"_.
*/
function queue(uint256 proposalId) external;
function queue(uint256 proposalId) public virtual;
/**
* @dev Part of the Governor Bravo's interface: _"Executes a queued proposal if eta has passed"_.
*/
function execute(uint256 proposalId) external payable;
function execute(uint256 proposalId) public payable virtual;
/**
* @dev Cancels a proposal only if sender is the proposer, or proposer delegates dropped below proposal threshold.
*/
function cancel(uint256 proposalId) public virtual;
/**
* @dev Part of the Governor Bravo's interface: _"Gets actions of a proposal"_.
*/
function getActions(uint256 proposalId)
external
public
view
virtual
returns (
address[] memory targets,
uint256[] memory values,
......@@ -102,10 +109,10 @@ interface IGovernorCompatibilityBravo is IGovernor {
/**
* @dev Part of the Governor Bravo's interface: _"Gets the receipt for a voter on a given proposal"_.
*/
function getReceipt(uint256 proposalId, address voter) external view returns (Receipt memory);
function getReceipt(uint256 proposalId, address voter) public view virtual returns (Receipt memory);
/**
* @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
*/
function proposalThreshold() external view returns (uint256);
function proposalThreshold() public view virtual returns (uint256);
}
......@@ -66,9 +66,7 @@ abstract contract GovernorCountingSimple is Governor {
function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
ProposalVote storage proposalvote = _proposalVotes[proposalId];
return
quorum(proposalSnapshot(proposalId)) <=
proposalvote.againstVotes + proposalvote.forVotes + proposalvote.abstainVotes;
return quorum(proposalSnapshot(proposalId)) <= proposalvote.forVotes + proposalvote.abstainVotes;
}
/**
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../Governor.sol";
/**
* @dev Extension of {Governor} for proposal restriction to token holders with a minimum balance.
*
* _Available since v4.3._
*/
abstract contract GovernorProposalThreshold is Governor {
/**
* @dev See {IGovernor-propose}.
*/
function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) public virtual override returns (uint256) {
require(
getVotes(msg.sender, block.number - 1) >= proposalThreshold(),
"GovernorCompatibilityBravo: proposer votes below proposal threshold"
);
return super.propose(targets, values, calldatas, description);
}
/**
* @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
*/
function proposalThreshold() public view virtual returns (uint256);
}
......@@ -9,17 +9,17 @@ import "../IGovernor.sol";
*
* _Available since v4.3._
*/
interface IGovernorTimelock is IGovernor {
abstract contract IGovernorTimelock is IGovernor {
event ProposalQueued(uint256 proposalId, uint256 eta);
function timelock() external view returns (address);
function timelock() public view virtual returns (address);
function proposalEta(uint256 proposalId) external view returns (uint256);
function proposalEta(uint256 proposalId) public view virtual returns (uint256);
function queue(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) external returns (uint256 proposalId);
) public virtual returns (uint256 proposalId);
}
......@@ -47,7 +47,7 @@ contract GovernorCompMock is Governor, GovernorVotesComp, GovernorCountingSimple
public
view
virtual
override(Governor, GovernorVotesComp)
override(IGovernor, GovernorVotesComp)
returns (uint256)
{
return super.getVotes(account, blockNumber);
......
......@@ -34,11 +34,11 @@ contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorT
return super.supportsInterface(interfaceId);
}
function votingDelay() public view override(IGovernor, Governor) returns (uint256) {
function votingDelay() public view override returns (uint256) {
return _votingDelay;
}
function votingPeriod() public view override(IGovernor, Governor) returns (uint256) {
function votingPeriod() public view override returns (uint256) {
return _votingPeriod;
}
......@@ -46,7 +46,7 @@ contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorT
return _proposalThreshold;
}
function quorum(uint256) public pure override(IGovernor, Governor) returns (uint256) {
function quorum(uint256) public pure override returns (uint256) {
return 0;
}
......@@ -64,7 +64,7 @@ contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorT
public
view
virtual
override(GovernorCompatibilityBravo, GovernorTimelockCompound)
override(IGovernorTimelock, GovernorTimelockCompound)
returns (uint256)
{
return super.proposalEta(proposalId);
......@@ -84,7 +84,7 @@ contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorT
uint256[] memory values,
bytes[] memory calldatas,
bytes32 salt
) public virtual override(GovernorCompatibilityBravo, GovernorTimelockCompound) returns (uint256) {
) public virtual override(IGovernorTimelock, GovernorTimelockCompound) returns (uint256) {
return super.queue(targets, values, calldatas, salt);
}
......
......@@ -44,7 +44,7 @@ contract GovernorMock is Governor, GovernorVotesQuorumFraction, GovernorCounting
public
view
virtual
override(Governor, GovernorVotes)
override(IGovernor, GovernorVotes)
returns (uint256)
{
return super.getVotes(account, blockNumber);
......
......@@ -37,18 +37,18 @@ contract GovernorTimelockCompoundMock is GovernorTimelockCompound, GovernorVotes
return super.supportsInterface(interfaceId);
}
function votingDelay() public view override(IGovernor, Governor) returns (uint256) {
function votingDelay() public view override returns (uint256) {
return _votingDelay;
}
function votingPeriod() public view override(IGovernor, Governor) returns (uint256) {
function votingPeriod() public view override returns (uint256) {
return _votingPeriod;
}
function quorum(uint256 blockNumber)
public
view
override(IGovernor, Governor, GovernorVotesQuorumFraction)
override(IGovernor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
......@@ -99,7 +99,7 @@ contract GovernorTimelockCompoundMock is GovernorTimelockCompound, GovernorVotes
public
view
virtual
override(IGovernor, Governor, GovernorVotes)
override(IGovernor, GovernorVotes)
returns (uint256)
{
return super.getVotes(account, blockNumber);
......
......@@ -37,18 +37,18 @@ contract GovernorTimelockControlMock is GovernorTimelockControl, GovernorVotesQu
return super.supportsInterface(interfaceId);
}
function votingDelay() public view override(IGovernor, Governor) returns (uint256) {
function votingDelay() public view override returns (uint256) {
return _votingDelay;
}
function votingPeriod() public view override(IGovernor, Governor) returns (uint256) {
function votingPeriod() public view override returns (uint256) {
return _votingPeriod;
}
function quorum(uint256 blockNumber)
public
view
override(IGovernor, Governor, GovernorVotesQuorumFraction)
override(IGovernor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
......@@ -99,7 +99,7 @@ contract GovernorTimelockControlMock is GovernorTimelockControl, GovernorVotesQu
public
view
virtual
override(IGovernor, Governor, GovernorVotes)
override(IGovernor, GovernorVotes)
returns (uint256)
{
return super.getVotes(account, blockNumber);
......
......@@ -14,4 +14,6 @@
** xref:erc777.adoc[ERC777]
** xref:erc1155.adoc[ERC1155]
* xref:governance.adoc[Governance]
* xref:utilities.adoc[Utilities]
= How to set up on-chain governance
In this guide we will learn how OpenZeppelin’s Govenor contract works, how to set it up, and how to use it to create proposals, vote for them, and execute them, using tools provided by Ethers.js and Tally.
== Introduction
Decentralized protocols are in constant evolution from the moment they are publicly released. Often, the initial team retains control of this evolution in the first stages, but eventually delegates it to a community of stakeholders. The process by which this community makes decisions is called on-chain governance, and it has become a central component of decentralized protocols, fueling varied decisions such as parameter tweaking, smart contract upgrades, integrations with other protocols, treasury management, grants, etc.
This governance protocol is generally implemented in a special-purpose contract called “Governor”. The GovernorAlpha and GovernorBravo contracts designed by Compound have been very successful and popular so far, with the downside that projects with different requirements have had to fork the code to customize it for their needs, which can pose a high risk of introducing security issues. For OpenZeppelin Contracts, we set out to build a modular system of Governor contracts so that forking is not needed, and different requirements can be accommodated by writing small modules using Solidity inheritance. You will find the most common requirements out of the box in OpenZeppelin Contracts, but writing additional ones is simple, and we will be adding new features as requested by the community in future releases. Additionally, the design of OpenZeppelin Governor requires minimal use of storage and results in more gas efficient operation.
== Compatibility
OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems.
=== ERC20Votes & ERC20VotesComp
The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only.
=== Governor & GovernorCompatibilityBravo
An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module.
=== GovernorTimelockControl & GovernorTimelockCompound
When using a timelock with your Governor contract, you can use either OpenZeppelin’s TimelockController or Compound’s Timelock. Based on the choice of timelock, you should choose the corresponding Governor module: GovernorTimelockControl or GovernorTimelockCompound respectively. This allows you to migrate an existing GovernorAlpha instance to an OpenZeppelin-based Governor without changing the timelock in use.
=== Tally
Tally is a full-fledged application for user owned on-chain governance. It comprises a voting dashboard, proposal creation wizard, real time research and analysis, and educational content.
For all of these options, the Governor will be compatible with Tally: users will be able to create proposals, visualize voting power and advocates, navigate proposals, and cast votes. For proposal creation in particular, projects can also use Defender Admin as an alternative interface.
In the rest of this guide, we will focus on a fresh deploy of the vanilla OpenZeppelin Governor features without concern for compatibility with GovernorAlpha or Bravo.
== Setup
=== Token
The voting power of each account in our governance setup will be determined by an ERC20 token. The token has to implement the ERC20Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
contract MyToken is ERC20, ERC20Permit, ERC20Votes {
constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {}
// The functions below are overrides required by Solidity.
function _afterTokenTransfer(address from, address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._mint(to, amount);
}
function _burn(address account, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._burn(account, amount);
}
}
```
If your project already has a live token that does not include ERC20Votes and is not upgradeable, you can wrap it in a governance token by using ERC20Wrapper. This will allow token holders to participate in governance by wrapping their tokens 1-to-1.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol";
contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper {
constructor(IERC20 wrappedToken)
ERC20("MyToken", "MTK")
ERC20Permit("MyToken")
ERC20Wrapper(wrappedToken)
{}
// The functions below are overrides required by Solidity.
function _afterTokenTransfer(address from, address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._mint(to, amount);
}
function _burn(address account, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._burn(account, amount);
}
}
```
NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor.
=== Governor
Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, and 3) what options people have when casting a vote and how those votes are counted. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts.
For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token.
For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Votes to define quorum as a percentage of the total supply at the block a proposal’s voting power is retrieved. This requires a constructor parameter to set the percentage. Most Governors nowadays use 4%, so we will initialize the module with parameter 4 (this indicates the percentage, resulting in 4%).
For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum.
Besides these modules, Governor itself has some parameters we must set.
votingDelay: How long after a proposal is created should voting power be fixed. A large voting delay gives users time to unstake tokens if necessary.
votingPeriod: How long does a proposal remain open to votes.
These parameters are specified in number of blocks. Assuming block time of around 13.14 seconds, we will set votingDelay = 1 day = 6570 blocks, and votingPeriod = 1 week = 45992 blocks.
We can optionally set a proposal threshold as well. This restricts proposal creation to accounts who have enough voting power.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "./governance/Governor.sol";
import "./governance/compatibility/GovernorCompatibilityBravo.sol";
import "./governance/extensions/GovernorVotes.sol";
import "./governance/extensions/GovernorVotesQuorumFraction.sol";
import "./governance/extensions/GovernorTimelockControl.sol";
contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
constructor(ERC20Votes _token, TimelockController _timelock)
Governor("MyGovernor")
GovernorVotes(_token)
GovernorVotesQuorumFraction(4)
GovernorTimelockControl(_timelock)
{}
function votingDelay() public pure override returns (uint256) {
return 6575; // 1 day
}
function votingPeriod() public pure override returns (uint256) {
return 46027; // 1 week
}
function proposalThreshold() public pure override returns (uint256) {
return 0;
}
// The functions below are overrides required by Solidity.
function quorum(uint256 blockNumber)
public
view
override(IGovernor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
}
function getVotes(address account, uint256 blockNumber)
public
view
override(IGovernor, GovernorVotes)
returns (uint256)
{
return super.getVotes(account, blockNumber);
}
function state(uint256 proposalId)
public
view
override(Governor, IGovernor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}
function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
public
override(Governor, GovernorCompatibilityBravo, IGovernor)
returns (uint256)
{
return super.propose(targets, values, calldatas, description);
}
function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
internal
override(Governor, GovernorTimelockControl)
{
super._execute(proposalId, targets, values, calldatas, descriptionHash);
}
function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
internal
override(Governor, GovernorTimelockControl)
returns (uint256)
{
return super._cancel(targets, values, calldatas, descriptionHash);
}
function _executor()
internal
view
override(Governor, GovernorTimelockControl)
returns (address)
{
return super._executor();
}
function supportsInterface(bytes4 interfaceId)
public
view
override(Governor, IERC165, GovernorTimelockControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
```
=== Timelock
It is good practice to add a timelock to governance decisions. This allows users to exit the system if they disagree with a decision before it is executed. We will use OpenZeppelin’s TimelockController in combination with the GovernorTimelockControl module.
TimelockController uses an AccessControl setup that we need to understand in order to set up roles. The Proposer role is in charge of queueing operations: this is the role the Governor instance should be granted, and it should likely be the only proposer in the system. The Executor role is in charge of executing already available operations: we can assign this role to the special zero address to allow anyone to execute (if operations can be particularly time sensitive, the Governor should be made Executor instead). Lastly, there is the Admin role, which can grant and revoke the two previous roles: this is a very sensitive role that will be granted automatically to both deployer and timelock itself, but should be renounced by the deployer after setup.
== Proposal Lifecycle
Let’s walk through how to create and execute a proposal on our newly deployed Governor.
A proposal is a sequence of actions that the Governor contract will perform if it passes. Each action consists of a target address, calldata encoding a function call, and an amount of ETH to include. Additionally, a proposal includes a human-readable description.
=== Create a Proposal
Let’s say we want to create a proposal to give a team a grant, in the form of ERC20 tokens from the governance treasury. This proposal will consist of a single action where the target is the ERC20 token, calldata is the encoded function call `transfer(<team wallet>, <grant amount>)`, and with 0 ETH attached.
Generally a proposal will be created with the help of an interface such as Tally or Defender. Here we will show how to create the proposal using Ethers.js.
First we get all the parameters necessary for the proposal action.
```javascript
const tokenAddress = ...;
const token = await ethers.getContractAt(‘ERC20’, tokenAddress);
const teamAddress = ...;
const grantAmount = ...;
const transferCalldata = token.interface.encodeFunctionData(‘transfer’, [teamAddress, grantAmount]);
```
Now we are ready to call the propose function of the governor. Note that we don’t pass in one array of actions, but instead three arrays corresponding to the list of targets, the list of values, and the list of calldatas. In this case it’s a single action, so it’s simple:
```javascript
await governor.propose(
[tokenAddress],
[0],
[transferCalldata],
“Proposal #1: Give grant to team”,
);
```
This will create a new proposal, with a proposal id that is obtained by hashing together the proposal data, and which will also be found in an event in the logs of the transaction.
=== Cast a Vote
Once a proposal is active, stakeholders can cast their vote. This is done through a function in the Governor contract that users can invoke directly from a governance UI such as Tally.
image::tally-vote.png[Voting in Tally]
=== Execute the Proposal
Once the voting period is over, if quorum was reached (enough voting power participated) and the majority voted in favor, the proposal is considered successful and can proceed to be executed. This can also be done in Tally in the "Administration Panel" section of a project.
image::tally-admin.png[Administration Panel in Tally]
We will see now how to do this manually using Ethers.js.
If a timelock was set up, the first step to execution is queueing. You will notice that both the queue and execute functions require passing in all of the proposal parameters, as opposed to just the proposal id. This is necessary because this data is not stored on chain, as a measure to save gas. Note that these parameters can always be found in the events emitted by the contract. The only parameter that is not sent in its entirety is the description, since this is only needed in its hashed form to compute the proposal id.
To queue, we call the queue function:
```javascript
const descriptionHash = ethers.utils.id(“Proposal #1: Give grant to team”);
await governor.queue(
[tokenAddress],
[0],
[transferCalldata],
descriptionHash,
);
```
This will cause the governor to interact with the timelock contract and queue the actions for execution after the required delay.
After enough time has passed (according to the timelock parameters), the proposal can be executed. If there was no timelock to begin with, this step can be ran immediately after the proposal succeeds.
```javascript
await governor.execute(
[tokenAddress],
[0],
[transferCalldata],
descriptionHash,
);
```
Executing the proposal will transfer the ERC20 tokens to the chosen recipient. To wrap up: we set up a system where a treasury is controlled by the collective decision of the token holders of a project, and all actions are executed via proposals enforced by on-chain votes.
......@@ -29,7 +29,7 @@ contract('Governor', function (accounts) {
beforeEach(async function () {
this.owner = owner;
this.token = await Token.new(tokenName, tokenSymbol);
this.mock = await Governor.new(name, this.token.address, 4, 16, 0);
this.mock = await Governor.new(name, this.token.address, 4, 16, 10);
this.receiver = await CallReceiver.new();
await this.token.mint(owner, tokenSupply);
await this.token.delegate(voter1, { from: voter1 });
......@@ -72,7 +72,7 @@ contract('Governor', function (accounts) {
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For, reason: 'This is nice' },
{ voter: voter2, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
{ voter: voter2, weight: web3.utils.toWei('7'), support: Enums.VoteType.For },
{ voter: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against },
{ voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain },
],
......@@ -194,7 +194,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: this.voter, signature, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: this.voter, signature, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
};
});
......@@ -252,8 +252,8 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter2, weight: web3.utils.toWei('1'), support: Enums.VoteType.Abstain },
{ voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For },
{ voter: voter2, weight: web3.utils.toWei('5'), support: Enums.VoteType.Abstain },
],
};
});
......@@ -285,7 +285,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
steps: {
execute: { error: 'Governor: call reverted without message' },
......@@ -306,7 +306,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
steps: {
execute: { error: 'CallReceiverMock: reverting' },
......@@ -329,13 +329,13 @@ contract('Governor', function (accounts) {
voters: [
{
voter: voter1,
weight: web3.utils.toWei('1'),
weight: web3.utils.toWei('5'),
support: Enums.VoteType.For,
error: 'Governor: unknown proposal id',
},
{
voter: voter2,
weight: web3.utils.toWei('1'),
weight: web3.utils.toWei('5'),
support: Enums.VoteType.Abstain,
error: 'Governor: unknown proposal id',
},
......@@ -382,8 +382,8 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter2, weight: web3.utils.toWei('1'), support: Enums.VoteType.Abstain },
{ voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For },
{ voter: voter2, weight: web3.utils.toWei('5'), support: Enums.VoteType.Abstain },
],
};
});
......@@ -406,7 +406,7 @@ contract('Governor', function (accounts) {
voters: [
{
voter: voter1,
weight: web3.utils.toWei('1'),
weight: web3.utils.toWei('10'),
support: new BN('255'),
error: 'GovernorVotingSimple: invalid value for enum VoteType',
},
......@@ -433,12 +433,12 @@ contract('Governor', function (accounts) {
voters: [
{
voter: voter1,
weight: web3.utils.toWei('1'),
weight: web3.utils.toWei('5'),
support: Enums.VoteType.For,
},
{
voter: voter1,
weight: web3.utils.toWei('1'),
weight: web3.utils.toWei('5'),
support: Enums.VoteType.For,
error: 'GovernorVotingSimple: vote already casted',
},
......@@ -459,7 +459,9 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('0'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For },
{ voter: voter2, weight: web3.utils.toWei('4'), support: Enums.VoteType.Abstain },
{ voter: voter3, weight: web3.utils.toWei('10'), support: Enums.VoteType.Against },
],
steps: {
execute: { error: 'Governor: proposal not successful' },
......@@ -480,7 +482,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.Against },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.Against },
],
steps: {
execute: { error: 'Governor: proposal not successful' },
......@@ -501,7 +503,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
steps: {
wait: { enable: false },
......@@ -593,7 +595,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
steps: {
execute: { enable: false },
......@@ -617,7 +619,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
};
});
......@@ -692,7 +694,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
steps: {
wait: { enable: false },
......@@ -723,7 +725,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
steps: {
execute: { enable: false },
......@@ -753,7 +755,7 @@ contract('Governor', function (accounts) {
],
tokenHolder: owner,
voters: [
{ voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
{ voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
],
};
});
......
......@@ -215,6 +215,51 @@ contract('GovernorCompatibilityBravo', function (accounts) {
runGovernorWorkflow();
});
describe('cancel', function () {
beforeEach(async function () {
this.settings = {
proposal: [
[ this.receiver.address ], // targets
[ web3.utils.toWei('0') ], // values
[ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas
'<proposal description>', // description
],
proposer,
tokenHolder: owner,
steps: {
wait: { enable: false },
queue: { enable: false },
execute: { enable: false },
},
};
});
describe('by proposer', function () {
afterEach(async function () {
await this.mock.cancel(this.id, { from: proposer });
});
runGovernorWorkflow();
});
describe('if proposer below threshold', function () {
afterEach(async function () {
await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer });
await this.mock.cancel(this.id);
});
runGovernorWorkflow();
});
describe('not if proposer above threshold', function () {
afterEach(async function () {
await expectRevert(
this.mock.cancel(this.id),
'GovernorBravo: proposer above threshold',
);
});
runGovernorWorkflow();
});
});
describe('with compatibility interface', function () {
beforeEach(async function () {
this.settings = {
......
......@@ -100,7 +100,7 @@ function shouldSupportInterfaces (interfaces = []) {
expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.be.lte(30000);
});
it('claims support', async function () {
it('claims support [skip-on-coverage]', async function () {
expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true);
});
});
......
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