diff --git a/CHANGELOG.md b/CHANGELOG.md index 950e6b24926..365c4014639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ * `ERC1155`: Add a `_afterTokenTransfer` hook for improved extensibility. ([#3166](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3166)) * `DoubleEndedQueue`: a new data structure that supports efficient push and pop to both front and back, useful for FIFO and LIFO queues. ([#3153](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3153)) * `Governor`: improved security of `onlyGovernance` modifier when using an external executor contract (e.g. a timelock) that can operate without necessarily going through the governance protocol. ([#3147](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3147)) + * `Governor`: Add a way to parameterize votes. This can be used to implement voting systems such as fractionalized voting, ERC721 based voting, or any number of other systems. The `params` argument added to `_countVote` method, and included in the newly added `_getVotes` method, can be used by counting and voting modules respectively for such purposes. + +### Breaking changes + +* `Governor`: Adds internal virtual `_getVotes` method that must be implemented; this is a breaking change for existing concrete extensions to `Governor`. To fix this on an existing voting module extension, rename `getVotes` to `_getVotes` and add a `bytes memory` argument. ([#3043](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3043)) +* `Governor`: Adds `params` parameter to internal virtual `_countVote ` method; this is a breaking change for existing concrete extensions to `Governor`. To fix this on an existing counting module extension, add a `bytes memory` argument to `_countVote`. ([#3043](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3043)) +* `Governor`: Does not emit `VoteCast` event when params data is non-empty; instead emits `VoteCastWithParams` event. To fix this on an integration that consumes the `VoteCast` event, also fetch/monitor `VoteCastWithParams` events. ([#3043](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3043)) ## 4.5.0 (2022-02-09) diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 0da3f11706a..8907d9468d5 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -19,7 +19,7 @@ import "./IGovernor.sol"; * This contract is abstract and requires several function to be implemented in various modules: * * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote} - * - A voting module must implement {getVotes} + * - A voting module must implement {_getVotes} * - Additionanly, the {votingPeriod} must also be implemented * * _Available since v4.3._ @@ -30,6 +30,8 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor { using Timers for Timers.BlockNumber; bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); + bytes32 public constant EXTENDED_BALLOT_TYPEHASH = + keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)"); struct ProposalCore { Timers.BlockNumber voteStart; @@ -86,7 +88,16 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor { * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { - return interfaceId == type(IGovernor).interfaceId || super.supportsInterface(interfaceId); + // In addition to the current interfaceId, also support previous version of the interfaceId that did not + // include the castVoteWithReasonAndParams() function as standard + return + interfaceId == + (type(IGovernor).interfaceId ^ + this.castVoteWithReasonAndParams.selector ^ + this.castVoteWithReasonAndParamsBySig.selector ^ + this.getVotesWithParams.selector) || + interfaceId == type(IGovernor).interfaceId || + super.supportsInterface(interfaceId); } /** @@ -193,6 +204,15 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor { */ function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool); + /** + * @dev Get the voting weight of `account` at a specific `blockNumber`, for a vote as described by `params`. + */ + function _getVotes( + address account, + uint256 blockNumber, + bytes memory params + ) internal view virtual returns (uint256); + /** * @dev Register a vote with a given support and voting weight. * @@ -202,9 +222,20 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor { uint256 proposalId, address account, uint8 support, - uint256 weight + uint256 weight, + bytes memory params ) internal virtual; + /** + * @dev Default additional encoded parameters used by castVote methods that don't include them + * + * Note: Should be overriden by specific implementations to use an appropriate value, the + * meaning of the additional params, in the context of that implementation + */ + function _defaultParams() internal view virtual returns (bytes memory) { + return ""; + } + /** * @dev See {IGovernor-propose}. */ @@ -355,6 +386,24 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor { return proposalId; } + /** + * @dev See {IGovernor-getVotes}. + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return _getVotes(account, blockNumber, _defaultParams()); + } + + /** + * @dev See {IGovernor-getVotesWithParams}. + */ + function getVotesWithParams( + address account, + uint256 blockNumber, + bytes memory params + ) public view virtual override returns (uint256) { + return _getVotes(account, blockNumber, params); + } + /** * @dev See {IGovernor-castVote}. */ @@ -375,6 +424,19 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor { return _castVote(proposalId, voter, support, reason); } + /** + * @dev See {IGovernor-castVoteWithReasonAndParams}. + */ + function castVoteWithReasonAndParams( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params + ) public virtual override returns (uint256) { + address voter = _msgSender(); + return _castVote(proposalId, voter, support, reason, params); + } + /** * @dev See {IGovernor-castVoteBySig}. */ @@ -394,9 +456,41 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor { return _castVote(proposalId, voter, support, ""); } + /** + * @dev See {IGovernor-castVoteWithReasonAndParamsBySig}. + */ + function castVoteWithReasonAndParamsBySig( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override returns (uint256) { + address voter = ECDSA.recover( + _hashTypedDataV4( + keccak256( + abi.encode( + EXTENDED_BALLOT_TYPEHASH, + proposalId, + support, + keccak256(bytes(reason)), + keccak256(params) + ) + ) + ), + v, + r, + s + ); + + return _castVote(proposalId, voter, support, reason, params); + } + /** * @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been cast yet, retrieve - * voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. + * voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. Uses the _defaultParams(). * * Emits a {IGovernor-VoteCast} event. */ @@ -405,14 +499,34 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor { address account, uint8 support, string memory reason + ) internal virtual returns (uint256) { + return _castVote(proposalId, account, support, reason, _defaultParams()); + } + + /** + * @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been cast 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, + bytes memory params ) 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); + uint256 weight = _getVotes(account, proposal.voteStart.getDeadline(), params); + _countVote(proposalId, account, support, weight, params); - emit VoteCast(account, proposalId, support, weight, reason); + if (params.length == 0) { + emit VoteCast(account, proposalId, support, weight, reason); + } else { + emit VoteCastWithParams(account, proposalId, support, weight, reason, params); + } return weight; } diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 3c65f40daae..eaf443a97d1 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -48,12 +48,27 @@ abstract contract IGovernor is IERC165 { event ProposalExecuted(uint256 proposalId); /** - * @dev Emitted when a vote is cast. + * @dev Emitted when a vote is cast without params. * - * Note: `support` values should be seen as buckets. There interpretation depends on the voting module used. + * Note: `support` values should be seen as buckets. Their interpretation depends on the voting module used. */ event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason); + /** + * @dev Emitted when a vote is cast with params. + * + * Note: `support` values should be seen as buckets. Their interpretation depends on the voting module used. + * `params` are additional encoded parameters. Their intepepretation also depends on the voting module used. + */ + event VoteCastWithParams( + address indexed voter, + uint256 proposalId, + uint8 support, + uint256 weight, + string reason, + bytes params + ); + /** * @notice module:core * @dev Name of the governor instance (used in building the ERC712 domain separator). @@ -78,6 +93,12 @@ abstract contract IGovernor is IERC165 { * - `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. * + * If a counting module makes use of encoded `params`, it should include this under a `params` key with a unique + * name that describes the behavior. For example: + * + * - `params=fractional` might refer to a scheme where votes are divided fractionally between for/against/abstain. + * - `params=erc721` might refer to a scheme where specific NFTs are delegated to vote. + * * NOTE: The string can be decoded by the standard * https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`] * JavaScript class. @@ -151,6 +172,16 @@ abstract contract IGovernor is IERC165 { */ function getVotes(address account, uint256 blockNumber) public view virtual returns (uint256); + /** + * @notice module:reputation + * @dev Voting power of an `account` at a specific `blockNumber` given additional encoded parameters. + */ + function getVotesWithParams( + address account, + uint256 blockNumber, + bytes memory params + ) public view virtual returns (uint256); + /** * @notice module:voting * @dev Returns weither `account` has cast a vote on `proposalId`. @@ -204,7 +235,19 @@ abstract contract IGovernor is IERC165 { ) public virtual returns (uint256 balance); /** - * @dev Cast a vote using the user cryptographic signature. + * @dev Cast a vote with a reason and additional encoded parameters + * + * Emits a {VoteCast} event. + */ + function castVoteWithReasonAndParams( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params + ) public virtual returns (uint256 balance); + + /** + * @dev Cast a vote using the user's cryptographic signature. * * Emits a {VoteCast} event. */ @@ -215,4 +258,19 @@ abstract contract IGovernor is IERC165 { bytes32 r, bytes32 s ) public virtual returns (uint256 balance); + + /** + * @dev Cast a vote with a reason and additional encoded parameters using the user's cryptographic signature. + * + * Emits a {VoteCast} event. + */ + function castVoteWithReasonAndParamsBySig( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual returns (uint256 balance); } diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index cbb2200b5f6..1d639f436a0 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -265,7 +265,8 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp uint256 proposalId, address account, uint8 support, - uint256 weight + uint256 weight, + bytes memory // params ) internal virtual override { ProposalDetails storage details = _proposalDetails[proposalId]; Receipt storage receipt = details.receipts[account]; diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index 38054d91789..a262ce4d836 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -86,7 +86,8 @@ abstract contract GovernorCountingSimple is Governor { uint256 proposalId, address account, uint8 support, - uint256 weight + uint256 weight, + bytes memory // params ) internal virtual override { ProposalVote storage proposalvote = _proposalVotes[proposalId]; diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index 6d96dc33d26..5b58f6032bf 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -57,9 +57,10 @@ abstract contract GovernorPreventLateQuorum is Governor { uint256 proposalId, address account, uint8 support, - string memory reason + string memory reason, + bytes memory params ) internal virtual override returns (uint256) { - uint256 result = super._castVote(proposalId, account, support, reason); + uint256 result = super._castVote(proposalId, account, support, reason, params); Timers.BlockNumber storage extendedDeadline = _extendedDeadlines[proposalId]; diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index 75f56356f3c..f0a2276fb38 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -19,9 +19,13 @@ abstract contract GovernorVotes is Governor { } /** - * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + * Read the voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}). */ - function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + function _getVotes( + address account, + uint256 blockNumber, + bytes memory /*params*/ + ) internal view virtual override returns (uint256) { return token.getPastVotes(account, blockNumber); } } diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol index 9eb87a35fa4..c31c9583b5a 100644 --- a/contracts/governance/extensions/GovernorVotesComp.sol +++ b/contracts/governance/extensions/GovernorVotesComp.sol @@ -19,9 +19,13 @@ abstract contract GovernorVotesComp is Governor { } /** - * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + * Read the voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}). */ - function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + function _getVotes( + address account, + uint256 blockNumber, + bytes memory /*params*/ + ) internal view virtual override returns (uint256) { return token.getPriorVotes(account, blockNumber); } } diff --git a/contracts/mocks/GovernorCompMock.sol b/contracts/mocks/GovernorCompMock.sol index 9dcbc536d0e..c2d8733e032 100644 --- a/contracts/mocks/GovernorCompMock.sol +++ b/contracts/mocks/GovernorCompMock.sol @@ -28,14 +28,4 @@ contract GovernorCompMock is GovernorVotesComp, GovernorCountingSimple { ) public returns (uint256 proposalId) { return _cancel(targets, values, calldatas, salt); } - - function getVotes(address account, uint256 blockNumber) - public - view - virtual - override(IGovernor, GovernorVotesComp) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } } diff --git a/contracts/mocks/GovernorCompatibilityBravoMock.sol b/contracts/mocks/GovernorCompatibilityBravoMock.sol index 60afbb918e9..8f295f62faa 100644 --- a/contracts/mocks/GovernorCompatibilityBravoMock.sol +++ b/contracts/mocks/GovernorCompatibilityBravoMock.sol @@ -124,16 +124,6 @@ contract GovernorCompatibilityBravoMock is 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 index 85233f55951..dafb0a0a7a5 100644 --- a/contracts/mocks/GovernorMock.sol +++ b/contracts/mocks/GovernorMock.sol @@ -35,16 +35,6 @@ contract GovernorMock is return _cancel(targets, values, calldatas, salt); } - function getVotes(address account, uint256 blockNumber) - public - view - virtual - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } - function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { return super.proposalThreshold(); } diff --git a/contracts/mocks/GovernorPreventLateQuorumMock.sol b/contracts/mocks/GovernorPreventLateQuorumMock.sol index 7de50a01f94..35bddc03b42 100644 --- a/contracts/mocks/GovernorPreventLateQuorumMock.sol +++ b/contracts/mocks/GovernorPreventLateQuorumMock.sol @@ -53,8 +53,9 @@ contract GovernorPreventLateQuorumMock is uint256 proposalId, address account, uint8 support, - string memory reason + string memory reason, + bytes memory params ) internal virtual override(Governor, GovernorPreventLateQuorum) returns (uint256) { - return super._castVote(proposalId, account, support, reason); + return super._castVote(proposalId, account, support, reason, params); } } diff --git a/contracts/mocks/GovernorTimelockCompoundMock.sol b/contracts/mocks/GovernorTimelockCompoundMock.sol index aeba5b86a80..88a8829280c 100644 --- a/contracts/mocks/GovernorTimelockCompoundMock.sol +++ b/contracts/mocks/GovernorTimelockCompoundMock.sol @@ -92,16 +92,6 @@ contract GovernorTimelockCompoundMock is return super._cancel(targets, values, calldatas, salt); } - function getVotes(address account, uint256 blockNumber) - public - view - virtual - override(IGovernor, 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 index 6150b7b3c6d..455eac9bacc 100644 --- a/contracts/mocks/GovernorTimelockControlMock.sol +++ b/contracts/mocks/GovernorTimelockControlMock.sol @@ -92,16 +92,6 @@ contract GovernorTimelockControlMock is return super._cancel(targets, values, calldatas, descriptionHash); } - function getVotes(address account, uint256 blockNumber) - public - view - virtual - override(IGovernor, 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/GovernorVoteMock.sol b/contracts/mocks/GovernorVoteMock.sol index 23ccf6bc09c..60a3d41355e 100644 --- a/contracts/mocks/GovernorVoteMock.sol +++ b/contracts/mocks/GovernorVoteMock.sol @@ -28,14 +28,4 @@ contract GovernorVoteMocks is GovernorVotes, GovernorCountingSimple { ) public returns (uint256 proposalId) { return _cancel(targets, values, calldatas, salt); } - - function getVotes(address account, uint256 blockNumber) - public - view - virtual - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } } diff --git a/contracts/mocks/GovernorWithParamsMock.sol b/contracts/mocks/GovernorWithParamsMock.sol new file mode 100644 index 00000000000..35eb7ade25a --- /dev/null +++ b/contracts/mocks/GovernorWithParamsMock.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotes.sol"; + +contract GovernorWithParamsMock is GovernorVotes, GovernorCountingSimple { + event CountParams(uint256 uintParam, string strParam); + + constructor(string memory name_, IVotes token_) Governor(name_) GovernorVotes(token_) {} + + function quorum(uint256) public pure override returns (uint256) { + return 0; + } + + function votingDelay() public pure override returns (uint256) { + return 4; + } + + function votingPeriod() public pure override returns (uint256) { + return 16; + } + + function _getVotes( + address account, + uint256 blockNumber, + bytes memory params + ) internal view virtual override(Governor, GovernorVotes) returns (uint256) { + uint256 reduction = 0; + // If the user provides parameters, we reduce the voting weight by the amount of the integer param + if (params.length > 0) { + (reduction, ) = abi.decode(params, (uint256, string)); + } + // reverts on overflow + return super._getVotes(account, blockNumber, params) - reduction; + } + + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + bytes memory params + ) internal virtual override(Governor, GovernorCountingSimple) { + if (params.length > 0) { + (uint256 _uintParam, string memory _strParam) = abi.decode(params, (uint256, string)); + emit CountParams(_uintParam, _strParam); + } + return super._countVote(proposalId, account, support, weight, params); + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } +} diff --git a/contracts/mocks/wizard/MyGovernor1.sol b/contracts/mocks/wizard/MyGovernor1.sol index bd524ee55a2..a80d8400c5e 100644 --- a/contracts/mocks/wizard/MyGovernor1.sol +++ b/contracts/mocks/wizard/MyGovernor1.sol @@ -40,15 +40,6 @@ contract MyGovernor1 is return super.quorum(blockNumber); } - function getVotes(address account, uint256 blockNumber) - public - view - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } - function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { return super.state(proposalId); } diff --git a/contracts/mocks/wizard/MyGovernor2.sol b/contracts/mocks/wizard/MyGovernor2.sol index 3a5c983e0b3..34c608c5cc2 100644 --- a/contracts/mocks/wizard/MyGovernor2.sol +++ b/contracts/mocks/wizard/MyGovernor2.sol @@ -46,15 +46,6 @@ contract MyGovernor2 is return super.quorum(blockNumber); } - function getVotes(address account, uint256 blockNumber) - public - view - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } - function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { return super.state(proposalId); } diff --git a/contracts/mocks/wizard/MyGovernor3.sol b/contracts/mocks/wizard/MyGovernor3.sol index 835a893a3a4..70e4e87f046 100644 --- a/contracts/mocks/wizard/MyGovernor3.sol +++ b/contracts/mocks/wizard/MyGovernor3.sol @@ -44,15 +44,6 @@ contract MyGovernor is return super.quorum(blockNumber); } - function getVotes(address account, uint256 blockNumber) - public - view - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } - function state(uint256 proposalId) public view diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 6017db0507b..7156abeabff 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -41,6 +41,7 @@ contract('Governor', function (accounts) { shouldSupportInterfaces([ 'ERC165', 'Governor', + 'GovernorWithParams', ]); it('deployment check', async function () { diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index bc4f15c99b5..e623c4571dd 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -48,6 +48,7 @@ contract('GovernorTimelockCompound', function (accounts) { shouldSupportInterfaces([ 'ERC165', 'Governor', + 'GovernorWithParams', 'GovernorTimelock', ]); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index 9efeb463518..ade1c545a7c 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -43,6 +43,7 @@ contract('GovernorTimelockControl', function (accounts) { shouldSupportInterfaces([ 'ERC165', 'Governor', + 'GovernorWithParams', 'GovernorTimelock', ]); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js new file mode 100644 index 00000000000..5b4ac8901d9 --- /dev/null +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -0,0 +1,238 @@ +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); +const { web3 } = require('@openzeppelin/test-helpers/src/setup'); +const Enums = require('../../helpers/enums'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; +const { EIP712Domain } = require('../../helpers/eip712'); +const { fromRpcSig } = require('ethereumjs-util'); + +const { runGovernorWorkflow } = require('../GovernorWorkflow.behavior'); +const { expect } = require('chai'); + +const Token = artifacts.require('ERC20VotesCompMock'); +const Governor = artifacts.require('GovernorWithParamsMock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorWithParams', 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'); + const votingDelay = new BN(4); + const votingPeriod = new BN(16); + + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.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 }); + }); + + 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(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + }); + + describe('nominal is unaffected', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [this.receiver.address], + [0], + [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('7'), support: Enums.VoteType.For }, + { voter: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against }, + { voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain }, + ], + }; + }); + + 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')), + ); + } + }); + + const startBlock = new BN(this.receipts.propose.blockNumber).add(votingDelay); + const endBlock = new BN(this.receipts.propose.blockNumber).add(votingDelay).add(votingPeriod); + expect(await this.mock.proposalSnapshot(this.id)).to.be.bignumber.equal(startBlock); + expect(await this.mock.proposalDeadline(this.id)).to.be.bignumber.equal(endBlock); + + 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, + endBlock, + description: this.settings.proposal[3], + }); + + this.receipts.castVote.filter(Boolean).forEach((vote) => { + const { voter } = vote.logs.filter(({ event }) => event === 'VoteCast').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('Voting with params is properly supported', function () { + const voter2Weight = web3.utils.toWei('1.0'); + beforeEach(async function () { + this.settings = { + proposal: [ + [this.receiver.address], + [0], + [this.receiver.contract.methods.mockFunction().encodeABI()], + '', + ], + proposer, + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('0.2'), support: Enums.VoteType.Against }, + { voter: voter2, weight: voter2Weight }, // do not actually vote, only getting tokenss + ], + steps: { + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + + const uintParam = new BN(1); + const strParam = 'These are my params'; + const reducedWeight = new BN(voter2Weight).sub(uintParam); + const params = web3.eth.abi.encodeParameters(['uint256', 'string'], [uintParam, strParam]); + const tx = await this.mock.castVoteWithReasonAndParams(this.id, Enums.VoteType.For, '', params, { from: voter2 }); + + expectEvent(tx, 'CountParams', { uintParam, strParam }); + expectEvent(tx, 'VoteCastWithParams', { voter: voter2, weight: reducedWeight, params }); + const votes = await this.mock.proposalVotes(this.id); + expect(votes.forVotes).to.be.bignumber.equal(reducedWeight); + }); + runGovernorWorkflow(); + }); + + describe('Voting with params by signature is properly supported', function () { + const voterBySig = Wallet.generate(); // generate voter by signature wallet + const sigVoterWeight = web3.utils.toWei('1.0'); + + beforeEach(async function () { + this.chainId = await web3.eth.getChainId(); + this.voter = web3.utils.toChecksumAddress(voterBySig.getAddressString()); + + // use delegateBySig to enable vote delegation sig voting 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: this.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); + + this.settings = { + proposal: [ + [this.receiver.address], + [0], + [this.receiver.contract.methods.mockFunction().encodeABI()], + '', + ], + proposer, + tokenHolder: owner, + voters: [ + { voter: voter1, weight: web3.utils.toWei('0.2'), support: Enums.VoteType.Against }, + { voter: this.voter, weight: sigVoterWeight }, // do not actually vote, only getting tokens + ], + steps: { + wait: { enable: false }, + execute: { enable: false }, + }, + }; + }); + + afterEach(async function () { + expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + + const reason = 'This is my reason'; + const uintParam = new BN(1); + const strParam = 'These are my params'; + const reducedWeight = new BN(sigVoterWeight).sub(uintParam); + const params = web3.eth.abi.encodeParameters(['uint256', 'string'], [uintParam, strParam]); + + // prepare signature for vote by signature + const { v, r, s } = fromRpcSig( + ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { + data: { + types: { + EIP712Domain, + ExtendedBallot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + { name: 'reason', type: 'string' }, + { name: 'params', type: 'bytes' }, + ], + }, + domain: { name, version, chainId: this.chainId, verifyingContract: this.mock.address }, + primaryType: 'ExtendedBallot', + message: { proposalId: this.id, support: Enums.VoteType.For, reason, params }, + }, + }), + ); + + const tx = await this.mock.castVoteWithReasonAndParamsBySig(this.id, Enums.VoteType.For, reason, params, v, r, s); + + expectEvent(tx, 'CountParams', { uintParam, strParam }); + expectEvent(tx, 'VoteCastWithParams', { voter: this.voter, weight: reducedWeight, params }); + const votes = await this.mock.proposalVotes(this.id); + expect(votes.forVotes).to.be.bignumber.equal(reducedWeight); + }); + runGovernorWorkflow(); + }); +}); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 4f29f0e00d9..a0aa3d7e50e 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -69,6 +69,28 @@ const INTERFACES = { 'castVoteWithReason(uint256,uint8,string)', 'castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)', ], + GovernorWithParams: [ + 'name()', + 'version()', + 'COUNTING_MODE()', + 'hashProposal(address[],uint256[],bytes[],bytes32)', + 'state(uint256)', + 'proposalSnapshot(uint256)', + 'proposalDeadline(uint256)', + 'votingDelay()', + 'votingPeriod()', + 'quorum(uint256)', + 'getVotes(address,uint256)', + 'getVotesWithParams(address,uint256,bytes)', + 'hasVoted(uint256,address)', + 'propose(address[],uint256[],bytes[],string)', + 'execute(address[],uint256[],bytes[],bytes32)', + 'castVote(uint256,uint8)', + 'castVoteWithReason(uint256,uint8,string)', + 'castVoteWithReasonAndParams(uint256,uint8,string,bytes)', + 'castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)', + 'castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32)', + ], GovernorTimelock: [ 'timelock()', 'proposalEta(uint256)',