From a2367da2f168c4b124f9bfd775ecd36716303324 Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Mon, 17 Jan 2022 10:28:24 -0500 Subject: [PATCH] Mock and test usage of Govenance extension params --- contracts/mocks/GovernorWithParamsMock.sol | 61 ++++++++ .../extensions/GovernorWithParams.test.js | 146 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 contracts/mocks/GovernorWithParamsMock.sol create mode 100644 test/governance/extensions/GovernorWithParams.test.js diff --git a/contracts/mocks/GovernorWithParamsMock.sol b/contracts/mocks/GovernorWithParamsMock.sol new file mode 100644 index 00000000000..0b3dc133906 --- /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 GovernorVotes._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 GovernorCountingSimple._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/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js new file mode 100644 index 00000000000..14dfbaf35de --- /dev/null +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -0,0 +1,146 @@ +const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { web3 } = require('@openzeppelin/test-helpers/src/setup'); +const Enums = require('../../helpers/enums'); + +const { runGovernorWorkflow } = require('../GovernorWorkflow.behavior'); + +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 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 tokens + { voter: voter3, weight: web3.utils.toWei('0.9') }, // 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 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, 'VoteCast', {voter: voter2, weight: reducedWeight}); + + // TODO: Cast vote with voter3 using params & signature; confirm events exist in tx receipt + }); + runGovernorWorkflow(); + }); +});