const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const Enums = require('../helpers/enums'); const { EIP712Domain } = require('../helpers/eip712'); const { fromRpcSig } = require('ethereumjs-util'); const { runGovernorWorkflow, } = require('./GovernorWorkflow.behavior'); const { shouldSupportInterfaces, } = require('../utils/introspection/SupportsInterface.behavior'); const Token = artifacts.require('ERC20VotesMock'); const Governor = artifacts.require('GovernorMock'); const CallReceiver = artifacts.require('CallReceiverMock'); contract('Governor', function (accounts) { const [ owner, proposer, voter1, voter2, voter3, voter4 ] = accounts; const name = 'OZ-Governor'; const version = '1'; const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); beforeEach(async function () { this.owner = owner; this.token = await Token.new(tokenName, tokenSymbol); this.mock = await Governor.new(name, this.token.address, 4, 16, 10); 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 }); }); shouldSupportInterfaces([ 'ERC165', 'Governor', ]); 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.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() ], '', ], 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(); }); 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], }, ); this.receipts.castVote.filter(Boolean).forEach(vote => { const { voter } = vote.logs.find(Boolean).args; expectEvent( vote, 'VoteCast', this.settings.voters.find(({ address }) => address === voter), ); }); expectEvent( this.receipts.execute, 'ProposalExecuted', { proposalId: this.id }, ); await expectEvent.inTransaction( this.receipts.execute.transactionHash, this.receiver, 'MockFunctionCalled', ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(this.value); }); 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() ], '', ], 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', ); }); runGovernorWorkflow(); }); describe('send ethers', function () { beforeEach(async function () { this.receiver = { address: web3.utils.toChecksumAddress(web3.utils.randomHex(20)) }; this.value = web3.utils.toWei('1'); await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: this.value }); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(this.value); expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); this.settings = { proposal: [ [ this.receiver.address ], [ this.value ], [ '0x' ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For }, { voter: voter2, weight: web3.utils.toWei('5'), support: Enums.VoteType.Abstain }, ], }; }); afterEach(async function () { expectEvent( this.receipts.propose, 'ProposalCreated', { proposalId: this.id }, ); expectEvent( this.receipts.execute, 'ProposalExecuted', { proposalId: this.id }, ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(this.value); }); runGovernorWorkflow(); }); describe('receiver revert without reason', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ 0 ], [ this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], steps: { execute: { error: 'Governor: call reverted without message' }, }, }; }); runGovernorWorkflow(); }); describe('receiver revert with reason', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ 0 ], [ this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], steps: { execute: { error: 'CallReceiverMock: reverting' }, }, }; }); runGovernorWorkflow(); }); describe('missing proposal', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For, error: 'Governor: unknown proposal id', }, { voter: voter2, weight: web3.utils.toWei('5'), support: Enums.VoteType.Abstain, error: 'Governor: unknown proposal id', }, ], steps: { propose: { enable: false }, wait: { enable: false }, execute: { error: 'Governor: unknown proposal id' }, }, }; }); runGovernorWorkflow(); }); describe('duplicate pending proposal', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], steps: { wait: { enable: false }, execute: { enable: false }, }, }; }); afterEach(async function () { await expectRevert(this.mock.propose(...this.settings.proposal), 'Governor: proposal already exists'); }); runGovernorWorkflow(); }); describe('duplicate executed proposal', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For }, { voter: voter2, weight: web3.utils.toWei('5'), support: Enums.VoteType.Abstain }, ], }; }); afterEach(async function () { await expectRevert(this.mock.propose(...this.settings.proposal), 'Governor: proposal already exists'); }); runGovernorWorkflow(); }); describe('Invalid vote type', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: new BN('255'), error: 'GovernorVotingSimple: invalid value for enum VoteType', }, ], steps: { wait: { enable: false }, execute: { enable: false }, }, }; }); runGovernorWorkflow(); }); describe('double cast', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For, }, { voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For, error: 'GovernorVotingSimple: vote already cast', }, ], }; }); runGovernorWorkflow(); }); describe('quorum not reached', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('5'), support: Enums.VoteType.For }, { voter: voter2, weight: web3.utils.toWei('4'), support: Enums.VoteType.Abstain }, { voter: voter3, weight: web3.utils.toWei('10'), support: Enums.VoteType.Against }, ], steps: { execute: { error: 'Governor: proposal not successful' }, }, }; }); runGovernorWorkflow(); }); describe('score not reached', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.Against }, ], steps: { execute: { error: 'Governor: proposal not successful' }, }, }; }); runGovernorWorkflow(); }); describe('vote not over', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], steps: { wait: { enable: false }, execute: { error: 'Governor: proposal not successful' }, }, }; }); runGovernorWorkflow(); }); }); describe('state', function () { describe('Unset', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], steps: { propose: { enable: false }, wait: { enable: false }, execute: { enable: false }, }, }; }); afterEach(async function () { await expectRevert(this.mock.state(this.id), 'Governor: unknown proposal id'); }); runGovernorWorkflow(); }); describe('Pending & Active', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], steps: { propose: { noadvance: true }, wait: { enable: false }, execute: { enable: false }, }, }; }); afterEach(async function () { expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); await time.advanceBlockTo(this.snapshot); expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); await time.advanceBlock(); expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active); }); runGovernorWorkflow(); }); describe('Defeated', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], steps: { execute: { enable: false }, }, }; }); afterEach(async function () { expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); }); runGovernorWorkflow(); }); describe('Succeeded', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], steps: { execute: { enable: false }, }, }; }); afterEach(async function () { expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); }); runGovernorWorkflow(); }); describe('Executed', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], }; }); afterEach(async function () { expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); }); runGovernorWorkflow(); }); }); describe('Cancel', function () { describe('Before proposal', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], steps: { propose: { enable: false }, wait: { enable: false }, execute: { enable: false }, }, }; }); afterEach(async function () { await expectRevert( this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash), 'Governor: unknown proposal id', ); }); runGovernorWorkflow(); }); describe('After proposal', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], steps: { wait: { enable: false }, execute: { enable: false }, }, }; }); afterEach(async function () { await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash); expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); await expectRevert( this.mock.castVote(this.id, new BN('100'), { from: voter1 }), 'Governor: vote not currently active', ); }); runGovernorWorkflow(); }); describe('After vote', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], steps: { wait: { enable: false }, execute: { enable: false }, }, }; }); afterEach(async function () { await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash); expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); await expectRevert( this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), 'Governor: proposal not successful', ); }); runGovernorWorkflow(); }); describe('After deadline', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], steps: { execute: { enable: false }, }, }; }); afterEach(async function () { await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash); expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); await expectRevert( this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash), 'Governor: proposal not successful', ); }); runGovernorWorkflow(); }); describe('After execution', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.receiver.address ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], }; }); afterEach(async function () { await expectRevert( this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash), 'Governor: proposal not active', ); }); runGovernorWorkflow(); }); }); describe('Proposal length', function () { it('empty', async function () { await expectRevert( this.mock.propose( [], [], [], '', ), 'Governor: empty proposal', ); }); it('missmatch #1', async function () { await expectRevert( this.mock.propose( [ ], [ web3.utils.toWei('0') ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ), 'Governor: invalid proposal length', ); }); it('missmatch #2', async function () { await expectRevert( this.mock.propose( [ this.receiver.address ], [ ], [ this.receiver.contract.methods.mockFunction().encodeABI() ], '', ), 'Governor: invalid proposal length', ); }); it('missmatch #3', async function () { await expectRevert( this.mock.propose( [ this.receiver.address ], [ web3.utils.toWei('0') ], [ ], '', ), 'Governor: invalid proposal length', ); }); }); describe('Settings update', function () { describe('setVotingDelay', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.mock.address ], [ web3.utils.toWei('0') ], [ this.mock.contract.methods.setVotingDelay('0').encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], }; }); afterEach(async function () { expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); expectEvent( this.receipts.execute, 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' }, ); }); runGovernorWorkflow(); }); describe('setVotingPeriod', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.mock.address ], [ web3.utils.toWei('0') ], [ this.mock.contract.methods.setVotingPeriod('32').encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], }; }); afterEach(async function () { expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); expectEvent( this.receipts.execute, 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' }, ); }); runGovernorWorkflow(); }); describe('setVotingPeriod to 0', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.mock.address ], [ web3.utils.toWei('0') ], [ this.mock.contract.methods.setVotingPeriod('0').encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], steps: { execute: { error: 'GovernorSettings: voting period too low' }, }, }; }); afterEach(async function () { expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); }); runGovernorWorkflow(); }); describe('setProposalThreshold', function () { beforeEach(async function () { this.settings = { proposal: [ [ this.mock.address ], [ web3.utils.toWei('0') ], [ this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI() ], '', ], tokenHolder: owner, voters: [ { voter: voter1, weight: web3.utils.toWei('10'), support: Enums.VoteType.For }, ], }; }); afterEach(async function () { expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); expectEvent( this.receipts.execute, 'ProposalThresholdSet', { oldProposalThreshold: '0', newProposalThreshold: '1000000000000000000' }, ); }); runGovernorWorkflow(); }); describe('update protected', function () { it('setVotingDelay', async function () { await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); }); it('setVotingPeriod', async function () { await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance'); }); it('setProposalThreshold', async function () { await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance'); }); }); }); });