diff --git a/CHANGELOG.md b/CHANGELOG.md index 2104d110ffc..cd715da5f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * `ERC2771Context`: use private variable from storage to store the forwarder address. Fixes issues where `_msgSender()` was not callable from constructors. ([#2754](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2754)) * `EnumerableSet`: add `values()` functions that returns an array containing all values in a single call. ([#2768](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2768)) + * `Governor`: added a modular system of `Governor` contracts based on `GovernorAlpha` and `GovernorBravo`. ([#2672](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2672)) ## 4.2.0 (2021-06-30) @@ -18,7 +19,7 @@ * `ERC1155Supply`: add a new `ERC1155` extension that keeps track of the totalSupply of each tokenId. ([#2593](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2593)) * `BitMaps`: add a new `BitMaps` library that provides a storage efficient datastructure for `uint256` to `bool` mapping with contiguous keys. ([#2710](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2710)) - ### Breaking Changes +### Breaking Changes * `ERC20FlashMint` is no longer a Draft ERC. ([#2673](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2673))) diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol new file mode 100644 index 00000000000..40db01e76d6 --- /dev/null +++ b/contracts/governance/Governor.sol @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/cryptography/ECDSA.sol"; +import "../utils/cryptography/draft-EIP712.sol"; +import "../utils/introspection/ERC165.sol"; +import "../utils/math/SafeCast.sol"; +import "../utils/Address.sol"; +import "../utils/Context.sol"; +import "../utils/Timers.sol"; +import "./IGovernor.sol"; + +/** + * @dev Core of the governance system, designed to be extended though various modules. + * + * This contract is abstract and requiers several function to be implemented in various modules: + * + * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote} + * - A voting module must implement {getVotes} + * - Additionanly, the {votingPeriod} must also be implemented + * + * _Available since v4.3._ + */ +abstract contract Governor is Context, ERC165, EIP712, IGovernor { + using SafeCast for uint256; + using Timers for Timers.BlockNumber; + + bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); + + struct ProposalCore { + Timers.BlockNumber voteStart; + Timers.BlockNumber voteEnd; + bool executed; + bool canceled; + } + + string private _name; + + mapping(uint256 => ProposalCore) private _proposals; + + /** + * @dev Restrict access to governor executing address. Some module might override the _executor function to make + * sure this modifier is consistant with the execution model. + */ + modifier onlyGovernance() { + require(_msgSender() == _executor(), "Governor: onlyGovernance"); + _; + } + + /** + * @dev Sets the value for {name} and {version} + */ + constructor(string memory name_) EIP712(name_, version()) { + _name = name_; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(IGovernor).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev See {IGovernor-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IGovernor-version}. + */ + function version() public view virtual override returns (string memory) { + return "1"; + } + + /** + * @dev See {IGovernor-hashProposal}. + * + * The proposal id is produced by hashing the RLC encoded `targets` array, the `values` array, the `calldatas` array + * and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id + * can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in + * advance, before the proposal is submitted. + * + * Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the + * same proposal (with same operation and same description) will have the same id if submitted on multiple governors + * accross multiple networks. This also means that in order to execute the same operation twice (on the same + * governor) the proposer will have to change the description in order to avoid proposal id conflicts. + */ + function hashProposal( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public pure virtual override returns (uint256) { + return uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash))); + } + + /** + * @dev See {IGovernor-state}. + */ + function state(uint256 proposalId) public view virtual override returns (ProposalState) { + ProposalCore memory proposal = _proposals[proposalId]; + + if (proposal.executed) { + return ProposalState.Executed; + } else if (proposal.canceled) { + return ProposalState.Canceled; + } else if (proposal.voteStart.isPending()) { + return ProposalState.Pending; + } else if (proposal.voteEnd.isPending()) { + return ProposalState.Active; + } else if (proposal.voteEnd.isExpired()) { + return + _quorumReached(proposalId) && _voteSucceeded(proposalId) + ? ProposalState.Succeeded + : ProposalState.Defeated; + } else { + revert("Governor: unknown proposal id"); + } + } + + /** + * @dev See {IGovernor-proposalSnapshot}. + */ + function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) { + return _proposals[proposalId].voteStart.getDeadline(); + } + + /** + * @dev See {IGovernor-proposalDeadline}. + */ + function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { + return _proposals[proposalId].voteEnd.getDeadline(); + } + + /** + * @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); + + /** + * @dev Is the proposal successful or not. + */ + function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool); + + /** + * @dev Register a vote with a given support and voting weight. + * + * Note: Support is generic and can represent various things depending on the voting system used. + */ + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight + ) internal virtual; + + /** + * @dev See {IGovernor-propose}. + */ + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) public virtual override returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description))); + + require(targets.length == values.length, "Governor: invalid proposal length"); + require(targets.length == calldatas.length, "Governor: invalid proposal length"); + require(targets.length > 0, "Governor: empty proposal"); + + ProposalCore storage proposal = _proposals[proposalId]; + require(proposal.voteStart.isUnset(), "Governor: proposal already exists"); + + uint64 snapshot = block.number.toUint64() + votingDelay().toUint64(); + uint64 deadline = snapshot + votingPeriod().toUint64(); + + proposal.voteStart.setDeadline(snapshot); + proposal.voteEnd.setDeadline(deadline); + + emit ProposalCreated( + proposalId, + _msgSender(), + targets, + values, + new string[](targets.length), + calldatas, + snapshot, + deadline, + description + ); + + return proposalId; + } + + /** + * @dev See {IGovernor-execute}. + */ + function execute( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public payable virtual override returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + + ProposalState status = state(proposalId); + require( + status == ProposalState.Succeeded || status == ProposalState.Queued, + "Governor: proposal not successful" + ); + _proposals[proposalId].executed = true; + + emit ProposalExecuted(proposalId); + + _execute(proposalId, targets, values, calldatas, descriptionHash); + + return proposalId; + } + + /** + * @dev Internal execution mechanism. Can be overriden to implement different execution mechanism + */ + function _execute( + uint256, /* proposalId */ + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 /*descriptionHash*/ + ) internal virtual { + string memory errorMessage = "Governor: call reverted without message"; + for (uint256 i = 0; i < targets.length; ++i) { + (bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]); + Address.verifyCallResult(success, returndata, errorMessage); + } + } + + /** + * @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as + * canceled to allow distinguishing it from executed proposals. + * + * Emits a {IGovernor-ProposalCanceled} event. + */ + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + ProposalState status = state(proposalId); + + require( + status != ProposalState.Canceled && status != ProposalState.Expired && status != ProposalState.Executed, + "Governor: proposal not active" + ); + _proposals[proposalId].canceled = true; + + emit ProposalCanceled(proposalId); + + return proposalId; + } + + /** + * @dev See {IGovernor-castVote}. + */ + function castVote(uint256 proposalId, uint8 support) public virtual override returns (uint256) { + address voter = _msgSender(); + return _castVote(proposalId, voter, support, ""); + } + + /** + * @dev See {IGovernor-castVoteWithReason}. + */ + function castVoteWithReason( + uint256 proposalId, + uint8 support, + string calldata reason + ) public virtual override returns (uint256) { + address voter = _msgSender(); + return _castVote(proposalId, voter, support, reason); + } + + /** + * @dev See {IGovernor-castVoteBySig}. + */ + function castVoteBySig( + uint256 proposalId, + uint8 support, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override returns (uint256) { + address voter = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support))), + v, + r, + s + ); + return _castVote(proposalId, voter, support, ""); + } + + /** + * @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been casted yet, retrieve + * voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. + * + * Emits a {IGovernor-VoteCast} event. + */ + function _castVote( + uint256 proposalId, + address account, + uint8 support, + string memory reason + ) internal virtual returns (uint256) { + ProposalCore storage proposal = _proposals[proposalId]; + require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active"); + + uint256 weight = getVotes(account, proposal.voteStart.getDeadline()); + _countVote(proposalId, account, support, weight); + + emit VoteCast(account, proposalId, support, weight, reason); + + return weight; + } + + /** + * @dev Address through which the governor executes action. Will be overloaded by module that execute actions + * through another contract such as a timelock. + */ + function _executor() internal view virtual returns (address) { + return address(this); + } +} diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol new file mode 100644 index 00000000000..222487771a1 --- /dev/null +++ b/contracts/governance/IGovernor.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/introspection/ERC165.sol"; + +/** + * @dev Interface of the {Governor} core. + * + * _Available since v4.3._ + */ +interface IGovernor is IERC165 { + enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Queued, + Expired, + Executed + } + + /** + * @dev Emitted when a proposal is created. + */ + event ProposalCreated( + uint256 proposalId, + address proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 startBlock, + uint256 endBlock, + string description + ); + + /** + * @dev Emitted when a proposal is canceled. + */ + event ProposalCanceled(uint256 proposalId); + + /** + * @dev Emitted when a proposal is executed. + */ + event ProposalExecuted(uint256 proposalId); + + /** + * @dev Emitted when a vote is casted. + * + * Note: `support` values should be seen as buckets. There interpretation depends on the voting module used. + */ + event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason); + + /** + * @notice module:core + * @dev Name of the governor instance (used in building the ERC712 domain separator). + */ + function name() external view 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); + + /** + * @notice module:voting + * @dev A description of the possible `support` values for {castVote} and the way these votes are counted, meant to + * be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of + * key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`. + * + * There are 2 standard keys: `support` and `quorum`. + * + * - `support=bravo` refers to the vote options 0 = For, 1 = Against, 2 = Abstain, as in `GovernorBravo`. + * - `quorum=bravo` means that only For votes are counted towards quorum. + * - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum. + * + * NOTE: The string can be decoded by the standard + * https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] + * JavaScript class. + */ + // solhint-disable-next-line func-name-mixedcase + function COUNTING_MODE() external pure returns (string memory); + + /** + * @notice module:core + * @dev Hashing function used to (re)build the proposal id from the proposal details.. + */ + function hashProposal( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata calldatas, + bytes32 descriptionHash + ) external pure returns (uint256); + + /** + * @notice module:core + * @dev Current state of a proposal, following Compound's convention + */ + function state(uint256 proposalId) external view returns (ProposalState); + + /** + * @notice module:core + * @dev block number used to retrieve user's votes and quorum. + */ + function proposalSnapshot(uint256 proposalId) external view returns (uint256); + + /** + * @notice module:core + * @dev timestamp at which votes close. + */ + function proposalDeadline(uint256 proposalId) external view 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); + + /** + * @notice module:user-config + * @dev delay, in number of blocks, between the vote start and vote ends. + * + * 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); + + /** + * @notice module:user-config + * @dev Minimum number of casted voted requiered for a proposal to be successful. + * + * 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); + + /** + * @notice module:reputation + * @dev Voting power of an `account` at a specific `blockNumber`. + * + * 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); + + /** + * @notice module:voting + * @dev Returns weither `account` has casted a vote on `proposalId`. + */ + function hasVoted(uint256 proposalId, address account) external view returns (bool); + + /** + * @dev Create a new proposal. Vote start {IGovernor-votingDelay} blocks after the proposal is created and ends + * {IGovernor-votingPeriod} blocks after the voting starts. + * + * Emits a {ProposalCreated} event. + */ + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) external returns (uint256 proposalId); + + /** + * @dev Execute a successful proposal. This requiers the quorum to be reached, the vote to be successful, and the + * deadline to be reached. + * + * Emits a {ProposalExecuted} event. + * + * Note: some module can modify the requierements for execution, for example by adding an additional timelock. + */ + function execute( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external payable returns (uint256 proposalId); + + /** + * @dev Cast a vote + * + * Emits a {VoteCast} event. + */ + function castVote(uint256 proposalId, uint8 support) external returns (uint256 balance); + + /** + * @dev Cast a with a reason + * + * Emits a {VoteCast} event. + */ + function castVoteWithReason( + uint256 proposalId, + uint8 support, + string calldata reason + ) external returns (uint256 balance); + + /** + * @dev Cast a vote using the user cryptographic signature. + * + * Emits a {VoteCast} event. + */ + function castVoteBySig( + uint256 proposalId, + uint8 support, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 balance); +} diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index 666aba46d3c..b51e3317249 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -3,10 +3,64 @@ [.readme-notice] NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/governance -This directory includes primitives for on-chain governance. We currently only offer the {TimelockController} contract, that can be used as a component in a governance system to introduce a delay between a proposal and its execution. +This directory includes primitives for on-chain governance. + +== Governor + +The {Governor} contract provides primitive to set an on-chain voting system similar to https://compound.finance/docs/governance[Compound's Governor Alpha & Bravo]. + +Similarly to our other contracts, it is customizable through inheritance and comes with extensions: + +* {GovernorTimelockControl}: A {Governor} extension that performs executions through a {TimelockController}. This requires a successful proposal to be queued before then can be executed. The {TimelockController} will enforce a delay between the queueing and the execution. With this module, proposals are executed by the external {TimelockController} contract, which would have to hold the assets that are being governed. + +* {GovernorTimelockCompound}: A {Governor} extension that performs executions through a compound https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol[`Timelock`]. This requires a successful proposal to be queued before then can be executed. The `Timelock` will enforce a delay between the queueing and the execution. With this module, proposals are executed by the external `Timelock` contract, which would have to hold the assets that are being governed. + +* {GovernorCountingSimple}: A simple voting mechanism for {Governor} with support 3 vote options: Against, For and Abstain. + +* {GovernorVotes}: Binding to extract voting weight from an {ERC20Votes} token. + +* {GovernorVotesQuorumFraction}: Binding to extract voting weight from an {ERC20Votes} token and set the quorum as a fraction of the (snapshoted) total token supply. + +* {GovernorVotesComp}: Binding to extract voting weight from a Comp or {ERC20VotesComp} token. + +In addition to modules, the {Governor} requires a few virtual functions to be implemented to your particular specifications: + +* <>: Delay (number of blocks), between the proposal, is submitted and the snapshot block used for voting. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes (default: 0). +* <>: Delay (in seconds), between the proposal, is submitted and the vote ends. +* <>: Quorum required for a proposal to be successful. This function includes a `blockNumber` argument so the quorum can adapt through time, for example, to follow a token's `totalSupply`. + +Note: Function of the {Governor} contract does NOT include access control. If you want to restrict access (for example to require a minimum balance to submit a proposal), you should add these checks by overloading the particular functions. For security reasons, the {Governor-_cancel} method is internal, and you will have to expose it (which the right access control mechanism) yourself if this is a mechanism you need. + +Events emitted by the {Governor} contract are compatible with Compound's `GovernorBravo`. Additionnaly, function compatibility can be added using the {GovernorCompatibilityBravo} compatibility layer. This layer includes a voting system but does not include token bindings. This layer also requiers a timelock module (either {GovernorTimelockControl} or {GovernorTimelockCompound}). + +=== Core + +{{IGovernor}} + +{{Governor}} + +=== Extensions + +{{GovernorTimelockControl}} + +{{GovernorTimelockCompound}} + +{{GovernorCountingSimple}} + +{{GovernorVotes}} + +{{GovernorVotesQuorumFraction}} + +{{GovernorVotesComp}} + +=== Compatibility + +{{GovernorCompatibilityBravo}} == Timelock +In a governance systems, the {TimelockController} contract is in carge of introducing a delay between a proposal and its execution. It can be used with or without a {Governor}. + {{TimelockController}} [[timelock-terminology]] @@ -27,7 +81,7 @@ This directory includes primitives for on-chain governance. We currently only of [[timelock-operation]] ==== Operation structure -Operation executed by the xref:api:governance.adoc#TimelockController[`TimelockControler`] can contain one or multiple subsequent calls. Depending on whether you need to multiple calls to be executed atomically, you can either use simple or batched operations. +Operation executed by the xref:api:governance.adoc#TimelockController[`TimelockController`] can contain one or multiple subsequent calls. Depending on whether you need to multiple calls to be executed atomically, you can either use simple or batched operations. Both operations contain: diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol new file mode 100644 index 00000000000..5b22bf1aafc --- /dev/null +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../utils/Counters.sol"; +import "../../utils/math/SafeCast.sol"; +import "../extensions/IGovernorTimelock.sol"; +import "../Governor.sol"; +import "./IGovernorCompatibilityBravo.sol"; + +/** + * @dev Compatibility layer that implements GovernorBravo compatibility on to of {Governor}. + * + * This compatibility layer includes a voting system and requires a {IGovernorTimelock} compatible module to be added + * through inheritance. It does not include token bindings, not does it include any variable upgrade patterns. + * + * _Available since v4.3._ + */ +abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorCompatibilityBravo, Governor { + using Counters for Counters.Counter; + using Timers for Timers.BlockNumber; + + enum VoteType { + Against, + For, + Abstain + } + + struct ProposalDetails { + address proposer; + address[] targets; + uint256[] values; + string[] signatures; + bytes[] calldatas; + uint256 forVotes; + uint256 againstVotes; + uint256 abstainVotes; + mapping(address => Receipt) receipts; + bytes32 descriptionHash; + } + + 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"; + } + + // ============================================== Proposal lifecycle ============================================== + /** + * @dev See {IGovernor-propose}. + */ + function propose( + address[] memory targets, + 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); + } + + /** + * @dev See {IGovernorCompatibilityBravo-propose}. + */ + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + 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; + } + + /** + * @dev See {IGovernorCompatibilityBravo-queue}. + */ + function queue(uint256 proposalId) public virtual override { + ProposalDetails storage details = _proposalDetails[proposalId]; + queue( + details.targets, + details.values, + _encodeCalldata(details.signatures, details.calldatas), + details.descriptionHash + ); + } + + /** + * @dev See {IGovernorCompatibilityBravo-execute}. + */ + function execute(uint256 proposalId) public payable virtual override { + ProposalDetails storage details = _proposalDetails[proposalId]; + execute( + details.targets, + details.values, + _encodeCalldata(details.signatures, details.calldatas), + details.descriptionHash + ); + } + + /** + * @dev Encodes calldatas with optional function signature. + */ + function _encodeCalldata(string[] memory signatures, bytes[] memory calldatas) + private + pure + returns (bytes[] memory) + { + bytes[] memory fullcalldatas = new bytes[](calldatas.length); + + 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]); + } + + return fullcalldatas; + } + + /** + * @dev Store proposal metadata for later lookup + */ + function _storeProposal( + uint256 proposalId, + address proposer, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) private { + ProposalDetails storage details = _proposalDetails[proposalId]; + + details.proposer = proposer; + details.targets = targets; + details.values = values; + details.signatures = signatures; + details.calldatas = calldatas; + details.descriptionHash = keccak256(bytes(description)); + } + + // ==================================================== Views ===================================================== + /** + * @dev See {IGovernorCompatibilityBravo-proposals}. + */ + function proposals(uint256 proposalId) + public + view + virtual + override + returns ( + uint256 id, + address proposer, + uint256 eta, + uint256 startBlock, + uint256 endBlock, + uint256 forVotes, + uint256 againstVotes, + uint256 abstainVotes, + bool canceled, + bool executed + ) + { + id = proposalId; + eta = proposalEta(proposalId); + startBlock = proposalSnapshot(proposalId); + endBlock = proposalDeadline(proposalId); + + ProposalDetails storage details = _proposalDetails[proposalId]; + proposer = details.proposer; + forVotes = details.forVotes; + againstVotes = details.againstVotes; + abstainVotes = details.abstainVotes; + + ProposalState status = state(proposalId); + canceled = status == ProposalState.Canceled; + executed = status == ProposalState.Executed; + } + + /** + * @dev See {IGovernorCompatibilityBravo-getActions}. + */ + function getActions(uint256 proposalId) + public + view + virtual + override + returns ( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas + ) + { + ProposalDetails storage details = _proposalDetails[proposalId]; + return (details.targets, details.values, details.signatures, details.calldatas); + } + + /** + * @dev See {IGovernorCompatibilityBravo-getReceipt}. + */ + function getReceipt(uint256 proposalId, address voter) public view virtual override returns (Receipt memory) { + return _proposalDetails[proposalId].receipts[voter]; + } + + /** + * @dev See {IGovernorCompatibilityBravo-quorumVotes}. + */ + function quorumVotes() public view virtual override returns (uint256) { + return quorum(block.number - 1); + } + + // ==================================================== Voting ==================================================== + /** + * @dev See {IGovernor-hasVoted}. + */ + function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) { + return _proposalDetails[proposalId].receipts[account].hasVoted; + } + + /** + * @dev See {Governor-_quorumReached}. In this module, only forVotes count toward the quorum. + */ + function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) { + ProposalDetails storage details = _proposalDetails[proposalId]; + return quorum(proposalSnapshot(proposalId)) < details.forVotes; + } + + /** + * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be scritly over the againstVotes. + */ + function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) { + ProposalDetails storage details = _proposalDetails[proposalId]; + return details.forVotes > details.againstVotes; + } + + /** + * @dev See {Governor-_countVote}. In this module, the support follows Governor Bravo. + */ + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight + ) internal virtual override { + ProposalDetails storage details = _proposalDetails[proposalId]; + Receipt storage receipt = details.receipts[account]; + + require(!receipt.hasVoted, "GovernorCompatibilityBravo: vote already casted"); + receipt.hasVoted = true; + receipt.support = support; + receipt.votes = SafeCast.toUint96(weight); + + if (support == uint8(VoteType.Against)) { + details.againstVotes += weight; + } else if (support == uint8(VoteType.For)) { + details.forVotes += weight; + } else if (support == uint8(VoteType.Abstain)) { + details.abstainVotes += weight; + } else { + revert("GovernorCompatibilityBravo: invalid vote type"); + } + } +} diff --git a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol new file mode 100644 index 00000000000..92725d4ad73 --- /dev/null +++ b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../IGovernor.sol"; + +/** + * @dev Interface extension that adds missing functions to the {Governor} core to provide `GovernorBravo` compatibility. + * + * _Available since v4.3._ + */ +interface IGovernorCompatibilityBravo is IGovernor { + /** + * @dev Proposal structure from Compound Governor Bravo. Not actually used by the compatibility layer, as + * {{proposal}} returns a very different structure. + */ + struct Proposal { + uint256 id; + address proposer; + uint256 eta; + address[] targets; + uint256[] values; + string[] signatures; + bytes[] calldatas; + uint256 startBlock; + uint256 endBlock; + uint256 forVotes; + uint256 againstVotes; + uint256 abstainVotes; + bool canceled; + bool executed; + mapping(address => Receipt) receipts; + } + + /** + * @dev Receipt structure from Compound Governor Bravo + */ + struct Receipt { + bool hasVoted; + uint8 support; + uint96 votes; + } + + /** + * @dev Part of the Governor Bravo's interface. + */ + function quorumVotes() external view returns (uint256); + + /** + * @dev Part of the Governor Bravo's interface: _"The official record of all proposals ever proposed"_. + */ + function proposals(uint256) + external + view + returns ( + uint256 id, + address proposer, + uint256 eta, + uint256 startBlock, + uint256 endBlock, + uint256 forVotes, + uint256 againstVotes, + uint256 abstainVotes, + bool canceled, + bool executed + ); + + /** + * @dev Part of the Governor Bravo's interface: _"Function used to propose a new proposal"_. + */ + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256); + + /** + * @dev Part of the Governor Bravo's interface: _"Queues a proposal of state succeeded"_. + */ + function queue(uint256 proposalId) external; + + /** + * @dev Part of the Governor Bravo's interface: _"Executes a queued proposal if eta has passed"_. + */ + function execute(uint256 proposalId) external payable; + + /** + * @dev Part of the Governor Bravo's interface: _"Gets actions of a proposal"_. + */ + function getActions(uint256 proposalId) + external + view + returns ( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas + ); + + /** + * @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); + + /** + * @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); +} diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol new file mode 100644 index 00000000000..abd0faee80a --- /dev/null +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../Governor.sol"; + +/** + * @dev Extension of {Governor} for simple, 3 options, vote counting. + * + * _Available since v4.3._ + */ +abstract contract GovernorCountingSimple is Governor { + /** + * @dev Supported vote types. Matches Governor Bravo ordering. + */ + enum VoteType { + Against, + For, + Abstain + } + + struct ProposalVote { + uint256 againstVotes; + uint256 forVotes; + uint256 abstainVotes; + mapping(address => bool) hasVoted; + } + + mapping(uint256 => ProposalVote) private _proposalVotes; + + /** + * @dev See {IGovernor-COUNTING_MODE}. + */ + // solhint-disable-next-line func-name-mixedcase + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=bravo&quorum=for,abstain"; + } + + /** + * @dev See {IGovernor-hasVoted}. + */ + function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) { + return _proposalVotes[proposalId].hasVoted[account]; + } + + /** + * @dev Accessor to the internal vote counts. + */ + function proposalVotes(uint256 proposalId) + public + view + virtual + returns ( + uint256 againstVotes, + uint256 forVotes, + uint256 abstainVotes + ) + { + ProposalVote storage proposalvote = _proposalVotes[proposalId]; + return (proposalvote.againstVotes, proposalvote.forVotes, proposalvote.abstainVotes); + } + + /** + * @dev See {Governor-_quorumReached}. + */ + 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; + } + + /** + * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be scritly over the againstVotes. + */ + function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) { + ProposalVote storage proposalvote = _proposalVotes[proposalId]; + + return proposalvote.forVotes > proposalvote.againstVotes; + } + + /** + * @dev See {Governor-_countVote}. In this module, the support follows the `VoteType` enum (from Governor Bravo). + */ + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight + ) internal virtual override { + ProposalVote storage proposalvote = _proposalVotes[proposalId]; + + require(!proposalvote.hasVoted[account], "GovernorVotingSimple: vote already casted"); + proposalvote.hasVoted[account] = true; + + if (support == uint8(VoteType.Against)) { + proposalvote.againstVotes += weight; + } else if (support == uint8(VoteType.For)) { + proposalvote.forVotes += weight; + } else if (support == uint8(VoteType.Abstain)) { + proposalvote.abstainVotes += weight; + } else { + revert("GovernorVotingSimple: invalid value for enum VoteType"); + } + } +} diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol new file mode 100644 index 00000000000..84fc272656e --- /dev/null +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IGovernorTimelock.sol"; +import "../Governor.sol"; +import "../../utils/math/SafeCast.sol"; + +/** + * https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol[Compound's timelock] interface + */ +interface ICompoundTimelock { + receive() external payable; + + // solhint-disable-next-line func-name-mixedcase + function GRACE_PERIOD() external view returns (uint256); + + // solhint-disable-next-line func-name-mixedcase + function MINIMUM_DELAY() external view returns (uint256); + + // solhint-disable-next-line func-name-mixedcase + function MAXIMUM_DELAY() external view returns (uint256); + + function admin() external view returns (address); + + function pendingAdmin() external view returns (address); + + function delay() external view returns (uint256); + + function queuedTransactions(bytes32) external view returns (bool); + + function setDelay(uint256) external; + + function acceptAdmin() external; + + function setPendingAdmin(address) external; + + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) external returns (bytes32); + + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) external; + + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) external payable returns (bytes memory); +} + +/** + * @dev Extension of {Governor} that binds the execution process to a Compound Timelock. This adds a delay, enforced by + * the external timelock to all successful proposal (in addition to the voting duration). The {Governor} needs to be + * the admin of the timelock for any operation to be performed. A public, unrestricted, + * {GovernorTimelockCompound-__acceptAdmin} is available to accept ownership of the timelock. + * + * Using this model means the proposal will be operated by the {TimelockController} and not by the {Governor}. Thus, + * the assets and permissions must be attached to the {TimelockController}. Any asset sent to the {Governor} will be + * inaccessible. + * + * _Available since v4.3._ + */ +abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { + using SafeCast for uint256; + using Timers for Timers.Timestamp; + + struct ProposalTimelock { + Timers.Timestamp timer; + } + + ICompoundTimelock private _timelock; + + mapping(uint256 => ProposalTimelock) private _proposalTimelocks; + + /** + * @dev Emitted when the timelock controller used for proposal execution is modified. + */ + event TimelockChange(address oldTimelock, address newTimelock); + + /** + * @dev Set the timelock. + */ + constructor(ICompoundTimelock timelockAddress) { + _updateTimelock(timelockAddress); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, Governor) returns (bool) { + return interfaceId == type(IGovernorTimelock).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev Overriden version of the {Governor-state} function with added support for the `Queued` and `Expired` status. + */ + function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) { + ProposalState status = super.state(proposalId); + + if (status != ProposalState.Succeeded) { + return status; + } + + uint256 eta = proposalEta(proposalId); + if (eta == 0) { + return status; + } else if (block.timestamp >= eta + _timelock.GRACE_PERIOD()) { + return ProposalState.Expired; + } else { + return ProposalState.Queued; + } + } + + /** + * @dev Public accessor to check the address of the timelock + */ + function timelock() public view virtual override returns (address) { + return address(_timelock); + } + + /** + * @dev Public accessor to check the eta of a queued proposal + */ + function proposalEta(uint256 proposalId) public view virtual override returns (uint256) { + return _proposalTimelocks[proposalId].timer.getDeadline(); + } + + /** + * @dev Function to queue a proposal to the timelock. + */ + function queue( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public virtual override returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + + require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful"); + + uint256 eta = block.timestamp + _timelock.delay(); + _proposalTimelocks[proposalId].timer.setDeadline(eta.toUint64()); + for (uint256 i = 0; i < targets.length; ++i) { + require( + !_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta))), + "GovernorTimelockCompound: identical proposal action already queued" + ); + _timelock.queueTransaction(targets[i], values[i], "", calldatas[i], eta); + } + + emit ProposalQueued(proposalId, eta); + + return proposalId; + } + + /** + * @dev Overriden execute function that run the already queued proposal through the timelock. + */ + function _execute( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 /*descriptionHash*/ + ) internal virtual override { + uint256 eta = proposalEta(proposalId); + require(eta > 0, "GovernorTimelockCompound: proposal not yet queued"); + for (uint256 i = 0; i < targets.length; ++i) { + _timelock.executeTransaction{value: values[i]}(targets[i], values[i], "", calldatas[i], eta); + } + } + + /** + * @dev Overriden version of the {Governor-_cancel} function to cancel the timelocked proposal if it as already + * been queued. + */ + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual override returns (uint256) { + uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash); + + uint256 eta = proposalEta(proposalId); + if (eta > 0) { + for (uint256 i = 0; i < targets.length; ++i) { + _timelock.cancelTransaction(targets[i], values[i], "", calldatas[i], eta); + } + _proposalTimelocks[proposalId].timer.reset(); + } + + return proposalId; + } + + /** + * @dev Address through which the governor executes action. In this case, the timelock. + */ + function _executor() internal view virtual override returns (address) { + return address(_timelock); + } + + /** + * @dev Accept admin right over the timelock. + */ + // solhint-disable-next-line private-vars-leading-underscore + function __acceptAdmin() public { + _timelock.acceptAdmin(); + } + + /** + * @dev Public endpoint to update the underlying timelock instance. Restricted to the timelock itself, so updates + * must be proposed, scheduled and executed using the {Governor} workflow. + * + * For security reason, the timelock must be handed over to another admin before setting up a new one. The two + * operations (hand over the timelock) and do the update can be batched in a single proposal. + * + * Note that if the timelock admin has been handed over in a previous operation, we refuse updates made through the + * timelock if admin of the timelock has already been accepted and the operation is executed outside the scope of + * governance. + */ + function updateTimelock(ICompoundTimelock newTimelock) external virtual onlyGovernance { + _updateTimelock(newTimelock); + } + + function _updateTimelock(ICompoundTimelock newTimelock) private { + emit TimelockChange(address(_timelock), address(newTimelock)); + _timelock = newTimelock; + } +} diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol new file mode 100644 index 00000000000..9472672376d --- /dev/null +++ b/contracts/governance/extensions/GovernorTimelockControl.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IGovernorTimelock.sol"; +import "../Governor.sol"; +import "../TimelockController.sol"; + +/** + * @dev Extension of {Governor} that binds the execution process to an instance of {TimelockController}. This adds a + * delay, enforced by the {TimelockController} to all successful proposal (in addition to the voting duration). The + * {Governor} needs the proposer (an ideally the executor) roles for the {Governor} to work properly. + * + * Using this model means the proposal will be operated by the {TimelockController} and not by the {Governor}. Thus, + * the assets and permissions must be attached to the {TimelockController}. Any asset sent to the {Governor} will be + * inaccessible. + * + * _Available since v4.3._ + */ +abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { + TimelockController private _timelock; + mapping(uint256 => bytes32) private _timelockIds; + + /** + * @dev Emitted when the timelock controller used for proposal execution is modified. + */ + event TimelockChange(address oldTimelock, address newTimelock); + + /** + * @dev Set the timelock. + */ + constructor(TimelockController timelockAddress) { + _updateTimelock(timelockAddress); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, Governor) returns (bool) { + return interfaceId == type(IGovernorTimelock).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev Overriden version of the {Governor-state} function with added support for the `Queued` status. + */ + function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) { + ProposalState status = super.state(proposalId); + + if (status != ProposalState.Succeeded) { + return status; + } + + // core tracks execution, so we just have to check if successful proposal have been queued. + bytes32 queueid = _timelockIds[proposalId]; + if (queueid == bytes32(0)) { + return status; + } else if (_timelock.isOperationDone(queueid)) { + return ProposalState.Executed; + } else { + return ProposalState.Queued; + } + } + + /** + * @dev Public accessor to check the address of the timelock + */ + function timelock() public view virtual override returns (address) { + return address(_timelock); + } + + /** + * @dev Public accessor to check the eta of a queued proposal + */ + function proposalEta(uint256 proposalId) public view virtual override returns (uint256) { + uint256 eta = _timelock.getTimestamp(_timelockIds[proposalId]); + return eta == 1 ? 0 : eta; // _DONE_TIMESTAMP (1) should be replaced with a 0 value + } + + /** + * @dev Function to queue a proposal to the timelock. + */ + function queue( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public virtual override returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + + require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful"); + + uint256 delay = _timelock.getMinDelay(); + _timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, descriptionHash); + _timelock.scheduleBatch(targets, values, calldatas, 0, descriptionHash, delay); + + emit ProposalQueued(proposalId, block.timestamp + delay); + + return proposalId; + } + + /** + * @dev Overriden execute function that run the already queued proposal through the timelock. + */ + function _execute( + uint256, /* proposalId */ + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual override { + _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash); + } + + /** + * @dev Overriden version of the {Governor-_cancel} function to cancel the timelocked proposal if it as already + * been queued. + */ + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual override returns (uint256) { + uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash); + + if (_timelockIds[proposalId] != 0) { + _timelock.cancel(_timelockIds[proposalId]); + delete _timelockIds[proposalId]; + } + + return proposalId; + } + + /** + * @dev Address through which the governor executes action. In this case, the timelock. + */ + function _executor() internal view virtual override returns (address) { + return address(_timelock); + } + + /** + * @dev Public endpoint to update the underlying timelock instance. Restricted to the timelock itself, so updates + * must be proposed, scheduled and executed using the {Governor} workflow. + */ + function updateTimelock(TimelockController newTimelock) external virtual onlyGovernance { + _updateTimelock(newTimelock); + } + + function _updateTimelock(TimelockController newTimelock) private { + emit TimelockChange(address(_timelock), address(newTimelock)); + _timelock = newTimelock; + } +} diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol new file mode 100644 index 00000000000..aa9884c18c6 --- /dev/null +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../Governor.sol"; +import "../../token/ERC20/extensions/ERC20Votes.sol"; +import "../../utils/math/Math.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token. + * + * _Available since v4.3._ + */ +abstract contract GovernorVotes is Governor { + ERC20Votes public immutable token; + + constructor(ERC20Votes tokenAddress) { + token = tokenAddress; + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return token.getPastVotes(account, blockNumber); + } +} diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol new file mode 100644 index 00000000000..3fc42dd8f96 --- /dev/null +++ b/contracts/governance/extensions/GovernorVotesComp.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../Governor.sol"; +import "../../token/ERC20/extensions/ERC20VotesComp.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from a Comp token. + * + * _Available since v4.3._ + */ +abstract contract GovernorVotesComp is Governor { + ERC20VotesComp public immutable token; + + constructor(ERC20VotesComp token_) { + token = token_; + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return token.getPriorVotes(account, blockNumber); + } +} diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol new file mode 100644 index 00000000000..8001150b611 --- /dev/null +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./GovernorVotes.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token and a quorum expressed as a + * fraction of the total supply. + * + * _Available since v4.3._ + */ +abstract contract GovernorVotesQuorumFraction is GovernorVotes { + uint256 private _quorumNumerator; + + event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator); + + constructor(uint256 quorumNumeratorValue) { + _updateQuorumNumerator(quorumNumeratorValue); + } + + function quorumNumerator() public view virtual returns (uint256) { + return _quorumNumerator; + } + + function quorumDenominator() public view virtual returns (uint256) { + return 100; + } + + function quorum(uint256 blockNumber) public view virtual override returns (uint256) { + return (token.getPastTotalSupply(blockNumber) * quorumNumerator()) / quorumDenominator(); + } + + function updateQuorumNumerator(uint256 newQuorumNumerator) external virtual onlyGovernance { + _updateQuorumNumerator(newQuorumNumerator); + } + + function _updateQuorumNumerator(uint256 newQuorumNumerator) internal virtual { + require( + newQuorumNumerator <= quorumDenominator(), + "GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator" + ); + + uint256 oldQuorumNumerator = _quorumNumerator; + _quorumNumerator = newQuorumNumerator; + + emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator); + } +} diff --git a/contracts/governance/extensions/IGovernorTimelock.sol b/contracts/governance/extensions/IGovernorTimelock.sol new file mode 100644 index 00000000000..5e8f384de0f --- /dev/null +++ b/contracts/governance/extensions/IGovernorTimelock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../IGovernor.sol"; + +/** + * @dev Extension of the {IGovernor} for timelock supporting modules. + * + * _Available since v4.3._ + */ +interface IGovernorTimelock is IGovernor { + event ProposalQueued(uint256 proposalId, uint256 eta); + + function timelock() external view returns (address); + + function proposalEta(uint256 proposalId) external view returns (uint256); + + function queue( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata calldatas, + bytes32 descriptionHash + ) external returns (uint256 proposalId); +} diff --git a/contracts/mocks/GovernorCompMock.sol b/contracts/mocks/GovernorCompMock.sol new file mode 100644 index 00000000000..400932bf7f3 --- /dev/null +++ b/contracts/mocks/GovernorCompMock.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/Governor.sol"; +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesComp.sol"; + +contract GovernorCompMock is Governor, GovernorVotesComp, GovernorCountingSimple { + uint256 immutable _votingDelay; + uint256 immutable _votingPeriod; + + constructor( + string memory name_, + ERC20VotesComp token_, + uint256 votingDelay_, + uint256 votingPeriod_ + ) Governor(name_) GovernorVotesComp(token_) { + _votingDelay = votingDelay_; + _votingPeriod = votingPeriod_; + } + + receive() external payable {} + + function votingDelay() public view override returns (uint256) { + return _votingDelay; + } + + function votingPeriod() public view override returns (uint256) { + return _votingPeriod; + } + + function quorum(uint256) public pure override returns (uint256) { + return 0; + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(Governor, GovernorVotesComp) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } +} diff --git a/contracts/mocks/GovernorCompatibilityBravoMock.sol b/contracts/mocks/GovernorCompatibilityBravoMock.sol new file mode 100644 index 00000000000..c46e0508dec --- /dev/null +++ b/contracts/mocks/GovernorCompatibilityBravoMock.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/compatibility/GovernorCompatibilityBravo.sol"; +import "../governance/extensions/GovernorVotesComp.sol"; +import "../governance/extensions/GovernorTimelockCompound.sol"; + +contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorTimelockCompound, GovernorVotesComp { + uint256 immutable _votingDelay; + uint256 immutable _votingPeriod; + uint256 immutable _proposalThreshold; + + constructor( + string memory name_, + ERC20VotesComp token_, + uint256 votingDelay_, + uint256 votingPeriod_, + uint256 proposalThreshold_, + ICompoundTimelock timelock_ + ) Governor(name_) GovernorVotesComp(token_) GovernorTimelockCompound(timelock_) { + _votingDelay = votingDelay_; + _votingPeriod = votingPeriod_; + _proposalThreshold = proposalThreshold_; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, Governor, GovernorTimelockCompound) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function votingDelay() public view override(IGovernor, Governor) returns (uint256) { + return _votingDelay; + } + + function votingPeriod() public view override(IGovernor, Governor) returns (uint256) { + return _votingPeriod; + } + + function proposalThreshold() public view virtual override returns (uint256) { + return _proposalThreshold; + } + + function quorum(uint256) public pure override(IGovernor, Governor) returns (uint256) { + return 0; + } + + function state(uint256 proposalId) + public + view + virtual + override(IGovernor, Governor, GovernorTimelockCompound) + returns (ProposalState) + { + return super.state(proposalId); + } + + function proposalEta(uint256 proposalId) + public + view + virtual + override(GovernorCompatibilityBravo, GovernorTimelockCompound) + returns (uint256) + { + return super.proposalEta(proposalId); + } + + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) public virtual override(IGovernor, Governor, GovernorCompatibilityBravo) returns (uint256) { + return super.propose(targets, values, calldatas, description); + } + + function queue( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public virtual override(GovernorCompatibilityBravo, GovernorTimelockCompound) returns (uint256) { + return super.queue(targets, values, calldatas, salt); + } + + function execute( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public payable virtual override(IGovernor, Governor) returns (uint256) { + return super.execute(targets, values, calldatas, salt); + } + + function _execute( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual override(Governor, GovernorTimelockCompound) { + super._execute(proposalId, targets, values, calldatas, descriptionHash); + } + + /** + * @notice WARNING: this is for mock purposes only. Ability to the _cancel function should be restricted for live + * deployments. + */ + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) internal virtual override(Governor, GovernorTimelockCompound) returns (uint256 proposalId) { + return super._cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(IGovernor, GovernorVotesComp) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } + + function _executor() internal view virtual override(Governor, GovernorTimelockCompound) returns (address) { + return super._executor(); + } +} diff --git a/contracts/mocks/GovernorMock.sol b/contracts/mocks/GovernorMock.sol new file mode 100644 index 00000000000..31f9dc24217 --- /dev/null +++ b/contracts/mocks/GovernorMock.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/Governor.sol"; +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesQuorumFraction.sol"; + +contract GovernorMock is Governor, GovernorVotesQuorumFraction, GovernorCountingSimple { + uint256 immutable _votingDelay; + uint256 immutable _votingPeriod; + + constructor( + string memory name_, + ERC20Votes token_, + uint256 votingDelay_, + uint256 votingPeriod_, + uint256 quorumNumerator_ + ) Governor(name_) GovernorVotes(token_) GovernorVotesQuorumFraction(quorumNumerator_) { + _votingDelay = votingDelay_; + _votingPeriod = votingPeriod_; + } + + receive() external payable {} + + function votingDelay() public view override returns (uint256) { + return _votingDelay; + } + + function votingPeriod() public view override returns (uint256) { + return _votingPeriod; + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(Governor, GovernorVotes) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } +} diff --git a/contracts/mocks/GovernorTimelockCompoundMock.sol b/contracts/mocks/GovernorTimelockCompoundMock.sol new file mode 100644 index 00000000000..211641a4a0e --- /dev/null +++ b/contracts/mocks/GovernorTimelockCompoundMock.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/extensions/GovernorTimelockCompound.sol"; +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesQuorumFraction.sol"; + +contract GovernorTimelockCompoundMock is GovernorTimelockCompound, GovernorVotesQuorumFraction, GovernorCountingSimple { + uint256 immutable _votingDelay; + uint256 immutable _votingPeriod; + + constructor( + string memory name_, + ERC20Votes token_, + uint256 votingDelay_, + uint256 votingPeriod_, + ICompoundTimelock timelock_, + uint256 quorumNumerator_ + ) + Governor(name_) + GovernorTimelockCompound(timelock_) + GovernorVotes(token_) + GovernorVotesQuorumFraction(quorumNumerator_) + { + _votingDelay = votingDelay_; + _votingPeriod = votingPeriod_; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(Governor, GovernorTimelockCompound) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function votingDelay() public view override(IGovernor, Governor) returns (uint256) { + return _votingDelay; + } + + function votingPeriod() public view override(IGovernor, Governor) returns (uint256) { + return _votingPeriod; + } + + function quorum(uint256 blockNumber) + public + view + override(IGovernor, Governor, GovernorVotesQuorumFraction) + returns (uint256) + { + return super.quorum(blockNumber); + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + /** + * Overriding nightmare + */ + function state(uint256 proposalId) + public + view + virtual + override(Governor, GovernorTimelockCompound) + returns (ProposalState) + { + return super.state(proposalId); + } + + function _execute( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual override(Governor, GovernorTimelockCompound) { + super._execute(proposalId, targets, values, calldatas, descriptionHash); + } + + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) internal virtual override(Governor, GovernorTimelockCompound) returns (uint256 proposalId) { + return super._cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(IGovernor, Governor, GovernorVotes) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } + + function _executor() internal view virtual override(Governor, GovernorTimelockCompound) returns (address) { + return super._executor(); + } +} diff --git a/contracts/mocks/GovernorTimelockControlMock.sol b/contracts/mocks/GovernorTimelockControlMock.sol new file mode 100644 index 00000000000..b3ceb86312e --- /dev/null +++ b/contracts/mocks/GovernorTimelockControlMock.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/extensions/GovernorTimelockControl.sol"; +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesQuorumFraction.sol"; + +contract GovernorTimelockControlMock is GovernorTimelockControl, GovernorVotesQuorumFraction, GovernorCountingSimple { + uint256 immutable _votingDelay; + uint256 immutable _votingPeriod; + + constructor( + string memory name_, + ERC20Votes token_, + uint256 votingDelay_, + uint256 votingPeriod_, + TimelockController timelock_, + uint256 quorumNumerator_ + ) + Governor(name_) + GovernorTimelockControl(timelock_) + GovernorVotes(token_) + GovernorVotesQuorumFraction(quorumNumerator_) + { + _votingDelay = votingDelay_; + _votingPeriod = votingPeriod_; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(Governor, GovernorTimelockControl) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function votingDelay() public view override(IGovernor, Governor) returns (uint256) { + return _votingDelay; + } + + function votingPeriod() public view override(IGovernor, Governor) returns (uint256) { + return _votingPeriod; + } + + function quorum(uint256 blockNumber) + public + view + override(IGovernor, Governor, GovernorVotesQuorumFraction) + returns (uint256) + { + return super.quorum(blockNumber); + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, descriptionHash); + } + + /** + * Overriding nightmare + */ + function state(uint256 proposalId) + public + view + virtual + override(Governor, GovernorTimelockControl) + returns (ProposalState) + { + return super.state(proposalId); + } + + function _execute( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual override(Governor, GovernorTimelockControl) { + super._execute(proposalId, targets, values, calldatas, descriptionHash); + } + + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual override(Governor, GovernorTimelockControl) returns (uint256 proposalId) { + return super._cancel(targets, values, calldatas, descriptionHash); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(IGovernor, Governor, GovernorVotes) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } + + function _executor() internal view virtual override(Governor, GovernorTimelockControl) returns (address) { + return super._executor(); + } +} diff --git a/contracts/mocks/TimersBlockNumberImpl.sol b/contracts/mocks/TimersBlockNumberImpl.sol new file mode 100644 index 00000000000..84633e6f830 --- /dev/null +++ b/contracts/mocks/TimersBlockNumberImpl.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Timers.sol"; + +contract TimersBlockNumberImpl { + using Timers for Timers.BlockNumber; + + Timers.BlockNumber private _timer; + + function getDeadline() public view returns (uint64) { + return _timer.getDeadline(); + } + + function setDeadline(uint64 timestamp) public { + _timer.setDeadline(timestamp); + } + + function reset() public { + _timer.reset(); + } + + function isUnset() public view returns (bool) { + return _timer.isUnset(); + } + + function isStarted() public view returns (bool) { + return _timer.isStarted(); + } + + function isPending() public view returns (bool) { + return _timer.isPending(); + } + + function isExpired() public view returns (bool) { + return _timer.isExpired(); + } +} diff --git a/contracts/mocks/TimersTimestampImpl.sol b/contracts/mocks/TimersTimestampImpl.sol new file mode 100644 index 00000000000..07f9a1b3f20 --- /dev/null +++ b/contracts/mocks/TimersTimestampImpl.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Timers.sol"; + +contract TimersTimestampImpl { + using Timers for Timers.Timestamp; + + Timers.Timestamp private _timer; + + function getDeadline() public view returns (uint64) { + return _timer.getDeadline(); + } + + function setDeadline(uint64 timestamp) public { + _timer.setDeadline(timestamp); + } + + function reset() public { + _timer.reset(); + } + + function isUnset() public view returns (bool) { + return _timer.isUnset(); + } + + function isStarted() public view returns (bool) { + return _timer.isStarted(); + } + + function isPending() public view returns (bool) { + return _timer.isPending(); + } + + function isExpired() public view returns (bool) { + return _timer.isExpired(); + } +} diff --git a/contracts/mocks/compound/CompTimelock.sol b/contracts/mocks/compound/CompTimelock.sol new file mode 100644 index 00000000000..49ffa4b77de --- /dev/null +++ b/contracts/mocks/compound/CompTimelock.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: BSD-3-Clause +// solhint-disable private-vars-leading-underscore +/** + * Copyright 2020 Compound Labs, Inc. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following + * disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +pragma solidity ^0.8.0; + +contract CompTimelock { + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint256 indexed newDelay); + event CancelTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event ExecuteTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event QueueTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + + uint256 public constant GRACE_PERIOD = 14 days; + uint256 public constant MINIMUM_DELAY = 2 days; + uint256 public constant MAXIMUM_DELAY = 30 days; + + address public admin; + address public pendingAdmin; + uint256 public delay; + + mapping(bytes32 => bool) public queuedTransactions; + + constructor(address admin_, uint256 delay_) { + require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + + admin = admin_; + delay = delay_; + } + + receive() external payable {} + + function setDelay(uint256 delay_) public { + require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); + require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + delay = delay_; + + emit NewDelay(delay); + } + + function acceptAdmin() public { + require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); + admin = msg.sender; + pendingAdmin = address(0); + + emit NewAdmin(admin); + } + + function setPendingAdmin(address pendingAdmin_) public { + require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); + pendingAdmin = pendingAdmin_; + + emit NewPendingAdmin(pendingAdmin); + } + + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public returns (bytes32) { + require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); + require( + eta >= getBlockTimestamp() + delay, + "Timelock::queueTransaction: Estimated execution block must satisfy delay." + ); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public { + require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public payable returns (bytes memory) { + require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); + require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); + require(getBlockTimestamp() <= eta + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale."); + + queuedTransactions[txHash] = false; + + bytes memory callData; + + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + // solium-disable-next-line security/no-call-value + (bool success, bytes memory returnData) = target.call{value: value}(callData); + require(success, "Timelock::executeTransaction: Transaction execution reverted."); + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + + return returnData; + } + + function getBlockTimestamp() internal view returns (uint256) { + // solium-disable-next-line security/no-block-members + return block.timestamp; + } +} diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index f77f4eafae6..e67a1f3597d 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -53,7 +53,7 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { * supported. * @param amount The amount of tokens to be loaned. * @param data An arbitrary datafield that is passed to the receiver. - * @return `true` is the flash loan was successfull. + * @return `true` is the flash loan was successful. */ function flashLoan( IERC3156FlashBorrower receiver, diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index 6b25ba2560f..0bb4a55e229 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -129,7 +129,7 @@ library Address { require(isContract(target), "Address: call to non-contract"); (bool success, bytes memory returndata) = target.call{value: value}(data); - return _verifyCallResult(success, returndata, errorMessage); + return verifyCallResult(success, returndata, errorMessage); } /** @@ -156,7 +156,7 @@ library Address { require(isContract(target), "Address: static call to non-contract"); (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); + return verifyCallResult(success, returndata, errorMessage); } /** @@ -183,14 +183,20 @@ library Address { require(isContract(target), "Address: delegate call to non-contract"); (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); + return verifyCallResult(success, returndata, errorMessage); } - function _verifyCallResult( + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( bool success, bytes memory returndata, string memory errorMessage - ) private pure returns (bytes memory) { + ) internal pure returns (bytes memory) { if (success) { return returndata; } else { diff --git a/contracts/utils/Timers.sol b/contracts/utils/Timers.sol new file mode 100644 index 00000000000..c69266699f0 --- /dev/null +++ b/contracts/utils/Timers.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Tooling for timepoints, timers and delays + */ +library Timers { + struct Timestamp { + uint64 _deadline; + } + + function getDeadline(Timestamp memory timer) internal pure returns (uint64) { + return timer._deadline; + } + + function setDeadline(Timestamp storage timer, uint64 timestamp) internal { + timer._deadline = timestamp; + } + + function reset(Timestamp storage timer) internal { + timer._deadline = 0; + } + + function isUnset(Timestamp memory timer) internal pure returns (bool) { + return timer._deadline == 0; + } + + function isStarted(Timestamp memory timer) internal pure returns (bool) { + return timer._deadline > 0; + } + + function isPending(Timestamp memory timer) internal view returns (bool) { + return timer._deadline > block.timestamp; + } + + function isExpired(Timestamp memory timer) internal view returns (bool) { + return isStarted(timer) && timer._deadline <= block.timestamp; + } + + struct BlockNumber { + uint64 _deadline; + } + + function getDeadline(BlockNumber memory timer) internal pure returns (uint64) { + return timer._deadline; + } + + function setDeadline(BlockNumber storage timer, uint64 timestamp) internal { + timer._deadline = timestamp; + } + + function reset(BlockNumber storage timer) internal { + timer._deadline = 0; + } + + function isUnset(BlockNumber memory timer) internal pure returns (bool) { + return timer._deadline == 0; + } + + function isStarted(BlockNumber memory timer) internal pure returns (bool) { + return timer._deadline > 0; + } + + function isPending(BlockNumber memory timer) internal view returns (bool) { + return timer._deadline > block.number; + } + + function isExpired(BlockNumber memory timer) internal view returns (bool) { + return isStarted(timer) && timer._deadline <= block.number; + } +} diff --git a/hardhat.config.js b/hardhat.config.js index eeead6d9544..30a9159d6ed 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -23,6 +23,8 @@ for (const f of fs.readdirSync(path.join(__dirname, 'hardhat'))) { require(path.join(__dirname, 'hardhat', f)); } +const withOptimizations = argv.enableGasReport || argv.compileMode === 'production'; + /** * @type import('hardhat/config').HardhatUserConfig */ @@ -31,7 +33,7 @@ module.exports = { version: '0.8.3', settings: { optimizer: { - enabled: argv.enableGasReport || argv.compileMode === 'production', + enabled: withOptimizations, runs: 200, }, }, @@ -39,6 +41,7 @@ module.exports = { networks: { hardhat: { blockGasLimit: 10000000, + allowUnlimitedContractSize: !withOptimizations, }, }, gasReporter: { diff --git a/package-lock.json b/package-lock.json index eba4ca4f418..ff90c0196c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "merkletreejs": "^0.2.13", "micromatch": "^4.0.2", "prettier": "^2.3.0", - "prettier-plugin-solidity": "^1.0.0-beta.13", + "prettier-plugin-solidity": "^1.0.0-beta.16", "rimraf": "^3.0.2", "solhint": "^3.3.6", "solidity-ast": "^0.4.25", @@ -223,28 +223,28 @@ "dev": true }, "node_modules/@ethereumjs/block": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/block/-/block-3.4.0.tgz", - "integrity": "sha512-umKAoTX32yXzErpIksPHodFc/5y8bmZMnOl6hWy5Vd8xId4+HKFUOyEiN16Y97zMwFRysRpcrR6wBejfqc6Bmg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/block/-/block-3.3.0.tgz", + "integrity": "sha512-WoefY9Rs4W8vZTxG9qwntAlV61xsSv0NPoXmHO7x3SH16dwJQtU15YvahPCz4HEEXbu7GgGgNgu0pv8JY7VauA==", "dev": true, "dependencies": { - "@ethereumjs/common": "^2.4.0", - "@ethereumjs/tx": "^3.3.0", - "ethereumjs-util": "^7.1.0", + "@ethereumjs/common": "^2.3.0", + "@ethereumjs/tx": "^3.2.0", + "ethereumjs-util": "^7.0.10", "merkle-patricia-tree": "^4.2.0" } }, "node_modules/@ethereumjs/blockchain": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/blockchain/-/blockchain-5.4.0.tgz", - "integrity": "sha512-wAuKLaew6PL52kH8YPXO7PbjjKV12jivRSyHQehkESw4slSLLfYA6Jv7n5YxyT2ajD7KNMPVh7oyF/MU6HcOvg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/blockchain/-/blockchain-5.3.1.tgz", + "integrity": "sha512-Sr39BoTOzmVSnuYzjiCIpgcBUFE5JWcMF0lYCvzrtx/5Lg1tnpZhw9yMQ6JfIomN421epg4oDz99DWlL9Aqz3g==", "dev": true, "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/common": "^2.4.0", + "@ethereumjs/block": "^3.3.0", + "@ethereumjs/common": "^2.3.1", "@ethereumjs/ethash": "^1.0.0", "debug": "^2.2.0", - "ethereumjs-util": "^7.1.0", + "ethereumjs-util": "^7.0.10", "level-mem": "^5.0.1", "lru-cache": "^5.1.1", "rlp": "^2.2.4", @@ -276,13 +276,13 @@ "dev": true }, "node_modules/@ethereumjs/common": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.4.0.tgz", - "integrity": "sha512-UdkhFWzWcJCZVsj1O/H8/oqj/0RVYjLc1OhPjBrQdALAkQHpCp8xXI4WLnuGTADqTdJZww0NtgwG+TRPkXt27w==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.3.1.tgz", + "integrity": "sha512-V8hrULExoq0H4HFs3cCmdRGbgmipmlNzak6Xg34nHYfQyqkSdrCuflvYjyWmsNpI8GtrcZhzifAbgX/1C1Cjwg==", "dev": true, "dependencies": { "crc-32": "^1.2.0", - "ethereumjs-util": "^7.1.0" + "ethereumjs-util": "^7.0.10" } }, "node_modules/@ethereumjs/ethash": { @@ -307,29 +307,29 @@ } }, "node_modules/@ethereumjs/tx": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.3.0.tgz", - "integrity": "sha512-yTwEj2lVzSMgE6Hjw9Oa1DZks/nKTWM8Wn4ykDNapBPua2f4nXO3qKnni86O6lgDj5fVNRqbDsD0yy7/XNGDEA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.2.1.tgz", + "integrity": "sha512-i9V39OtKvwWos1uVNZxdVhd7zFOyzFLjgt69CoiOY0EmXugS0HjO3uxpLBSglDKFMRriuGqw6ddKEv+RP1UNEw==", "dev": true, "dependencies": { - "@ethereumjs/common": "^2.4.0", - "ethereumjs-util": "^7.1.0" + "@ethereumjs/common": "^2.3.1", + "ethereumjs-util": "^7.0.10" } }, "node_modules/@ethereumjs/vm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/vm/-/vm-5.5.0.tgz", - "integrity": "sha512-h6Kr6WqKUP8nVuEzCWPWEPrC63v7HFwt3gRuK7CJiyg9S0iWSBKUA/YVD4YgaSVACuxUfWaOBbwV5uGVupm5PQ==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/vm/-/vm-5.4.1.tgz", + "integrity": "sha512-cpQcg5CtjzXJBn8QNiobaiWckeN/ZQwsDHLYa9df2wBEUvzuEZgFWK48YEXSpx3CnUY9fNT/lgA9CzKdq8HTzQ==", "dev": true, "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/blockchain": "^5.4.0", - "@ethereumjs/common": "^2.4.0", - "@ethereumjs/tx": "^3.3.0", + "@ethereumjs/block": "^3.3.0", + "@ethereumjs/blockchain": "^5.3.0", + "@ethereumjs/common": "^2.3.1", + "@ethereumjs/tx": "^3.2.1", "async-eventemitter": "^0.2.4", "core-js-pure": "^3.0.1", "debug": "^2.2.0", - "ethereumjs-util": "^7.1.0", + "ethereumjs-util": "^7.0.10", "functional-red-black-tree": "^1.0.1", "mcl-wasm": "^0.7.1", "merkle-patricia-tree": "^4.2.0", @@ -1123,9 +1123,9 @@ } }, "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", + "integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==", "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -1317,9 +1317,9 @@ } }, "node_modules/@oclif/errors": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.5.tgz", - "integrity": "sha512-OivucXPH/eLLlOT7FkCMoZXiaVYf8I/w1eTAM1+gKzfhALwWTusxEx7wBmW0uzvkSg/9ovWLycPaBgJbM3LOCQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.4.tgz", + "integrity": "sha512-pJKXyEqwdfRTUdM8n5FIHiQQHg5ETM0Wlso8bF9GodczO40mF5Z3HufnYWJE7z8sGKxOeJCdbAVZbS8Y+d5GCw==", "dev": true, "dependencies": { "clean-stack": "^3.0.0", @@ -1798,9 +1798,9 @@ } }, "node_modules/@openzeppelin/test-helpers": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@openzeppelin/test-helpers/-/test-helpers-0.5.12.tgz", - "integrity": "sha512-ZPhLmMb8PLGImYLen7YsPnni22i1bXHzrSiY7XZ7cgwuKvk4MRBunzfZ4xGTn/p+1V2/a1XHsjMRDKn7AMVb3Q==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@openzeppelin/test-helpers/-/test-helpers-0.5.11.tgz", + "integrity": "sha512-HkFpCjtTD8dk+wdYhsT07YbMGCE+Z4Wp5sBKXvPDF3Lynoc0H2KqZgCWV+qr2YZ0WW1oX/sXkKFrrKJ0caBTjw==", "dev": true, "dependencies": { "@openzeppelin/contract-loader": "^0.6.2", @@ -2028,15 +2028,15 @@ } }, "node_modules/@truffle/contract": { - "version": "4.3.24", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.3.24.tgz", - "integrity": "sha512-cKijyDzJLRu96KWb/g7soHJkBXgY1HkmCeVhUun9iUJpA93w7StInCP+SPs0gd8Oj7GfcoYcCcnA38l/zJBEsQ==", + "version": "4.3.23", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.3.23.tgz", + "integrity": "sha512-YqoFmy6bFMcFnbC1OXNBF1MrhNGYf1JEvY65EsbNe3cjkgbFfk0wVwuDkSYqXBIyWAOQBkPL+GTq4ouvnjVlnw==", "dev": true, "dependencies": { "@ensdomains/ensjs": "^2.0.1", "@truffle/blockchain-utils": "^0.0.31", "@truffle/contract-schema": "^3.4.1", - "@truffle/debug-utils": "^5.1.4", + "@truffle/debug-utils": "^5.1.3", "@truffle/error": "^0.0.14", "@truffle/interface-adapter": "^0.5.2", "bignumber.js": "^7.2.1", @@ -2066,9 +2066,9 @@ "dev": true }, "node_modules/@truffle/contract/node_modules/@truffle/codec": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.11.4.tgz", - "integrity": "sha512-NK/ah1aWQFhYYv8kVL382wB+JgSOnWWDdoeGOOMnA3iviRnV7k0JJV+C6uTmaLKOTNGTBNp4hlISLFeKKNnbXA==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.11.3.tgz", + "integrity": "sha512-LBunwP2ZQ0CygcbuLGsQS8Zm4+2kpVKBkvA/DOiCI4IaNxMie3LMFCBFEVrHUey2a07J4wn7WivrRY3dUdS9zQ==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -2085,12 +2085,12 @@ } }, "node_modules/@truffle/contract/node_modules/@truffle/debug-utils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-5.1.4.tgz", - "integrity": "sha512-0LdtJzhOuAgEIP4xj3QsB7CKIzoAgRERKx5hYktId9pfm//9leo3luwN0aY8f919rWJynmnaXvE0lQZD3Pcu0Q==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-5.1.3.tgz", + "integrity": "sha512-++IPNmxBH/uwaVqZ+hokEDavN9r0TayU2o0/geHoBCwcC0eW5xr9zbciFxsgKQCCuBESi23eOflNuQUK1Awa5w==", "dev": true, "dependencies": { - "@truffle/codec": "^0.11.4", + "@truffle/codec": "^0.11.3", "@trufflesuite/chromafi": "^2.2.2", "bn.js": "^5.1.3", "chalk": "^2.4.2", @@ -2441,9 +2441,9 @@ } }, "node_modules/@types/abstract-leveldown": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.2.tgz", - "integrity": "sha512-+jA1XXF3jsz+Z7FcuiNqgK53hTa/luglT2TyTpKPqoYbxVY+mCPF22Rm+q3KPBrMHJwNXFrTViHszBOfU4vftQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz", + "integrity": "sha512-wYxU3kp5zItbxKmeRYCEplS2MW7DzyBnxPGj+GJVHZEUZiK/nn5Ei1sUFgURDh+X051+zsGe28iud3oHjrYWQQ==", "dev": true }, "node_modules/@types/bignumber.js": { @@ -2466,15 +2466,15 @@ } }, "node_modules/@types/chai": { - "version": "4.2.21", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.21.tgz", - "integrity": "sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.19.tgz", + "integrity": "sha512-jRJgpRBuY+7izT7/WNXP/LsMO9YonsstuL+xuvycDyESpoDoIAsMd7suwpB4h9oEWB+ZlPTqJJ8EHomzNhwTPQ==", "dev": true }, "node_modules/@types/concat-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", - "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", "dev": true, "dependencies": { "@types/node": "*" @@ -2490,15 +2490,21 @@ } }, "node_modules/@types/glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "dependencies": { "@types/minimatch": "*", "@types/node": "*" } }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "node_modules/@types/level-errors": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/level-errors/-/level-errors-3.0.0.tgz", @@ -2506,9 +2512,9 @@ "dev": true }, "node_modules/@types/levelup": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/levelup/-/levelup-4.3.3.tgz", - "integrity": "sha512-K+OTIjJcZHVlZQN1HmU64VtrC0jC3dXWQozuEIR9zVvltIk90zaGPM2AgT+fIkChpzHhFE3YnvFLCbLtzAmexA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/levelup/-/levelup-4.3.2.tgz", + "integrity": "sha512-5Su1Dkl6nMjkXqUb2z72gbroG0WFLs+6nMH+wQt4GWIrDwR/IconLTojHtC0klLJODCJ64Cr6P5cWqVeuxAbSg==", "dev": true, "dependencies": { "@types/abstract-leveldown": "*", @@ -2517,21 +2523,21 @@ } }, "node_modules/@types/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz", + "integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==", "dev": true }, "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==", "dev": true }, "node_modules/@types/node": { - "version": "12.20.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.16.tgz", - "integrity": "sha512-6CLxw83vQf6DKqXxMPwl8qpF8I7THFZuIwLt4TnNsumxkp1VsRZWT8txQxncT/Rl2UojTsFzWgDG4FRMwafrlA==", + "version": "12.20.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.15.tgz", + "integrity": "sha512-F6S4Chv4JicJmyrwlDkxUdGNSplsQdGwp1A0AJloEVDirWdZOAiRHhovDlsFkKUrquUXhz1imJhXHsf59auyAg==", "dev": true }, "node_modules/@types/pbkdf2": { @@ -2544,15 +2550,15 @@ } }, "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==", "dev": true }, "node_modules/@types/secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.2.tgz", + "integrity": "sha512-QMg+9v0bbNJ2peLuHRWxzmy0HRJIG6gFZNhaRSp7S3ggSbCCxiqQB2/ybvhXyhHOCequpNkrx7OavNhrWOsW0A==", "dev": true, "dependencies": { "@types/node": "*" @@ -2639,9 +2645,9 @@ } }, "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -6245,9 +6251,9 @@ } }, "node_modules/ethereumjs-util": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.0.tgz", - "integrity": "sha512-kR+vhu++mUDARrsMMhsjjzPduRVAeundLGXucGRHF3B4oEltOUspfgCVco4kckucj3FMlLaZHUl9n7/kdmr6Tw==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.0.10.tgz", + "integrity": "sha512-c/xThw6A+EAnej5Xk5kOzFzyoSnw0WX0tSlZ6pAsfGVvQj3TItaDg9b1+Fz1RJXA+y2YksKwQnuzgt1eY6LKzw==", "dev": true, "dependencies": { "@types/bn.js": "^5.1.0", @@ -6800,9 +6806,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.6.tgz", + "integrity": "sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7200,7 +7206,6 @@ "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -7209,15 +7214,13 @@ "version": "14.11.2", "integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/@types/pbkdf2": { "version": "3.1.0", "integrity": "sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -7227,7 +7230,6 @@ "integrity": "sha512-+ZjSA8ELlOp8SlKi0YLB2tz9d5iPNEmOBd+8Rz21wTMdaXQIa9b6TEnD6l5qKOCypE7FSyPyck12qZJxSDNoog==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -7237,7 +7239,6 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -7247,7 +7248,6 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -7260,7 +7260,6 @@ "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } @@ -7269,29 +7268,25 @@ "version": "1.1.0", "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U=", "dev": true, - "inBundle": true, - "license": "CC0-1.0" + "inBundle": true }, "node_modules/ganache-cli/node_modules/bn.js": { "version": "4.11.9", "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/brorand": { "version": "1.1.0", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/browserify-aes": { "version": "1.2.0", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -7306,7 +7301,6 @@ "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "base-x": "^3.0.2" } @@ -7316,7 +7310,6 @@ "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "bs58": "^4.0.0", "create-hash": "^1.1.0", @@ -7327,22 +7320,19 @@ "version": "1.1.1", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/buffer-xor": { "version": "1.0.3", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/camelcase": { "version": "5.3.1", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -7352,7 +7342,6 @@ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -7363,7 +7352,6 @@ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "inBundle": true, - "license": "ISC", "dependencies": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", @@ -7375,7 +7363,6 @@ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -7384,15 +7371,13 @@ "version": "1.1.3", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/create-hash": { "version": "1.2.0", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -7406,7 +7391,6 @@ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -7421,7 +7405,6 @@ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -7438,7 +7421,6 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7448,7 +7430,6 @@ "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -7463,15 +7444,13 @@ "version": "7.0.3", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/end-of-stream": { "version": "1.4.4", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -7481,7 +7460,6 @@ "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", @@ -7505,7 +7483,6 @@ "integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==", "dev": true, "inBundle": true, - "license": "MPL-2.0", "dependencies": { "@types/bn.js": "^4.11.3", "bn.js": "^4.11.0", @@ -7521,7 +7498,6 @@ "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "is-hex-prefixed": "1.0.0", "strip-hex-prefix": "1.0.0" @@ -7536,7 +7512,6 @@ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -7547,7 +7522,6 @@ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -7566,7 +7540,6 @@ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "locate-path": "^3.0.0" }, @@ -7579,7 +7552,6 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "inBundle": true, - "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -7589,7 +7561,6 @@ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -7602,7 +7573,6 @@ "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -7617,7 +7587,6 @@ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -7628,7 +7597,6 @@ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -7639,15 +7607,13 @@ "version": "2.0.4", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/invert-kv": { "version": "2.0.0", "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -7657,7 +7623,6 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -7667,7 +7632,6 @@ "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=6.5.0", "npm": ">=3" @@ -7678,7 +7642,6 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7687,8 +7650,7 @@ "version": "2.0.0", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/keccak": { "version": "3.0.1", @@ -7696,7 +7658,6 @@ "dev": true, "hasInstallScript": true, "inBundle": true, - "license": "MIT", "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0" @@ -7710,7 +7671,6 @@ "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "invert-kv": "^2.0.0" }, @@ -7723,7 +7683,6 @@ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -7737,7 +7696,6 @@ "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "p-defer": "^1.0.0" }, @@ -7750,7 +7708,6 @@ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -7762,7 +7719,6 @@ "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", @@ -7777,7 +7733,6 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -7786,36 +7741,31 @@ "version": "1.0.1", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/nice-try": { "version": "1.0.5", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/node-addon-api": { "version": "2.0.2", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/node-gyp-build": { "version": "4.2.3", "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", "dev": true, "inBundle": true, - "license": "MIT", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -7827,7 +7777,6 @@ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "path-key": "^2.0.0" }, @@ -7840,7 +7789,6 @@ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "inBundle": true, - "license": "ISC", "dependencies": { "wrappy": "1" } @@ -7850,7 +7798,6 @@ "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "execa": "^1.0.0", "lcid": "^2.0.0", @@ -7865,7 +7812,6 @@ "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -7875,7 +7821,6 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -7885,7 +7830,6 @@ "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -7895,7 +7839,6 @@ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -7911,7 +7854,6 @@ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "p-limit": "^2.0.0" }, @@ -7924,7 +7866,6 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -7934,7 +7875,6 @@ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -7944,7 +7884,6 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -7954,7 +7893,6 @@ "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -7971,7 +7909,6 @@ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7982,7 +7919,6 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -7992,7 +7928,6 @@ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8007,7 +7942,6 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8016,15 +7950,13 @@ "version": "2.0.0", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/ripemd160": { "version": "2.0.2", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -8035,7 +7967,6 @@ "integrity": "sha512-HAfAmL6SDYNWPUOJNrM500x4Thn4PZsEy5pijPh40U9WfNk0z15hUYzO9xVIMAdIHdFtD8CBDHd75Td1g36Mjg==", "dev": true, "inBundle": true, - "license": "MPL-2.0", "dependencies": { "bn.js": "^4.11.1" }, @@ -8061,15 +7992,13 @@ "url": "https://feross.org/support" } ], - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/scrypt-js": { "version": "3.0.1", "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/secp256k1": { "version": "4.0.2", @@ -8077,7 +8006,6 @@ "dev": true, "hasInstallScript": true, "inBundle": true, - "license": "MIT", "dependencies": { "elliptic": "^6.5.2", "node-addon-api": "^2.0.0", @@ -8092,7 +8020,6 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true, "inBundle": true, - "license": "ISC", "bin": { "semver": "bin/semver" } @@ -8101,22 +8028,19 @@ "version": "2.0.0", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/setimmediate": { "version": "1.0.5", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/sha.js": { "version": "2.4.11", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "inBundle": true, - "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -8130,7 +8054,6 @@ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" }, @@ -8143,7 +8066,6 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8152,15 +8074,13 @@ "version": "3.0.3", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/source-map": { "version": "0.6.1", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "inBundle": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -8170,7 +8090,6 @@ "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -8181,7 +8100,6 @@ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } @@ -8191,7 +8109,6 @@ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -8206,7 +8123,6 @@ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "ansi-regex": "^4.1.0" }, @@ -8219,7 +8135,6 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true, "inBundle": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8229,7 +8144,6 @@ "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "is-hex-prefixed": "1.0.0" }, @@ -8242,15 +8156,13 @@ "version": "1.0.2", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/ganache-cli/node_modules/which": { "version": "1.3.1", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "inBundle": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -8262,15 +8174,13 @@ "version": "2.0.0", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/wrap-ansi": { "version": "5.1.0", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", @@ -8284,22 +8194,19 @@ "version": "1.0.2", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/y18n": { "version": "4.0.0", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true, - "inBundle": true, - "license": "ISC" + "inBundle": true }, "node_modules/ganache-cli/node_modules/yargs": { "version": "13.2.4", "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", "dev": true, "inBundle": true, - "license": "MIT", "dependencies": { "cliui": "^5.0.0", "find-up": "^3.0.0", @@ -8319,7 +8226,6 @@ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "inBundle": true, - "license": "ISC", "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -8627,16 +8533,16 @@ } }, "node_modules/hardhat": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.4.3.tgz", - "integrity": "sha512-xgbnhEnmaKau8xT6wPJlzoyMLAZyxQoElACQQCyEeAY1DURpZbYwjIQUywmL/ZNv3QEl38Yqu/n8mPOc2HXyGA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.4.1.tgz", + "integrity": "sha512-vwllrFypukeE/Q+4ZfWj7j7nUo4ncUhRpsAYUM0Ruuuk6pQlKmRa0A6c0kxRSvvVgQsMud6j+/weYhbMX1wPmQ==", "dev": true, "dependencies": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/blockchain": "^5.4.0", - "@ethereumjs/common": "^2.4.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.3.0", + "@ethereumjs/blockchain": "^5.3.0", + "@ethereumjs/common": "^2.3.1", + "@ethereumjs/tx": "^3.2.1", + "@ethereumjs/vm": "^5.3.2", "@ethersproject/abi": "^5.1.2", "@sentry/node": "^5.18.1", "@solidity-parser/parser": "^0.11.0", @@ -8654,7 +8560,7 @@ "eth-sig-util": "^2.5.2", "ethereum-cryptography": "^0.1.2", "ethereumjs-abi": "^0.6.8", - "ethereumjs-util": "^7.1.0", + "ethereumjs-util": "^7.0.10", "find-up": "^2.1.0", "fp-ts": "1.19.3", "fs-extra": "^7.0.1", @@ -8971,9 +8877,9 @@ } }, "node_modules/hardhat/node_modules/ws": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", - "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.2.tgz", + "integrity": "sha512-lkF7AWRicoB9mAgjeKbGqVUekLnSNO4VjKVnuPHpQeOxZOErX6BPXwJk70nFslRCEEA8EVW7ZjKwXaP9N+1sKQ==", "dev": true, "engines": { "node": ">=8.3.0" @@ -9401,9 +9307,9 @@ "dev": true }, "node_modules/immutable": { - "version": "4.0.0-rc.14", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.14.tgz", - "integrity": "sha512-pfkvmRKJSoW7JFx0QeYlAmT+kNYvn5j0u7bnpNq4N2RCvHSTlLT208G8jgaquNe+Q8kCPHKOSpxJkyvLDpYq0w==", + "version": "4.0.0-rc.12", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.12.tgz", + "integrity": "sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==", "dev": true }, "node_modules/import-fresh": { @@ -13086,9 +12992,9 @@ } }, "node_modules/prettier-plugin-solidity": { - "version": "1.0.0-beta.15", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.15.tgz", - "integrity": "sha512-5+pM/Z1BgQN6xMCe/AfXmiVhoteaVgruVoGT8aa0zJMRiNOiK+SeaW2kOyzTI6sgg3QsUW58+ahf1hxlc/Adbw==", + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.16.tgz", + "integrity": "sha512-xVBcnoWpe52dNnCCbqPHC9ZrTWXcNfldf852ZD0DBcHDqVMwjHTAPEdfBVy6FczbFpVa8bmxQil+G5XkEz5WHA==", "dev": true, "dependencies": { "@solidity-parser/parser": "^0.13.2", @@ -16423,29 +16329,27 @@ "dev": true }, "node_modules/tsconfig-paths": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz", - "integrity": "sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", "dev": true, "dependencies": { - "json5": "^2.2.0", + "@types/json5": "^0.0.29", + "json5": "^1.0.1", "minimist": "^1.2.0", "strip-bom": "^3.0.0" } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "dependencies": { - "minimist": "^1.2.5" + "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" } }, "node_modules/tslib": { @@ -19488,28 +19392,28 @@ "dev": true }, "@ethereumjs/block": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/block/-/block-3.4.0.tgz", - "integrity": "sha512-umKAoTX32yXzErpIksPHodFc/5y8bmZMnOl6hWy5Vd8xId4+HKFUOyEiN16Y97zMwFRysRpcrR6wBejfqc6Bmg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/block/-/block-3.3.0.tgz", + "integrity": "sha512-WoefY9Rs4W8vZTxG9qwntAlV61xsSv0NPoXmHO7x3SH16dwJQtU15YvahPCz4HEEXbu7GgGgNgu0pv8JY7VauA==", "dev": true, "requires": { - "@ethereumjs/common": "^2.4.0", - "@ethereumjs/tx": "^3.3.0", - "ethereumjs-util": "^7.1.0", + "@ethereumjs/common": "^2.3.0", + "@ethereumjs/tx": "^3.2.0", + "ethereumjs-util": "^7.0.10", "merkle-patricia-tree": "^4.2.0" } }, "@ethereumjs/blockchain": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/blockchain/-/blockchain-5.4.0.tgz", - "integrity": "sha512-wAuKLaew6PL52kH8YPXO7PbjjKV12jivRSyHQehkESw4slSLLfYA6Jv7n5YxyT2ajD7KNMPVh7oyF/MU6HcOvg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/blockchain/-/blockchain-5.3.1.tgz", + "integrity": "sha512-Sr39BoTOzmVSnuYzjiCIpgcBUFE5JWcMF0lYCvzrtx/5Lg1tnpZhw9yMQ6JfIomN421epg4oDz99DWlL9Aqz3g==", "dev": true, "requires": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/common": "^2.4.0", + "@ethereumjs/block": "^3.3.0", + "@ethereumjs/common": "^2.3.1", "@ethereumjs/ethash": "^1.0.0", "debug": "^2.2.0", - "ethereumjs-util": "^7.1.0", + "ethereumjs-util": "^7.0.10", "level-mem": "^5.0.1", "lru-cache": "^5.1.1", "rlp": "^2.2.4", @@ -19543,13 +19447,13 @@ } }, "@ethereumjs/common": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.4.0.tgz", - "integrity": "sha512-UdkhFWzWcJCZVsj1O/H8/oqj/0RVYjLc1OhPjBrQdALAkQHpCp8xXI4WLnuGTADqTdJZww0NtgwG+TRPkXt27w==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.3.1.tgz", + "integrity": "sha512-V8hrULExoq0H4HFs3cCmdRGbgmipmlNzak6Xg34nHYfQyqkSdrCuflvYjyWmsNpI8GtrcZhzifAbgX/1C1Cjwg==", "dev": true, "requires": { "crc-32": "^1.2.0", - "ethereumjs-util": "^7.1.0" + "ethereumjs-util": "^7.0.10" } }, "@ethereumjs/ethash": { @@ -19576,29 +19480,29 @@ } }, "@ethereumjs/tx": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.3.0.tgz", - "integrity": "sha512-yTwEj2lVzSMgE6Hjw9Oa1DZks/nKTWM8Wn4ykDNapBPua2f4nXO3qKnni86O6lgDj5fVNRqbDsD0yy7/XNGDEA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.2.1.tgz", + "integrity": "sha512-i9V39OtKvwWos1uVNZxdVhd7zFOyzFLjgt69CoiOY0EmXugS0HjO3uxpLBSglDKFMRriuGqw6ddKEv+RP1UNEw==", "dev": true, "requires": { - "@ethereumjs/common": "^2.4.0", - "ethereumjs-util": "^7.1.0" + "@ethereumjs/common": "^2.3.1", + "ethereumjs-util": "^7.0.10" } }, "@ethereumjs/vm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/vm/-/vm-5.5.0.tgz", - "integrity": "sha512-h6Kr6WqKUP8nVuEzCWPWEPrC63v7HFwt3gRuK7CJiyg9S0iWSBKUA/YVD4YgaSVACuxUfWaOBbwV5uGVupm5PQ==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/vm/-/vm-5.4.1.tgz", + "integrity": "sha512-cpQcg5CtjzXJBn8QNiobaiWckeN/ZQwsDHLYa9df2wBEUvzuEZgFWK48YEXSpx3CnUY9fNT/lgA9CzKdq8HTzQ==", "dev": true, "requires": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/blockchain": "^5.4.0", - "@ethereumjs/common": "^2.4.0", - "@ethereumjs/tx": "^3.3.0", + "@ethereumjs/block": "^3.3.0", + "@ethereumjs/blockchain": "^5.3.0", + "@ethereumjs/common": "^2.3.1", + "@ethereumjs/tx": "^3.2.1", "async-eventemitter": "^0.2.4", "core-js-pure": "^3.0.1", "debug": "^2.2.0", - "ethereumjs-util": "^7.1.0", + "ethereumjs-util": "^7.0.10", "functional-red-black-tree": "^1.0.1", "mcl-wasm": "^0.7.1", "merkle-patricia-tree": "^4.2.0", @@ -20082,9 +19986,9 @@ "dev": true }, "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", + "integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==", "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", @@ -20233,9 +20137,9 @@ } }, "@oclif/errors": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.5.tgz", - "integrity": "sha512-OivucXPH/eLLlOT7FkCMoZXiaVYf8I/w1eTAM1+gKzfhALwWTusxEx7wBmW0uzvkSg/9ovWLycPaBgJbM3LOCQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.4.tgz", + "integrity": "sha512-pJKXyEqwdfRTUdM8n5FIHiQQHg5ETM0Wlso8bF9GodczO40mF5Z3HufnYWJE7z8sGKxOeJCdbAVZbS8Y+d5GCw==", "dev": true, "requires": { "clean-stack": "^3.0.0", @@ -20612,9 +20516,9 @@ } }, "@openzeppelin/test-helpers": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@openzeppelin/test-helpers/-/test-helpers-0.5.12.tgz", - "integrity": "sha512-ZPhLmMb8PLGImYLen7YsPnni22i1bXHzrSiY7XZ7cgwuKvk4MRBunzfZ4xGTn/p+1V2/a1XHsjMRDKn7AMVb3Q==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@openzeppelin/test-helpers/-/test-helpers-0.5.11.tgz", + "integrity": "sha512-HkFpCjtTD8dk+wdYhsT07YbMGCE+Z4Wp5sBKXvPDF3Lynoc0H2KqZgCWV+qr2YZ0WW1oX/sXkKFrrKJ0caBTjw==", "dev": true, "requires": { "@openzeppelin/contract-loader": "^0.6.2", @@ -20823,15 +20727,15 @@ } }, "@truffle/contract": { - "version": "4.3.24", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.3.24.tgz", - "integrity": "sha512-cKijyDzJLRu96KWb/g7soHJkBXgY1HkmCeVhUun9iUJpA93w7StInCP+SPs0gd8Oj7GfcoYcCcnA38l/zJBEsQ==", + "version": "4.3.23", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.3.23.tgz", + "integrity": "sha512-YqoFmy6bFMcFnbC1OXNBF1MrhNGYf1JEvY65EsbNe3cjkgbFfk0wVwuDkSYqXBIyWAOQBkPL+GTq4ouvnjVlnw==", "dev": true, "requires": { "@ensdomains/ensjs": "^2.0.1", "@truffle/blockchain-utils": "^0.0.31", "@truffle/contract-schema": "^3.4.1", - "@truffle/debug-utils": "^5.1.4", + "@truffle/debug-utils": "^5.1.3", "@truffle/error": "^0.0.14", "@truffle/interface-adapter": "^0.5.2", "bignumber.js": "^7.2.1", @@ -20850,9 +20754,9 @@ "dev": true }, "@truffle/codec": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.11.4.tgz", - "integrity": "sha512-NK/ah1aWQFhYYv8kVL382wB+JgSOnWWDdoeGOOMnA3iviRnV7k0JJV+C6uTmaLKOTNGTBNp4hlISLFeKKNnbXA==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.11.3.tgz", + "integrity": "sha512-LBunwP2ZQ0CygcbuLGsQS8Zm4+2kpVKBkvA/DOiCI4IaNxMie3LMFCBFEVrHUey2a07J4wn7WivrRY3dUdS9zQ==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -20869,12 +20773,12 @@ } }, "@truffle/debug-utils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-5.1.4.tgz", - "integrity": "sha512-0LdtJzhOuAgEIP4xj3QsB7CKIzoAgRERKx5hYktId9pfm//9leo3luwN0aY8f919rWJynmnaXvE0lQZD3Pcu0Q==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-5.1.3.tgz", + "integrity": "sha512-++IPNmxBH/uwaVqZ+hokEDavN9r0TayU2o0/geHoBCwcC0eW5xr9zbciFxsgKQCCuBESi23eOflNuQUK1Awa5w==", "dev": true, "requires": { - "@truffle/codec": "^0.11.4", + "@truffle/codec": "^0.11.3", "@trufflesuite/chromafi": "^2.2.2", "bn.js": "^5.1.3", "chalk": "^2.4.2", @@ -21217,9 +21121,9 @@ } }, "@types/abstract-leveldown": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.2.tgz", - "integrity": "sha512-+jA1XXF3jsz+Z7FcuiNqgK53hTa/luglT2TyTpKPqoYbxVY+mCPF22Rm+q3KPBrMHJwNXFrTViHszBOfU4vftQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz", + "integrity": "sha512-wYxU3kp5zItbxKmeRYCEplS2MW7DzyBnxPGj+GJVHZEUZiK/nn5Ei1sUFgURDh+X051+zsGe28iud3oHjrYWQQ==", "dev": true }, "@types/bignumber.js": { @@ -21241,15 +21145,15 @@ } }, "@types/chai": { - "version": "4.2.21", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.21.tgz", - "integrity": "sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.19.tgz", + "integrity": "sha512-jRJgpRBuY+7izT7/WNXP/LsMO9YonsstuL+xuvycDyESpoDoIAsMd7suwpB4h9oEWB+ZlPTqJJ8EHomzNhwTPQ==", "dev": true }, "@types/concat-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", - "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", "dev": true, "requires": { "@types/node": "*" @@ -21265,15 +21169,21 @@ } }, "@types/glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { "@types/minimatch": "*", "@types/node": "*" } }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/level-errors": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/level-errors/-/level-errors-3.0.0.tgz", @@ -21281,9 +21191,9 @@ "dev": true }, "@types/levelup": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/levelup/-/levelup-4.3.3.tgz", - "integrity": "sha512-K+OTIjJcZHVlZQN1HmU64VtrC0jC3dXWQozuEIR9zVvltIk90zaGPM2AgT+fIkChpzHhFE3YnvFLCbLtzAmexA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/levelup/-/levelup-4.3.2.tgz", + "integrity": "sha512-5Su1Dkl6nMjkXqUb2z72gbroG0WFLs+6nMH+wQt4GWIrDwR/IconLTojHtC0klLJODCJ64Cr6P5cWqVeuxAbSg==", "dev": true, "requires": { "@types/abstract-leveldown": "*", @@ -21292,21 +21202,21 @@ } }, "@types/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz", + "integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==", "dev": true }, "@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==", "dev": true }, "@types/node": { - "version": "12.20.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.16.tgz", - "integrity": "sha512-6CLxw83vQf6DKqXxMPwl8qpF8I7THFZuIwLt4TnNsumxkp1VsRZWT8txQxncT/Rl2UojTsFzWgDG4FRMwafrlA==", + "version": "12.20.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.15.tgz", + "integrity": "sha512-F6S4Chv4JicJmyrwlDkxUdGNSplsQdGwp1A0AJloEVDirWdZOAiRHhovDlsFkKUrquUXhz1imJhXHsf59auyAg==", "dev": true }, "@types/pbkdf2": { @@ -21319,15 +21229,15 @@ } }, "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==", "dev": true }, "@types/secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.2.tgz", + "integrity": "sha512-QMg+9v0bbNJ2peLuHRWxzmy0HRJIG6gFZNhaRSp7S3ggSbCCxiqQB2/ybvhXyhHOCequpNkrx7OavNhrWOsW0A==", "dev": true, "requires": { "@types/node": "*" @@ -21395,9 +21305,9 @@ } }, "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true, "requires": {} }, @@ -24374,9 +24284,9 @@ } }, "ethereumjs-util": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.0.tgz", - "integrity": "sha512-kR+vhu++mUDARrsMMhsjjzPduRVAeundLGXucGRHF3B4oEltOUspfgCVco4kckucj3FMlLaZHUl9n7/kdmr6Tw==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.0.10.tgz", + "integrity": "sha512-c/xThw6A+EAnej5Xk5kOzFzyoSnw0WX0tSlZ6pAsfGVvQj3TItaDg9b1+Fz1RJXA+y2YksKwQnuzgt1eY6LKzw==", "dev": true, "requires": { "@types/bn.js": "^5.1.0", @@ -24875,9 +24785,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.6.tgz", + "integrity": "sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -26274,16 +26184,16 @@ } }, "hardhat": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.4.3.tgz", - "integrity": "sha512-xgbnhEnmaKau8xT6wPJlzoyMLAZyxQoElACQQCyEeAY1DURpZbYwjIQUywmL/ZNv3QEl38Yqu/n8mPOc2HXyGA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.4.1.tgz", + "integrity": "sha512-vwllrFypukeE/Q+4ZfWj7j7nUo4ncUhRpsAYUM0Ruuuk6pQlKmRa0A6c0kxRSvvVgQsMud6j+/weYhbMX1wPmQ==", "dev": true, "requires": { - "@ethereumjs/block": "^3.4.0", - "@ethereumjs/blockchain": "^5.4.0", - "@ethereumjs/common": "^2.4.0", - "@ethereumjs/tx": "^3.3.0", - "@ethereumjs/vm": "^5.5.0", + "@ethereumjs/block": "^3.3.0", + "@ethereumjs/blockchain": "^5.3.0", + "@ethereumjs/common": "^2.3.1", + "@ethereumjs/tx": "^3.2.1", + "@ethereumjs/vm": "^5.3.2", "@ethersproject/abi": "^5.1.2", "@sentry/node": "^5.18.1", "@solidity-parser/parser": "^0.11.0", @@ -26301,7 +26211,7 @@ "eth-sig-util": "^2.5.2", "ethereum-cryptography": "^0.1.2", "ethereumjs-abi": "^0.6.8", - "ethereumjs-util": "^7.1.0", + "ethereumjs-util": "^7.0.10", "find-up": "^2.1.0", "fp-ts": "1.19.3", "fs-extra": "^7.0.1", @@ -26547,9 +26457,9 @@ "dev": true }, "ws": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", - "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.2.tgz", + "integrity": "sha512-lkF7AWRicoB9mAgjeKbGqVUekLnSNO4VjKVnuPHpQeOxZOErX6BPXwJk70nFslRCEEA8EVW7ZjKwXaP9N+1sKQ==", "dev": true, "requires": {} } @@ -26887,9 +26797,9 @@ "dev": true }, "immutable": { - "version": "4.0.0-rc.14", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.14.tgz", - "integrity": "sha512-pfkvmRKJSoW7JFx0QeYlAmT+kNYvn5j0u7bnpNq4N2RCvHSTlLT208G8jgaquNe+Q8kCPHKOSpxJkyvLDpYq0w==", + "version": "4.0.0-rc.12", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.12.tgz", + "integrity": "sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==", "dev": true }, "import-fresh": { @@ -29859,9 +29769,9 @@ "dev": true }, "prettier-plugin-solidity": { - "version": "1.0.0-beta.15", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.15.tgz", - "integrity": "sha512-5+pM/Z1BgQN6xMCe/AfXmiVhoteaVgruVoGT8aa0zJMRiNOiK+SeaW2kOyzTI6sgg3QsUW58+ahf1hxlc/Adbw==", + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.16.tgz", + "integrity": "sha512-xVBcnoWpe52dNnCCbqPHC9ZrTWXcNfldf852ZD0DBcHDqVMwjHTAPEdfBVy6FczbFpVa8bmxQil+G5XkEz5WHA==", "dev": true, "requires": { "@solidity-parser/parser": "^0.13.2", @@ -32541,23 +32451,24 @@ "dev": true }, "tsconfig-paths": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz", - "integrity": "sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", "dev": true, "requires": { - "json5": "^2.2.0", + "@types/json5": "^0.0.29", + "json5": "^1.0.1", "minimist": "^1.2.0", "strip-bom": "^3.0.0" }, "dependencies": { "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "requires": { - "minimist": "^1.2.5" + "minimist": "^1.2.0" } } } diff --git a/package.json b/package.json index aa1136060ca..bb28aa1a9f7 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "merkletreejs": "^0.2.13", "micromatch": "^4.0.2", "prettier": "^2.3.0", - "prettier-plugin-solidity": "^1.0.0-beta.13", + "prettier-plugin-solidity": "^1.0.0-beta.16", "rimraf": "^3.0.2", "solhint": "^3.3.6", "solidity-ast": "^0.4.25", diff --git a/scripts/inheritanceOrdering.js b/scripts/inheritanceOrdering.js index dd965989b16..8a052f36fe1 100644 --- a/scripts/inheritanceOrdering.js +++ b/scripts/inheritanceOrdering.js @@ -21,16 +21,22 @@ for (const artifact of artifacts) { } } - graphlib.alg.findCycles(graph).forEach(([ c1, c2 ]) => { - console.log(`Conflict between ${names[c1]} and ${names[c2]} detected in the following dependency chains:`); - linearized - .filter(chain => chain.includes(parseInt(c1)) && chain.includes(parseInt(c2))) - .forEach(chain => { - const comp = chain.indexOf(c1) < chain.indexOf(c2) ? '>' : '<'; - console.log(`- ${names[c1]} ${comp} ${names[c2]}: ${chain.reverse().map(id => names[id]).join(', ')}`); - }); - process.exitCode = 1; - }); + /// graphlib.alg.findCycles will not find minimal cycles. + /// We are only interested int cycles of lengths 2 (needs proof) + graph.nodes().forEach((x, i, nodes) => nodes + .slice(i + 1) + .filter(y => graph.hasEdge(x, y) && graph.hasEdge(y, x)) + .map(y => { + console.log(`Conflict between ${names[x]} and ${names[y]} detected in the following dependency chains:`); + linearized + .filter(chain => chain.includes(parseInt(x)) && chain.includes(parseInt(y))) + .forEach(chain => { + const comp = chain.indexOf(parseInt(x)) < chain.indexOf(parseInt(y)) ? '>' : '<'; + console.log(`- ${names[x]} ${comp} ${names[y]} in ${names[chain.find(Boolean)]}`); + // console.log(`- ${names[x]} ${comp} ${names[y]}: ${chain.reverse().map(id => names[id]).join(', ')}`); + }); + process.exitCode = 1; + })); } if (!process.exitCode) { diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js new file mode 100644 index 00000000000..e9c2e75dbd2 --- /dev/null +++ b/test/governance/Governor.test.js @@ -0,0 +1,819 @@ +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; +const Enums = require('../helpers/enums'); +const { EIP712Domain } = require('../helpers/eip712'); +const { fromRpcSig } = require('ethereumjs-util'); + +const { + runGovernorWorkflow, +} = require('./GovernorWorkflow.behavior'); + +const { + shouldSupportInterfaces, +} = require('../utils/introspection/SupportsInterface.behavior'); + +const Token = artifacts.require('ERC20VotesMock'); +const Governor = artifacts.require('GovernorMock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('Governor', function (accounts) { + const [ owner, proposer, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + const version = '1'; + const tokenName = 'MockToken'; + const tokenSymbol = 'MTKN'; + const tokenSupply = web3.utils.toWei('100'); + + 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.receiver = await CallReceiver.new(); + await this.token.mint(owner, tokenSupply); + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + }); + + shouldSupportInterfaces([ + 'ERC165', + 'Governor', + ]); + + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain'); + }); + + describe('scenario', function () { + describe('nominal', function () { + beforeEach(async function () { + this.value = web3.utils.toWei('1'); + + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: this.value }); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(this.value); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); + + this.settings = { + proposal: [ + [ this.receiver.address ], + [ this.value ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + proposer, + 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: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against }, + { voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain }, + ], + }; + this.votingDelay = await this.mock.votingDelay(); + this.votingPeriod = await this.mock.votingPeriod(); + }); + + afterEach(async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); + + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { weight }) => acc.add(new BN(weight)), + new BN('0'), + ), + ); + } + }); + + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { + proposalId: this.id, + proposer, + targets: this.settings.proposal[0], + // values: this.settings.proposal[1].map(value => new BN(value)), + signatures: this.settings.proposal[2].map(() => ''), + calldatas: this.settings.proposal[2], + startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay), + endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod), + description: this.settings.proposal[3], + }, + ); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.receiver, + 'MockFunctionCalled', + ); + + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(this.value); + }); + runGovernorWorkflow(); + }); + + describe('vote with signature', function () { + beforeEach(async function () { + const chainId = await web3.eth.getChainId(); + // generate voter by signature wallet + const voterBySig = Wallet.generate(); + this.voter = web3.utils.toChecksumAddress(voterBySig.getAddressString()); + // use delegateBySig to enable vote delegation for this wallet + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + voterBySig.getPrivateKey(), + { + data: { + types: { + EIP712Domain, + Delegation: [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, + ], + }, + domain: { name: tokenName, version: '1', chainId, verifyingContract: this.token.address }, + primaryType: 'Delegation', + message: { delegatee: this.voter, nonce: 0, expiry: constants.MAX_UINT256 }, + }, + }, + )); + await this.token.delegateBySig(this.voter, 0, constants.MAX_UINT256, v, r, s); + // prepare signature for vote by signature + const signature = async (message) => { + return fromRpcSig(ethSigUtil.signTypedMessage( + voterBySig.getPrivateKey(), + { + data: { + types: { + EIP712Domain, + Ballot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + ], + }, + domain: { name, version, chainId, verifyingContract: this.mock.address }, + primaryType: 'Ballot', + message, + }, + }, + )); + }; + + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: this.voter, signature, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + }; + }); + afterEach(async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, this.voter)).to.be.equal(true); + + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { weight }) => acc.add(new BN(weight)), + new BN('0'), + ), + ); + } + }); + + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.receiver, + 'MockFunctionCalled', + ); + }); + runGovernorWorkflow(); + }); + + describe('send ethers', function () { + beforeEach(async function () { + this.receiver = { address: web3.utils.toChecksumAddress(web3.utils.randomHex(20)) }; + this.value = web3.utils.toWei('1'); + + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: this.value }); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(this.value); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); + + this.settings = { + proposal: [ + [ this.receiver.address ], + [ this.value ], + [ '0x' ], + '', + ], + 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 }, + ], + }; + }); + afterEach(async function () { + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(this.value); + }); + runGovernorWorkflow(); + }); + + describe('receiver revert without reason', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ 0 ], + [ this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + steps: { + execute: { error: 'Governor: call reverted without message' }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('receiver revert with reason', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ 0 ], + [ this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + steps: { + execute: { error: 'CallReceiverMock: reverting' }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('missing proposal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { + voter: voter1, + weight: web3.utils.toWei('1'), + support: Enums.VoteType.For, + error: 'Governor: unknown proposal id', + }, + { + voter: voter2, + weight: web3.utils.toWei('1'), + support: Enums.VoteType.Abstain, + error: 'Governor: unknown proposal id', + }, + ], + steps: { + propose: { enable: false }, + wait: { enable: false }, + execute: { error: 'Governor: unknown proposal id' }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('duplicate pending proposal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + steps: { + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + await expectRevert(this.mock.propose(...this.settings.proposal), 'Governor: proposal already exists'); + }); + runGovernorWorkflow(); + }); + + describe('duplicate executed proposal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + 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 }, + ], + }; + }); + afterEach(async function () { + await expectRevert(this.mock.propose(...this.settings.proposal), 'Governor: proposal already exists'); + }); + runGovernorWorkflow(); + }); + + describe('Invalid vote type', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { + voter: voter1, + weight: web3.utils.toWei('1'), + support: new BN('255'), + error: 'GovernorVotingSimple: invalid value for enum VoteType', + }, + ], + steps: { + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('double cast', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { + voter: voter1, + weight: web3.utils.toWei('1'), + support: Enums.VoteType.For, + }, + { + voter: voter1, + weight: web3.utils.toWei('1'), + support: Enums.VoteType.For, + error: 'GovernorVotingSimple: vote already casted', + }, + ], + }; + }); + runGovernorWorkflow(); + }); + + describe('quorum not reached', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('0'), support: Enums.VoteType.For }, + ], + steps: { + execute: { error: 'Governor: proposal not successful' }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('score not reached', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.Against }, + ], + steps: { + execute: { error: 'Governor: proposal not successful' }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('vote not over', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + steps: { + wait: { enable: false }, + execute: { error: 'Governor: proposal not successful' }, + }, + }; + }); + runGovernorWorkflow(); + }); + }); + + describe('state', function () { + describe('Unset', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + steps: { + propose: { enable: false }, + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + await expectRevert(this.mock.state(this.id), 'Governor: unknown proposal id'); + }); + runGovernorWorkflow(); + }); + + describe('Pending & Active', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + steps: { + propose: { noadvance: true }, + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + + await time.advanceBlockTo(this.snapshot); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + }); + runGovernorWorkflow(); + }); + + describe('Defeated', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + steps: { + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); + }); + runGovernorWorkflow(); + }); + + describe('Succeeded', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + steps: { + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + }); + runGovernorWorkflow(); + }); + + describe('Executed', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); + }); + runGovernorWorkflow(); + }); + }); + + describe('Cancel', function () { + describe('Before proposal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + steps: { + propose: { enable: false }, + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + await expectRevert( + this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: unknown proposal id', + ); + }); + runGovernorWorkflow(); + }); + + describe('After proposal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + steps: { + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash); + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevert( + this.mock.castVote(this.id, new BN('100'), { from: voter1 }), + 'Governor: vote not currently active', + ); + }); + runGovernorWorkflow(); + }); + + describe('After vote', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + steps: { + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash); + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevert( + this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('After deadline', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + steps: { + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash); + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevert( + this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('After execution', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + }; + }); + afterEach(async function () { + await expectRevert( + this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not active', + ); + }); + runGovernorWorkflow(); + }); + }); + + describe('Proposal length', function () { + it('empty', async function () { + await expectRevert( + this.mock.propose( + [], + [], + [], + '', + ), + 'Governor: empty proposal', + ); + }); + + it('missmatch #1', async function () { + await expectRevert( + this.mock.propose( + [ ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ), + 'Governor: invalid proposal length', + ); + }); + + it('missmatch #2', async function () { + await expectRevert( + this.mock.propose( + [ this.receiver.address ], + [ ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ), + 'Governor: invalid proposal length', + ); + }); + + it('missmatch #3', async function () { + await expectRevert( + this.mock.propose( + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ ], + '', + ), + 'Governor: invalid proposal length', + ); + }); + }); +}); diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js new file mode 100644 index 00000000000..433a4e6a2fb --- /dev/null +++ b/test/governance/GovernorWorkflow.behavior.js @@ -0,0 +1,133 @@ +const { expectRevert, time } = require('@openzeppelin/test-helpers'); + +async function getReceiptOrRevert (promise, error = undefined) { + if (error) { + await expectRevert(promise, error); + return undefined; + } else { + const { receipt } = await promise; + return receipt; + } +} + +function tryGet (obj, path = '') { + try { + return path.split('.').reduce((o, k) => o[k], obj); + } catch (_) { + return undefined; + } +} + +function runGovernorWorkflow () { + beforeEach(async function () { + this.receipts = {}; + this.descriptionHash = web3.utils.keccak256(this.settings.proposal.slice(-1).find(Boolean)); + this.id = await this.mock.hashProposal(...this.settings.proposal.slice(0, -1), this.descriptionHash); + }); + + it('run', async function () { + // transfer tokens + if (tryGet(this.settings, 'voters')) { + for (const voter of this.settings.voters) { + if (voter.weight) { + await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); + } + } + } + + // propose + if (this.mock.propose && tryGet(this.settings, 'steps.propose.enable') !== false) { + this.receipts.propose = await getReceiptOrRevert( + this.mock.methods['propose(address[],uint256[],bytes[],string)']( + ...this.settings.proposal, + { from: this.settings.proposer }, + ), + tryGet(this.settings, 'steps.propose.error'), + ); + + if (tryGet(this.settings, 'steps.propose.error') === undefined) { + this.deadline = await this.mock.proposalDeadline(this.id); + this.snapshot = await this.mock.proposalSnapshot(this.id); + } + + if (tryGet(this.settings, 'steps.propose.delay')) { + await time.increase(tryGet(this.settings, 'steps.propose.delay')); + } + + if ( + tryGet(this.settings, 'steps.propose.error') === undefined && + tryGet(this.settings, 'steps.propose.noadvance') !== true + ) { + await time.advanceBlockTo(this.snapshot); + } + } + + // vote + if (tryGet(this.settings, 'voters')) { + this.receipts.castVote = []; + for (const voter of this.settings.voters) { + if (!voter.signature) { + this.receipts.castVote.push( + await getReceiptOrRevert( + voter.reason + ? this.mock.castVoteWithReason(this.id, voter.support, voter.reason, { from: voter.voter }) + : this.mock.castVote(this.id, voter.support, { from: voter.voter }), + voter.error, + ), + ); + } else { + const { v, r, s } = await voter.signature({ proposalId: this.id, support: voter.support }); + this.receipts.castVote.push( + await getReceiptOrRevert( + this.mock.castVoteBySig(this.id, voter.support, v, r, s), + voter.error, + ), + ); + } + if (tryGet(voter, 'delay')) { + await time.increase(tryGet(voter, 'delay')); + } + } + } + + // fast forward + if (tryGet(this.settings, 'steps.wait.enable') !== false) { + await time.advanceBlockTo(this.deadline); + } + + // queue + if (this.mock.queue && tryGet(this.settings, 'steps.queue.enable') !== false) { + this.receipts.queue = await getReceiptOrRevert( + this.mock.methods['queue(address[],uint256[],bytes[],bytes32)']( + ...this.settings.proposal.slice(0, -1), + this.descriptionHash, + { from: this.settings.queuer }, + ), + tryGet(this.settings, 'steps.queue.error'), + ); + this.eta = await this.mock.proposalEta(this.id); + if (tryGet(this.settings, 'steps.queue.delay')) { + await time.increase(tryGet(this.settings, 'steps.queue.delay')); + } + } + + // execute + if (this.mock.execute && tryGet(this.settings, 'steps.execute.enable') !== false) { + this.receipts.execute = await getReceiptOrRevert( + this.mock.methods['execute(address[],uint256[],bytes[],bytes32)']( + ...this.settings.proposal.slice(0, -1), + this.descriptionHash, + { from: this.settings.executer }, + ), + tryGet(this.settings, 'steps.execute.error'), + ); + if (tryGet(this.settings, 'steps.execute.delay')) { + await time.increase(tryGet(this.settings, 'steps.execute.delay')); + } + } + }); +} + +module.exports = { + runGovernorWorkflow, +}; diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js new file mode 100644 index 00000000000..5b6b40eb7c7 --- /dev/null +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -0,0 +1,430 @@ +const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const Enums = require('../../helpers/enums'); +const RLP = require('rlp'); + +const { + runGovernorWorkflow, +} = require('../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC20VotesCompMock'); +const Timelock = artifacts.require('CompTimelock'); +const Governor = artifacts.require('GovernorCompatibilityBravoMock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +async function getReceiptOrRevert (promise, error = undefined) { + if (error) { + await expectRevert(promise, error); + return undefined; + } else { + const { receipt } = await promise; + return receipt; + } +} + +function tryGet (obj, path = '') { + try { + return path.split('.').reduce((o, k) => o[k], obj); + } catch (_) { + return undefined; + } +} + +function makeContractAddress (creator, nonce) { + return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([creator, nonce])).slice(12).substring(14)); +} + +contract('GovernorCompatibilityBravo', function (accounts) { + const [ owner, proposer, voter1, voter2, voter3, voter4, other ] = accounts; + + const name = 'OZ-Governor'; + // const version = '1'; + const tokenName = 'MockToken'; + const tokenSymbol = 'MTKN'; + const tokenSupply = web3.utils.toWei('100'); + const proposalThreshold = web3.utils.toWei('10'); + + beforeEach(async function () { + const [ deployer ] = await web3.eth.getAccounts(); + + this.token = await Token.new(tokenName, tokenSymbol); + + // Need to predict governance address to set it as timelock admin with a delayed transfer + const nonce = await web3.eth.getTransactionCount(deployer); + const predictGovernor = makeContractAddress(deployer, nonce + 1); + + this.timelock = await Timelock.new(predictGovernor, 2 * 86400); + this.mock = await Governor.new(name, this.token.address, 4, 16, proposalThreshold, this.timelock.address); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, tokenSupply); + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + + await this.token.transfer(proposer, proposalThreshold, { from: owner }); + await this.token.delegate(proposer, { from: proposer }); + }); + + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0'); + expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo'); + }); + + describe('nominal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], // targets + [ web3.utils.toWei('0') ], // values + [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas + '', // description + ], + proposer, + tokenHolder: owner, + voters: [ + { + voter: voter1, + weight: web3.utils.toWei('1'), + support: Enums.VoteType.Abstain, + }, + { + voter: voter2, + weight: web3.utils.toWei('10'), + support: Enums.VoteType.For, + }, + { + voter: voter3, + weight: web3.utils.toWei('5'), + support: Enums.VoteType.Against, + }, + { + voter: voter4, + support: '100', + error: 'GovernorCompatibilityBravo: invalid vote type', + }, + { + voter: voter1, + support: Enums.VoteType.For, + error: 'GovernorCompatibilityBravo: vote already casted', + skip: true, + }, + ], + steps: { + queue: { delay: 7 * 86400 }, + }, + }; + this.votingDelay = await this.mock.votingDelay(); + this.votingPeriod = await this.mock.votingPeriod(); + this.receipts = {}; + }); + afterEach(async function () { + const proposal = await this.mock.proposals(this.id); + expect(proposal.id).to.be.bignumber.equal(this.id); + expect(proposal.proposer).to.be.equal(proposer); + expect(proposal.eta).to.be.bignumber.equal(this.eta); + expect(proposal.startBlock).to.be.bignumber.equal(this.snapshot); + expect(proposal.endBlock).to.be.bignumber.equal(this.deadline); + expect(proposal.canceled).to.be.equal(false); + expect(proposal.executed).to.be.equal(true); + + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(proposal[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { weight }) => acc.add(new BN(weight)), + new BN('0'), + ), + ); + } + + const action = await this.mock.getActions(this.id); + expect(action.targets).to.be.deep.equal(this.settings.proposal[0]); + // expect(action.values).to.be.deep.equal(this.settings.proposal[1]); + expect(action.signatures).to.be.deep.equal(Array(this.settings.proposal[2].length).fill('')); + expect(action.calldatas).to.be.deep.equal(this.settings.proposal[2]); + + for (const voter of this.settings.voters.filter(({ skip }) => !skip)) { + expect(await this.mock.hasVoted(this.id, voter.voter)).to.be.equal(voter.error === undefined); + + const receipt = await this.mock.getReceipt(this.id, voter.voter); + expect(receipt.hasVoted).to.be.equal(voter.error === undefined); + expect(receipt.support).to.be.bignumber.equal(voter.error === undefined ? voter.support : '0'); + expect(receipt.votes).to.be.bignumber.equal(voter.error === undefined ? voter.weight : '0'); + } + + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { + proposalId: this.id, + proposer, + targets: this.settings.proposal[0], + // values: this.settings.proposal[1].map(value => new BN(value)), + signatures: this.settings.proposal[2].map(() => ''), + calldatas: this.settings.proposal[2], + startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay), + endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod), + description: this.settings.proposal[3], + }, + ); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.receiver, + 'MockFunctionCalled', + ); + }); + runGovernorWorkflow(); + }); + + describe('proposalThreshold not reached', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], // targets + [ web3.utils.toWei('0') ], // values + [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas + '', // description + ], + proposer: other, + steps: { + propose: { error: 'GovernorCompatibilityBravo: proposer votes below proposal threshold' }, + wait: { enable: false }, + queue: { enable: false }, + execute: { enable: false }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('with compatibility interface', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], // targets + [ web3.utils.toWei('0') ], // values + [ '' ], // signatures + [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas + '', // description + ], + proposer, + tokenHolder: owner, + voters: [ + { + voter: voter1, + weight: web3.utils.toWei('1'), + support: Enums.VoteType.Abstain, + }, + { + voter: voter2, + weight: web3.utils.toWei('10'), + support: Enums.VoteType.For, + }, + { + voter: voter3, + weight: web3.utils.toWei('5'), + support: Enums.VoteType.Against, + }, + { + voter: voter4, + support: '100', + error: 'GovernorCompatibilityBravo: invalid vote type', + }, + { + voter: voter1, + support: Enums.VoteType.For, + error: 'GovernorCompatibilityBravo: vote already casted', + skip: true, + }, + ], + steps: { + queue: { delay: 7 * 86400 }, + }, + }; + this.votingDelay = await this.mock.votingDelay(); + this.votingPeriod = await this.mock.votingPeriod(); + this.receipts = {}; + }); + + afterEach(async function () { + const proposal = await this.mock.proposals(this.id); + expect(proposal.id).to.be.bignumber.equal(this.id); + expect(proposal.proposer).to.be.equal(proposer); + expect(proposal.eta).to.be.bignumber.equal(this.eta); + expect(proposal.startBlock).to.be.bignumber.equal(this.snapshot); + expect(proposal.endBlock).to.be.bignumber.equal(this.deadline); + expect(proposal.canceled).to.be.equal(false); + expect(proposal.executed).to.be.equal(true); + + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(proposal[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { weight }) => acc.add(new BN(weight)), + new BN('0'), + ), + ); + } + + const action = await this.mock.getActions(this.id); + expect(action.targets).to.be.deep.equal(this.settings.proposal[0]); + // expect(action.values).to.be.deep.equal(this.settings.proposal[1]); + expect(action.signatures).to.be.deep.equal(this.settings.proposal[2]); + expect(action.calldatas).to.be.deep.equal(this.settings.proposal[3]); + + for (const voter of this.settings.voters.filter(({ skip }) => !skip)) { + expect(await this.mock.hasVoted(this.id, voter.voter)).to.be.equal(voter.error === undefined); + + const receipt = await this.mock.getReceipt(this.id, voter.voter); + expect(receipt.hasVoted).to.be.equal(voter.error === undefined); + expect(receipt.support).to.be.bignumber.equal(voter.error === undefined ? voter.support : '0'); + expect(receipt.votes).to.be.bignumber.equal(voter.error === undefined ? voter.weight : '0'); + } + + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { + proposalId: this.id, + proposer, + targets: this.settings.proposal[0], + // values: this.settings.proposal[1].map(value => new BN(value)), + signatures: this.settings.proposal[2], + calldatas: this.settings.proposal[3], + startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay), + endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod), + description: this.settings.proposal[4], + }, + ); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.receiver, + 'MockFunctionCalled', + ); + }); + + it('run', async function () { + // transfer tokens + if (tryGet(this.settings, 'voters')) { + for (const voter of this.settings.voters) { + if (voter.weight) { + await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); + } + } + } + + // propose + if (this.mock.propose && tryGet(this.settings, 'steps.propose.enable') !== false) { + this.receipts.propose = await getReceiptOrRevert( + this.mock.methods['propose(address[],uint256[],string[],bytes[],string)']( + ...this.settings.proposal, + { from: this.settings.proposer }, + ), + tryGet(this.settings, 'steps.propose.error'), + ); + + if (tryGet(this.settings, 'steps.propose.error') === undefined) { + this.id = this.receipts.propose.logs.find(({ event }) => event === 'ProposalCreated').args.proposalId; + this.snapshot = await this.mock.proposalSnapshot(this.id); + this.deadline = await this.mock.proposalDeadline(this.id); + } + + if (tryGet(this.settings, 'steps.propose.delay')) { + await time.increase(tryGet(this.settings, 'steps.propose.delay')); + } + + if ( + tryGet(this.settings, 'steps.propose.error') === undefined && + tryGet(this.settings, 'steps.propose.noadvance') !== true + ) { + await time.advanceBlockTo(this.snapshot); + } + } + + // vote + if (tryGet(this.settings, 'voters')) { + this.receipts.castVote = []; + for (const voter of this.settings.voters) { + if (!voter.signature) { + this.receipts.castVote.push( + await getReceiptOrRevert( + this.mock.castVote(this.id, voter.support, { from: voter.voter }), + voter.error, + ), + ); + } else { + const { v, r, s } = await voter.signature({ proposalId: this.id, support: voter.support }); + this.receipts.castVote.push( + await getReceiptOrRevert( + this.mock.castVoteBySig(this.id, voter.support, v, r, s), + voter.error, + ), + ); + } + if (tryGet(voter, 'delay')) { + await time.increase(tryGet(voter, 'delay')); + } + } + } + + // fast forward + if (tryGet(this.settings, 'steps.wait.enable') !== false) { + await time.advanceBlockTo(this.deadline); + } + + // queue + if (this.mock.queue && tryGet(this.settings, 'steps.queue.enable') !== false) { + this.receipts.queue = await getReceiptOrRevert( + this.mock.methods['queue(uint256)'](this.id, { from: this.settings.queuer }), + tryGet(this.settings, 'steps.queue.error'), + ); + this.eta = await this.mock.proposalEta(this.id); + if (tryGet(this.settings, 'steps.queue.delay')) { + await time.increase(tryGet(this.settings, 'steps.queue.delay')); + } + } + + // execute + if (this.mock.execute && tryGet(this.settings, 'steps.execute.enable') !== false) { + this.receipts.execute = await getReceiptOrRevert( + this.mock.methods['execute(uint256)'](this.id, { from: this.settings.executer }), + tryGet(this.settings, 'steps.execute.error'), + ); + if (tryGet(this.settings, 'steps.execute.delay')) { + await time.increase(tryGet(this.settings, 'steps.execute.delay')); + } + } + }); + }); +}); diff --git a/test/governance/extensions/GovernorComp.test.js b/test/governance/extensions/GovernorComp.test.js new file mode 100644 index 00000000000..6761b16e409 --- /dev/null +++ b/test/governance/extensions/GovernorComp.test.js @@ -0,0 +1,87 @@ +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC20VotesCompMock'); +const Governor = artifacts.require('GovernorCompMock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorComp', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + // const version = '1'; + const tokenName = 'MockToken'; + const tokenSymbol = 'MTKN'; + const tokenSupply = web3.utils.toWei('100'); + + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address, 4, 16); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, tokenSupply); + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + }); + + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); + + describe('voting with comp token', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + { voter: voter2, weight: web3.utils.toWei('10'), 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 }, + ], + }; + }); + afterEach(async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { weight }) => acc.add(new BN(weight)), + new BN('0'), + ), + ); + } + }); + }); + runGovernorWorkflow(); + }); +}); diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js new file mode 100644 index 00000000000..6ac2dcaf571 --- /dev/null +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -0,0 +1,432 @@ +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const Enums = require('../../helpers/enums'); +const RLP = require('rlp'); + +const { + runGovernorWorkflow, +} = require('../GovernorWorkflow.behavior'); + +const { + shouldSupportInterfaces, +} = require('../../utils/introspection/SupportsInterface.behavior'); + +const Token = artifacts.require('ERC20VotesMock'); +const Timelock = artifacts.require('CompTimelock'); +const Governor = artifacts.require('GovernorTimelockCompoundMock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +function makeContractAddress (creator, nonce) { + return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([creator, nonce])).slice(12).substring(14)); +} + +contract('GovernorTimelockCompound', function (accounts) { + const [ admin, voter ] = accounts; + + const name = 'OZ-Governor'; + // const version = '1'; + const tokenName = 'MockToken'; + const tokenSymbol = 'MTKN'; + const tokenSupply = web3.utils.toWei('100'); + + beforeEach(async function () { + const [ deployer ] = await web3.eth.getAccounts(); + + this.token = await Token.new(tokenName, tokenSymbol); + + // Need to predict governance address to set it as timelock admin with a delayed transfer + const nonce = await web3.eth.getTransactionCount(deployer); + const predictGovernor = makeContractAddress(deployer, nonce + 1); + + this.timelock = await Timelock.new(predictGovernor, 2 * 86400); + this.mock = await Governor.new(name, this.token.address, 4, 16, this.timelock.address, 0); + this.receiver = await CallReceiver.new(); + await this.token.mint(voter, tokenSupply); + await this.token.delegate(voter, { from: voter }); + }); + + shouldSupportInterfaces([ + 'ERC165', + 'Governor', + 'GovernorTimelock', + ]); + + it('post deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + + expect(await this.mock.timelock()).to.be.equal(this.timelock.address); + expect(await this.timelock.admin()).to.be.equal(this.mock.address); + }); + + describe('nominal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 7 * 86400 }, + }, + }; + }); + afterEach(async function () { + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.queue, + 'ProposalQueued', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.queue.transactionHash, + this.timelock, + 'QueueTransaction', + { eta: this.eta }, + ); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.timelock, + 'ExecuteTransaction', + { eta: this.eta }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.receiver, + 'MockFunctionCalled', + ); + }); + runGovernorWorkflow(); + }); + + describe('not queued', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { enable: false }, + execute: { error: 'GovernorTimelockCompound: proposal not yet queued' }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + }); + runGovernorWorkflow(); + }); + + describe('to early', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + execute: { error: 'Timelock::executeTransaction: Transaction hasn\'t surpassed time lock' }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + }); + runGovernorWorkflow(); + }); + + describe('to late', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 30 * 86400 }, + execute: { error: 'Governor: proposal not successful' }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); + }); + runGovernorWorkflow(); + }); + + describe('deplicated underlying call', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + Array(2).fill(this.token.address), + Array(2).fill(web3.utils.toWei('0')), + Array(2).fill(this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI()), + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { + error: 'GovernorTimelockCompound: identical proposal action already queued', + }, + execute: { + error: 'GovernorTimelockCompound: proposal not yet queued', + }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('re-queue / re-execute', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 7 * 86400 }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); + + await expectRevert( + this.mock.queue(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + await expectRevert( + this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('cancel before queue prevents scheduling', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { enable: false }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + expectEvent( + await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'ProposalCanceled', + { proposalId: this.id }, + ); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevert( + this.mock.queue(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('cancel after queue prevents executing', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 7 * 86400 }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + const receipt = await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash); + expectEvent( + receipt, + 'ProposalCanceled', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + receipt.receipt.transactionHash, + this.timelock, + 'CancelTransaction', + ); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevert( + this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('updateTimelock', function () { + beforeEach(async function () { + this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400); + }); + + it('protected', async function () { + await expectRevert( + this.mock.updateTimelock(this.newTimelock.address), + 'Governor: onlyGovernance', + ); + }); + + describe('using workflow', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ + this.timelock.address, + this.mock.address, + ], + [ + web3.utils.toWei('0'), + web3.utils.toWei('0'), + ], + [ + this.timelock.contract.methods.setPendingAdmin(admin).encodeABI(), + this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 7 * 86400 }, + }, + }; + }); + afterEach(async function () { + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.execute, + 'TimelockChange', + { oldTimelock: this.timelock.address, newTimelock: this.newTimelock.address }, + ); + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + }); + runGovernorWorkflow(); + }); + }); + + describe('transfer timelock to new governor', function () { + beforeEach(async function () { + this.newGovernor = await Governor.new(name, this.token.address, 8, 32, this.timelock.address, 0); + }); + + describe('using workflow', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.timelock.address ], + [ web3.utils.toWei('0') ], + [ this.timelock.contract.methods.setPendingAdmin(this.newGovernor.address).encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 7 * 86400 }, + }, + }; + }); + afterEach(async function () { + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.timelock, + 'NewPendingAdmin', + { newPendingAdmin: this.newGovernor.address }, + ); + await this.newGovernor.__acceptAdmin(); + expect(await this.timelock.admin()).to.be.bignumber.equal(this.newGovernor.address); + }); + runGovernorWorkflow(); + }); + }); +}); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js new file mode 100644 index 00000000000..303a1b6441d --- /dev/null +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -0,0 +1,369 @@ +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('../GovernorWorkflow.behavior'); + +const { + shouldSupportInterfaces, +} = require('../../utils/introspection/SupportsInterface.behavior'); + +const Token = artifacts.require('ERC20VotesMock'); +const Timelock = artifacts.require('TimelockController'); +const Governor = artifacts.require('GovernorTimelockControlMock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorTimelockControl', function (accounts) { + const [ voter ] = accounts; + + const name = 'OZ-Governor'; + // const version = '1'; + const tokenName = 'MockToken'; + const tokenSymbol = 'MTKN'; + const tokenSupply = web3.utils.toWei('100'); + + beforeEach(async function () { + const [ deployer ] = await web3.eth.getAccounts(); + + this.token = await Token.new(tokenName, tokenSymbol); + this.timelock = await Timelock.new(3600, [], []); + this.mock = await Governor.new(name, this.token.address, 4, 16, this.timelock.address, 0); + this.receiver = await CallReceiver.new(); + // normal setup: governor is proposer, everyone is executor, timelock is its own admin + await this.timelock.grantRole(await this.timelock.PROPOSER_ROLE(), this.mock.address); + await this.timelock.grantRole(await this.timelock.EXECUTOR_ROLE(), constants.ZERO_ADDRESS); + await this.timelock.revokeRole(await this.timelock.TIMELOCK_ADMIN_ROLE(), deployer); + await this.token.mint(voter, tokenSupply); + await this.token.delegate(voter, { from: voter }); + }); + + shouldSupportInterfaces([ + 'ERC165', + 'Governor', + 'GovernorTimelock', + ]); + + it('post deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + + expect(await this.mock.timelock()).to.be.equal(this.timelock.address); + }); + + describe('nominal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 3600 }, + }, + }; + }); + afterEach(async function () { + const timelockid = await this.timelock.hashOperationBatch( + ...this.settings.proposal.slice(0, 3), + '0x0', + this.descriptionHash, + ); + + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.queue, + 'ProposalQueued', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.queue.transactionHash, + this.timelock, + 'CallScheduled', + { id: timelockid }, + ); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.timelock, + 'CallExecuted', + { id: timelockid }, + ); + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.receiver, + 'MockFunctionCalled', + ); + }); + runGovernorWorkflow(); + }); + + describe('executed by other proposer', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 3600 }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + await this.timelock.executeBatch( + ...this.settings.proposal.slice(0, 3), + '0x0', + this.descriptionHash, + ); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); + + await expectRevert( + this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('not queued', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { enable: false }, + execute: { error: 'TimelockController: operation is not ready' }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + }); + runGovernorWorkflow(); + }); + + describe('to early', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + execute: { error: 'TimelockController: operation is not ready' }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + }); + runGovernorWorkflow(); + }); + + describe('re-queue / re-execute', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 3600 }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); + + await expectRevert( + this.mock.queue(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + await expectRevert( + this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('cancel before queue prevents scheduling', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { enable: false }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + expectEvent( + await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'ProposalCanceled', + { proposalId: this.id }, + ); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevert( + this.mock.queue(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('cancel after queue prevents execution', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 3600 }, + execute: { enable: false }, + }, + }; + }); + afterEach(async function () { + const timelockid = await this.timelock.hashOperationBatch( + ...this.settings.proposal.slice(0, 3), + '0x0', + this.descriptionHash, + ); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + const receipt = await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash); + expectEvent( + receipt, + 'ProposalCanceled', + { proposalId: this.id }, + ); + await expectEvent.inTransaction( + receipt.receipt.transactionHash, + this.timelock, + 'Cancelled', + { id: timelockid }, + ); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevert( + this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), + 'Governor: proposal not successful', + ); + }); + runGovernorWorkflow(); + }); + + describe('updateTimelock', function () { + beforeEach(async function () { + this.newTimelock = await Timelock.new(3600, [], []); + }); + + it('protected', async function () { + await expectRevert( + this.mock.updateTimelock(this.newTimelock.address), + 'Governor: onlyGovernance', + ); + }); + + describe('using workflow', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.mock.address ], + [ web3.utils.toWei('0') ], + [ this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI() ], + '', + ], + voters: [ + { voter: voter, support: Enums.VoteType.For }, + ], + steps: { + queue: { delay: 3600 }, + }, + }; + }); + afterEach(async function () { + expectEvent( + this.receipts.propose, + 'ProposalCreated', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.execute, + 'ProposalExecuted', + { proposalId: this.id }, + ); + expectEvent( + this.receipts.execute, + 'TimelockChange', + { oldTimelock: this.timelock.address, newTimelock: this.newTimelock.address }, + ); + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + }); + runGovernorWorkflow(); + }); + }); +}); diff --git a/test/governance/extensions/GovernorWeightQuorumFraction.test.js b/test/governance/extensions/GovernorWeightQuorumFraction.test.js new file mode 100644 index 00000000000..4d3f90dad7e --- /dev/null +++ b/test/governance/extensions/GovernorWeightQuorumFraction.test.js @@ -0,0 +1,122 @@ +const { BN, expectEvent, time } = require('@openzeppelin/test-helpers'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC20VotesMock'); +const Governor = artifacts.require('GovernorMock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorVotesQuorumFraction', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + // const version = '1'; + const tokenName = 'MockToken'; + const tokenSymbol = 'MTKN'; + const tokenSupply = new BN(web3.utils.toWei('100')); + const ratio = new BN(8); // percents + const newRatio = new BN(6); // percents + + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address, 4, 16, ratio); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, tokenSupply); + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + }); + + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(ratio); + expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))) + .to.be.bignumber.equal(tokenSupply.mul(ratio).divn(100)); + }); + + describe('quroum not reached', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For }, + ], + steps: { + execute: { error: 'Governor: proposal not successful' }, + }, + }; + }); + runGovernorWorkflow(); + }); + + describe('update quorum ratio through proposal', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.mock.address ], + [ web3.utils.toWei('0') ], + [ this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: tokenSupply, support: Enums.VoteType.For }, + ], + }; + }); + afterEach(async function () { + await expectEvent.inTransaction( + this.receipts.execute.transactionHash, + this.mock, + 'QuorumNumeratorUpdated', + { + oldQuorumNumerator: ratio, + newQuorumNumerator: newRatio, + }, + ); + + expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio); + expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))) + .to.be.bignumber.equal(tokenSupply.mul(newRatio).divn(100)); + }); + runGovernorWorkflow(); + }); + + describe('update quorum over the maximum', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.mock.address ], + [ web3.utils.toWei('0') ], + [ this.mock.contract.methods.updateQuorumNumerator(new BN(101)).encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, weight: tokenSupply, support: Enums.VoteType.For }, + ], + steps: { + execute: { error: 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator' }, + }, + }; + }); + runGovernorWorkflow(); + }); +}); diff --git a/test/helpers/enums.js b/test/helpers/enums.js new file mode 100644 index 00000000000..de5cdc5708e --- /dev/null +++ b/test/helpers/enums.js @@ -0,0 +1,24 @@ +const { BN } = require('@openzeppelin/test-helpers'); + +function Enum (...options) { + return Object.fromEntries(options.map((key, i) => [ key, new BN(i) ])); +} + +module.exports = { + Enum, + ProposalState: Enum( + 'Pending', + 'Active', + 'Canceled', + 'Defeated', + 'Succeeded', + 'Queued', + 'Expired', + 'Executed', + ), + VoteType: Enum( + 'Against', + 'For', + 'Abstain', + ), +}; diff --git a/test/utils/TimersBlockNumberImpl.test.js b/test/utils/TimersBlockNumberImpl.test.js new file mode 100644 index 00000000000..d9f83d933b2 --- /dev/null +++ b/test/utils/TimersBlockNumberImpl.test.js @@ -0,0 +1,55 @@ +const { BN, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); + +const TimersBlockNumberImpl = artifacts.require('TimersBlockNumberImpl'); + +contract('TimersBlockNumber', function (accounts) { + beforeEach(async function () { + this.instance = await TimersBlockNumberImpl.new(); + this.now = await web3.eth.getBlock('latest').then(({ number }) => number); + }); + + it('unset', async function () { + expect(await this.instance.getDeadline()).to.be.bignumber.equal('0'); + expect(await this.instance.isUnset()).to.be.equal(true); + expect(await this.instance.isStarted()).to.be.equal(false); + expect(await this.instance.isPending()).to.be.equal(false); + expect(await this.instance.isExpired()).to.be.equal(false); + }); + + it('pending', async function () { + await this.instance.setDeadline(this.now + 3); + expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 3)); + expect(await this.instance.isUnset()).to.be.equal(false); + expect(await this.instance.isStarted()).to.be.equal(true); + expect(await this.instance.isPending()).to.be.equal(true); + expect(await this.instance.isExpired()).to.be.equal(false); + }); + + it('expired', async function () { + await this.instance.setDeadline(this.now - 3); + expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 3)); + expect(await this.instance.isUnset()).to.be.equal(false); + expect(await this.instance.isStarted()).to.be.equal(true); + expect(await this.instance.isPending()).to.be.equal(false); + expect(await this.instance.isExpired()).to.be.equal(true); + }); + + it('reset', async function () { + await this.instance.reset(); + expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0)); + expect(await this.instance.isUnset()).to.be.equal(true); + expect(await this.instance.isStarted()).to.be.equal(false); + expect(await this.instance.isPending()).to.be.equal(false); + expect(await this.instance.isExpired()).to.be.equal(false); + }); + + it('fast forward', async function () { + await this.instance.setDeadline(this.now + 3); + expect(await this.instance.isPending()).to.be.equal(true); + expect(await this.instance.isExpired()).to.be.equal(false); + await time.advanceBlockTo(this.now + 3); + expect(await this.instance.isPending()).to.be.equal(false); + expect(await this.instance.isExpired()).to.be.equal(true); + }); +}); diff --git a/test/utils/TimersTimestamp.test.js b/test/utils/TimersTimestamp.test.js new file mode 100644 index 00000000000..b08118d4f00 --- /dev/null +++ b/test/utils/TimersTimestamp.test.js @@ -0,0 +1,55 @@ +const { BN, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); + +const TimersTimestampImpl = artifacts.require('TimersTimestampImpl'); + +contract('TimersTimestamp', function (accounts) { + beforeEach(async function () { + this.instance = await TimersTimestampImpl.new(); + this.now = await web3.eth.getBlock('latest').then(({ timestamp }) => timestamp); + }); + + it('unset', async function () { + expect(await this.instance.getDeadline()).to.be.bignumber.equal('0'); + expect(await this.instance.isUnset()).to.be.equal(true); + expect(await this.instance.isStarted()).to.be.equal(false); + expect(await this.instance.isPending()).to.be.equal(false); + expect(await this.instance.isExpired()).to.be.equal(false); + }); + + it('pending', async function () { + await this.instance.setDeadline(this.now + 100); + expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 100)); + expect(await this.instance.isUnset()).to.be.equal(false); + expect(await this.instance.isStarted()).to.be.equal(true); + expect(await this.instance.isPending()).to.be.equal(true); + expect(await this.instance.isExpired()).to.be.equal(false); + }); + + it('expired', async function () { + await this.instance.setDeadline(this.now - 100); + expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 100)); + expect(await this.instance.isUnset()).to.be.equal(false); + expect(await this.instance.isStarted()).to.be.equal(true); + expect(await this.instance.isPending()).to.be.equal(false); + expect(await this.instance.isExpired()).to.be.equal(true); + }); + + it('reset', async function () { + await this.instance.reset(); + expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0)); + expect(await this.instance.isUnset()).to.be.equal(true); + expect(await this.instance.isStarted()).to.be.equal(false); + expect(await this.instance.isPending()).to.be.equal(false); + expect(await this.instance.isExpired()).to.be.equal(false); + }); + + it('fast forward', async function () { + await this.instance.setDeadline(this.now + 100); + expect(await this.instance.isPending()).to.be.equal(true); + expect(await this.instance.isExpired()).to.be.equal(false); + await time.increaseTo(this.now + 100); + expect(await this.instance.isPending()).to.be.equal(false); + expect(await this.instance.isExpired()).to.be.equal(true); + }); +}); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 1be01c42634..8ffe78f24f2 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -50,6 +50,30 @@ const INTERFACES = { 'getRoleMember(bytes32,uint256)', 'getRoleMemberCount(bytes32)', ], + Governor: [ + 'name()', + 'version()', + 'COUNTING_MODE()', + 'hashProposal(address[],uint256[],bytes[],bytes32)', + 'state(uint256)', + 'proposalSnapshot(uint256)', + 'proposalDeadline(uint256)', + 'votingDelay()', + 'votingPeriod()', + 'quorum(uint256)', + 'getVotes(address,uint256)', + 'hasVoted(uint256,address)', + 'propose(address[],uint256[],bytes[],string)', + 'execute(address[],uint256[],bytes[],bytes32)', + 'castVote(uint256,uint8)', + 'castVoteWithReason(uint256,uint8,string)', + 'castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)', + ], + GovernorTimelock: [ + 'timelock()', + 'proposalEta(uint256)', + 'queue(address[],uint256[],bytes[],bytes32)', + ], }; const INTERFACE_IDS = {};