浏览代码

Refactor governor testing (#3194)

* starting a governor test refactor

* improve governor tests

* refactor compatibility tests using the governor helper

* improve governor helper

* improve governor helper

* refactor governor tests

* refactor testing

* fix testing (still TODO)

* fix tests

* fix tests

* fix spelling

* use different instances of GovernorHelper

* add vote with params support

* coverage

* simplify ERC165 helper

* remove unused proposal argument

* refactor setProposal

* lint

* refactor setProposal return values

* add a data default value

* improve proposal reconstruction and storage in helper

* proposal object refactoring

* lint

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
Hadrien Croubois 3 年之前
父节点
当前提交
6a5bbfc4cb

+ 443 - 813
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,
+      },
+    ], '<proposal description>');
   });
 
   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');
-
-        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: this.value });
-        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(this.value);
-        expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
-
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ this.value ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          proposer,
-          tokenHolder: owner,
-          voters: [
-            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For, reason: 'This is nice' },
-            { voter: voter2, weight: web3.utils.toWei('7'), support: Enums.VoteType.For },
-            { voter: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against },
-            { voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain },
-          ],
-        };
-        this.votingDelay = await this.mock.votingDelay();
-        this.votingPeriod = await this.mock.votingPeriod();
-      });
+  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');
+
+    // Run proposal
+    const txPropose = await this.helper.propose({ from: proposer });
+
+    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);
+  });
 
-      afterEach(async function () {
-        expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false);
-        expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true);
-        expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true);
-
-        await this.mock.proposalVotes(this.id).then(result => {
-          for (const [key, value] of Object.entries(Enums.VoteType)) {
-            expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
-              Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
-                (acc, { weight }) => acc.add(new BN(weight)),
-                new BN('0'),
-              ),
-            );
-          }
-        });
-
-        expectEvent(
-          this.receipts.propose,
-          'ProposalCreated',
-          {
-            proposalId: this.id,
-            proposer,
-            targets: this.settings.proposal[0],
-            // values: this.settings.proposal[1].map(value => new BN(value)),
-            signatures: this.settings.proposal[2].map(() => ''),
-            calldatas: this.settings.proposal[2],
-            startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay),
-            endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod),
-            description: this.settings.proposal[3],
+  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);
+  });
 
-        this.receipts.castVote.filter(Boolean).forEach(vote => {
-          const { voter } = vote.logs.find(Boolean).args;
-          expectEvent(
-            vote,
-            'VoteCast',
-            this.settings.voters.find(({ address }) => address === voter),
-          );
-        });
-        expectEvent(
-          this.receipts.execute,
-          'ProposalExecuted',
-          { proposalId: this.id },
-        );
-        await expectEvent.inTransaction(
-          this.receipts.execute.transactionHash,
-          this.receiver,
-          'MockFunctionCalled',
-        );
+  it('send ethers', async function () {
+    this.proposal = this.helper.setProposal([
+      {
+        target: empty,
+        value,
+      },
+    ], '<proposal description>');
+
+    // 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);
+  });
 
-        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);
+  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');
       });
-      runGovernorWorkflow();
     });
 
-    describe('vote with signature', function () {
-      beforeEach(async function () {
-        const chainId = await web3.eth.getChainId();
-        // generate voter by signature wallet
-        const voterBySig = Wallet.generate();
-        this.voter = web3.utils.toChecksumAddress(voterBySig.getAddressString());
-        // use delegateBySig to enable vote delegation for this wallet
-        const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
-          voterBySig.getPrivateKey(),
-          {
-            data: {
-              types: {
-                EIP712Domain,
-                Delegation: [
-                  { name: 'delegatee', type: 'address' },
-                  { name: 'nonce', type: 'uint256' },
-                  { name: 'expiry', type: 'uint256' },
-                ],
-              },
-              domain: { name: tokenName, version: '1', chainId, verifyingContract: this.token.address },
-              primaryType: 'Delegation',
-              message: { delegatee: this.voter, nonce: 0, expiry: constants.MAX_UINT256 },
-            },
-          },
-        ));
-        await this.token.delegateBySig(this.voter, 0, constants.MAX_UINT256, v, r, s);
-        // prepare signature for vote by signature
-        const signature = async (message) => {
-          return fromRpcSig(ethSigUtil.signTypedMessage(
-            voterBySig.getPrivateKey(),
-            {
-              data: {
-                types: {
-                  EIP712Domain,
-                  Ballot: [
-                    { name: 'proposalId', type: 'uint256' },
-                    { name: 'support', type: 'uint8' },
-                  ],
-                },
-                domain: { name, version, chainId, verifyingContract: this.mock.address },
-                primaryType: 'Ballot',
-                message,
-              },
-            },
-          ));
-        };
-
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          tokenHolder: owner,
-          voters: [
-            { voter: this.voter, signature, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
-          ],
-        };
-      });
-      afterEach(async function () {
-        expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false);
-        expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(false);
-        expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(false);
-        expect(await this.mock.hasVoted(this.id, this.voter)).to.be.equal(true);
-
-        await this.mock.proposalVotes(this.id).then(result => {
-          for (const [key, value] of Object.entries(Enums.VoteType)) {
-            expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
-              Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
-                (acc, { weight }) => acc.add(new BN(weight)),
-                new BN('0'),
-              ),
-            );
-          }
-        });
-
-        expectEvent(
-          this.receipts.propose,
-          'ProposalCreated',
-          { proposalId: this.id },
-        );
-        expectEvent(
-          this.receipts.execute,
-          'ProposalExecuted',
-          { proposalId: this.id },
-        );
-        await expectEvent.inTransaction(
-          this.receipts.execute.transactionHash,
-          this.receiver,
-          'MockFunctionCalled',
+    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',
         );
       });
-      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' ],
-            '<proposal description>',
-          ],
-          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 },
+      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',
         );
-        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() ],
-            '<proposal description>',
-          ],
-          tokenHolder: owner,
-          voters: [
-            { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
-          ],
-          steps: {
-            execute: { error: 'Governor: call reverted without message' },
-          },
-        };
+      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',
+        );
       });
-      runGovernorWorkflow();
-    });
 
-    describe('receiver revert with reason', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ 0 ],
-            [ this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI() ],
-            '<proposal description>',
-          ],
-          tokenHolder: owner,
-          voters: [
-            { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
-          ],
-          steps: {
-            execute: { error: 'CallReceiverMock: reverting' },
-          },
-        };
+      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',
+        );
       });
-      runGovernorWorkflow();
-    });
 
-    describe('missing proposal', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          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' },
-          },
-        };
+      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',
+        );
       });
-      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() ],
-            '<proposal description>',
-          ],
-          steps: {
-            wait: { enable: false },
-            execute: { enable: false },
-          },
-        };
+    describe('on execute', function () {
+      it('if proposal does not exist', async function () {
+        await expectRevert(this.helper.execute(), 'Governor: unknown proposal id');
       });
-      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() ],
-            '<proposal description>',
-          ],
-          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 },
-          ],
-        };
+      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');
       });
-      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() ],
-            '<proposal description>',
-          ],
-          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 },
-          },
-        };
+      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');
       });
-      runGovernorWorkflow();
-    });
 
-    describe('double cast', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          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',
-            },
-          ],
-        };
+      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');
       });
-      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() ],
-            '<proposal description>',
-          ],
-          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' },
+      it('if receiver revert without reason', async function () {
+        this.proposal = this.helper.setProposal([
+          {
+            target: this.receiver.address,
+            data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(),
           },
-        };
+        ], '<proposal description>');
+
+        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('score not reached', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          tokenHolder: owner,
-          voters: [
-            { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.Against },
-          ],
-          steps: {
-            execute: { error: 'Governor: proposal not successful' },
+      it('if receiver revert with reason', async function () {
+        this.proposal = this.helper.setProposal([
+          {
+            target: this.receiver.address,
+            data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(),
           },
-        };
+        ], '<proposal description>');
+
+        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');
       });
-      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() ],
-            '<proposal description>',
-          ],
-          tokenHolder: owner,
-          voters: [
-            { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
-          ],
-          steps: {
-            wait: { enable: false },
-            execute: { error: 'Governor: proposal not successful' },
-          },
-        };
+      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('state', function () {
-    describe('Unset', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          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() ],
-            '<proposal description>',
-          ],
-          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() ],
-            '<proposal description>',
-          ],
-          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() ],
-            '<proposal description>',
-          ],
-          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() ],
-            '<proposal description>',
-          ],
-          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() ],
-            '<proposal description>',
-          ],
-          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() ],
-            '<proposal description>',
-          ],
-          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);
+
+      await this.helper.waitForSnapshot();
+      await expectRevert(
+        this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
+        'Governor: vote not currently active',
+      );
     });
 
-    describe('After vote', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          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);
+    it('after vote', async function () {
+      await this.helper.propose();
+      await this.helper.waitForSnapshot();
+      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
 
-        await expectRevert(
-          this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
-          'Governor: proposal not successful',
-        );
-      });
-      runGovernorWorkflow();
+      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');
     });
 
-    describe('After deadline', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          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);
+    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 expectRevert(
-          this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
-          'Governor: proposal not successful',
-        );
-      });
-      runGovernorWorkflow();
+      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');
     });
 
-    describe('After execution', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.receiver.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-            '<proposal description>',
-          ],
-          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();
+    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 () {
+  describe('proposal length', function () {
     it('empty', async function () {
-      await expectRevert(
-        this.mock.propose(
-          [],
-          [],
-          [],
-          '<proposal description>',
-        ),
-        'Governor: empty proposal',
-      );
+      this.helper.setProposal([ ], '<proposal description>');
+      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() ],
-          '<proposal description>',
-        ),
-        'Governor: invalid proposal length',
-      );
+      this.helper.setProposal({
+        targets: [ ],
+        values: [ web3.utils.toWei('0') ],
+        data: [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+      }, '<proposal description>');
+      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() ],
-          '<proposal description>',
-        ),
-        'Governor: invalid proposal length',
-      );
+      this.helper.setProposal({
+        targets: [ this.receiver.address ],
+        values: [ ],
+        data: [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+      }, '<proposal description>');
+      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') ],
-          [ ],
-          '<proposal description>',
-        ),
-        'Governor: invalid proposal length',
-      );
+      this.helper.setProposal({
+        targets: [ this.receiver.address ],
+        values: [ web3.utils.toWei('0') ],
+        data: [ ],
+      }, '<proposal description>');
+      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() ],
-            '<proposal description>',
-          ],
-          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');
+  describe('onlyGovernance updates', function () {
+    it('setVotingDelay is protected', async function () {
+      await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance');
+    });
 
-        expectEvent(
-          this.receipts.execute,
-          'VotingDelaySet',
-          { oldVotingDelay: '4', newVotingDelay: '0' },
-        );
-      });
-      runGovernorWorkflow();
+    it('setVotingPeriod is protected', async function () {
+      await expectRevert(this.mock.setVotingPeriod('32'), '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() ],
-            '<proposal description>',
-          ],
-          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');
+    it('setProposalThreshold is protected', async function () {
+      await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance');
+    });
 
-        expectEvent(
-          this.receipts.execute,
-          'VotingPeriodSet',
-          { oldVotingPeriod: '16', newVotingPeriod: '32' },
-        );
-      });
-      runGovernorWorkflow();
+    it('can setVotingDelay through governance', async function () {
+      this.helper.setProposal([
+        {
+          target: this.mock.address,
+          data: this.mock.contract.methods.setVotingDelay('0').encodeABI(),
+        },
+      ], '<proposal description>');
+
+      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('setVotingPeriod to 0', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.mock.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.mock.contract.methods.setVotingPeriod('0').encodeABI() ],
-            '<proposal description>',
-          ],
-          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('can setVotingPeriod through governance', async function () {
+      this.helper.setProposal([
+        {
+          target: this.mock.address,
+          data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(),
+        },
+      ], '<proposal description>');
+
+      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(),
+        'VotingPeriodSet',
+        { oldVotingPeriod: '16', newVotingPeriod: '32' },
+      );
+
+      expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32');
     });
 
-    describe('setProposalThreshold', function () {
-      beforeEach(async function () {
-        this.settings = {
-          proposal: [
-            [ this.mock.address ],
-            [ web3.utils.toWei('0') ],
-            [ this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI() ],
-            '<proposal description>',
-          ],
-          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('cannot setVotingPeriod to 0 through governance', async function () {
+      this.helper.setProposal([
+        {
+          target: this.mock.address,
+          data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(),
+        },
+      ], '<proposal description>');
 
-        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();
+
+      await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low');
     });
 
-    describe('update protected', function () {
-      it('setVotingDelay', async function () {
-        await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance');
-      });
+    it('can setProposalThreshold to 0 through governance', async function () {
+      this.helper.setProposal([
+        {
+          target: this.mock.address,
+          data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(),
+        },
+      ], '<proposal description>');
 
-      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(),
+        'ProposalThresholdSet',
+        { oldProposalThreshold: '0', newProposalThreshold: '1000000000000000000' },
+      );
+
+      expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000');
     });
   });
 });

+ 0 - 186
test/governance/GovernorWorkflow.behavior.js

@@ -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,
-};

+ 191 - 362
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()',
+      },
+    ], '<proposal description>');
   });
 
   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
-          '<proposal description>', // 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(Array(this.settings.proposal[2].length).fill(''));
-      expect(action.calldatas).to.be.deep.equal(this.settings.proposal[2]);
-
-      for (const voter of this.settings.voters.filter(({ skip }) => !skip)) {
-        expect(await this.mock.hasVoted(this.id, voter.voter)).to.be.equal(voter.error === undefined);
-
-        const receipt = await this.mock.getReceipt(this.id, voter.voter);
-        expect(receipt.hasVoted).to.be.equal(voter.error === undefined);
-        expect(receipt.support).to.be.bignumber.equal(voter.error === undefined ? voter.support : '0');
-        expect(receipt.votes).to.be.bignumber.equal(voter.error === undefined ? voter.weight : '0');
-      }
-
-      expectEvent(
-        this.receipts.propose,
-        'ProposalCreated',
-        {
-          proposalId: this.id,
-          proposer,
-          targets: this.settings.proposal[0],
-          // values: this.settings.proposal[1].map(value => new BN(value)),
-          signatures: this.settings.proposal[2].map(() => ''),
-          calldatas: this.settings.proposal[2],
-          startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay),
-          endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod),
-          description: this.settings.proposal[3],
-        },
-      );
-
-      this.receipts.castVote.filter(Boolean).forEach(vote => {
-        const { voter } = vote.logs.find(Boolean).args;
-        expectEvent(
-          vote,
-          'VoteCast',
-          this.settings.voters.find(({ address }) => address === voter),
-        );
-      });
-      expectEvent(
-        this.receipts.execute,
-        'ProposalExecuted',
-        { proposalId: this.id },
-      );
-      await expectEvent.inTransaction(
-        this.receipts.execute.transactionHash,
-        this.receiver,
-        'MockFunctionCalled',
-      );
-    });
-    runGovernorWorkflow();
-  });
-
-  describe('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]),
-          ],
-          '<proposal description>', // 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('10'),
-            support: Enums.VoteType.For,
-          },
-        ],
-        steps: {
-          queue: { delay: 7 * 86400 },
-        },
-      };
-    });
-    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' },
-      );
-    });
+        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',
+    );
   });
 
-  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
-          '<proposal description>', // description
-        ],
-        proposer: other,
-        steps: {
-          propose: { error: 'GovernorCompatibilityBravo: proposer votes below proposal threshold' },
-          wait: { enable: false },
-          queue: { enable: false },
-          execute: { enable: false },
-        },
-      };
-    });
-    runGovernorWorkflow();
+  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]),
+      },
+    ], '<proposal description>');
+
+    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();
+
+    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' },
+    );
   });
 
-  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
-          '<proposal description>', // description
-        ],
-        proposer,
-        tokenHolder: owner,
-        steps: {
-          wait: { enable: false },
-          queue: { enable: false },
-          execute: { enable: false },
-        },
-      };
-    });
-
-    describe('by proposer', function () {
-      afterEach(async function () {
-        await this.mock.cancel(this.id, { from: proposer });
-      });
-      runGovernorWorkflow();
-    });
-
-    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);
+  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',
+        );
       });
-      runGovernorWorkflow();
     });
 
-    describe('not if proposer above threshold', function () {
-      afterEach(async function () {
+    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.mock.cancel(this.id),
-          'GovernorBravo: proposer above threshold',
+          this.helper.vote({ support: 5 }, { from: voter1 }),
+          'GovernorCompatibilityBravo: invalid vote type',
         );
       });
-      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
-          '<proposal description>', // 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 = {};
+  describe('cancel', function () {
+    it('proposer can cancel', async function () {
+      await this.helper.propose({ from: proposer });
+      await this.helper.cancel({ from: proposer });
     });
 
-    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],
-        },
-      );
+    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();
+    });
 
-      this.receipts.castVote.filter(Boolean).forEach(vote => {
-        const { voter } = vote.logs.find(Boolean).args;
-        expectEvent(
-          vote,
-          'VoteCast',
-          this.settings.voters.find(({ address }) => address === voter),
-        );
-      });
-      expectEvent(
-        this.receipts.execute,
-        'ProposalExecuted',
-        { proposalId: this.id },
-      );
-      await expectEvent.inTransaction(
-        this.receipts.execute.transactionHash,
-        this.receiver,
-        'MockFunctionCalled',
-      );
+    it('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');
     });
-    runGovernorWorkflow();
   });
 });

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

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

+ 112 - 182
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(),
+      },
+    ], '<proposal description>');
   });
 
   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() ],
-          '<proposal description>',
-        ],
-        proposer,
-        tokenHolder: owner,
-        voters: [
-          { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For, reason: 'This is nice' },
-          { voter: voter2, weight: web3.utils.toWei('7'), support: Enums.VoteType.For },
-          { voter: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against },
-          { voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain },
-        ],
-      };
+  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'));
     });
 
-    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);
+    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(
-        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();
-  });
-
-  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() ],
-          '<proposal description>',
-        ],
+    expectEvent(
+      txPropose,
+      'ProposalCreated',
+      {
+        proposalId: this.proposal.id,
         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 },
-        },
-      };
-    });
+        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,
+      },
+    );
+  });
 
-    afterEach(async function () {
-      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
+  it('Delay is extended to prevent last minute take-over', async function () {
+    const txPropose = await this.helper.propose({ from: proposer });
 
-      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);
+    // 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);
 
-      // 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);
+    // wait for the last minute to vote
+    await this.helper.waitForDeadline(-1);
+    const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
 
-      // try to overtake the vote at the last minute
-      const tx = await this.mock.castVote(this.id, Enums.VoteType.For, { from: voter2 });
+    // cannot execute yet
+    expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
 
-      // 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);
+    // 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);
 
-      expectEvent(
-        tx,
-        'ProposalExtended',
-        { proposalId: this.id, extendedDeadline: extendedBlock },
-      );
+    // still possible to vote
+    await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
 
-      // 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);
+    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);
 
-      // 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() ],
-            '<proposal description>',
-          ],
-          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(),
+        },
+      ], '<proposal description>');
+
+      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');
     });
   });
 });

+ 260 - 380
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(),
+      },
+    ], '<proposal description>');
   });
 
   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() ],
-          '<proposal description>',
-        ],
-        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() ],
-          '<proposal description>',
-        ],
-        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() ],
-          '<proposal description>',
-        ],
-        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);
+      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 ], '<proposal description>');
+
+        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',
+        );
+      });
     });
-    runGovernorWorkflow();
-  });
 
-  describe('to late', function () {
-    beforeEach(async function () {
-      this.settings = {
-        proposal: [
-          [ this.receiver.address ],
-          [ web3.utils.toWei('0') ],
-          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-          '<proposal description>',
-        ],
-        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('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);
 
-  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()),
-          '<proposal description>',
-        ],
-        voters: [
-          { voter: voter, support: Enums.VoteType.For },
-        ],
-        steps: {
-          queue: {
-            error: 'GovernorTimelockCompound: identical proposal action already queued',
-          },
-          execute: {
-            error: 'GovernorTimelockCompound: proposal not yet queued',
-          },
-        },
-      };
-    });
-    runGovernorWorkflow();
-  });
+        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
 
-  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() ],
-          '<proposal description>',
-        ],
-        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.helper.execute(),
+          'GovernorTimelockCompound: proposal not yet queued',
+        );
+      });
 
-      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',
-      );
+      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',
+        );
+      });
     });
-    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() ],
-          '<proposal description>',
-        ],
-        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);
+  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.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 executing', function () {
-    beforeEach(async function () {
-      this.settings = {
-        proposal: [
-          [ this.receiver.address ],
-          [ web3.utils.toWei('0') ],
-          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-          '<proposal description>',
-        ],
-        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);
+    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();
 
-      const receipt = await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash);
       expectEvent(
-        receipt,
+        await this.helper.cancel(),
         'ProposalCanceled',
-        { proposalId: this.id },
-      );
-      await expectEvent.inTransaction(
-        receipt.receipt.transactionHash,
-        this.timelock,
-        'CancelTransaction',
+        { proposalId: this.proposal.id },
       );
 
-      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);
+      await expectRevert(this.helper.execute(), '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(),
-      ];
-    });
+  describe('onlyGovernance', function () {
+    describe('relay', function () {
+      beforeEach(async function () {
+        await this.token.mint(this.mock.address, 1);
+      });
 
-    it('protected', async function () {
-      await expectRevert(
-        this.mock.relay(...this.call),
-        'Governor: onlyGovernance',
-      );
-    });
+      it('is protected', async function () {
+        await expectRevert(
+          this.mock.relay(
+            this.token.address,
+            0,
+            this.token.contract.methods.transfer(other, 1).encodeABI(),
+          ),
+          '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(),
-            ],
-            '<proposal description>',
-          ],
-          voters: [
-            { voter: voter, support: Enums.VoteType.For },
-          ],
-          steps: {
-            queue: { delay: 7 * 86400 },
+      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(),
           },
-        };
+        ], '<proposal description>');
 
         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);
-      });
-      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',
-      );
+        expectEvent.inTransaction(
+          txExecute.tx,
+          this.token,
+          'Transfer',
+          { from: this.mock.address, to: other, value: '1' },
+        );
+      });
     });
 
-    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(),
-            ],
-            '<proposal description>',
-          ],
-          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 },
-        );
-        expectEvent(
-          this.receipts.execute,
-          'ProposalExecuted',
-          { 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(),
+          },
+        ], '<proposal description>');
+
+        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,
+          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);
-    });
+    it('can transfer timelock to new governor', async function () {
+      const 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() ],
-            '<proposal description>',
-          ],
-          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();
+      this.helper.setProposal([
+        {
+          target: this.timelock.address,
+          data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(),
+        },
+      ], '<proposal description>');
+
+      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);
     });
   });
 });

+ 261 - 413
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(),
+      },
+    ], '<proposal description>');
+    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() ],
-          '<proposal description>',
-        ],
-        voters: [
-          { voter: voter, support: Enums.VoteType.For },
-        ],
-        steps: {
-          queue: { delay: 3600 },
-        },
-      };
-    });
-    afterEach(async function () {
-      const timelockid = await this.timelock.hashOperationBatch(
-        ...this.settings.proposal.slice(0, 3),
-        '0x0',
-        this.descriptionHash,
-      );
-
-      expectEvent(
-        this.receipts.propose,
-        'ProposalCreated',
-        { proposalId: this.id },
-      );
-      expectEvent(
-        this.receipts.queue,
-        'ProposalQueued',
-        { proposalId: this.id },
-      );
-      await expectEvent.inTransaction(
-        this.receipts.queue.transactionHash,
-        this.timelock,
-        'CallScheduled',
-        { id: timelockid },
-      );
-      expectEvent(
-        this.receipts.execute,
-        'ProposalExecuted',
-        { proposalId: this.id },
-      );
-      await expectEvent.inTransaction(
-        this.receipts.execute.transactionHash,
-        this.timelock,
-        'CallExecuted',
-        { id: timelockid },
-      );
-      await expectEvent.inTransaction(
-        this.receipts.execute.transactionHash,
-        this.receiver,
-        'MockFunctionCalled',
-      );
-    });
-    runGovernorWorkflow();
+  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('executed by other proposer', function () {
-    beforeEach(async function () {
-      this.settings = {
-        proposal: [
-          [ this.receiver.address ],
-          [ web3.utils.toWei('0') ],
-          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-          '<proposal description>',
-        ],
-        voters: [
-          { voter: voter, support: Enums.VoteType.For },
-        ],
-        steps: {
-          queue: { delay: 3600 },
-          execute: { enable: false },
-        },
-      };
+  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 () {
-      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);
+    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);
 
-      await expectRevert(
-        this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
-        'Governor: proposal not successful',
-      );
-    });
-    runGovernorWorkflow();
-  });
+        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
 
-  describe('not queued', function () {
-    beforeEach(async function () {
-      this.settings = {
-        proposal: [
-          [ this.receiver.address ],
-          [ web3.utils.toWei('0') ],
-          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-          '<proposal description>',
-        ],
-        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();
-  });
+        await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready');
+      });
 
-  describe('to early', function () {
-    beforeEach(async function () {
-      this.settings = {
-        proposal: [
-          [ this.receiver.address ],
-          [ web3.utils.toWei('0') ],
-          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
-          '<proposal description>',
-        ],
-        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();
-  });
+      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();
 
-  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() ],
-          '<proposal description>',
-        ],
-        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);
+        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
 
-      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',
-      );
+        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');
+      });
     });
-    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() ],
-          '<proposal description>',
-        ],
-        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);
+  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.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        await this.helper.cancel(),
         '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',
+        { proposalId: this.proposal.id },
       );
-    });
-    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() ],
-          '<proposal description>',
-        ],
-        voters: [
-          { voter: voter, support: Enums.VoteType.For },
-        ],
-        steps: {
-          queue: { delay: 3600 },
-          execute: { enable: false },
-        },
-      };
+      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+      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,
-      );
 
-      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+    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();
 
-      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,
-        'Cancelled',
-        { id: timelockid },
-      );
-
-      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
 
-      await expectRevert(
-        this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
-        'Governor: proposal not successful',
-      );
+      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+      await expectRevert(this.helper.execute(), '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('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();
 
-    it('protected', async function () {
-      await expectRevert(
-        this.mock.relay(...this.call),
-        'Governor: onlyGovernance',
-      );
-    });
+      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
 
-    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 },
+      expectEvent(
+        await this.timelock.cancel(this.proposal.timelockid, { from: owner }),
+        'Cancelled',
+        { id: this.proposal.timelockid },
       );
 
-      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',
-      );
+      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
     });
+  });
 
-    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(),
-            ],
-            '<proposal description>',
-          ],
-          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(),
           },
-        };
+        ], '<proposal description>');
 
         expect(await this.token.balanceOf(this.mock.address), 1);
         expect(await this.token.balanceOf(other), 0);
-      });
-      afterEach(async function () {
-        expect(await this.token.balanceOf(this.mock.address), 0);
-        expect(await this.token.balanceOf(other), 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() ],
-          '<proposal description>',
-        ],
-        voters: [
-          { voter: voter, support: Enums.VoteType.For },
-        ],
-        steps: {
-          queue: { delay: 3600 },
-          execute: { enable: false },
-        },
-      };
-    });
-    afterEach(async function () {
-      const timelockid = await this.timelock.hashOperationBatch(
-        ...this.settings.proposal.slice(0, 3),
-        '0x0',
-        this.descriptionHash,
-      );
 
-      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+        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();
 
-      const receipt = await this.timelock.cancel(timelockid, { from: admin });
-      expectEvent(
-        receipt,
-        'Cancelled',
-        { id: timelockid },
-      );
+        expect(await this.token.balanceOf(this.mock.address), 0);
+        expect(await this.token.balanceOf(other), 1);
 
-      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
-    });
-    runGovernorWorkflow();
-  });
+        expectEvent.inTransaction(
+          txExecute.tx,
+          this.token,
+          'Transfer',
+          { from: this.mock.address, to: other, value: '1' },
+        );
+      });
 
-  describe('updateTimelock', function () {
-    beforeEach(async function () {
-      this.newTimelock = await Timelock.new(3600, [], []);
-    });
+      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 },
+        );
 
-    it('protected', async function () {
-      await expectRevert(
-        this.mock.updateTimelock(this.newTimelock.address),
-        'Governor: onlyGovernance',
-      );
+        await time.increase(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',
+        );
+      });
     });
 
-    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() ],
-            '<proposal description>',
-          ],
-          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 },
-        );
-        expectEvent(
-          this.receipts.execute,
-          'ProposalExecuted',
-          { 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(),
+          },
+        ], '<proposal description>');
+
+        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,
+          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() ],
-          '<proposal description>',
-        ],
-        voters: [
-          { voter: voter, support: Enums.VoteType.For },
-        ],
-        steps: {
-          queue: { delay: 3600 },
-        },
-      };
-    });
-
-    afterEach(async function () {
-      expectEvent(
-        this.receipts.execute,
-        'ProposalExecuted',
-        { proposalId: this.id },
-      );
-    });
-
-    runGovernorWorkflow();
+  it('clear queue of pending governor calls', async function () {
+    this.helper.setProposal([
+      {
+        target: this.mock.address,
+        data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(),
+      },
+    ], '<proposal description>');
+
+    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();
+
+    // 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.
   });
 });

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

+ 109 - 181
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(),
+      },
+    ], '<proposal description>');
   });
 
   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()],
-          '<proposal description>',
-        ],
-        proposer,
-        tokenHolder: owner,
-        voters: [
-          { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For, reason: 'This is nice' },
-          { voter: voter2, weight: web3.utils.toWei('7'), support: Enums.VoteType.For },
-          { voter: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against },
-          { voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain },
-        ],
-      };
-    });
-
-    afterEach(async function () {
-      expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false);
-      expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true);
-      expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true);
-
-      await this.mock.proposalVotes(this.id).then((result) => {
-        for (const [key, value] of Object.entries(Enums.VoteType)) {
-          expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
-            Object.values(this.settings.voters)
-              .filter(({ support }) => support === value)
-              .reduce((acc, { weight }) => acc.add(new BN(weight)), new BN('0')),
-          );
-        }
-      });
-
-      const startBlock = new BN(this.receipts.propose.blockNumber).add(votingDelay);
-      const endBlock = new BN(this.receipts.propose.blockNumber).add(votingDelay).add(votingPeriod);
-      expect(await this.mock.proposalSnapshot(this.id)).to.be.bignumber.equal(startBlock);
-      expect(await this.mock.proposalDeadline(this.id)).to.be.bignumber.equal(endBlock);
-
-      expectEvent(this.receipts.propose, 'ProposalCreated', {
-        proposalId: this.id,
-        proposer,
-        targets: this.settings.proposal[0],
-        // values: this.settings.proposal[1].map(value => new BN(value)),
-        signatures: this.settings.proposal[2].map(() => ''),
-        calldatas: this.settings.proposal[2],
-        startBlock,
-        endBlock,
-        description: this.settings.proposal[3],
-      });
-
-      this.receipts.castVote.filter(Boolean).forEach((vote) => {
-        const { voter } = vote.logs.filter(({ event }) => event === 'VoteCast').find(Boolean).args;
-        expectEvent(
-          vote,
-          'VoteCast',
-          this.settings.voters.find(({ address }) => address === voter),
-        );
-      });
-      expectEvent(this.receipts.execute, 'ProposalExecuted', { proposalId: this.id });
-      await expectEvent.inTransaction(this.receipts.execute.transactionHash, this.receiver, 'MockFunctionCalled');
-    });
-    runGovernorWorkflow();
+  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();
+
+    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()],
-          '<proposal description>',
-        ],
-        proposer,
-        tokenHolder: owner,
-        voters: [
-          { voter: voter1, weight: web3.utils.toWei('0.2'), support: Enums.VoteType.Against },
-          { voter: voter2, weight: voter2Weight }, // do not actually vote, only getting tokenss
-        ],
-        steps: {
-          wait: { enable: false },
-          execute: { enable: false },
-        },
-      };
+  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()],
-          '<proposal description>',
-        ],
-        proposer,
-        tokenHolder: owner,
-        voters: [
-          { voter: voter1, weight: web3.utils.toWei('0.2'), support: Enums.VoteType.Against },
-          { voter: this.voter, weight: sigVoterWeight }, // do not actually vote, only getting tokens
-        ],
-        steps: {
-          wait: { enable: false },
-          execute: { enable: false },
-        },
-      };
-    });
-
-    afterEach(async function () {
-      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
-
-      const reason = 'This is my reason';
-      const uintParam = new BN(1);
-      const strParam = 'These are my params';
-      const reducedWeight = new BN(sigVoterWeight).sub(uintParam);
-      const params = web3.eth.abi.encodeParameters(['uint256', 'string'], [uintParam, strParam]);
-
-      // prepare signature for vote by signature
-      const { v, r, s } = fromRpcSig(
-        ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), {
+    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,
           },
-        }),
-      );
+        },
+      ));
+    };
+
+    await this.token.delegate(voterBySigAddress, { from: voter2 });
 
-      const tx = await this.mock.castVoteWithReasonAndParamsBySig(this.id, Enums.VoteType.For, reason, params, v, r, s);
+    // Run proposal
+    await this.helper.propose();
+    await this.helper.waitForSnapshot();
 
-      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);
+    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,
+    });
+
+    expectEvent(tx, 'CountParams', { ...rawParams });
+    expectEvent(tx, 'VoteCastWithParams', {
+      voter: voterBySigAddress,
+      proposalId: this.proposal.id,
+      support: Enums.VoteType.For,
+      weight,
+      reason: 'no particular reason',
+      params: encodedParams,
     });
-    runGovernorWorkflow();
+
+    const votes = await this.mock.proposalVotes(this.proposal.id);
+    expect(votes.forVotes).to.be.bignumber.equal(weight);
   });
 });

+ 211 - 0
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,
+};

+ 18 - 19
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);
         }
-      });
-    }
+      }
+    });
   });
 }