diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 7156abeab..8ef3b3b7a 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -1,13 +1,11 @@ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; +const { fromRpcSig } = require('ethereumjs-util'); const Enums = require('../helpers/enums'); const { EIP712Domain } = require('../helpers/eip712'); -const { fromRpcSig } = require('ethereumjs-util'); - -const { - runGovernorWorkflow, -} = require('./GovernorWorkflow.behavior'); +const { GovernorHelper } = require('../helpers/governance'); const { shouldSupportInterfaces, @@ -19,23 +17,40 @@ const CallReceiver = artifacts.require('CallReceiverMock'); contract('Governor', function (accounts) { const [ owner, proposer, voter1, voter2, voter3, voter4 ] = accounts; + const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); 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); + const value = web3.utils.toWei('1'); beforeEach(async function () { - this.owner = owner; + this.chainId = await web3.eth.getChainId(); this.token = await Token.new(tokenName, tokenSymbol); - this.mock = await Governor.new(name, this.token.address, 4, 16, 10); + this.mock = await Governor.new(name, this.token.address, votingDelay, votingPeriod, 10); this.receiver = await CallReceiver.new(); + + this.helper = new GovernorHelper(this.mock); + + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + 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.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, + }, + ], ''); }); shouldSupportInterfaces([ @@ -47,901 +62,516 @@ contract('Governor', function (accounts) { 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.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); 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'); + it('nominal workflow', async function () { + // Before + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); - 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'); + // Run proposal + const txPropose = await this.helper.propose({ from: proposer }); - 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('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 }, - ], - }; - this.votingDelay = await this.mock.votingDelay(); - this.votingPeriod = await this.mock.votingPeriod(); + expectEvent( + txPropose, + 'ProposalCreated', + { + proposalId: this.proposal.id, + proposer, + targets: this.proposal.targets, + // values: this.proposal.values, + signatures: this.proposal.signatures, + calldatas: this.proposal.data, + startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay), + endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod), + description: this.proposal.description, + }, + ); + + await this.helper.waitForSnapshot(); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }), + 'VoteCast', + { + voter: voter1, + support: Enums.VoteType.For, + reason: 'This is nice', + weight: web3.utils.toWei('10'), + }, + ); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), + 'VoteCast', + { + voter: voter2, + support: Enums.VoteType.For, + weight: web3.utils.toWei('7'), + }, + ); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), + 'VoteCast', + { + voter: voter3, + support: Enums.VoteType.Against, + weight: web3.utils.toWei('5'), + }, + ); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), + 'VoteCast', + { + voter: voter4, + support: Enums.VoteType.Abstain, + weight: web3.utils.toWei('2'), + }, + ); + + await this.helper.waitForDeadline(); + + const txExecute = await this.helper.execute(); + + expectEvent( + txExecute, + 'ProposalExecuted', + { proposalId: this.proposal.id }, + ); + + await expectEvent.inTransaction( + txExecute.tx, + this.receiver, + 'MockFunctionCalled', + ); + + // After + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + 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(value); + }); + + it('vote with signature', async function () { + const voterBySig = Wallet.generate(); + const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); + + 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: this.chainId, verifyingContract: this.mock.address }, + primaryType: 'Ballot', + message, + }, + }, + )); + }; + + await this.token.delegate(voterBySigAddress, { from: voter1 }); + + // Run proposal + await this.helper.propose(); + await this.helper.waitForSnapshot(); + expectEvent( + await this.helper.vote({ support: Enums.VoteType.For, signature }), + 'VoteCast', + { voter: voterBySigAddress, support: Enums.VoteType.For }, + ); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + // After + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true); + }); + + it('send ethers', async function () { + this.proposal = this.helper.setProposal([ + { + target: empty, + value, + }, + ], ''); + + // Before + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0'); + + // Run proposal + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + // After + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value); + }); + + describe('should revert', function () { + describe('on propose', function () { + it('if proposal already exists', async function () { + await this.helper.propose(); + await expectRevert(this.helper.propose(), 'Governor: proposal already exists'); + }); + }); + + describe('on vote', function () { + it('if proposal does not exist', async function () { + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: unknown proposal id', + ); }); - 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); + it('if voting has not started', async function () { + await this.helper.propose(); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: vote not currently active', + ); + }); - 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'), - ), - ); - } - }); + it('if support value is invalid', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await expectRevert( + this.helper.vote({ support: new BN('255') }), + 'GovernorVotingSimple: invalid value for enum VoteType', + ); + }); - expectEvent( - this.receipts.propose, - 'ProposalCreated', + it('if vote was already casted', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorVotingSimple: vote already cast', + ); + }); + + it('if voting is over', async function () { + await this.helper.propose(); + await this.helper.waitForDeadline(); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: vote not currently active', + ); + }); + }); + + describe('on execute', function () { + it('if proposal does not exist', async function () { + await expectRevert(this.helper.execute(), 'Governor: unknown proposal id'); + }); + + it('if quorum is not reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 }); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('if score not reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('if voting is not over', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('if receiver revert without reason', async function () { + this.proposal = this.helper.setProposal([ { - 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], + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), }, - ); + ], ''); - 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); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await expectRevert(this.helper.execute(), 'Governor: call reverted without message'); }); - 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(), + it('if receiver revert with reason', async function () { + this.proposal = this.helper.setProposal([ { - 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 }, - }, + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(), }, - )); - 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('10'), support: Enums.VoteType.For }, - ], - }; + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting'); }); - 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', - ); + it('if proposal was already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); }); - 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('5'), support: Enums.VoteType.For }, - { voter: voter2, weight: web3.utils.toWei('5'), 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('10'), 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('10'), 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('5'), - support: Enums.VoteType.For, - error: 'Governor: unknown proposal id', - }, - { - voter: voter2, - weight: web3.utils.toWei('5'), - 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('5'), support: Enums.VoteType.For }, - { voter: voter2, weight: web3.utils.toWei('5'), 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('10'), - 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('5'), - support: Enums.VoteType.For, - }, - { - voter: voter1, - weight: web3.utils.toWei('5'), - support: Enums.VoteType.For, - error: 'GovernorVotingSimple: vote already cast', - }, - ], - }; - }); - 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('5'), support: Enums.VoteType.For }, - { voter: voter2, weight: web3.utils.toWei('4'), support: Enums.VoteType.Abstain }, - { voter: voter3, weight: web3.utils.toWei('10'), support: Enums.VoteType.Against }, - ], - steps: { - execute: { error: 'Governor: proposal not successful' }, - }, - }; - }); - 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('10'), 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('10'), 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(); + it('Unset', async function () { + await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id'); }); - 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.Pending); - - await time.advanceBlock(); - - expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - }); - runGovernorWorkflow(); + it('Pending & Active', async function () { + await this.helper.propose(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + await this.helper.waitForSnapshot(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + await this.helper.waitForSnapshot(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); }); - 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(); + it('Defeated', async function () { + await this.helper.propose(); + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); }); - 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('10'), 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(); + it('Succeeded', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); }); - 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('10'), support: Enums.VoteType.For }, - ], - }; - }); - afterEach(async function () { - expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); - }); - runGovernorWorkflow(); + it('Executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); }); }); - 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('cancel', function () { + it('before proposal', async function () { + await expectRevert(this.helper.cancel(), 'Governor: unknown proposal id'); }); - 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); + it('after proposal', async function () { + await this.helper.propose(); - await expectRevert( - this.mock.castVote(this.id, new BN('100'), { from: voter1 }), - 'Governor: vote not currently active', - ); - }); - runGovernorWorkflow(); - }); + await this.helper.cancel(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - 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('10'), 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('10'), 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('10'), 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 this.helper.waitForSnapshot(); await expectRevert( - this.mock.propose( - [], - [], - [], - '', - ), - 'Governor: empty proposal', + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: vote not currently active', ); }); + it('after vote', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + + await this.helper.cancel(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await this.helper.waitForDeadline(); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('after deadline', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + await this.helper.cancel(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('after execution', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + await expectRevert(this.helper.cancel(), 'Governor: proposal not active'); + }); + }); + + describe('proposal length', function () { + it('empty', async function () { + this.helper.setProposal([ ], ''); + await expectRevert(this.helper.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', - ); + this.helper.setProposal({ + targets: [ ], + values: [ web3.utils.toWei('0') ], + data: [ this.receiver.contract.methods.mockFunction().encodeABI() ], + }, ''); + await expectRevert(this.helper.propose(), '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', - ); + this.helper.setProposal({ + targets: [ this.receiver.address ], + values: [ ], + data: [ this.receiver.contract.methods.mockFunction().encodeABI() ], + }, ''); + await expectRevert(this.helper.propose(), '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', - ); + this.helper.setProposal({ + targets: [ this.receiver.address ], + values: [ web3.utils.toWei('0') ], + data: [ ], + }, ''); + await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); }); }); - describe('Settings update', function () { - describe('setVotingDelay', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ this.mock.address ], - [ web3.utils.toWei('0') ], - [ this.mock.contract.methods.setVotingDelay('0').encodeABI() ], - '', - ], - tokenHolder: owner, - voters: [ - { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, - ], - }; - }); - afterEach(async function () { - expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); - - expectEvent( - this.receipts.execute, - 'VotingDelaySet', - { oldVotingDelay: '4', newVotingDelay: '0' }, - ); - }); - runGovernorWorkflow(); + describe('onlyGovernance updates', function () { + it('setVotingDelay is protected', async function () { + await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); }); - describe('setVotingPeriod', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ this.mock.address ], - [ web3.utils.toWei('0') ], - [ this.mock.contract.methods.setVotingPeriod('32').encodeABI() ], - '', - ], - tokenHolder: owner, - voters: [ - { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, - ], - }; - }); - afterEach(async function () { - expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); - - expectEvent( - this.receipts.execute, - 'VotingPeriodSet', - { oldVotingPeriod: '16', newVotingPeriod: '32' }, - ); - }); - runGovernorWorkflow(); + it('setVotingPeriod is protected', async function () { + await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance'); }); - describe('setVotingPeriod to 0', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ this.mock.address ], - [ web3.utils.toWei('0') ], - [ this.mock.contract.methods.setVotingPeriod('0').encodeABI() ], - '', - ], - tokenHolder: owner, - voters: [ - { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, - ], - steps: { - execute: { error: 'GovernorSettings: voting period too low' }, - }, - }; - }); - afterEach(async function () { - expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); - }); - runGovernorWorkflow(); + it('setProposalThreshold is protected', async function () { + await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance'); }); - describe('setProposalThreshold', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ this.mock.address ], - [ web3.utils.toWei('0') ], - [ this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI() ], - '', - ], - tokenHolder: owner, - voters: [ - { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, - ], - }; - }); - afterEach(async function () { - expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); + it('can setVotingDelay through governance', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingDelay('0').encodeABI(), + }, + ], ''); - expectEvent( - this.receipts.execute, - 'ProposalThresholdSet', - { oldProposalThreshold: '0', newProposalThreshold: '1000000000000000000' }, - ); - }); - runGovernorWorkflow(); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent( + await this.helper.execute(), + 'VotingDelaySet', + { oldVotingDelay: '4', newVotingDelay: '0' }, + ); + + expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); }); - describe('update protected', function () { - it('setVotingDelay', async function () { - await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); - }); + it('can setVotingPeriod through governance', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(), + }, + ], ''); - it('setVotingPeriod', async function () { - await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance'); - }); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); - it('setProposalThreshold', async function () { - await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance'); - }); + expectEvent( + await this.helper.execute(), + 'VotingPeriodSet', + { oldVotingPeriod: '16', newVotingPeriod: '32' }, + ); + + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); + }); + + it('cannot setVotingPeriod to 0 through governance', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(), + }, + ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low'); + }); + + it('can setProposalThreshold to 0 through governance', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(), + }, + ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent( + await this.helper.execute(), + 'ProposalThresholdSet', + { oldProposalThreshold: '0', newProposalThreshold: '1000000000000000000' }, + ); + + expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); }); }); }); diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js deleted file mode 100644 index 8cfa9dcaf..000000000 --- a/test/governance/GovernorWorkflow.behavior.js +++ /dev/null @@ -1,186 +0,0 @@ -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 zip (...args) { - return Array(Math.max(...args.map(array => array.length))) - .fill() - .map((_, i) => args.map(array => array[i])); -} - -function concatHex (...args) { - return web3.utils.bytesToHex([].concat(...args.map(h => web3.utils.hexToBytes(h || '0x')))); -} - -function runGovernorWorkflow () { - beforeEach(async function () { - this.receipts = {}; - - // distinguish depending on the proposal length - // - length 4: propose(address[], uint256[], bytes[], string) → GovernorCore - // - length 5: propose(address[], uint256[], string[], bytes[], string) → GovernorCompatibilityBravo - this.useCompatibilityInterface = this.settings.proposal.length === 5; - - // compute description hash - this.descriptionHash = web3.utils.keccak256(this.settings.proposal.slice(-1).find(Boolean)); - - // condensed proposal, used for queue and execute operation - this.settings.shortProposal = [ - // targets - this.settings.proposal[0], - // values - this.settings.proposal[1], - // calldata (prefix selector if necessary) - this.useCompatibilityInterface - ? zip( - this.settings.proposal[2].map(selector => selector && web3.eth.abi.encodeFunctionSignature(selector)), - this.settings.proposal[3], - ).map(hexs => concatHex(...hexs)) - : this.settings.proposal[2], - // descriptionHash - this.descriptionHash, - ]; - - // proposal id - this.id = await this.mock.hashProposal(...this.settings.shortProposal); - }); - - 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 }); - } else if (voter.nfts) { - for (const nft of voter.nfts) { - await this.token.transferFrom(this.settings.tokenHolder, voter.voter, nft, - { from: this.settings.tokenHolder }); - } - } - } - } - - // propose - if (this.mock.propose && tryGet(this.settings, 'steps.propose.enable') !== false) { - this.receipts.propose = await getReceiptOrRevert( - this.mock.methods[ - this.useCompatibilityInterface - ? 'propose(address[],uint256[],string[],bytes[],string)' - : '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.addn(1)); - } - } - - // vote - if (tryGet(this.settings, 'voters')) { - this.receipts.castVote = []; - for (const voter of this.settings.voters.filter(({ support }) => !!support)) { - 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.addn(1)); - } - - // queue - if (this.mock.queue && tryGet(this.settings, 'steps.queue.enable') !== false) { - this.receipts.queue = await getReceiptOrRevert( - this.useCompatibilityInterface - ? this.mock.methods['queue(uint256)']( - this.id, - { from: this.settings.queuer }, - ) - : this.mock.methods['queue(address[],uint256[],bytes[],bytes32)']( - ...this.settings.shortProposal, - { 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.useCompatibilityInterface - ? this.mock.methods['execute(uint256)']( - this.id, - { from: this.settings.executer }, - ) - : this.mock.methods['execute(address[],uint256[],bytes[],bytes32)']( - ...this.settings.shortProposal, - { 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 index e877a5528..7ab8ed328 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -1,10 +1,8 @@ const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const Enums = require('../../helpers/enums'); +const { expect } = require('chai'); const RLP = require('rlp'); - -const { - runGovernorWorkflow, -} = require('../GovernorWorkflow.behavior'); +const Enums = require('../../helpers/enums'); +const { GovernorHelper } = require('../../helpers/governance'); const Token = artifacts.require('ERC20VotesCompMock'); const Timelock = artifacts.require('CompTimelock'); @@ -23,7 +21,10 @@ contract('GovernorCompatibilityBravo', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); + const votingDelay = new BN(4); + const votingPeriod = new BN(16); const proposalThreshold = web3.utils.toWei('10'); + const value = web3.utils.toWei('1'); beforeEach(async function () { const [ deployer ] = await web3.eth.getAccounts(); @@ -35,392 +36,220 @@ contract('GovernorCompatibilityBravo', function (accounts) { 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.mock = await Governor.new( + name, + this.token.address, + votingDelay, + votingPeriod, + 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 }); + this.helper = new GovernorHelper(this.mock); + + await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + + await this.token.mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: proposer, value: proposalThreshold }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + value, + signature: 'mockFunction()', + }, + ], ''); }); 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.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); 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 - ], + it('nominal workflow', async function () { + // Before + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); + + // Run proposal + const txPropose = await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + // After + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); + + const proposal = await this.mock.proposals(this.proposal.id); + expect(proposal.id).to.be.bignumber.equal(this.proposal.id); + expect(proposal.proposer).to.be.equal(proposer); + expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id)); + expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id)); + expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id)); + expect(proposal.canceled).to.be.equal(false); + expect(proposal.executed).to.be.equal(true); + + const action = await this.mock.getActions(this.proposal.id); + expect(action.targets).to.be.deep.equal(this.proposal.targets); + // expect(action.values).to.be.deep.equal(this.proposal.values); + expect(action.signatures).to.be.deep.equal(this.proposal.signatures); + expect(action.calldatas).to.be.deep.equal(this.proposal.data); + + const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1); + expect(voteReceipt1.hasVoted).to.be.equal(true); + expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For); + expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10')); + + const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2); + expect(voteReceipt2.hasVoted).to.be.equal(true); + expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For); + expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7')); + + const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3); + expect(voteReceipt3.hasVoted).to.be.equal(true); + expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against); + expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5')); + + const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4); + expect(voteReceipt4.hasVoted).to.be.equal(true); + expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain); + expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2')); + + expectEvent( + txPropose, + 'ProposalCreated', + { + proposalId: this.proposal.id, 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 cast', - 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); + targets: this.proposal.targets, + // values: this.proposal.values, + signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail + calldatas: this.proposal.fulldata, + startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay), + endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod), + description: this.proposal.description, + }, + ); + expectEvent( + txExecute, + 'ProposalExecuted', + { proposalId: this.proposal.id }, + ); + await expectEvent.inTransaction( + txExecute.tx, + this.receiver, + 'MockFunctionCalled', + ); + }); - 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'), - ), - ); - } + it('with function selector and arguments', async function () { + const target = this.receiver.address; + this.helper.setProposal([ + { target, data: this.receiver.contract.methods.mockFunction().encodeABI() }, + { target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() }, + { target, signature: 'mockFunctionNonPayable()' }, + { + target, + signature: 'mockFunctionWithArgs(uint256,uint256)', + data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]), + }, + ], ''); - 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]); + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); - 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); + await expectEvent.inTransaction( + txExecute.tx, + this.receiver, + 'MockFunctionCalled', + ); + await expectEvent.inTransaction( + txExecute.tx, + this.receiver, + 'MockFunctionCalled', + ); + await expectEvent.inTransaction( + txExecute.tx, + this.receiver, + 'MockFunctionCalledWithArgs', + { a: '17', b: '42' }, + ); + await expectEvent.inTransaction( + txExecute.tx, + this.receiver, + 'MockFunctionCalledWithArgs', + { a: '18', b: '43' }, + ); + }); - 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), + describe('should revert', function () { + describe('on propose', function () { + it('if proposal doesnt meet proposalThreshold', async function () { + await expectRevert( + this.helper.propose({ from: other }), + 'GovernorCompatibilityBravo: proposer votes below proposal threshold', ); }); - expectEvent( - this.receipts.execute, - 'ProposalExecuted', - { proposalId: this.id }, - ); - await expectEvent.inTransaction( - this.receipts.execute.transactionHash, - this.receiver, - 'MockFunctionCalled', - ); }); - runGovernorWorkflow(); - }); - describe('with function selector and arguments', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - Array(4).fill(this.receiver.address), - Array(4).fill(web3.utils.toWei('0')), - [ - '', - '', - 'mockFunctionNonPayable()', - 'mockFunctionWithArgs(uint256,uint256)', - ], - [ - this.receiver.contract.methods.mockFunction().encodeABI(), - this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI(), - '0x', - web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]), - ], - '', // description - ], - proposer, - tokenHolder: owner, - voters: [ - { - voter: voter1, - weight: web3.utils.toWei('10'), - support: Enums.VoteType.For, - }, - ], - steps: { - queue: { delay: 7 * 86400 }, - }, - }; + describe('on vote', function () { + it('if vote type is invalide', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await expectRevert( + this.helper.vote({ support: 5 }, { from: voter1 }), + 'GovernorCompatibilityBravo: invalid vote type', + ); + }); }); - runGovernorWorkflow(); - afterEach(async function () { - await expectEvent.inTransaction( - this.receipts.execute.transactionHash, - this.receiver, - 'MockFunctionCalled', - ); - await expectEvent.inTransaction( - this.receipts.execute.transactionHash, - this.receiver, - 'MockFunctionCalled', - ); - await expectEvent.inTransaction( - this.receipts.execute.transactionHash, - this.receiver, - 'MockFunctionCalledWithArgs', - { a: '17', b: '42' }, - ); - await expectEvent.inTransaction( - this.receipts.execute.transactionHash, - this.receiver, - 'MockFunctionCalledWithArgs', - { a: '18', b: '43' }, - ); - }); - }); - - 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('cancel', 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, - steps: { - wait: { enable: false }, - queue: { enable: false }, - execute: { enable: false }, - }, - }; + it('proposer can cancel', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.cancel({ from: proposer }); }); - describe('by proposer', function () { - afterEach(async function () { - await this.mock.cancel(this.id, { from: proposer }); - }); - runGovernorWorkflow(); + it('anyone can cancel if proposer drop below threshold', async function () { + await this.helper.propose({ from: proposer }); + await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer }); + await this.helper.cancel(); }); - describe('if proposer below threshold', function () { - afterEach(async function () { - await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer }); - await this.mock.cancel(this.id); - }); - runGovernorWorkflow(); + it('cannot cancel is proposer is still above threshold', async function () { + await this.helper.propose({ from: proposer }); + await expectRevert(this.helper.cancel(), 'GovernorBravo: proposer above threshold'); }); - - describe('not if proposer above threshold', function () { - afterEach(async function () { - await expectRevert( - this.mock.cancel(this.id), - 'GovernorBravo: proposer above threshold', - ); - }); - runGovernorWorkflow(); - }); - }); - - describe('with compatibility interface', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ this.receiver.address ], // targets - [ web3.utils.toWei('0') ], // values - [ 'mockFunction()' ], // signatures - [ '0x' ], // 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 cast', - 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].map(_ => ''), - calldatas: this.settings.shortProposal[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[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', - ); - }); - runGovernorWorkflow(); }); }); diff --git a/test/governance/extensions/GovernorComp.test.js b/test/governance/extensions/GovernorComp.test.js index 78cf1e931..06d2d6251 100644 --- a/test/governance/extensions/GovernorComp.test.js +++ b/test/governance/extensions/GovernorComp.test.js @@ -1,9 +1,7 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); const Enums = require('../../helpers/enums'); - -const { - runGovernorWorkflow, -} = require('./../GovernorWorkflow.behavior'); +const { GovernorHelper } = require('../../helpers/governance'); const Token = artifacts.require('ERC20VotesCompMock'); const Governor = artifacts.require('GovernorCompMock'); @@ -17,71 +15,64 @@ contract('GovernorComp', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); + const votingDelay = new BN(4); + const votingPeriod = new BN(16); + const value = web3.utils.toWei('1'); 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(); + + this.helper = new GovernorHelper(this.mock); + + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + 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.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], ''); }); 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.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); 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); + it('voting with comp token', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); - 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'), - ), - ); - } - }); + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); + + await this.mock.proposalVotes(this.proposal.id).then(results => { + expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); + expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); + expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); }); - runGovernorWorkflow(); }); }); diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 3f89c02b4..086fca4c8 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,10 +1,7 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); -const { BN } = require('bn.js'); +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); const Enums = require('../../helpers/enums'); - -const { - runGovernorWorkflow, -} = require('./../GovernorWorkflow.behavior'); +const { GovernorHelper } = require('../../helpers/governance'); const Token = artifacts.require('ERC721VotesMock'); const Governor = artifacts.require('GovernorVoteMocks'); @@ -14,105 +11,94 @@ contract('GovernorERC721Mock', function (accounts) { const [ owner, voter1, voter2, voter3, voter4 ] = accounts; const name = 'OZ-Governor'; + // const version = '1'; const tokenName = 'MockNFToken'; const tokenSymbol = 'MTKN'; - const NFT0 = web3.utils.toWei('100'); - const NFT1 = web3.utils.toWei('10'); - const NFT2 = web3.utils.toWei('20'); - const NFT3 = web3.utils.toWei('30'); - const NFT4 = web3.utils.toWei('40'); - - // Must be the same as in contract - const ProposalState = { - Pending: new BN('0'), - Active: new BN('1'), - Canceled: new BN('2'), - Defeated: new BN('3'), - Succeeded: new BN('4'), - Queued: new BN('5'), - Expired: new BN('6'), - Executed: new BN('7'), - }; + const NFT0 = new BN(0); + const NFT1 = new BN(1); + const NFT2 = new BN(2); + const NFT3 = new BN(3); + const NFT4 = new BN(4); + const votingDelay = new BN(4); + const votingPeriod = new BN(16); + const value = web3.utils.toWei('1'); 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, NFT0); - await this.token.mint(owner, NFT1); - await this.token.mint(owner, NFT2); - await this.token.mint(owner, NFT3); - await this.token.mint(owner, NFT4); - 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 }); + this.helper = new GovernorHelper(this.mock); + + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + + await Promise.all([ NFT0, NFT1, NFT2, NFT3, NFT4 ].map(tokenId => this.token.mint(owner, tokenId))); + await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], ''); }); 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.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe('voting with ERC721 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, nfts: [NFT0], support: Enums.VoteType.For }, - { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, - { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, - { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, - ], - }; + it('voting with ERC721 token', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'VoteCast', + { voter: voter1, support: Enums.VoteType.For, weight: '1' }, + ); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), + 'VoteCast', + { voter: voter2, support: Enums.VoteType.For, weight: '2' }, + ); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), + 'VoteCast', + { voter: voter3, support: Enums.VoteType.Against, weight: '1' }, + ); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), + 'VoteCast', + { voter: voter4, support: Enums.VoteType.Abstain, weight: '1' }, + ); + + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); + + await this.mock.proposalVotes(this.proposal.id).then(results => { + expect(results.forVotes).to.be.bignumber.equal('3'); + expect(results.againstVotes).to.be.bignumber.equal('1'); + expect(results.abstainVotes).to.be.bignumber.equal('1'); }); - - afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - - for (const vote of this.receipts.castVote.filter(Boolean)) { - const { voter } = vote.logs.find(Boolean).args; - - expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); - - expectEvent( - vote, - 'VoteCast', - this.settings.voters.find(({ address }) => address === voter), - ); - - if (voter === voter2) { - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); - } else { - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); - } - } - - 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, { nfts }) => acc.add(new BN(nfts.length)), - new BN('0'), - ), - ); - } - }); - - expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); - }); - - runGovernorWorkflow(); }); }); diff --git a/test/governance/extensions/GovernorPreventLateQuorum.test.js b/test/governance/extensions/GovernorPreventLateQuorum.test.js index e4ae5c17c..6a5d644e7 100644 --- a/test/governance/extensions/GovernorPreventLateQuorum.test.js +++ b/test/governance/extensions/GovernorPreventLateQuorum.test.js @@ -1,9 +1,7 @@ -const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); const Enums = require('../../helpers/enums'); - -const { - runGovernorWorkflow, -} = require('../GovernorWorkflow.behavior'); +const { GovernorHelper } = require('../../helpers/governance'); const Token = artifacts.require('ERC20VotesCompMock'); const Governor = artifacts.require('GovernorPreventLateQuorumMock'); @@ -21,6 +19,7 @@ contract('GovernorPreventLateQuorum', function (accounts) { const votingPeriod = new BN(16); const lateQuorumVoteExtension = new BN(8); const quorum = web3.utils.toWei('1'); + const value = web3.utils.toWei('1'); beforeEach(async function () { this.owner = owner; @@ -34,11 +33,25 @@ contract('GovernorPreventLateQuorum', function (accounts) { lateQuorumVoteExtension, ); this.receiver = await CallReceiver.new(); + + this.helper = new GovernorHelper(this.mock); + + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + 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.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], ''); }); it('deployment check', async function () { @@ -50,198 +63,115 @@ contract('GovernorPreventLateQuorum', function (accounts) { expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension); }); - describe('nominal is unaffected', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ this.receiver.address ], - [ 0 ], - [ this.receiver.contract.methods.mockFunction().encodeABI() ], - '', - ], + it('nominal workflow unaffected', async function () { + const txPropose = await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); + + await this.mock.proposalVotes(this.proposal.id).then(results => { + expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); + expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); + expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); + }); + + const startBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay); + const endBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); + + expectEvent( + txPropose, + 'ProposalCreated', + { + proposalId: this.proposal.id, 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.find(Boolean).args; - expectEvent( - vote, - 'VoteCast', - this.settings.voters.find(({ address }) => address === voter), - ); - expectEvent.notEmitted( - vote, - 'ProposalExtended', - ); - }); - expectEvent( - this.receipts.execute, - 'ProposalExecuted', - { proposalId: this.id }, - ); - await expectEvent.inTransaction( - this.receipts.execute.transactionHash, - this.receiver, - 'MockFunctionCalled', - ); - }); - runGovernorWorkflow(); + targets: this.proposal.targets, + // values: this.proposal.values.map(value => new BN(value)), + signatures: this.proposal.signatures, + calldatas: this.proposal.data, + startBlock, + endBlock, + description: this.proposal.description, + }, + ); }); - describe('Delay is extended to prevent last minute take-over', 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('0.2'), support: Enums.VoteType.Against }, - { voter: voter2, weight: web3.utils.toWei('1.0') }, // 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 }, - }, - }; - }); + it('Delay is extended to prevent last minute take-over', async function () { + const txPropose = await this.helper.propose({ from: proposer }); - afterEach(async function () { - expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + // compute original schedule + const startBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay); + const endBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); - 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); + // wait for the last minute to vote + await this.helper.waitForDeadline(-1); + const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - // wait until the vote is almost over - await time.advanceBlockTo(endBlock.subn(1)); - expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + // cannot execute yet + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - // try to overtake the vote at the last minute - const tx = await this.mock.castVote(this.id, Enums.VoteType.For, { from: voter2 }); + // compute new extended schedule + const extendedDeadline = new BN(txVote.receipt.blockNumber).add(lateQuorumVoteExtension); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline); - // vote duration is extended - const extendedBlock = new BN(tx.receipt.blockNumber).add(lateQuorumVoteExtension); - expect(await this.mock.proposalDeadline(this.id)).to.be.bignumber.equal(extendedBlock); + // still possible to vote + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); - expectEvent( - tx, - 'ProposalExtended', - { proposalId: this.id, extendedDeadline: extendedBlock }, - ); + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); - // vote is still active after expected end - await time.advanceBlockTo(endBlock.addn(1)); - expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - - // Still possible to vote - await this.mock.castVote(this.id, Enums.VoteType.Against, { from: voter3 }); - - // proposal fails - await time.advanceBlockTo(extendedBlock.addn(1)); - expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); - }); - runGovernorWorkflow(); + // check extension event + expectEvent( + txVote, + 'ProposalExtended', + { proposalId: this.proposal.id, extendedDeadline }, + ); }); - describe('setLateQuorumVoteExtension', function () { - beforeEach(async function () { - this.newVoteExtension = new BN(0); // disable voting delay extension - }); - - it('protected', async function () { + describe('onlyGovernance updates', function () { + it('setLateQuorumVoteExtension is protected', async function () { await expectRevert( - this.mock.setLateQuorumVoteExtension(this.newVoteExtension), + this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance', ); }); - describe('using workflow', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ this.mock.address ], - [ web3.utils.toWei('0') ], - [ this.mock.contract.methods.setLateQuorumVoteExtension(this.newVoteExtension).encodeABI() ], - '', - ], - proposer, - tokenHolder: owner, - voters: [ - { voter: voter1, weight: web3.utils.toWei('1.0'), support: Enums.VoteType.For }, - ], - }; - }); - afterEach(async function () { - expectEvent( - this.receipts.propose, - 'ProposalCreated', - { proposalId: this.id }, - ); - expectEvent( - this.receipts.execute, - 'ProposalExecuted', - { proposalId: this.id }, - ); - expectEvent( - this.receipts.execute, - 'LateQuorumVoteExtensionSet', - { oldVoteExtension: lateQuorumVoteExtension, newVoteExtension: this.newVoteExtension }, - ); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(this.newVoteExtension); - }); - runGovernorWorkflow(); + it('can setLateQuorumVoteExtension through governance', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(), + }, + ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent( + await this.helper.execute(), + 'LateQuorumVoteExtensionSet', + { oldVoteExtension: lateQuorumVoteExtension, newVoteExtension: '0' }, + ); + + expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0'); }); }); }); diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index e623c4571..a31df68a2 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -1,11 +1,8 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, 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 Enums = require('../../helpers/enums'); +const { GovernorHelper } = require('../../helpers/governance'); const { shouldSupportInterfaces, @@ -21,13 +18,16 @@ function makeContractAddress (creator, nonce) { } contract('GovernorTimelockCompound', function (accounts) { - const [ admin, voter, other ] = accounts; + const [ owner, 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 votingDelay = new BN(4); + const votingPeriod = new BN(16); + const value = web3.utils.toWei('1'); beforeEach(async function () { const [ deployer ] = await web3.eth.getAccounts(); @@ -39,10 +39,34 @@ contract('GovernorTimelockCompound', function (accounts) { 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.mock = await Governor.new( + name, + this.token.address, + votingDelay, + votingPeriod, + this.timelock.address, + 0, + ); this.receiver = await CallReceiver.new(); - await this.token.mint(voter, tokenSupply); - await this.token.delegate(voter, { from: voter }); + + this.helper = new GovernorHelper(this.mock); + + await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + + await this.token.mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], ''); }); shouldSupportInterfaces([ @@ -53,436 +77,292 @@ contract('GovernorTimelockCompound', function (accounts) { ]); it('doesn\'t accept ether transfers', async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: voter, to: this.mock.address, value: 1 })); + await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); }); 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.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); 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(); + it('nominal', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + const txQueue = await this.helper.queue(); + const eta = await this.mock.proposalEta(this.proposal.id); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta }); + + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); }); - 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('should revert', function () { + describe('on queue', function () { + it('if already queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + }); - 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('relay', function () { - beforeEach(async function () { - await this.token.mint(this.mock.address, 1); - this.call = [ - this.token.address, - 0, - this.token.contract.methods.transfer(other, 1).encodeABI(), - ]; - }); - - it('protected', async function () { - await expectRevert( - this.mock.relay(...this.call), - 'Governor: onlyGovernance', - ); - }); - - describe('using workflow', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ - this.mock.address, - ], - [ - web3.utils.toWei('0'), - ], - [ - this.mock.contract.methods.relay(...this.call).encodeABI(), - ], - '', - ], - voters: [ - { voter: voter, support: Enums.VoteType.For }, - ], - steps: { - queue: { delay: 7 * 86400 }, - }, + it('if proposal contains duplicate calls', async function () { + const action = { + target: this.token.address, + data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(), }; + this.helper.setProposal([ action, action ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await expectRevert( + this.helper.queue(), + 'GovernorTimelockCompound: identical proposal action already queued', + ); + await expectRevert( + this.helper.execute(), + 'GovernorTimelockCompound: proposal not yet queued', + ); + }); + }); + + describe('on execute', function () { + it('if not queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(+1); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + await expectRevert( + this.helper.execute(), + 'GovernorTimelockCompound: proposal not yet queued', + ); + }); + + it('if too early', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + await expectRevert( + this.helper.execute(), + 'Timelock::executeTransaction: Transaction hasn\'t surpassed time lock', + ); + }); + + it('if too late', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(+30 * 86400); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); + + await expectRevert( + this.helper.execute(), + 'Governor: proposal not successful', + ); + }); + + it('if already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + await expectRevert( + this.helper.execute(), + 'Governor: proposal not successful', + ); + }); + }); + }); + + describe('cancel', function () { + it('cancel before queue prevents scheduling', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent( + await this.helper.cancel(), + 'ProposalCanceled', + { proposalId: this.proposal.id }, + ); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + }); + + it('cancel after queue prevents executing', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expectEvent( + await this.helper.cancel(), + 'ProposalCanceled', + { proposalId: this.proposal.id }, + ); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + }); + + describe('onlyGovernance', function () { + describe('relay', function () { + beforeEach(async function () { + await this.token.mint(this.mock.address, 1); + }); + + it('is protected', async function () { + await expectRevert( + this.mock.relay( + this.token.address, + 0, + this.token.contract.methods.transfer(other, 1).encodeABI(), + ), + 'Governor: onlyGovernance', + ); + }); + + it('can be executed through governance', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.relay( + this.token.address, + 0, + this.token.contract.methods.transfer(other, 1).encodeABI(), + ).encodeABI(), + }, + ], ''); expect(await this.token.balanceOf(this.mock.address), 1); expect(await this.token.balanceOf(other), 0); - }); - afterEach(async function () { + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + expect(await this.token.balanceOf(this.mock.address), 0); expect(await this.token.balanceOf(other), 1); + + expectEvent.inTransaction( + txExecute.tx, + this.token, + 'Transfer', + { from: this.mock.address, to: other, value: '1' }, + ); }); - 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 () { + describe('updateTimelock', 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 }, - }, - }; + this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400); }); - afterEach(async function () { - expectEvent( - this.receipts.propose, - 'ProposalCreated', - { proposalId: this.id }, + + it('is protected', async function () { + await expectRevert( + this.mock.updateTimelock(this.newTimelock.address), + 'Governor: onlyGovernance', ); + }); + + it('can be executed through governance to', async function () { + this.helper.setProposal([ + { + target: this.timelock.address, + data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(), + }, + { + target: this.mock.address, + data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + }, + ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + expectEvent( - this.receipts.execute, - 'ProposalExecuted', - { proposalId: this.id }, - ); - expectEvent( - this.receipts.execute, + txExecute, '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(); + it('can transfer timelock to new governor', async function () { + const newGovernor = await Governor.new(name, this.token.address, 8, 32, this.timelock.address, 0); + + this.helper.setProposal([ + { + target: this.timelock.address, + data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(), + }, + ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + await expectEvent.inTransaction( + txExecute.tx, + this.timelock, + 'NewPendingAdmin', + { newPendingAdmin: newGovernor.address }, + ); + + await newGovernor.__acceptAdmin(); + expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address); }); }); }); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index 85dd0ea75..45e26c935 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -1,10 +1,7 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); - -const { - runGovernorWorkflow, -} = require('../GovernorWorkflow.behavior'); +const { GovernorHelper } = require('../../helpers/governance'); const { shouldSupportInterfaces, @@ -16,7 +13,7 @@ const Governor = artifacts.require('GovernorTimelockControlMock'); const CallReceiver = artifacts.require('CallReceiverMock'); contract('GovernorTimelockControl', function (accounts) { - const [ admin, voter, other ] = accounts; + const [ owner, voter1, voter2, voter3, voter4, other ] = accounts; const TIMELOCK_ADMIN_ROLE = web3.utils.soliditySha3('TIMELOCK_ADMIN_ROLE'); const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE'); @@ -28,30 +25,61 @@ contract('GovernorTimelockControl', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); + const votingDelay = new BN(4); + const votingPeriod = new BN(16); + const value = web3.utils.toWei('1'); 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.mock = await Governor.new( + name, + this.token.address, + votingDelay, + votingPeriod, + this.timelock.address, + 0, + ); this.receiver = await CallReceiver.new(); + this.helper = new GovernorHelper(this.mock); + this.TIMELOCK_ADMIN_ROLE = await this.timelock.TIMELOCK_ADMIN_ROLE(); this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE(); this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE(); this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE(); + await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + // normal setup: governor is proposer, everyone is executor, timelock is its own admin await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address); - await this.timelock.grantRole(PROPOSER_ROLE, admin); + await this.timelock.grantRole(PROPOSER_ROLE, owner); await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); - await this.timelock.grantRole(CANCELLER_ROLE, admin); + await this.timelock.grantRole(CANCELLER_ROLE, owner); await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); await this.timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer); - await this.token.mint(voter, tokenSupply); - await this.token.delegate(voter, { from: voter }); + await this.token.mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], ''); + this.proposal.timelockid = await this.timelock.hashOperationBatch( + ...this.proposal.shortProposal.slice(0, 3), + '0x0', + this.proposal.shortProposal[3], + ); }); shouldSupportInterfaces([ @@ -62,473 +90,293 @@ contract('GovernorTimelockControl', function (accounts) { ]); it('doesn\'t accept ether transfers', async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: voter, to: this.mock.address, value: 1 })); + await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); }); 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.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); 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 }, - }, - }; + it('nominal', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + const txQueue = await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); + + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + }); + + describe('should revert', function () { + describe('on queue', function () { + it('if already queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + }); }); - afterEach(async function () { - const timelockid = await this.timelock.hashOperationBatch( - ...this.settings.proposal.slice(0, 3), - '0x0', - this.descriptionHash, - ); + + describe('on execute', function () { + it('if not queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(+1); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready'); + }); + + it('if too early', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready'); + }); + + it('if already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('if already executed by another proposer', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + + await this.timelock.executeBatch( + ...this.proposal.shortProposal.slice(0, 3), + '0x0', + this.proposal.shortProposal[3], + ); + + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + }); + }); + + describe('cancel', function () { + it('cancel before queue prevents scheduling', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); 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), + await this.helper.cancel(), 'ProposalCanceled', - { proposalId: this.id }, + { proposalId: this.proposal.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', - ); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.queue(), '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, - ); + it('cancel after queue prevents executing', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); - 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, + await this.helper.cancel(), 'ProposalCanceled', - { proposalId: this.id }, + { proposalId: this.proposal.id }, ); - await expectEvent.inTransaction( - receipt.receipt.transactionHash, - this.timelock, + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('cancel on timelock is reflected on governor', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + expectEvent( + await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', - { id: timelockid }, + { id: this.proposal.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', - ); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); }); - runGovernorWorkflow(); }); - describe('relay', function () { - beforeEach(async function () { - await this.token.mint(this.mock.address, 1); - this.call = [ - this.token.address, - 0, - this.token.contract.methods.transfer(other, 1).encodeABI(), - ]; - }); - - it('protected', async function () { - await expectRevert( - this.mock.relay(...this.call), - 'Governor: onlyGovernance', - ); - }); - - it('protected against other proposers', async function () { - await this.timelock.schedule( - this.mock.address, - web3.utils.toWei('0'), - this.mock.contract.methods.relay(...this.call).encodeABI(), - constants.ZERO_BYTES32, - constants.ZERO_BYTES32, - 3600, - { from: admin }, - ); - - await time.increase(3600); - - await expectRevert( - this.timelock.execute( - this.mock.address, - web3.utils.toWei('0'), - this.mock.contract.methods.relay(...this.call).encodeABI(), - constants.ZERO_BYTES32, - constants.ZERO_BYTES32, - { from: admin }, - ), - 'TimelockController: underlying transaction reverted', - ); - }); - - describe('using workflow', function () { + describe('onlyGovernance', function () { + describe('relay', function () { beforeEach(async function () { - this.settings = { - proposal: [ - [ - this.mock.address, - ], - [ - web3.utils.toWei('0'), - ], - [ - this.mock.contract.methods.relay(...this.call).encodeABI(), - ], - '', - ], - voters: [ - { voter: voter, support: Enums.VoteType.For }, - ], - steps: { - queue: { delay: 7 * 86400 }, + await this.token.mint(this.mock.address, 1); + }); + + it('is protected', async function () { + await expectRevert( + this.mock.relay( + this.token.address, + 0, + this.token.contract.methods.transfer(other, 1).encodeABI(), + ), + 'Governor: onlyGovernance', + ); + }); + + it('can be executed through governance', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.relay( + this.token.address, + 0, + this.token.contract.methods.transfer(other, 1).encodeABI(), + ).encodeABI(), }, - }; + ], ''); expect(await this.token.balanceOf(this.mock.address), 1); expect(await this.token.balanceOf(other), 0); - }); - afterEach(async function () { + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + expect(await this.token.balanceOf(this.mock.address), 0); expect(await this.token.balanceOf(other), 1); + + expectEvent.inTransaction( + txExecute.tx, + this.token, + 'Transfer', + { from: this.mock.address, to: other, value: '1' }, + ); }); - runGovernorWorkflow(); - }); - }); - describe('cancel on timelock is forwarded in state', 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, - ); + it('protected against other proposers', async function () { + await this.timelock.schedule( + this.mock.address, + web3.utils.toWei('0'), + this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), + constants.ZERO_BYTES32, + constants.ZERO_BYTES32, + 3600, + { from: owner }, + ); - expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + await time.increase(3600); - const receipt = await this.timelock.cancel(timelockid, { from: admin }); - expectEvent( - receipt, - 'Cancelled', - { id: timelockid }, - ); - - expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - }); - runGovernorWorkflow(); - }); - - describe('updateTimelock', function () { - beforeEach(async function () { - this.newTimelock = await Timelock.new(3600, [], []); + await expectRevert( + this.timelock.execute( + this.mock.address, + web3.utils.toWei('0'), + this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), + constants.ZERO_BYTES32, + constants.ZERO_BYTES32, + { from: owner }, + ), + 'TimelockController: underlying transaction reverted', + ); + }); }); - it('protected', async function () { - await expectRevert( - this.mock.updateTimelock(this.newTimelock.address), - 'Governor: onlyGovernance', - ); - }); - - describe('using workflow', function () { + describe('updateTimelock', 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 }, - }, - }; + this.newTimelock = await Timelock.new(3600, [], []); }); - afterEach(async function () { - expectEvent( - this.receipts.propose, - 'ProposalCreated', - { proposalId: this.id }, + + it('is protected', async function () { + await expectRevert( + this.mock.updateTimelock(this.newTimelock.address), + 'Governor: onlyGovernance', ); + }); + + it('can be executed through governance to', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + }, + ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + expectEvent( - this.receipts.execute, - 'ProposalExecuted', - { proposalId: this.id }, - ); - expectEvent( - this.receipts.execute, + txExecute, 'TimelockChange', { oldTimelock: this.timelock.address, newTimelock: this.newTimelock.address }, ); + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); }); - runGovernorWorkflow(); }); }); - describe('clear queue of pending governor calls', function () { - beforeEach(async function () { - this.settings = { - proposal: [ - [ this.mock.address ], - [ web3.utils.toWei('0') ], - [ this.mock.contract.methods.nonGovernanceFunction().encodeABI() ], - '', - ], - voters: [ - { voter: voter, support: Enums.VoteType.For }, - ], - steps: { - queue: { delay: 3600 }, - }, - }; - }); + it('clear queue of pending governor calls', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), + }, + ], ''); - afterEach(async function () { - expectEvent( - this.receipts.execute, - 'ProposalExecuted', - { proposalId: this.id }, - ); - }); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); - runGovernorWorkflow(); + // This path clears _governanceCall as part of the afterExecute call, + // but we have not way to check that the cleanup actually happened other + // then coverage reports. }); }); diff --git a/test/governance/extensions/GovernorWeightQuorumFraction.test.js b/test/governance/extensions/GovernorWeightQuorumFraction.test.js index 4d3f90dad..ea2b55e8d 100644 --- a/test/governance/extensions/GovernorWeightQuorumFraction.test.js +++ b/test/governance/extensions/GovernorWeightQuorumFraction.test.js @@ -1,9 +1,7 @@ -const { BN, expectEvent, time } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); const Enums = require('../../helpers/enums'); - -const { - runGovernorWorkflow, -} = require('./../GovernorWorkflow.behavior'); +const { GovernorHelper } = require('../../helpers/governance'); const Token = artifacts.require('ERC20VotesMock'); const Governor = artifacts.require('GovernorMock'); @@ -19,24 +17,41 @@ contract('GovernorVotesQuorumFraction', function (accounts) { const tokenSupply = new BN(web3.utils.toWei('100')); const ratio = new BN(8); // percents const newRatio = new BN(6); // percents + const votingDelay = new BN(4); + const votingPeriod = new BN(16); + const value = web3.utils.toWei('1'); 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.mock = await Governor.new(name, this.token.address, votingDelay, votingPeriod, ratio); this.receiver = await CallReceiver.new(); + + this.helper = new GovernorHelper(this.mock); + + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + 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.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], ''); }); 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.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); 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'); @@ -44,51 +59,47 @@ contract('GovernorVotesQuorumFraction', function (accounts) { .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(); + it('quroum reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); }); - 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 }, - ], - }; + it('quroum not reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.waitForDeadline(); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + describe('onlyGovernance updates', function () { + it('updateQuorumNumerator is protected', async function () { + await expectRevert( + this.mock.updateQuorumNumerator(newRatio), + 'Governor: onlyGovernance', + ); }); - afterEach(async function () { - await expectEvent.inTransaction( - this.receipts.execute.transactionHash, - this.mock, - 'QuorumNumeratorUpdated', + + it('can updateQuorumNumerator through governance', async function () { + this.helper.setProposal([ { - oldQuorumNumerator: ratio, - newQuorumNumerator: newRatio, + target: this.mock.address, + data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(), }, + ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent( + await this.helper.execute(), + 'QuorumNumeratorUpdated', + { oldQuorumNumerator: ratio, newQuorumNumerator: newRatio }, ); expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio); @@ -96,27 +107,24 @@ contract('GovernorVotesQuorumFraction', function (accounts) { 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' }, + it('cannot updateQuorumNumerator over the maximum', async function () { + this.helper.setProposal([ + { + target: this.mock.address, + data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(), }, - }; + ], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + await expectRevert( + this.helper.execute(), + 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator', + ); }); - runGovernorWorkflow(); }); }); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index 5b4ac8901..875b7053a 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -1,20 +1,28 @@ -const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); -const { web3 } = require('@openzeppelin/test-helpers/src/setup'); -const Enums = require('../../helpers/enums'); +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); 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 Enums = require('../../helpers/enums'); +const { EIP712Domain } = require('../../helpers/eip712'); +const { GovernorHelper } = require('../../helpers/governance'); const Token = artifacts.require('ERC20VotesCompMock'); const Governor = artifacts.require('GovernorWithParamsMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const rawParams = { + uintParam: new BN('42'), + strParam: 'These are my params', +}; + +const encodedParams = web3.eth.abi.encodeParameters( + [ 'uint256', 'string' ], + Object.values(rawParams), +); + contract('GovernorWithParams', function (accounts) { - const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; + const [ owner, proposer, voter1, voter2, voter3, voter4 ] = accounts; const name = 'OZ-Governor'; const version = '1'; @@ -23,17 +31,32 @@ contract('GovernorWithParams', function (accounts) { const tokenSupply = web3.utils.toWei('100'); const votingDelay = new BN(4); const votingPeriod = new BN(16); + const value = web3.utils.toWei('1'); beforeEach(async function () { - this.owner = owner; + this.chainId = await web3.eth.getChainId(); this.token = await Token.new(tokenName, tokenSymbol); this.mock = await Governor.new(name, this.token.address); this.receiver = await CallReceiver.new(); + + this.helper = new GovernorHelper(this.mock); + + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + 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.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal([ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], ''); }); it('deployment check', async function () { @@ -43,172 +66,57 @@ contract('GovernorWithParams', function (accounts) { 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 }, - ], - }; - }); + it('nominal is unaffected', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); - 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(); + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + 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(value); }); - 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 }, - }, - }; + it('Voting with params is properly supported', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + + const weight = new BN(web3.utils.toWei('7')).sub(rawParams.uintParam); + + const tx = await this.helper.vote({ + support: Enums.VoteType.For, + reason: 'no particular reason', + params: encodedParams, + }, { from: voter2 }); + + expectEvent(tx, 'CountParams', { ...rawParams }); + expectEvent(tx, 'VoteCastWithParams', { + voter: voter2, + proposalId: this.proposal.id, + support: Enums.VoteType.For, + weight, + reason: 'no particular reason', + params: encodedParams, }); - 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(); + const votes = await this.mock.proposalVotes(this.proposal.id); + expect(votes.forVotes).to.be.bignumber.equal(weight); }); - 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'); + it('Voting with params by signature is properly supported', async function () { + const voterBySig = Wallet.generate(); + const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - 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(), { + const signature = async (message) => { + return fromRpcSig(ethSigUtil.signTypedMessage( + voterBySig.getPrivateKey(), + { data: { types: { EIP712Domain, @@ -221,18 +129,38 @@ contract('GovernorWithParams', function (accounts) { }, domain: { name, version, chainId: this.chainId, verifyingContract: this.mock.address }, primaryType: 'ExtendedBallot', - message: { proposalId: this.id, support: Enums.VoteType.For, reason, params }, + message, }, - }), - ); + }, + )); + }; - const tx = await this.mock.castVoteWithReasonAndParamsBySig(this.id, Enums.VoteType.For, reason, params, v, r, s); + await this.token.delegate(voterBySigAddress, { from: voter2 }); - 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); + // Run proposal + await this.helper.propose(); + await this.helper.waitForSnapshot(); + + const weight = new BN(web3.utils.toWei('7')).sub(rawParams.uintParam); + + const tx = await this.helper.vote({ + support: Enums.VoteType.For, + reason: 'no particular reason', + params: encodedParams, + signature, }); - runGovernorWorkflow(); + + expectEvent(tx, 'CountParams', { ...rawParams }); + expectEvent(tx, 'VoteCastWithParams', { + voter: voterBySigAddress, + proposalId: this.proposal.id, + support: Enums.VoteType.For, + weight, + reason: 'no particular reason', + params: encodedParams, + }); + + const votes = await this.mock.proposalVotes(this.proposal.id); + expect(votes.forVotes).to.be.bignumber.equal(weight); }); }); diff --git a/test/helpers/governance.js b/test/helpers/governance.js new file mode 100644 index 000000000..accbf79ac --- /dev/null +++ b/test/helpers/governance.js @@ -0,0 +1,211 @@ +const { time } = require('@openzeppelin/test-helpers'); + +function zip (...args) { + return Array(Math.max(...args.map(array => array.length))) + .fill() + .map((_, i) => args.map(array => array[i])); +} + +function concatHex (...args) { + return web3.utils.bytesToHex([].concat(...args.map(h => web3.utils.hexToBytes(h || '0x')))); +} + +function concatOpts (args, opts = null) { + return opts ? args.concat(opts) : args; +} + +class GovernorHelper { + constructor (governor) { + this.governor = governor; + } + + delegate (delegation = {}, opts = null) { + return Promise.all([ + delegation.token.delegate(delegation.to, { from: delegation.to }), + delegation.value && + delegation.token.transfer(...concatOpts([ delegation.to, delegation.value ]), opts), + delegation.tokenId && + delegation.token.ownerOf(delegation.tokenId).then(owner => + delegation.token.transferFrom(...concatOpts([ owner, delegation.to, delegation.tokenId ], opts)), + ), + ]); + } + + propose (opts = null) { + const proposal = this.currentProposal; + + return this.governor.methods[ + proposal.useCompatibilityInterface + ? 'propose(address[],uint256[],string[],bytes[],string)' + : 'propose(address[],uint256[],bytes[],string)' + ](...concatOpts(proposal.fullProposal, opts)); + } + + queue (opts = null) { + const proposal = this.currentProposal; + + return proposal.useCompatibilityInterface + ? this.governor.methods['queue(uint256)'](...concatOpts( + [ proposal.id ], + opts, + )) + : this.governor.methods['queue(address[],uint256[],bytes[],bytes32)'](...concatOpts( + proposal.shortProposal, + opts, + )); + } + + execute (opts = null) { + const proposal = this.currentProposal; + + return proposal.useCompatibilityInterface + ? this.governor.methods['execute(uint256)'](...concatOpts( + [ proposal.id ], + opts, + )) + : this.governor.methods['execute(address[],uint256[],bytes[],bytes32)'](...concatOpts( + proposal.shortProposal, + opts, + )); + } + + cancel (opts = null) { + const proposal = this.currentProposal; + + return proposal.useCompatibilityInterface + ? this.governor.methods['cancel(uint256)'](...concatOpts( + [ proposal.id ], + opts, + )) + : this.governor.methods['cancel(address[],uint256[],bytes[],bytes32)'](...concatOpts( + proposal.shortProposal, + opts, + )); + } + + vote (vote = {}, opts = null) { + const proposal = this.currentProposal; + + return vote.signature + // if signature, and either params or reason → + ? vote.params || vote.reason + ? vote.signature({ + proposalId: proposal.id, + support: vote.support, + reason: vote.reason || '', + params: vote.params || '', + }).then(({ v, r, s }) => this.governor.castVoteWithReasonAndParamsBySig(...concatOpts( + [ proposal.id, vote.support, vote.reason || '', vote.params || '', v, r, s ], + opts, + ))) + : vote.signature({ + proposalId: proposal.id, + support: vote.support, + }).then(({ v, r, s }) => this.governor.castVoteBySig(...concatOpts( + [ proposal.id, vote.support, v, r, s ], + opts, + ))) + : vote.params + // otherwize if params + ? this.governor.castVoteWithReasonAndParams(...concatOpts( + [ proposal.id, vote.support, vote.reason || '', vote.params ], + opts, + )) + : vote.reason + // otherwize if reason + ? this.governor.castVoteWithReason(...concatOpts( + [ proposal.id, vote.support, vote.reason ], + opts, + )) + : this.governor.castVote(...concatOpts( + [ proposal.id, vote.support ], + opts, + )); + } + + waitForSnapshot (offset = 0) { + const proposal = this.currentProposal; + return this.governor.proposalSnapshot(proposal.id) + .then(blockNumber => time.advanceBlockTo(blockNumber.addn(offset))); + } + + waitForDeadline (offset = 0) { + const proposal = this.currentProposal; + return this.governor.proposalDeadline(proposal.id) + .then(blockNumber => time.advanceBlockTo(blockNumber.addn(offset))); + } + + waitForEta (offset = 0) { + const proposal = this.currentProposal; + return this.governor.proposalEta(proposal.id) + .then(timestamp => time.increaseTo(timestamp.addn(offset))); + } + + /** + * Specify a proposal either as + * 1) an array of objects [{ target, value, data, signature? }] + * 2) an object of arrays { targets: [], values: [], data: [], signatures?: [] } + */ + setProposal (actions, description) { + let targets, values, signatures, data, useCompatibilityInterface; + + if (Array.isArray(actions)) { + useCompatibilityInterface = actions.some(a => 'signature' in a); + targets = actions.map(a => a.target); + values = actions.map(a => a.value || '0'); + signatures = actions.map(a => a.signature || ''); + data = actions.map(a => a.data || '0x'); + } else { + useCompatibilityInterface = Array.isArray(actions.signatures); + ({ targets, values, signatures = [], data } = actions); + } + + const fulldata = zip(signatures.map(s => s && web3.eth.abi.encodeFunctionSignature(s)), data) + .map(hexs => concatHex(...hexs)); + + const descriptionHash = web3.utils.keccak256(description); + + // condensed version for queing end executing + const shortProposal = [ + targets, + values, + fulldata, + descriptionHash, + ]; + + // full version for proposing + const fullProposal = [ + targets, + values, + ...(useCompatibilityInterface ? [ signatures ] : []), + data, + description, + ]; + + // proposal id + const id = web3.utils.toBN(web3.utils.keccak256(web3.eth.abi.encodeParameters( + [ 'address[]', 'uint256[]', 'bytes[]', 'bytes32' ], + shortProposal, + ))); + + this.currentProposal = { + id, + targets, + values, + signatures, + data, + fulldata, + description, + descriptionHash, + shortProposal, + fullProposal, + useCompatibilityInterface, + }; + + return this.currentProposal; + } +} + +module.exports = { + GovernorHelper, +}; diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index a0aa3d7e5..78e327248 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -112,34 +112,33 @@ for (const k of Object.getOwnPropertyNames(INTERFACES)) { } function shouldSupportInterfaces (interfaces = []) { - describe('Contract interface', function () { + describe('ERC165', function () { beforeEach(function () { this.contractUnderTest = this.mock || this.token || this.holder || this.accessControl; }); - for (const k of interfaces) { - const interfaceId = INTERFACE_IDS[k]; - describe(k, function () { - describe('ERC165\'s supportsInterface(bytes4)', function () { - it('uses less than 30k gas', async function () { - expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.be.lte(30000); - }); + it('supportsInterface uses less than 30k gas', async function () { + for (const k of interfaces) { + const interfaceId = INTERFACE_IDS[k]; + expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.be.lte(30000); + } + }); - it('claims support', async function () { - expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true); - }); - }); + it('all interfaces are reported as supported', async function () { + for (const k of interfaces) { + const interfaceId = INTERFACE_IDS[k]; + expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true); + } + }); + it('all interface functions are in ABI', async function () { + for (const k of interfaces) { for (const fnName of INTERFACES[k]) { const fnSig = FN_SIGNATURES[fnName]; - describe(fnName, function () { - it('has to be implemented', function () { - expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(1); - }); - }); + expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(1); } - }); - } + } + }); }); }