Sfoglia il codice sorgente

Extend Governor with parameterized votes (#3043)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Ben DiFrancesco 3 anni fa
parent
commit
fd07cc6290

+ 7 - 0
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)
 

+ 121 - 7
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;
     }

+ 61 - 3
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);
 }

+ 2 - 1
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];

+ 2 - 1
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];
 

+ 3 - 2
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];
 

+ 6 - 2
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);
     }
 }

+ 6 - 2
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);
     }
 }

+ 0 - 10
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);
-    }
 }

+ 0 - 10
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();
     }

+ 0 - 10
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();
     }

+ 3 - 2
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);
     }
 }

+ 0 - 10
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();
     }

+ 0 - 10
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();
     }

+ 0 - 10
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);
-    }
 }

+ 61 - 0
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);
+    }
+}

+ 0 - 9
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);
     }

+ 0 - 9
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);
     }

+ 0 - 9
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

+ 1 - 0
test/governance/Governor.test.js

@@ -41,6 +41,7 @@ contract('Governor', function (accounts) {
   shouldSupportInterfaces([
     'ERC165',
     'Governor',
+    'GovernorWithParams',
   ]);
 
   it('deployment check', async function () {

+ 1 - 0
test/governance/extensions/GovernorTimelockCompound.test.js

@@ -48,6 +48,7 @@ contract('GovernorTimelockCompound', function (accounts) {
   shouldSupportInterfaces([
     'ERC165',
     'Governor',
+    'GovernorWithParams',
     'GovernorTimelock',
   ]);
 

+ 1 - 0
test/governance/extensions/GovernorTimelockControl.test.js

@@ -43,6 +43,7 @@ contract('GovernorTimelockControl', function (accounts) {
   shouldSupportInterfaces([
     'ERC165',
     'Governor',
+    'GovernorWithParams',
     'GovernorTimelock',
   ]);
 

+ 238 - 0
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()],
+          '<proposal description>',
+        ],
+        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()],
+          '<proposal description>',
+        ],
+        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()],
+          '<proposal description>',
+        ],
+        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();
+  });
+});

+ 22 - 0
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)',