From c3cd70811b0993aab26840034c47e63fb3a2c993 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Mon, 18 Dec 2023 17:09:23 -0300 Subject: [PATCH] Migrate governance tests to ethers.js (#4728) Co-authored-by: ernestognw Co-authored-by: Hadrien Croubois --- package-lock.json | 20 +- package.json | 1 - test/governance/Governor.test.js | 994 +++++----- test/governance/TimelockController.test.js | 1653 ++++++++--------- .../extensions/GovernorERC721.test.js | 181 +- .../GovernorPreventLateQuorum.test.js | 246 ++- .../extensions/GovernorStorage.test.js | 209 ++- .../extensions/GovernorTimelockAccess.test.js | 858 +++++---- .../GovernorTimelockCompound.test.js | 447 ++--- .../GovernorTimelockControl.test.js | 519 +++--- .../GovernorVotesQuorumFraction.test.js | 164 +- .../extensions/GovernorWithParams.test.js | 327 ++-- test/governance/utils/ERC6372.behavior.js | 10 +- test/governance/utils/Votes.behavior.js | 467 +++-- test/governance/utils/Votes.test.js | 110 +- test/helpers/governance.js | 339 ++-- test/helpers/iterate.js | 7 + test/helpers/time.js | 11 +- test/helpers/txpool.js | 25 +- .../ERC20/extensions/ERC20Permit.test.js | 6 +- .../token/ERC20/extensions/ERC20Votes.test.js | 811 ++++---- .../ERC721/extensions/ERC721Votes.test.js | 261 +-- 22 files changed, 3734 insertions(+), 3932 deletions(-) diff --git a/package-lock.json b/package-lock.json index 922723c432f..fc8a1ca7f40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openzeppelin-solidity", - "version": "5.0.0", + "version": "5.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openzeppelin-solidity", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "devDependencies": { "@changesets/changelog-github": "^0.5.0", @@ -25,7 +25,6 @@ "@openzeppelin/test-helpers": "^0.5.13", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", - "array.prototype.at": "^1.1.1", "chai": "^4.2.0", "eslint": "^8.30.0", "eslint-config-prettier": "^9.0.0", @@ -4908,21 +4907,6 @@ "node": ">=0.10.0" } }, - "node_modules/array.prototype.at": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.at/-/array.prototype.at-1.1.2.tgz", - "integrity": "sha512-TPj626jUZMc2Qbld8uXKZrXM/lSStx2KfbIyF70Ui9RgdgibpTWC6WGCuff6qQ7xYzqXtir60WAHrfmknkF3Vw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.3.tgz", diff --git a/package.json b/package.json index d1005679ca7..644de03b9bd 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "@openzeppelin/test-helpers": "^0.5.13", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", - "array.prototype.at": "^1.1.1", "chai": "^4.2.0", "eslint": "^8.30.0", "eslint-config-prettier": "^9.0.0", diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index b277d8c1241..f27e0d9f2c0 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -1,77 +1,94 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../helpers/enums'); -const { getDomain, domainType, Ballot } = require('../helpers/eip712'); -const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance'); -const { clockFromReceipt } = require('../helpers/time'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { GovernorHelper } = require('../helpers/governance'); +const { getDomain, Ballot } = require('../helpers/eip712'); +const { bigint: Enums } = require('../helpers/enums'); +const { bigint: time } = require('../helpers/time'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); const { shouldBehaveLikeERC6372 } = require('./utils/ERC6372.behavior'); -const { ZERO_BYTES32 } = require('@openzeppelin/test-helpers/src/constants'); - -const Governor = artifacts.require('$GovernorMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); -const ERC721 = artifacts.require('$ERC721'); -const ERC1155 = artifacts.require('$ERC1155'); -const ERC1271WalletMock = artifacts.require('ERC1271WalletMock'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, - { Token: artifacts.require('$ERC20VotesLegacyMock'), mode: 'blocknumber' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, + { Token: '$ERC20VotesLegacyMock', mode: 'blocknumber' }, ]; -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'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +const signBallot = account => (contract, message) => + getDomain(contract).then(domain => account.signTypedData(domain, { Ballot }, message)); + +async function deployToken(contractName) { + try { + return await ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName, version]); + } catch (error) { + if (error.message == 'incorrect number of arguments to constructor') { + // ERC20VotesLegacyMock has a different construction that uses version='1' by default. + return ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName]); + } + throw error; + } +} + +describe('Governor', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, voter1, voter2, voter3, voter4, userEOA] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await deployToken(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + 10n, // quorumNumeratorValue + ]); + + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token: token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token: token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token: token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token: token, to: voter4, value: ethers.parseEther('2') }); + + return { + owner, + proposer, + voter1, + voter2, + voter3, + voter4, + userEOA, + receiver, + token, + mock, + helper, + }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - this.chainId = await web3.eth.getChainId(); - try { - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - } catch { - // ERC20VotesLegacyMock has a different construction that uses version='1' by default. - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - } - this.mock = await Governor.new( - name, // name - votingDelay, // initialVotingDelay - votingPeriod, // initialVotingPeriod - 0, // initialProposalThreshold - this.token.address, // tokenAddress - 10, // quorumNumeratorValue - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: owner, to: this.mock.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 }); - + Object.assign(this, await loadFixture(fixture)); + // initiate fresh proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, }, ], @@ -83,216 +100,163 @@ contract('Governor', function (accounts) { shouldBehaveLikeERC6372(mode); it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - 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'); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.equal(0n); + expect(await this.mock.COUNTING_MODE()).to.equal('support=bravo&quorum=for,abstain'); }); it('nominal workflow', async function () { // Before - expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS); - 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'); + expect(await this.mock.proposalProposer(this.proposal.id)).to.equal(ethers.ZeroAddress); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.false; + expect(await ethers.provider.getBalance(this.mock)).to.equal(value); + expect(await ethers.provider.getBalance(this.receiver)).to.equal(0n); - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0'); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(false); + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.false; // 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, - voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay), - voteEnd: web3.utils - .toBN(await clockFromReceipt[mode](txPropose.receipt)) - .add(votingDelay) - .add(votingPeriod), - description: this.proposal.description, - }); + const txPropose = await this.helper.connect(this.proposer).propose(); + const timepoint = await time.clockFromReceipt[mode](txPropose); + + await expect(txPropose) + .to.emit(this.mock, 'ProposalCreated') + .withArgs( + this.proposal.id, + this.proposer.address, + this.proposal.targets, + this.proposal.values, + this.proposal.signatures, + this.proposal.data, + timepoint + votingDelay, + timepoint + votingDelay + votingPeriod, + 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'), - }, - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For, reason: 'This is nice' })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter1.address, this.proposal.id, Enums.VoteType.For, ethers.parseEther('10'), 'This is nice'); - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { - voter: voter2, - support: Enums.VoteType.For, - weight: web3.utils.toWei('7'), - }); + await expect(this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter2.address, this.proposal.id, Enums.VoteType.For, ethers.parseEther('7'), ''); - expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { - voter: voter3, - support: Enums.VoteType.Against, - weight: web3.utils.toWei('5'), - }); + await expect(this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter3.address, this.proposal.id, Enums.VoteType.Against, ethers.parseEther('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 expect(this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter4.address, this.proposal.id, Enums.VoteType.Abstain, ethers.parseEther('2'), ''); await this.helper.waitForDeadline(); const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expect(txExecute).to.emit(this.mock, 'ProposalExecuted').withArgs(this.proposal.id); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expect(txExecute).to.emit(this.receiver, 'MockFunctionCalled'); // After - expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(proposer); - 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); - - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0'); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(false); + expect(await this.mock.proposalProposer(this.proposal.id)).to.equal(this.proposer.address); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.receiver)).to.equal(value); + + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.false; }); it('send ethers', async function () { - const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); - - this.proposal = this.helper.setProposal( + this.helper.setProposal( [ { - target: empty, + target: this.userEOA.address, value, }, ], '', ); - // Before - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0'); - // Run proposal - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - // After - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value); + await expect(async () => { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(); + return this.helper.execute(); + }).to.changeEtherBalances([this.mock, this.userEOA], [-value, value]); }); describe('vote with signature', function () { - const sign = privateKey => async (contract, message) => { - const domain = await getDomain(contract); - return ethSigUtil.signTypedMessage(privateKey, { - data: { - primaryType: 'Ballot', - types: { - EIP712Domain: domainType(domain), - Ballot, - }, - domain, - message, - }, - }); - }; - - afterEach('no other votes are cast for proposalId', async function () { - 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); - }); - it('votes with an EOA signature', async function () { - const voterBySig = Wallet.generate(); - const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - - await this.token.delegate(voterBySigAddress, { from: voter1 }); + await this.token.connect(this.voter1).delegate(this.userEOA); - const nonce = await this.mock.nonces(voterBySigAddress); + const nonce = await this.mock.nonces(this.userEOA); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - expectEvent( - await this.helper.vote({ + await expect( + this.helper.vote({ support: Enums.VoteType.For, - voter: voterBySigAddress, + voter: this.userEOA.address, nonce, - signature: sign(voterBySig.getPrivateKey()), + signature: signBallot(this.userEOA), }), - 'VoteCast', - { - voter: voterBySigAddress, - support: Enums.VoteType.For, - }, - ); + ) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.userEOA.address, this.proposal.id, Enums.VoteType.For, ethers.parseEther('10'), ''); + await this.helper.waitForDeadline(); await this.helper.execute(); // After - expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true); - expect(await this.mock.nonces(voterBySigAddress)).to.be.bignumber.equal(nonce.addn(1)); + expect(await this.mock.hasVoted(this.proposal.id, this.userEOA)).to.be.true; + expect(await this.mock.nonces(this.userEOA)).to.equal(nonce + 1n); }); it('votes with a valid EIP-1271 signature', async function () { - const ERC1271WalletOwner = Wallet.generate(); - ERC1271WalletOwner.address = web3.utils.toChecksumAddress(ERC1271WalletOwner.getAddressString()); - - const wallet = await ERC1271WalletMock.new(ERC1271WalletOwner.address); + const wallet = await ethers.deployContract('ERC1271WalletMock', [this.userEOA]); - await this.token.delegate(wallet.address, { from: voter1 }); + await this.token.connect(this.voter1).delegate(wallet); - const nonce = await this.mock.nonces(wallet.address); + const nonce = await this.mock.nonces(this.userEOA); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - expectEvent( - await this.helper.vote({ + await expect( + this.helper.vote({ support: Enums.VoteType.For, - voter: wallet.address, + voter: wallet.target, nonce, - signature: sign(ERC1271WalletOwner.getPrivateKey()), + signature: signBallot(this.userEOA), }), - 'VoteCast', - { - voter: wallet.address, - support: Enums.VoteType.For, - }, - ); + ) + .to.emit(this.mock, 'VoteCast') + .withArgs(wallet.target, this.proposal.id, Enums.VoteType.For, ethers.parseEther('10'), ''); await this.helper.waitForDeadline(); await this.helper.execute(); // After - expect(await this.mock.hasVoted(this.proposal.id, wallet.address)).to.be.equal(true); - expect(await this.mock.nonces(wallet.address)).to.be.bignumber.equal(nonce.addn(1)); + expect(await this.mock.hasVoted(this.proposal.id, wallet)).to.be.true; + expect(await this.mock.nonces(wallet)).to.equal(nonce + 1n); }); afterEach('no other votes are cast', async function () { - 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, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.false; }); }); @@ -300,97 +264,73 @@ contract('Governor', function (accounts) { describe('on propose', function () { it('if proposal already exists', async function () { await this.helper.propose(); - await expectRevertCustomError(this.helper.propose(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Pending, - ZERO_BYTES32, - ]); + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs(this.proposal.id, Enums.ProposalState.Pending, ethers.ZeroHash); }); it('if proposer has below threshold votes', async function () { - const votes = web3.utils.toWei('10'); - const threshold = web3.utils.toWei('1000'); + const votes = ethers.parseEther('10'); + const threshold = ethers.parseEther('1000'); await this.mock.$_setProposalThreshold(threshold); - await expectRevertCustomError(this.helper.propose({ from: voter1 }), 'GovernorInsufficientProposerVotes', [ - voter1, - votes, - threshold, - ]); + await expect(this.helper.connect(this.voter1).propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInsufficientProposerVotes') + .withArgs(this.voter1.address, votes, threshold); }); }); describe('on vote', function () { it('if proposal does not exist', async function () { - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorNonexistentProposal', - [this.proposal.id], - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('if voting has not started', async function () { await this.helper.propose(); - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorUnexpectedProposalState', - [this.proposal.id, Enums.ProposalState.Pending, proposalStatesToBitMap([Enums.ProposalState.Active])], - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Pending, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Active]), + ); }); it('if support value is invalid', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await expectRevertCustomError( - this.helper.vote({ support: web3.utils.toBN('255') }), + await expect(this.helper.vote({ support: 255 })).to.be.revertedWithCustomError( + this.mock, 'GovernorInvalidVoteType', - [], ); }); 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 expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorAlreadyCastVote', - [voter1], - ); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyCastVote') + .withArgs(this.voter1.address); }); it('if voting is over', async function () { await this.helper.propose(); await this.helper.waitForDeadline(); - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorUnexpectedProposalState', - [this.proposal.id, Enums.ProposalState.Defeated, proposalStatesToBitMap([Enums.ProposalState.Active])], - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Defeated, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Active]), + ); }); }); describe('on vote by signature', function () { beforeEach(async function () { - this.voterBySig = Wallet.generate(); - this.voterBySig.address = web3.utils.toChecksumAddress(this.voterBySig.getAddressString()); - - this.data = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'Ballot', - types: { - EIP712Domain: domainType(domain), - Ballot, - }, - domain, - message, - })); - - this.signature = (contract, message) => - this.data(contract, message).then(data => - ethSigUtil.signTypedMessage(this.voterBySig.getPrivateKey(), { data }), - ); - - await this.token.delegate(this.voterBySig.address, { from: voter1 }); + await this.token.connect(this.voter1).delegate(this.userEOA); // Run proposal await this.helper.propose(); @@ -398,96 +338,104 @@ contract('Governor', function (accounts) { }); it('if signature does not match signer', async function () { - const nonce = await this.mock.nonces(this.voterBySig.address); + const nonce = await this.mock.nonces(this.userEOA); + + function tamper(str, index, mask) { + const arrayStr = ethers.toBeArray(BigInt(str)); + arrayStr[index] ^= mask; + return ethers.hexlify(arrayStr); + } const voteParams = { support: Enums.VoteType.For, - voter: this.voterBySig.address, + voter: this.userEOA.address, nonce, - signature: async (...params) => { - const sig = await this.signature(...params); - const tamperedSig = web3.utils.hexToBytes(sig); - tamperedSig[42] ^= 0xff; - return web3.utils.bytesToHex(tamperedSig); - }, + signature: (...args) => signBallot(this.userEOA)(...args).then(sig => tamper(sig, 42, 0xff)), }; - await expectRevertCustomError(this.helper.vote(voteParams), 'GovernorInvalidSignature', [voteParams.voter]); + await expect(this.helper.vote(voteParams)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(voteParams.voter); }); it('if vote nonce is incorrect', async function () { - const nonce = await this.mock.nonces(this.voterBySig.address); + const nonce = await this.mock.nonces(this.userEOA); const voteParams = { support: Enums.VoteType.For, - voter: this.voterBySig.address, - nonce: nonce.addn(1), - signature: this.signature, + voter: this.userEOA.address, + nonce: nonce + 1n, + signature: signBallot(this.userEOA), }; - await expectRevertCustomError( - this.helper.vote(voteParams), - // The signature check implies the nonce can't be tampered without changing the signer - 'GovernorInvalidSignature', - [voteParams.voter], - ); + await expect(this.helper.vote(voteParams)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(voteParams.voter); }); }); describe('on queue', function () { it('always', async function () { - await this.helper.propose({ from: proposer }); + await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.queue(), 'GovernorQueueNotImplemented', []); + await expect(this.helper.queue()).to.be.revertedWithCustomError(this.mock, 'GovernorQueueNotImplemented'); }); }); describe('on execute', function () { it('if proposal does not exist', async function () { - await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [this.proposal.id]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('if quorum is not reached', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 }); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.For }); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); 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 expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.Against }); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); 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 expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('if receiver revert without reason', async function () { this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason'), }, ], '', @@ -495,17 +443,17 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'FailedInnerCall', []); + await expect(this.helper.execute()).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); }); it('if receiver revert with reason', async function () { this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsReason'), }, ], '', @@ -513,147 +461,157 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting'); + await expect(this.helper.execute()).to.be.revertedWith('CallReceiverMock: reverting'); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); }); }); describe('state', function () { it('Unset', async function () { - await expectRevertCustomError(this.mock.state(this.proposal.id), 'GovernorNonexistentProposal', [ - this.proposal.id, - ]); + await expect(this.mock.state(this.proposal.id)) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('Pending & Active', async function () { await this.helper.propose(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + expect(await this.mock.state(this.proposal.id)).to.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); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Pending); + await this.helper.waitForSnapshot(1n); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); }); 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); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(1n); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Defeated); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); 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); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(1n); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Succeeded); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Executed); }); }); describe('cancel', function () { describe('internal', function () { it('before proposal', async function () { - await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorNonexistentProposal', [ - this.proposal.id, - ]); + await expect(this.helper.cancel('internal')) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('after proposal', async function () { await this.helper.propose(); await this.helper.cancel('internal'); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); await this.helper.waitForSnapshot(); - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorUnexpectedProposalState', - [this.proposal.id, Enums.ProposalState.Canceled, proposalStatesToBitMap([Enums.ProposalState.Active])], - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Active]), + ); }); it('after vote', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.cancel('internal'); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.cancel('internal'); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap( - [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed], - { inverted: true }, - ), - ]); + await expect(this.helper.cancel('internal')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap( + [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed], + { inverted: true }, + ), + ); }); }); describe('public', function () { it('before proposal', async function () { - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorNonexistentProposal', [ - this.proposal.id, - ]); + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('after proposal', async function () { @@ -663,61 +621,69 @@ contract('Governor', function (accounts) { }); it('after proposal - restricted to proposer', async function () { - await this.helper.propose(); + await this.helper.connect(this.proposer).propose(); - await expectRevertCustomError(this.helper.cancel('external', { from: owner }), 'GovernorOnlyProposer', [ - owner, - ]); + await expect(this.helper.connect(this.owner).cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyProposer') + .withArgs(this.owner.address); }); it('after vote started', async function () { await this.helper.propose(); - await this.helper.waitForSnapshot(1); // snapshot + 1 block - - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Pending]), - ]); + await this.helper.waitForSnapshot(1n); // snapshot + 1 block + + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Pending]), + ); }); it('after vote', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Pending]), - ]); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Pending]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Succeeded, - proposalStatesToBitMap([Enums.ProposalState.Pending]), - ]); + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Succeeded, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Pending]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Pending]), - ]); + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Pending]), + ); }); }); }); @@ -725,88 +691,123 @@ contract('Governor', function (accounts) { describe('proposal length', function () { it('empty', async function () { this.helper.setProposal([], ''); - await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 0, 0]); + + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength') + .withArgs(0, 0, 0); }); it('mismatch #1', async function () { this.helper.setProposal( { targets: [], - values: [web3.utils.toWei('0')], - data: [this.receiver.contract.methods.mockFunction().encodeABI()], + values: [0n], + data: [this.receiver.interface.encodeFunctionData('mockFunction')], }, '', ); - await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 1, 1]); + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength') + .withArgs(0, 1, 1); }); it('mismatch #2', async function () { this.helper.setProposal( { - targets: [this.receiver.address], + targets: [this.receiver.target], values: [], - data: [this.receiver.contract.methods.mockFunction().encodeABI()], + data: [this.receiver.interface.encodeFunctionData('mockFunction')], }, '', ); - await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 1, 0]); + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength') + .withArgs(1, 1, 0); }); it('mismatch #3', async function () { this.helper.setProposal( { - targets: [this.receiver.address], - values: [web3.utils.toWei('0')], + targets: [this.receiver.target], + values: [0n], data: [], }, '', ); - await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 0, 1]); + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength') + .withArgs(1, 0, 1); }); }); describe('frontrun protection using description suffix', function () { + function shouldPropose() { + it('proposer can propose', async function () { + const txPropose = await this.helper.connect(this.proposer).propose(); + + await expect(txPropose) + .to.emit(this.mock, 'ProposalCreated') + .withArgs( + this.proposal.id, + this.proposer.address, + this.proposal.targets, + this.proposal.values, + this.proposal.signatures, + this.proposal.data, + (await time.clockFromReceipt[mode](txPropose)) + votingDelay, + (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod, + this.proposal.description, + ); + }); + + it('someone else can propose', async function () { + const txPropose = await this.helper.connect(this.voter1).propose(); + + await expect(txPropose) + .to.emit(this.mock, 'ProposalCreated') + .withArgs( + this.proposal.id, + this.voter1.address, + this.proposal.targets, + this.proposal.values, + this.proposal.signatures, + this.proposal.data, + (await time.clockFromReceipt[mode](txPropose)) + votingDelay, + (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod, + this.proposal.description, + ); + }); + } + describe('without protection', function () { describe('without suffix', function () { - it('proposer can propose', async function () { - expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); - }); - - it('someone else can propose', async function () { - expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); - }); + shouldPropose(); }); describe('with different suffix', function () { - beforeEach(async function () { + beforeEach(function () { this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, }, ], - `#wrong-suffix=${proposer}`, + `#wrong-suffix=${this.proposer}`, ); }); - it('proposer can propose', async function () { - expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); - }); - - it('someone else can propose', async function () { - expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); - }); + shouldPropose(); }); describe('with proposer suffix but bad address part', function () { - beforeEach(async function () { + beforeEach(function () { this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, }, ], @@ -814,69 +815,53 @@ contract('Governor', function (accounts) { ); }); - it('propose can propose', async function () { - expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); - }); - - it('someone else can propose', async function () { - expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); - }); + shouldPropose(); }); }); describe('with protection via proposer suffix', function () { - beforeEach(async function () { + beforeEach(function () { this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, }, ], - `#proposer=${proposer}`, + `#proposer=${this.proposer}`, ); }); - it('proposer can propose', async function () { - expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); - }); - - it('someone else cannot propose', async function () { - await expectRevertCustomError(this.helper.propose({ from: voter1 }), 'GovernorRestrictedProposer', [ - voter1, - ]); - }); + shouldPropose(); }); }); describe('onlyGovernance updates', function () { it('setVotingDelay is protected', async function () { - await expectRevertCustomError(this.mock.setVotingDelay('0', { from: owner }), 'GovernorOnlyExecutor', [ - owner, - ]); + await expect(this.mock.connect(this.owner).setVotingDelay(0n)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('setVotingPeriod is protected', async function () { - await expectRevertCustomError(this.mock.setVotingPeriod('32', { from: owner }), 'GovernorOnlyExecutor', [ - owner, - ]); + await expect(this.mock.connect(this.owner).setVotingPeriod(32n)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('setProposalThreshold is protected', async function () { - await expectRevertCustomError( - this.mock.setProposalThreshold('1000000000000000000', { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).setProposalThreshold(1_000_000_000_000_000_000n)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can setVotingDelay through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setVotingDelay('0').encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setVotingDelay', [0n]), }, ], '', @@ -884,20 +869,20 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' }); + await expect(this.helper.execute()).to.emit(this.mock, 'VotingDelaySet').withArgs(4n, 0n); - expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); + expect(await this.mock.votingDelay()).to.equal(0n); }); it('can setVotingPeriod through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setVotingPeriod', [32n]), }, ], '', @@ -905,21 +890,22 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' }); + await expect(this.helper.execute()).to.emit(this.mock, 'VotingPeriodSet').withArgs(16n, 32n); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); + expect(await this.mock.votingPeriod()).to.equal(32n); }); it('cannot setVotingPeriod to 0 through governance', async function () { - const votingPeriod = 0; + const votingPeriod = 0n; + this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod(votingPeriod).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setVotingPeriod', [votingPeriod]), }, ], '', @@ -927,18 +913,20 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidVotingPeriod', [votingPeriod]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVotingPeriod') + .withArgs(votingPeriod); }); it('can setProposalThreshold to 0 through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setProposalThreshold', [1_000_000_000_000_000_000n]), }, ], '', @@ -946,66 +934,62 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'ProposalThresholdSet', { - oldProposalThreshold: '0', - newProposalThreshold: '1000000000000000000', - }); + await expect(this.helper.execute()) + .to.emit(this.mock, 'ProposalThresholdSet') + .withArgs(0n, 1_000_000_000_000_000_000n); - expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); + expect(await this.mock.proposalThreshold()).to.equal(1_000_000_000_000_000_000n); }); }); describe('safe receive', function () { describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = web3.utils.toBN(1); + const tokenId = 1n; beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(owner, tokenId); + this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']); + await this.token.$_mint(this.owner, tokenId); }); it('can receive an ERC721 safeTransfer', async function () { - await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }); + await this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock.target, tokenId); }); }); describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { - 1: web3.utils.toBN(1000), - 2: web3.utils.toBN(2000), - 3: web3.utils.toBN(3000), + 1: 1000n, + 2: 2000n, + 3: 3000n, }; beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it('can receive ERC1155 safeTransfer', async function () { - await this.token.safeTransferFrom( - owner, - this.mock.address, + await this.token.connect(this.owner).safeTransferFrom( + this.owner, + this.mock.target, ...Object.entries(tokenIds)[0], // id + amount '0x', - { from: owner }, ); }); it('can receive ERC1155 safeBatchTransfer', async function () { - await this.token.safeBatchTransferFrom( - owner, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: owner }, - ); + await this.token + .connect(this.owner) + .safeBatchTransferFrom( + this.owner, + this.mock.target, + Object.keys(tokenIds), + Object.values(tokenIds), + '0x', + ); }); }); }); diff --git a/test/governance/TimelockController.test.js b/test/governance/TimelockController.test.js index ce051e7870a..9d3f5188b41 100644 --- a/test/governance/TimelockController.test.js +++ b/test/governance/TimelockController.test.js @@ -1,93 +1,112 @@ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS, ZERO_BYTES32 } = constants; -const { proposalStatesToBitMap } = require('../helpers/governance'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); + +const { GovernorHelper } = require('../helpers/governance'); +const { bigint: time } = require('../helpers/time'); +const { + bigint: { OperationState }, +} = require('../helpers/enums'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../helpers/customError'); -const { OperationState } = require('../helpers/enums'); -const TimelockController = artifacts.require('TimelockController'); -const CallReceiverMock = artifacts.require('CallReceiverMock'); -const Implementation2 = artifacts.require('Implementation2'); -const ERC721 = artifacts.require('$ERC721'); -const ERC1155 = artifacts.require('$ERC1155'); -const TimelockReentrant = artifacts.require('$TimelockReentrant'); +const salt = '0x025e7b0be353a74631ad648c667493c0e1cd31caa4cc2d3520fdc171ea0cc726'; // a random value const MINDELAY = time.duration.days(1); +const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; +const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE'); +const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE'); +const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE'); -const salt = '0x025e7b0be353a74631ad648c667493c0e1cd31caa4cc2d3520fdc171ea0cc726'; // a random value +const getAddress = obj => obj.address ?? obj.target ?? obj; function genOperation(target, value, data, predecessor, salt) { - const id = web3.utils.keccak256( - web3.eth.abi.encodeParameters( + const id = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( ['address', 'uint256', 'bytes', 'uint256', 'bytes32'], - [target, value, data, predecessor, salt], + [getAddress(target), value, data, predecessor, salt], ), ); return { id, target, value, data, predecessor, salt }; } function genOperationBatch(targets, values, payloads, predecessor, salt) { - const id = web3.utils.keccak256( - web3.eth.abi.encodeParameters( + const id = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( ['address[]', 'uint256[]', 'bytes[]', 'uint256', 'bytes32'], - [targets, values, payloads, predecessor, salt], + [targets.map(getAddress), values, payloads, predecessor, salt], ), ); return { id, targets, values, payloads, predecessor, salt }; } -contract('TimelockController', function (accounts) { - const [, admin, proposer, canceller, executor, other] = accounts; - - const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE'); - const EXECUTOR_ROLE = web3.utils.soliditySha3('EXECUTOR_ROLE'); - const CANCELLER_ROLE = web3.utils.soliditySha3('CANCELLER_ROLE'); +async function fixture() { + const [admin, proposer, canceller, executor, other] = await ethers.getSigners(); + + const mock = await ethers.deployContract('TimelockController', [MINDELAY, [proposer], [executor], admin]); + const callreceivermock = await ethers.deployContract('CallReceiverMock'); + const implementation2 = await ethers.deployContract('Implementation2'); + + expect(await mock.hasRole(CANCELLER_ROLE, proposer)).to.be.true; + await mock.connect(admin).revokeRole(CANCELLER_ROLE, proposer); + await mock.connect(admin).grantRole(CANCELLER_ROLE, canceller); + + return { + admin, + proposer, + canceller, + executor, + other, + mock, + callreceivermock, + implementation2, + }; +} +describe('TimelockController', function () { beforeEach(async function () { - // Deploy new timelock - this.mock = await TimelockController.new(MINDELAY, [proposer], [executor], admin); - - expect(await this.mock.hasRole(CANCELLER_ROLE, proposer)).to.be.equal(true); - await this.mock.revokeRole(CANCELLER_ROLE, proposer, { from: admin }); - await this.mock.grantRole(CANCELLER_ROLE, canceller, { from: admin }); - - // Mocks - this.callreceivermock = await CallReceiverMock.new({ from: admin }); - this.implementation2 = await Implementation2.new({ from: admin }); + Object.assign(this, await loadFixture(fixture)); }); shouldSupportInterfaces(['ERC1155Receiver']); it('initial state', async function () { - expect(await this.mock.getMinDelay()).to.be.bignumber.equal(MINDELAY); + expect(await this.mock.getMinDelay()).to.equal(MINDELAY); - expect(await this.mock.DEFAULT_ADMIN_ROLE()).to.be.equal(DEFAULT_ADMIN_ROLE); - expect(await this.mock.PROPOSER_ROLE()).to.be.equal(PROPOSER_ROLE); - expect(await this.mock.EXECUTOR_ROLE()).to.be.equal(EXECUTOR_ROLE); - expect(await this.mock.CANCELLER_ROLE()).to.be.equal(CANCELLER_ROLE); + expect(await this.mock.DEFAULT_ADMIN_ROLE()).to.equal(DEFAULT_ADMIN_ROLE); + expect(await this.mock.PROPOSER_ROLE()).to.equal(PROPOSER_ROLE); + expect(await this.mock.EXECUTOR_ROLE()).to.equal(EXECUTOR_ROLE); + expect(await this.mock.CANCELLER_ROLE()).to.equal(CANCELLER_ROLE); expect( - await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, proposer))), - ).to.be.deep.equal([true, false, false]); + await Promise.all( + [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.proposer)), + ), + ).to.deep.equal([true, false, false]); expect( - await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, canceller))), - ).to.be.deep.equal([false, true, false]); + await Promise.all( + [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.canceller)), + ), + ).to.deep.equal([false, true, false]); expect( - await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, executor))), - ).to.be.deep.equal([false, false, true]); + await Promise.all( + [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.executor)), + ), + ).to.deep.equal([false, false, true]); }); it('optional admin', async function () { - const mock = await TimelockController.new(MINDELAY, [proposer], [executor], ZERO_ADDRESS, { from: other }); - - expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, admin)).to.be.equal(false); - expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, mock.address)).to.be.equal(true); + const mock = await ethers.deployContract('TimelockController', [ + MINDELAY, + [this.proposer], + [this.executor], + ethers.ZeroAddress, + ]); + expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, this.admin)).to.be.false; + expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, mock.target)).to.be.true; }); describe('methods', function () { @@ -108,7 +127,7 @@ contract('TimelockController', function (accounts) { this.operation.predecessor, this.operation.salt, ), - ).to.be.equal(this.operation.id); + ).to.equal(this.operation.id); }); it('hashOperationBatch', async function () { @@ -127,7 +146,7 @@ contract('TimelockController', function (accounts) { this.operation.predecessor, this.operation.salt, ), - ).to.be.equal(this.operation.id); + ).to.equal(this.operation.id); }); }); describe('simple', function () { @@ -135,114 +154,119 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperation( '0x31754f590B97fD975Eb86938f18Cc304E264D2F2', - 0, + 0n, '0x3bf92ccc', - ZERO_BYTES32, + ethers.ZeroHash, salt, ); }); it('proposer can schedule', async function () { - const receipt = await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ); - expectEvent(receipt, 'CallScheduled', { - id: this.operation.id, - index: web3.utils.toBN(0), - target: this.operation.target, - value: web3.utils.toBN(this.operation.value), - data: this.operation.data, - predecessor: this.operation.predecessor, - delay: MINDELAY, - }); - - expectEvent(receipt, 'CallSalt', { - id: this.operation.id, - salt: this.operation.salt, - }); - - const block = await web3.eth.getBlock(receipt.receipt.blockHash); - - expect(await this.mock.getTimestamp(this.operation.id)).to.be.bignumber.equal( - web3.utils.toBN(block.timestamp).add(MINDELAY), - ); - }); - - it('prevent overwriting active operation', async function () { - await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ); - - await expectRevertCustomError( - this.mock.schedule( + const tx = await this.mock + .connect(this.proposer) + .schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY, - { from: proposer }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Unset)], - ); - }); + ); - it('prevent non-proposer from committing', async function () { - await expectRevertCustomError( - this.mock.schedule( + expect(tx) + .to.emit(this.mock, 'CallScheduled') + .withArgs( + this.operation.id, + 0n, this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, - this.operation.salt, MINDELAY, - { from: other }, - ), - `AccessControlUnauthorizedAccount`, - [other, PROPOSER_ROLE], + ) + .to.emit(this.mock, 'CallSalt') + .withArgs(this.operation.id, this.operation.salt); + + expect(await this.mock.getTimestamp(this.operation.id)).to.equal( + (await time.clockFromReceipt.timestamp(tx)) + MINDELAY, ); }); - it('enforce minimum delay', async function () { - await expectRevertCustomError( - this.mock.schedule( + it('prevent overwriting active operation', async function () { + await this.mock + .connect(this.proposer) + .schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, - MINDELAY - 1, - { from: proposer }, - ), - 'TimelockInsufficientDelay', - [MINDELAY, MINDELAY - 1], - ); + MINDELAY, + ); + + await expect( + this.mock + .connect(this.proposer) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Unset)); + }); + + it('prevent non-proposer from committing', async function () { + await expect( + this.mock + .connect(this.other) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, PROPOSER_ROLE); + }); + + it('enforce minimum delay', async function () { + await expect( + this.mock + .connect(this.proposer) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + MINDELAY - 1n, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInsufficientDelay') + .withArgs(MINDELAY - 1n, MINDELAY); }); it('schedule operation with salt zero', async function () { - const { receipt } = await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - ZERO_BYTES32, - MINDELAY, - { from: proposer }, - ); - expectEvent.notEmitted(receipt, 'CallSalt'); + await expect( + this.mock + .connect(this.proposer) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + ethers.ZeroHash, + MINDELAY, + ), + ).to.not.emit(this.mock, 'CallSalt'); }); }); @@ -250,188 +274,193 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperation( '0xAe22104DCD970750610E6FE15E623468A98b15f7', - 0, + 0n, '0x13e414de', - ZERO_BYTES32, + ethers.ZeroHash, '0xc1059ed2dc130227aa1d1d539ac94c641306905c020436c636e19e3fab56fc7f', ); }); it('revert if operation is not scheduled', async function () { - await expectRevertCustomError( - this.mock.execute( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); }); describe('with scheduled operation', function () { beforeEach(async function () { - ({ receipt: this.receipt, logs: this.logs } = await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - )); - }); - - it('revert if execution comes too early 1/2', async function () { - await expectRevertCustomError( - this.mock.execute( + await this.mock + .connect(this.proposer) + .schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + MINDELAY, + ); + }); + + it('revert if execution comes too early 1/2', async function () { + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); }); it('revert if execution comes too early 2/2', async function () { - const timestamp = await this.mock.getTimestamp(this.operation.id); - await time.increaseTo(timestamp - 5); // -1 is too tight, test sometime fails + // -1 is too tight, test sometime fails + await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock - 5n)); - await expectRevertCustomError( - this.mock.execute( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); }); describe('on time', function () { beforeEach(async function () { - const timestamp = await this.mock.getTimestamp(this.operation.id); - await time.increaseTo(timestamp); + await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock)); }); it('executor can reveal', async function () { - const receipt = await this.mock.execute( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ); - expectEvent(receipt, 'CallExecuted', { - id: this.operation.id, - index: web3.utils.toBN(0), - target: this.operation.target, - value: web3.utils.toBN(this.operation.value), - data: this.operation.data, - }); + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.emit(this.mock, 'CallExecuted') + .withArgs(this.operation.id, 0n, this.operation.target, this.operation.value, this.operation.data); }); it('prevent non-executor from revealing', async function () { - await expectRevertCustomError( - this.mock.execute( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - { from: other }, - ), - `AccessControlUnauthorizedAccount`, - [other, EXECUTOR_ROLE], - ); + await expect( + this.mock + .connect(this.other) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, EXECUTOR_ROLE); }); it('prevents reentrancy execution', async function () { // Create operation - const reentrant = await TimelockReentrant.new(); + const reentrant = await ethers.deployContract('$TimelockReentrant'); const reentrantOperation = genOperation( - reentrant.address, - 0, - reentrant.contract.methods.reenter().encodeABI(), - ZERO_BYTES32, + reentrant, + 0n, + reentrant.interface.encodeFunctionData('reenter'), + ethers.ZeroHash, salt, ); // Schedule so it can be executed - await this.mock.schedule( - reentrantOperation.target, - reentrantOperation.value, - reentrantOperation.data, - reentrantOperation.predecessor, - reentrantOperation.salt, - MINDELAY, - { from: proposer }, - ); + await this.mock + .connect(this.proposer) + .schedule( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + MINDELAY, + ); // Advance on time to make the operation executable - const timestamp = await this.mock.getTimestamp(reentrantOperation.id); - await time.increaseTo(timestamp); + await this.mock.getTimestamp(reentrantOperation.id).then(clock => time.forward.timestamp(clock)); // Grant executor role to the reentrant contract - await this.mock.grantRole(EXECUTOR_ROLE, reentrant.address, { from: admin }); + await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant); // Prepare reenter - const data = this.mock.contract.methods - .execute( - reentrantOperation.target, - reentrantOperation.value, - reentrantOperation.data, - reentrantOperation.predecessor, - reentrantOperation.salt, - ) - .encodeABI(); - await reentrant.enableRentrancy(this.mock.address, data); + const data = this.mock.interface.encodeFunctionData('execute', [ + getAddress(reentrantOperation.target), + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + ]); + await reentrant.enableRentrancy(this.mock, data); // Expect to fail - await expectRevertCustomError( - this.mock.execute( - reentrantOperation.target, - reentrantOperation.value, - reentrantOperation.data, - reentrantOperation.predecessor, - reentrantOperation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [reentrantOperation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + await expect( + this.mock + .connect(this.executor) + .execute( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(reentrantOperation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); // Disable reentrancy await reentrant.disableReentrancy(); const nonReentrantOperation = reentrantOperation; // Not anymore // Try again successfully - const receipt = await this.mock.execute( - nonReentrantOperation.target, - nonReentrantOperation.value, - nonReentrantOperation.data, - nonReentrantOperation.predecessor, - nonReentrantOperation.salt, - { from: executor }, - ); - expectEvent(receipt, 'CallExecuted', { - id: nonReentrantOperation.id, - index: web3.utils.toBN(0), - target: nonReentrantOperation.target, - value: web3.utils.toBN(nonReentrantOperation.value), - data: nonReentrantOperation.data, - }); + await expect( + this.mock + .connect(this.executor) + .execute( + nonReentrantOperation.target, + nonReentrantOperation.value, + nonReentrantOperation.data, + nonReentrantOperation.predecessor, + nonReentrantOperation.salt, + ), + ) + .to.emit(this.mock, 'CallExecuted') + .withArgs( + nonReentrantOperation.id, + 0n, + getAddress(nonReentrantOperation.target), + nonReentrantOperation.value, + nonReentrantOperation.data, + ); }); }); }); @@ -443,135 +472,139 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperationBatch( Array(8).fill('0xEd912250835c812D4516BBD80BdaEA1bB63a293C'), - Array(8).fill(0), + Array(8).fill(0n), Array(8).fill('0x2fcb7a88'), - ZERO_BYTES32, + ethers.ZeroHash, '0x6cf9d042ade5de78bed9ffd075eb4b2a4f6b1736932c2dc8af517d6e066f51f5', ); }); it('proposer can schedule', async function () { - const receipt = await this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ); + const tx = this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ); for (const i in this.operation.targets) { - expectEvent(receipt, 'CallScheduled', { - id: this.operation.id, - index: web3.utils.toBN(i), - target: this.operation.targets[i], - value: web3.utils.toBN(this.operation.values[i]), - data: this.operation.payloads[i], - predecessor: this.operation.predecessor, - delay: MINDELAY, - }); - - expectEvent(receipt, 'CallSalt', { - id: this.operation.id, - salt: this.operation.salt, - }); + await expect(tx) + .to.emit(this.mock, 'CallScheduled') + .withArgs( + this.operation.id, + i, + getAddress(this.operation.targets[i]), + this.operation.values[i], + this.operation.payloads[i], + this.operation.predecessor, + MINDELAY, + ) + .to.emit(this.mock, 'CallSalt') + .withArgs(this.operation.id, this.operation.salt); } - const block = await web3.eth.getBlock(receipt.receipt.blockHash); - - expect(await this.mock.getTimestamp(this.operation.id)).to.be.bignumber.equal( - web3.utils.toBN(block.timestamp).add(MINDELAY), + expect(await this.mock.getTimestamp(this.operation.id)).to.equal( + (await time.clockFromReceipt.timestamp(tx)) + MINDELAY, ); }); it('prevent overwriting active operation', async function () { - await this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ); - - await expectRevertCustomError( - this.mock.scheduleBatch( + await this.mock + .connect(this.proposer) + .scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY, - { from: proposer }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Unset)], - ); + ); + + await expect( + this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Unset)); }); it('length of batch parameter must match #1', async function () { - await expectRevertCustomError( - this.mock.scheduleBatch( - this.operation.targets, - [], - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ), - 'TimelockInvalidOperationLength', - [this.operation.targets.length, this.operation.payloads.length, 0], - ); + await expect( + this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + [], + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(this.operation.targets.length, this.operation.payloads.length, 0n); }); it('length of batch parameter must match #1', async function () { - await expectRevertCustomError( - this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - [], - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ), - 'TimelockInvalidOperationLength', - [this.operation.targets.length, 0, this.operation.payloads.length], - ); + await expect( + this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + this.operation.values, + [], + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(this.operation.targets.length, 0n, this.operation.payloads.length); }); it('prevent non-proposer from committing', async function () { - await expectRevertCustomError( - this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: other }, - ), - `AccessControlUnauthorizedAccount`, - [other, PROPOSER_ROLE], - ); + await expect( + this.mock + .connect(this.other) + .scheduleBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, PROPOSER_ROLE); }); it('enforce minimum delay', async function () { - await expectRevertCustomError( - this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY - 1, - { from: proposer }, - ), - 'TimelockInsufficientDelay', - [MINDELAY, MINDELAY - 1], - ); + await expect( + this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY - 1n, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInsufficientDelay') + .withArgs(MINDELAY - 1n, MINDELAY); }); }); @@ -579,236 +612,248 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperationBatch( Array(8).fill('0x76E53CcEb05131Ef5248553bEBDb8F70536830b1'), - Array(8).fill(0), + Array(8).fill(0n), Array(8).fill('0x58a60f63'), - ZERO_BYTES32, + ethers.ZeroHash, '0x9545eeabc7a7586689191f78a5532443698538e54211b5bd4d7dc0fc0102b5c7', ); }); it('revert if operation is not scheduled', async function () { - await expectRevertCustomError( - this.mock.executeBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); - }); - - describe('with scheduled operation', function () { - beforeEach(async function () { - ({ receipt: this.receipt, logs: this.logs } = await this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - )); - }); - - it('revert if execution comes too early 1/2', async function () { - await expectRevertCustomError( - this.mock.executeBatch( + await expect( + this.mock + .connect(this.executor) + .executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: executor }, ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); - }); - - it('revert if execution comes too early 2/2', async function () { - const timestamp = await this.mock.getTimestamp(this.operation.id); - await time.increaseTo(timestamp - 5); // -1 is to tight, test sometime fails - - await expectRevertCustomError( - this.mock.executeBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); - }); - - describe('on time', function () { - beforeEach(async function () { - const timestamp = await this.mock.getTimestamp(this.operation.id); - await time.increaseTo(timestamp); - }); + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); + }); - it('executor can reveal', async function () { - const receipt = await this.mock.executeBatch( + describe('with scheduled operation', function () { + beforeEach(async function () { + await this.mock + .connect(this.proposer) + .scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: executor }, + MINDELAY, ); - for (const i in this.operation.targets) { - expectEvent(receipt, 'CallExecuted', { - id: this.operation.id, - index: web3.utils.toBN(i), - target: this.operation.targets[i], - value: web3.utils.toBN(this.operation.values[i]), - data: this.operation.payloads[i], - }); - } - }); + }); - it('prevent non-executor from revealing', async function () { - await expectRevertCustomError( - this.mock.executeBatch( + it('revert if execution comes too early 1/2', async function () { + await expect( + this.mock + .connect(this.executor) + .executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: other }, ), - `AccessControlUnauthorizedAccount`, - [other, EXECUTOR_ROLE], - ); - }); + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); + }); - it('length mismatch #1', async function () { - await expectRevertCustomError( - this.mock.executeBatch( - [], + it('revert if execution comes too early 2/2', async function () { + // -1 is to tight, test sometime fails + await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock - 5n)); + + await expect( + this.mock + .connect(this.executor) + .executeBatch( + this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: executor }, ), - 'TimelockInvalidOperationLength', - [0, this.operation.payloads.length, this.operation.values.length], - ); + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); + }); + + describe('on time', function () { + beforeEach(async function () { + await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock)); }); - it('length mismatch #2', async function () { - await expectRevertCustomError( - this.mock.executeBatch( + it('executor can reveal', async function () { + const tx = this.mock + .connect(this.executor) + .executeBatch( this.operation.targets, - [], + this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: executor }, - ), - 'TimelockInvalidOperationLength', - [this.operation.targets.length, this.operation.payloads.length, 0], - ); + ); + for (const i in this.operation.targets) { + expect(tx) + .to.emit(this.mock, 'CallExecuted') + .withArgs( + this.operation.id, + i, + this.operation.targets[i], + this.operation.values[i], + this.operation.payloads[i], + ); + } + }); + + it('prevent non-executor from revealing', async function () { + await expect( + this.mock + .connect(this.other) + .executeBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, EXECUTOR_ROLE); + }); + + it('length mismatch #1', async function () { + await expect( + this.mock + .connect(this.executor) + .executeBatch( + [], + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(0, this.operation.payloads.length, this.operation.values.length); + }); + + it('length mismatch #2', async function () { + await expect( + this.mock + .connect(this.executor) + .executeBatch( + this.operation.targets, + [], + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(this.operation.targets.length, this.operation.payloads.length, 0n); }); it('length mismatch #3', async function () { - await expectRevertCustomError( - this.mock.executeBatch( - this.operation.targets, - this.operation.values, - [], - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockInvalidOperationLength', - [this.operation.targets.length, 0, this.operation.values.length], - ); + await expect( + this.mock + .connect(this.executor) + .executeBatch( + this.operation.targets, + this.operation.values, + [], + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(this.operation.targets.length, 0n, this.operation.values.length); }); it('prevents reentrancy execution', async function () { // Create operation - const reentrant = await TimelockReentrant.new(); + const reentrant = await ethers.deployContract('$TimelockReentrant'); const reentrantBatchOperation = genOperationBatch( - [reentrant.address], - [0], - [reentrant.contract.methods.reenter().encodeABI()], - ZERO_BYTES32, + [reentrant], + [0n], + [reentrant.interface.encodeFunctionData('reenter')], + ethers.ZeroHash, salt, ); // Schedule so it can be executed - await this.mock.scheduleBatch( - reentrantBatchOperation.targets, - reentrantBatchOperation.values, - reentrantBatchOperation.payloads, - reentrantBatchOperation.predecessor, - reentrantBatchOperation.salt, - MINDELAY, - { from: proposer }, - ); + await this.mock + .connect(this.proposer) + .scheduleBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + MINDELAY, + ); // Advance on time to make the operation executable - const timestamp = await this.mock.getTimestamp(reentrantBatchOperation.id); - await time.increaseTo(timestamp); + await this.mock.getTimestamp(reentrantBatchOperation.id).then(clock => time.forward.timestamp(clock)); // Grant executor role to the reentrant contract - await this.mock.grantRole(EXECUTOR_ROLE, reentrant.address, { from: admin }); + await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant); // Prepare reenter - const data = this.mock.contract.methods - .executeBatch( - reentrantBatchOperation.targets, - reentrantBatchOperation.values, - reentrantBatchOperation.payloads, - reentrantBatchOperation.predecessor, - reentrantBatchOperation.salt, - ) - .encodeABI(); - await reentrant.enableRentrancy(this.mock.address, data); + const data = this.mock.interface.encodeFunctionData('executeBatch', [ + reentrantBatchOperation.targets.map(getAddress), + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + ]); + await reentrant.enableRentrancy(this.mock, data); // Expect to fail - await expectRevertCustomError( - this.mock.executeBatch( - reentrantBatchOperation.targets, - reentrantBatchOperation.values, - reentrantBatchOperation.payloads, - reentrantBatchOperation.predecessor, - reentrantBatchOperation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [reentrantBatchOperation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + await expect( + this.mock + .connect(this.executor) + .executeBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(reentrantBatchOperation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); // Disable reentrancy await reentrant.disableReentrancy(); const nonReentrantBatchOperation = reentrantBatchOperation; // Not anymore // Try again successfully - const receipt = await this.mock.executeBatch( - nonReentrantBatchOperation.targets, - nonReentrantBatchOperation.values, - nonReentrantBatchOperation.payloads, - nonReentrantBatchOperation.predecessor, - nonReentrantBatchOperation.salt, - { from: executor }, - ); + const tx = this.mock + .connect(this.executor) + .executeBatch( + nonReentrantBatchOperation.targets, + nonReentrantBatchOperation.values, + nonReentrantBatchOperation.payloads, + nonReentrantBatchOperation.predecessor, + nonReentrantBatchOperation.salt, + ); for (const i in nonReentrantBatchOperation.targets) { - expectEvent(receipt, 'CallExecuted', { - id: nonReentrantBatchOperation.id, - index: web3.utils.toBN(i), - target: nonReentrantBatchOperation.targets[i], - value: web3.utils.toBN(nonReentrantBatchOperation.values[i]), - data: nonReentrantBatchOperation.payloads[i], - }); + expect(tx) + .to.emit(this.mock, 'CallExecuted') + .withArgs( + nonReentrantBatchOperation.id, + i, + nonReentrantBatchOperation.targets[i], + nonReentrantBatchOperation.values[i], + nonReentrantBatchOperation.payloads[i], + ); } }); }); @@ -816,39 +861,41 @@ contract('TimelockController', function (accounts) { it('partial execution', async function () { const operation = genOperationBatch( - [this.callreceivermock.address, this.callreceivermock.address, this.callreceivermock.address], - [0, 0, 0], + [this.callreceivermock, this.callreceivermock, this.callreceivermock], + [0n, 0n, 0n], [ - this.callreceivermock.contract.methods.mockFunction().encodeABI(), - this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), - this.callreceivermock.contract.methods.mockFunction().encodeABI(), + this.callreceivermock.interface.encodeFunctionData('mockFunction'), + this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'), + this.callreceivermock.interface.encodeFunctionData('mockFunction'), ], - ZERO_BYTES32, + ethers.ZeroHash, '0x8ac04aa0d6d66b8812fb41d39638d37af0a9ab11da507afd65c509f8ed079d3e', ); - await this.mock.scheduleBatch( - operation.targets, - operation.values, - operation.payloads, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - await expectRevertCustomError( - this.mock.executeBatch( + await this.mock + .connect(this.proposer) + .scheduleBatch( operation.targets, operation.values, operation.payloads, operation.predecessor, operation.salt, - { from: executor }, - ), - 'FailedInnerCall', - [], - ); + MINDELAY, + ); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await expect( + this.mock + .connect(this.executor) + .executeBatch( + operation.targets, + operation.values, + operation.payloads, + operation.predecessor, + operation.salt, + ), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); }); }); }); @@ -857,81 +904,78 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperation( '0xC6837c44AA376dbe1d2709F13879E040CAb653ca', - 0, + 0n, '0x296e58dd', - ZERO_BYTES32, + ethers.ZeroHash, '0xa2485763600634800df9fc9646fb2c112cf98649c55f63dd1d9c7d13a64399d9', ); - ({ receipt: this.receipt, logs: this.logs } = await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - )); + await this.mock + .connect(this.proposer) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ); }); it('canceller can cancel', async function () { - const receipt = await this.mock.cancel(this.operation.id, { from: canceller }); - expectEvent(receipt, 'Cancelled', { id: this.operation.id }); + await expect(this.mock.connect(this.canceller).cancel(this.operation.id)) + .to.emit(this.mock, 'Cancelled') + .withArgs(this.operation.id); }); it('cannot cancel invalid operation', async function () { - await expectRevertCustomError( - this.mock.cancel(constants.ZERO_BYTES32, { from: canceller }), - 'TimelockUnexpectedOperationState', - [constants.ZERO_BYTES32, proposalStatesToBitMap([OperationState.Waiting, OperationState.Ready])], - ); + await expect(this.mock.connect(this.canceller).cancel(ethers.ZeroHash)) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs( + ethers.ZeroHash, + GovernorHelper.proposalStatesToBitMap([OperationState.Waiting, OperationState.Ready]), + ); }); it('prevent non-canceller from canceling', async function () { - await expectRevertCustomError( - this.mock.cancel(this.operation.id, { from: other }), - `AccessControlUnauthorizedAccount`, - [other, CANCELLER_ROLE], - ); + await expect(this.mock.connect(this.other).cancel(this.operation.id)) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, CANCELLER_ROLE); }); }); }); describe('maintenance', function () { it('prevent unauthorized maintenance', async function () { - await expectRevertCustomError(this.mock.updateDelay(0, { from: other }), 'TimelockUnauthorizedCaller', [other]); + await expect(this.mock.connect(this.other).updateDelay(0n)) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnauthorizedCaller') + .withArgs(this.other.address); }); it('timelock scheduled maintenance', async function () { const newDelay = time.duration.hours(6); const operation = genOperation( - this.mock.address, - 0, - this.mock.contract.methods.updateDelay(newDelay.toString()).encodeABI(), - ZERO_BYTES32, + this.mock, + 0n, + this.mock.interface.encodeFunctionData('updateDelay', [newDelay]), + ethers.ZeroHash, '0xf8e775b2c5f4d66fb5c7fa800f35ef518c262b6014b3c0aee6ea21bff157f108', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - const receipt = await this.mock.execute( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - { from: executor }, - ); - expectEvent(receipt, 'MinDelayChange', { newDuration: newDelay.toString(), oldDuration: MINDELAY }); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - expect(await this.mock.getMinDelay()).to.be.bignumber.equal(newDelay); + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ) + .to.emit(this.mock, 'MinDelayChange') + .withArgs(MINDELAY, newDelay); + + expect(await this.mock.getMinDelay()).to.equal(newDelay); }); }); @@ -939,71 +983,77 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation1 = genOperation( '0xdE66bD4c97304200A95aE0AadA32d6d01A867E39', - 0, + 0n, '0x01dc731a', - ZERO_BYTES32, + ethers.ZeroHash, '0x64e932133c7677402ead2926f86205e2ca4686aebecf5a8077627092b9bb2feb', ); this.operation2 = genOperation( '0x3c7944a3F1ee7fc8c5A5134ba7c79D11c3A1FCa3', - 0, + 0n, '0x8f531849', this.operation1.id, '0x036e1311cac523f9548e6461e29fb1f8f9196b91910a41711ea22f5de48df07d', ); - await this.mock.schedule( - this.operation1.target, - this.operation1.value, - this.operation1.data, - this.operation1.predecessor, - this.operation1.salt, - MINDELAY, - { from: proposer }, - ); - await this.mock.schedule( - this.operation2.target, - this.operation2.value, - this.operation2.data, - this.operation2.predecessor, - this.operation2.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - }); - - it('cannot execute before dependency', async function () { - await expectRevertCustomError( - this.mock.execute( + await this.mock + .connect(this.proposer) + .schedule( + this.operation1.target, + this.operation1.value, + this.operation1.data, + this.operation1.predecessor, + this.operation1.salt, + MINDELAY, + ); + await this.mock + .connect(this.proposer) + .schedule( this.operation2.target, this.operation2.value, this.operation2.data, this.operation2.predecessor, this.operation2.salt, - { from: executor }, - ), - 'TimelockUnexecutedPredecessor', - [this.operation1.id], - ); + MINDELAY, + ); + + await this.mock.getTimestamp(this.operation2.id).then(clock => time.forward.timestamp(clock)); + }); + + it('cannot execute before dependency', async function () { + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation2.target, + this.operation2.value, + this.operation2.data, + this.operation2.predecessor, + this.operation2.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexecutedPredecessor') + .withArgs(this.operation1.id); }); it('can execute after dependency', async function () { - await this.mock.execute( - this.operation1.target, - this.operation1.value, - this.operation1.data, - this.operation1.predecessor, - this.operation1.salt, - { from: executor }, - ); - await this.mock.execute( - this.operation2.target, - this.operation2.value, - this.operation2.data, - this.operation2.predecessor, - this.operation2.salt, - { from: executor }, - ); + await this.mock + .connect(this.executor) + .execute( + this.operation1.target, + this.operation1.value, + this.operation1.data, + this.operation1.predecessor, + this.operation1.salt, + ); + await this.mock + .connect(this.executor) + .execute( + this.operation2.target, + this.operation2.value, + this.operation2.data, + this.operation2.predecessor, + this.operation2.salt, + ); }); }); @@ -1012,274 +1062,219 @@ contract('TimelockController', function (accounts) { it('call', async function () { const operation = genOperation( - this.implementation2.address, - 0, - this.implementation2.contract.methods.setValue(42).encodeABI(), - ZERO_BYTES32, + this.implementation2, + 0n, + this.implementation2.interface.encodeFunctionData('setValue', [42n]), + ethers.ZeroHash, '0x8043596363daefc89977b25f9d9b4d06c3910959ef0c4d213557a903e1b555e2', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - await this.mock.execute( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - { from: executor }, - ); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - expect(await this.implementation2.getValue()).to.be.bignumber.equal(web3.utils.toBN(42)); + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt); + + expect(await this.implementation2.getValue()).to.equal(42n); }); it('call reverting', async function () { const operation = genOperation( - this.callreceivermock.address, - 0, - this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), - ZERO_BYTES32, + this.callreceivermock, + 0n, + this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'), + ethers.ZeroHash, '0xb1b1b276fdf1a28d1e00537ea73b04d56639128b08063c1a2f70a52e38cba693', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - await expectRevertCustomError( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - }), - 'FailedInnerCall', - [], - ); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); }); it('call throw', async function () { const operation = genOperation( - this.callreceivermock.address, - 0, - this.callreceivermock.contract.methods.mockFunctionThrows().encodeABI(), - ZERO_BYTES32, + this.callreceivermock, + 0n, + this.callreceivermock.interface.encodeFunctionData('mockFunctionThrows'), + ethers.ZeroHash, '0xe5ca79f295fc8327ee8a765fe19afb58f4a0cbc5053642bfdd7e73bc68e0fc67', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + // Targeted function reverts with a panic code (0x1) + the timelock bubble the panic code - await expectRevert.unspecified( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - }), - ); + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ).to.be.revertedWithPanic(PANIC_CODES.ASSERTION_ERROR); }); it('call out of gas', async function () { const operation = genOperation( - this.callreceivermock.address, - 0, - this.callreceivermock.contract.methods.mockFunctionOutOfGas().encodeABI(), - ZERO_BYTES32, + this.callreceivermock, + 0n, + this.callreceivermock.interface.encodeFunctionData('mockFunctionOutOfGas'), + ethers.ZeroHash, '0xf3274ce7c394c5b629d5215723563a744b817e1730cca5587c567099a14578fd', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - await expectRevertCustomError( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - gas: '100000', - }), - 'FailedInnerCall', - [], - ); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { + gasLimit: '100000', + }), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); }); it('call payable with eth', async function () { const operation = genOperation( - this.callreceivermock.address, + this.callreceivermock, 1, - this.callreceivermock.contract.methods.mockFunction().encodeABI(), - ZERO_BYTES32, + this.callreceivermock.interface.encodeFunctionData('mockFunction'), + ethers.ZeroHash, '0x5ab73cd33477dcd36c1e05e28362719d0ed59a7b9ff14939de63a43073dc1f44', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - - await this.mock.execute( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - { from: executor, value: 1 }, - ); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(1)); + await this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { + value: 1, + }); + + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(1n); }); it('call nonpayable with eth', async function () { const operation = genOperation( - this.callreceivermock.address, + this.callreceivermock, 1, - this.callreceivermock.contract.methods.mockFunctionNonPayable().encodeABI(), - ZERO_BYTES32, + this.callreceivermock.interface.encodeFunctionData('mockFunctionNonPayable'), + ethers.ZeroHash, '0xb78edbd920c7867f187e5aa6294ae5a656cfbf0dea1ccdca3751b740d0f2bdf8', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); - await expectRevertCustomError( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - }), - 'FailedInnerCall', - [], - ); + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); + + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); }); it('call reverting with eth', async function () { const operation = genOperation( - this.callreceivermock.address, + this.callreceivermock, 1, - this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), - ZERO_BYTES32, + this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'), + ethers.ZeroHash, '0xdedb4563ef0095db01d81d3f2decf57cf83e4a72aa792af14c43a792b56f4de6', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); - await expectRevertCustomError( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - }), - 'FailedInnerCall', - [], - ); + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); + + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); }); }); describe('safe receive', function () { describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = new BN(1); + const tokenId = 1n; beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(other, tokenId); + this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']); + await this.token.$_mint(this.other, tokenId); }); it('can receive an ERC721 safeTransfer', async function () { - await this.token.safeTransferFrom(other, this.mock.address, tokenId, { from: other }); + await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, tokenId); }); }); describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { - 1: new BN(1000), - 2: new BN(2000), - 3: new BN(3000), + 1: 1000n, + 2: 2000n, + 3: 3000n, }; beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(other, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + await this.token.$_mintBatch(this.other, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it('can receive ERC1155 safeTransfer', async function () { - await this.token.safeTransferFrom( - other, - this.mock.address, - ...Object.entries(tokenIds)[0], // id + amount + await this.token.connect(this.other).safeTransferFrom( + this.other, + this.mock, + ...Object.entries(tokenIds)[0n], // id + amount '0x', - { from: other }, ); }); it('can receive ERC1155 safeBatchTransfer', async function () { - await this.token.safeBatchTransferFrom( - other, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: other }, - ); + await this.token + .connect(this.other) + .safeBatchTransferFrom(this.other, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); }); }); diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 22265cc25fe..0d2a33c7345 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,59 +1,77 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); - -const Governor = artifacts.require('$GovernorVoteMocks'); -const CallReceiver = artifacts.require('CallReceiverMock'); +const { bigint: Enums } = require('../../helpers/enums'); const TOKENS = [ - { Token: artifacts.require('$ERC721Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC721VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC721Votes', mode: 'blocknumber' }, + { Token: '$ERC721VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorERC721', 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.toBN(0); - const NFT1 = web3.utils.toBN(1); - const NFT2 = web3.utils.toBN(2); - const NFT3 = web3.utils.toBN(3); - const NFT4 = web3.utils.toBN(4); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockNFToken'; +const tokenSymbol = 'MTKN'; +const NFT0 = 0n; +const NFT1 = 1n; +const NFT2 = 2n; +const NFT3 = 3n; +const NFT4 = 4n; +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +describe('GovernorERC721', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, voter1, voter2, voter3, voter4] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + 10n, // quorumNumeratorValue + ]); + + await owner.sendTransaction({ to: mock, value }); + await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => token.$_mint(owner, tokenId))); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, tokenId: NFT0 }); + await helper.connect(owner).delegate({ token, to: voter2, tokenId: NFT1 }); + await helper.connect(owner).delegate({ token, to: voter2, tokenId: NFT2 }); + await helper.connect(owner).delegate({ token, to: voter3, tokenId: NFT3 }); + await helper.connect(owner).delegate({ token, to: voter4, tokenId: NFT4 }); + + return { + owner, + voter1, + voter2, + voter3, + voter4, + receiver, + token, + mock, + helper, + }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - 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 + Object.assign(this, await loadFixture(fixture)); + // initiate fresh proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), }, ], '', @@ -61,55 +79,52 @@ contract('GovernorERC721', 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(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.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0n)).to.equal(0n); + + expect(await this.token.getVotes(this.voter1)).to.equal(1n); // NFT0 + expect(await this.token.getVotes(this.voter2)).to.equal(2n); // NFT1 & NFT2 + expect(await this.token.getVotes(this.voter3)).to.equal(1n); // NFT3 + expect(await this.token.getVotes(this.voter4)).to.equal(1n); // NFT4 }); 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 expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter1.address, this.proposal.id, Enums.VoteType.For, 1n, ''); + + await expect(this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter2.address, this.proposal.id, Enums.VoteType.For, 2n, ''); + + await expect(this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter3.address, this.proposal.id, Enums.VoteType.Against, 1n, ''); + + await expect(this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter4.address, this.proposal.id, Enums.VoteType.Abstain, 1n, ''); 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'); - }); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter3)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter4)).to.be.true; + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([ + 1n, // againstVotes + 3n, // forVotes + 1n, // abstainVotes + ]); }); }); } diff --git a/test/governance/extensions/GovernorPreventLateQuorum.test.js b/test/governance/extensions/GovernorPreventLateQuorum.test.js index 17ae05a73fb..8defa70144c 100644 --- a/test/governance/extensions/GovernorPreventLateQuorum.test.js +++ b/test/governance/extensions/GovernorPreventLateQuorum.test.js @@ -1,66 +1,66 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); -const { clockFromReceipt } = require('../../helpers/time'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const Governor = artifacts.require('$GovernorPreventLateQuorumMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorPreventLateQuorum', function (accounts) { - const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const lateQuorumVoteExtension = web3.utils.toBN(8); - const quorum = web3.utils.toWei('1'); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const lateQuorumVoteExtension = 8n; +const quorum = ethers.parseEther('1'); +const value = ethers.parseEther('1'); + +describe('GovernorPreventLateQuorum', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorPreventLateQuorumMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + lateQuorumVoteExtension, + quorum, + ]); + + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { owner, proposer, voter1, voter2, voter3, voter4, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.token.address, - lateQuorumVoteExtension, - quorum, - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: owner, to: this.mock.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 + Object.assign(this, await loadFixture(fixture)); + // initiate fresh proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), }, ], '', @@ -68,110 +68,101 @@ contract('GovernorPreventLateQuorum', 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(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.equal(quorum); + expect(await this.mock.lateQuorumVoteExtension()).to.equal(lateQuorumVoteExtension); }); it('nominal workflow unaffected', async function () { - const txPropose = await this.helper.propose({ from: proposer }); + const txPropose = await this.helper.connect(this.proposer).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.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); await this.helper.execute(); - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); - expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); - expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); - }); - - const voteStart = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay); - const voteEnd = web3.utils - .toBN(await clockFromReceipt[mode](txPropose.receipt)) - .add(votingDelay) - .add(votingPeriod); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(voteStart); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(voteEnd); - - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values.map(value => web3.utils.toBN(value)), - signatures: this.proposal.signatures, - calldatas: this.proposal.data, - voteStart, - voteEnd, - description: this.proposal.description, - }); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter3)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter4)).to.be.true; + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([ + ethers.parseEther('5'), // againstVotes + ethers.parseEther('17'), // forVotes + ethers.parseEther('2'), // abstainVotes + ]); + + const voteStart = (await time.clockFromReceipt[mode](txPropose)) + votingDelay; + const voteEnd = (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod; + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(voteStart); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(voteEnd); + + await expect(txPropose) + .to.emit(this.mock, 'ProposalCreated') + .withArgs( + this.proposal.id, + this.proposer.address, + this.proposal.targets, + this.proposal.values, + this.proposal.signatures, + this.proposal.data, + voteStart, + voteEnd, + this.proposal.description, + ); }); it('Delay is extended to prevent last minute take-over', async function () { - const txPropose = await this.helper.propose({ from: proposer }); + const txPropose = await this.helper.connect(this.proposer).propose(); // compute original schedule - const startBlock = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay); - const endBlock = web3.utils - .toBN(await clockFromReceipt[mode](txPropose.receipt)) - .add(votingDelay) - .add(votingPeriod); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); - + const snapshotTimepoint = (await time.clockFromReceipt[mode](txPropose)) + votingDelay; + const deadlineTimepoint = (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod; + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(snapshotTimepoint); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(deadlineTimepoint); // wait for the last minute to vote - await this.helper.waitForDeadline(-1); - const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.waitForDeadline(-1n); + const txVote = await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); // cannot execute yet - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); // compute new extended schedule - const extendedDeadline = web3.utils - .toBN(await clockFromReceipt[mode](txVote.receipt)) - .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); + const extendedDeadline = (await time.clockFromReceipt[mode](txVote)) + lateQuorumVoteExtension; + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(snapshotTimepoint); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(extendedDeadline); // still possible to vote - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.Against }); 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); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(1n); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Defeated); // check extension event - expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline }); + await expect(txVote).to.emit(this.mock, 'ProposalExtended').withArgs(this.proposal.id, extendedDeadline); }); describe('onlyGovernance updates', function () { it('setLateQuorumVoteExtension is protected', async function () { - await expectRevertCustomError( - this.mock.setLateQuorumVoteExtension(0, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).setLateQuorumVoteExtension(0n)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can setLateQuorumVoteExtension through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setLateQuorumVoteExtension', [0n]), }, ], '', @@ -179,15 +170,14 @@ contract('GovernorPreventLateQuorum', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', { - oldVoteExtension: lateQuorumVoteExtension, - newVoteExtension: '0', - }); + await expect(this.helper.execute()) + .to.emit(this.mock, 'LateQuorumVoteExtensionSet') + .withArgs(lateQuorumVoteExtension, 0n); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0'); + expect(await this.mock.lateQuorumVoteExtension()).to.equal(0n); }); }); }); diff --git a/test/governance/extensions/GovernorStorage.test.js b/test/governance/extensions/GovernorStorage.test.js index 99a97886c37..911c3244076 100644 --- a/test/governance/extensions/GovernorStorage.test.js +++ b/test/governance/extensions/GovernorStorage.test.js @@ -1,149 +1,154 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const Enums = require('../../helpers/enums'); const { GovernorHelper, timelockSalt } = require('../../helpers/governance'); - -const Timelock = artifacts.require('TimelockController'); -const Governor = artifacts.require('$GovernorStorageMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); +const { bigint: Enums } = require('../../helpers/enums'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorStorage', function (accounts) { - const [owner, voter1, voter2, voter3, voter4] = accounts; - - const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE'); - const EXECUTOR_ROLE = web3.utils.soliditySha3('EXECUTOR_ROLE'); - const CANCELLER_ROLE = web3.utils.soliditySha3('CANCELLER_ROLE'); - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; +const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE'); +const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE'); +const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE'); + +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); +const delay = 3600n; + +describe('GovernorStorage', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [deployer, owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]); + const mock = await ethers.deployContract('$GovernorStorageMock', [ + name, + votingDelay, + votingPeriod, + 0n, + timelock, + token, + 0n, + ]); + + await owner.sendTransaction({ to: timelock, value }); + await token.$_mint(owner, tokenSupply); + await timelock.grantRole(PROPOSER_ROLE, mock); + await timelock.grantRole(PROPOSER_ROLE, owner); + await timelock.grantRole(CANCELLER_ROLE, mock); + await timelock.grantRole(CANCELLER_ROLE, owner); + await timelock.grantRole(EXECUTOR_ROLE, ethers.ZeroAddress); + await timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { deployer, owner, proposer, voter1, voter2, voter3, voter4, receiver, token, timelock, mock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); - - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.timelock = await Timelock.new(3600, [], [], deployer); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.timelock.address, - this.token.address, - 0, - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - 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, owner); - await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); - await this.timelock.grantRole(CANCELLER_ROLE, owner); - await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); - await this.timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); - - 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 + Object.assign(this, await loadFixture(fixture)); + // initiate fresh proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), }, ], '', ); this.proposal.timelockid = await this.timelock.hashOperationBatch( ...this.proposal.shortProposal.slice(0, 3), - '0x0', - timelockSalt(this.mock.address, this.proposal.shortProposal[3]), + ethers.ZeroHash, + timelockSalt(this.mock.target, this.proposal.shortProposal[3]), ); }); describe('proposal indexing', function () { it('before propose', async function () { - expect(await this.mock.proposalCount()).to.be.bignumber.equal('0'); + expect(await this.mock.proposalCount()).to.equal(0n); - // panic code 0x32 (out-of-bound) - await expectRevert.unspecified(this.mock.proposalDetailsAt(0)); + await expect(this.mock.proposalDetailsAt(0n)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - await expectRevertCustomError(this.mock.proposalDetails(this.proposal.id), 'GovernorNonexistentProposal', [ - this.proposal.id, - ]); + await expect(this.mock.proposalDetails(this.proposal.id)) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('after propose', async function () { await this.helper.propose(); - expect(await this.mock.proposalCount()).to.be.bignumber.equal('1'); + expect(await this.mock.proposalCount()).to.equal(1n); - const proposalDetailsAt0 = await this.mock.proposalDetailsAt(0); - expect(proposalDetailsAt0[0]).to.be.bignumber.equal(this.proposal.id); - expect(proposalDetailsAt0[1]).to.be.deep.equal(this.proposal.targets); - expect(proposalDetailsAt0[2].map(x => x.toString())).to.be.deep.equal(this.proposal.values); - expect(proposalDetailsAt0[3]).to.be.deep.equal(this.proposal.fulldata); - expect(proposalDetailsAt0[4]).to.be.equal(this.proposal.descriptionHash); + expect(await this.mock.proposalDetailsAt(0n)).to.deep.equal([ + this.proposal.id, + this.proposal.targets, + this.proposal.values, + this.proposal.data, + this.proposal.descriptionHash, + ]); - const proposalDetailsForId = await this.mock.proposalDetails(this.proposal.id); - expect(proposalDetailsForId[0]).to.be.deep.equal(this.proposal.targets); - expect(proposalDetailsForId[1].map(x => x.toString())).to.be.deep.equal(this.proposal.values); - expect(proposalDetailsForId[2]).to.be.deep.equal(this.proposal.fulldata); - expect(proposalDetailsForId[3]).to.be.equal(this.proposal.descriptionHash); + expect(await this.mock.proposalDetails(this.proposal.id)).to.deep.equal([ + this.proposal.targets, + this.proposal.values, + this.proposal.data, + this.proposal.descriptionHash, + ]); }); }); it('queue and execute by id', 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); - const txQueue = await this.mock.queue(this.proposal.id); - await this.helper.waitForEta(); - const txExecute = await this.mock.execute(this.proposal.id); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', { - id: this.proposal.timelockid, - }); + await expect(this.mock.queue(this.proposal.id)) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, anyValue) + .to.emit(this.timelock, 'CallScheduled') + .withArgs(this.proposal.timelockid, ...Array(6).fill(anyValue)) + .to.emit(this.timelock, 'CallSalt') + .withArgs(this.proposal.timelockid, anyValue); + + await this.helper.waitForEta(); - 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'); + await expect(this.mock.execute(this.proposal.id)) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.timelock, 'CallExecuted') + .withArgs(this.proposal.timelockid, ...Array(4).fill(anyValue)) + .to.emit(this.receiver, 'MockFunctionCalled'); }); it('cancel by id', async function () { - await this.helper.propose(); - const txCancel = await this.mock.cancel(this.proposal.id); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id }); + await this.helper.connect(this.proposer).propose(); + await expect(this.mock.connect(this.proposer).cancel(this.proposal.id)) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); }); }); } diff --git a/test/governance/extensions/GovernorTimelockAccess.test.js b/test/governance/extensions/GovernorTimelockAccess.test.js index 252a3d52ed5..2f16d1b991e 100644 --- a/test/governance/extensions/GovernorTimelockAccess.test.js +++ b/test/governance/extensions/GovernorTimelockAccess.test.js @@ -1,132 +1,120 @@ -const { expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); -const Enums = require('../../helpers/enums'); -const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { clockFromReceipt } = require('../../helpers/time'); +const { GovernorHelper } = require('../../helpers/governance'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); +const { max } = require('../../helpers/math'); const { selector } = require('../../helpers/methods'); const { hashOperation } = require('../../helpers/access-manager'); -const AccessManager = artifacts.require('$AccessManager'); -const Governor = artifacts.require('$GovernorTimelockAccessMock'); -const AccessManagedTarget = artifacts.require('$AccessManagedTarget'); -const Ownable = artifacts.require('$Ownable'); +function prepareOperation({ sender, target, value = 0n, data = '0x' }) { + return { + id: hashOperation(sender, target, data), + operation: { target, value, data }, + selector: data.slice(0, 10).padEnd(10, '0'), + }; +} const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorTimelockAccess', function (accounts) { - const [admin, 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 = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +describe('GovernorTimelockAccess', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [admin, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + + const manager = await ethers.deployContract('$AccessManager', [admin]); + const receiver = await ethers.deployContract('$AccessManagedTarget', [manager]); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorTimelockAccessMock', [ + name, + votingDelay, + votingPeriod, + 0n, + manager, + 0n, + token, + 0n, + ]); + + await admin.sendTransaction({ to: mock, value }); + await token.$_mint(admin, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(admin).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(admin).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(admin).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(admin).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { admin, voter1, voter2, voter3, voter4, other, manager, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.manager = await AccessManager.new(admin); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, // proposal threshold - this.manager.address, - 0, // base delay - this.token.address, - 0, // quorum - ); - this.receiver = await AccessManagedTarget.new(this.manager.address); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: admin, to: this.mock.address, value }); - - await this.token.$_mint(admin, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: admin }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: admin }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: admin }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: admin }); - - // default proposals - this.restricted = {}; - this.restricted.selector = this.receiver.contract.methods.fnRestricted().encodeABI(); - this.restricted.operation = { - target: this.receiver.address, - value: '0', - data: this.restricted.selector, - }; - this.restricted.operationId = hashOperation( - this.mock.address, - this.restricted.operation.target, - this.restricted.operation.data, - ); + Object.assign(this, await loadFixture(fixture)); - this.unrestricted = {}; - this.unrestricted.selector = this.receiver.contract.methods.fnUnrestricted().encodeABI(); - this.unrestricted.operation = { - target: this.receiver.address, - value: '0', - data: this.unrestricted.selector, - }; - this.unrestricted.operationId = hashOperation( - this.mock.address, - this.unrestricted.operation.target, - this.unrestricted.operation.data, - ); + // restricted proposal + this.restricted = prepareOperation({ + sender: this.mock.target, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('fnRestricted'), + }); - this.fallback = {}; - this.fallback.operation = { - target: this.receiver.address, - value: '0', + this.unrestricted = prepareOperation({ + sender: this.mock.target, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('fnUnrestricted'), + }); + + this.fallback = prepareOperation({ + sender: this.mock.target, + target: this.receiver.target, data: '0x1234', - }; - this.fallback.operationId = hashOperation( - this.mock.address, - this.fallback.operation.target, - this.fallback.operation.data, - ); + }); }); it('accepts ether transfers', async function () { - await web3.eth.sendTransaction({ from: admin, to: this.mock.address, value: 1 }); + await this.admin.sendTransaction({ to: this.mock, value: 1n }); }); 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(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.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0n)).to.equal(0n); - expect(await this.mock.accessManager()).to.be.equal(this.manager.address); + expect(await this.mock.accessManager()).to.equal(this.manager.target); }); it('sets base delay (seconds)', async function () { - const baseDelay = time.duration.hours(10); + const baseDelay = time.duration.hours(10n); // Only through governance - await expectRevertCustomError( - this.mock.setBaseDelaySeconds(baseDelay, { from: voter1 }), - 'GovernorOnlyExecutor', - [voter1], - ); + await expect(this.mock.connect(this.voter1).setBaseDelaySeconds(baseDelay)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.voter1.address); this.proposal = await this.helper.setProposal( [ { - target: this.mock.address, - value: '0', - data: this.mock.contract.methods.setBaseDelaySeconds(baseDelay).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setBaseDelaySeconds', [baseDelay]), }, ], 'descr', @@ -134,95 +122,90 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - const receipt = await this.helper.execute(); - expectEvent(receipt, 'BaseDelaySet', { - oldBaseDelaySeconds: '0', - newBaseDelaySeconds: baseDelay, - }); + await expect(this.helper.execute()).to.emit(this.mock, 'BaseDelaySet').withArgs(0n, baseDelay); - expect(await this.mock.baseDelaySeconds()).to.be.bignumber.eq(baseDelay); + expect(await this.mock.baseDelaySeconds()).to.equal(baseDelay); }); it('sets access manager ignored', async function () { const selectors = ['0x12345678', '0x87654321', '0xabcdef01']; // Only through governance - await expectRevertCustomError( - this.mock.setAccessManagerIgnored(other, selectors, true, { from: voter1 }), - 'GovernorOnlyExecutor', - [voter1], - ); + await expect(this.mock.connect(this.voter1).setAccessManagerIgnored(this.other, selectors, true)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.voter1.address); // Ignore - const helperIgnore = new GovernorHelper(this.mock, mode); - await helperIgnore.setProposal( + await this.helper.setProposal( [ { - target: this.mock.address, - value: '0', - data: this.mock.contract.methods.setAccessManagerIgnored(other, selectors, true).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [ + this.other.address, + selectors, + true, + ]), }, ], 'descr', ); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(); - await helperIgnore.propose(); - await helperIgnore.waitForSnapshot(); - await helperIgnore.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await helperIgnore.waitForDeadline(); - const ignoreReceipt = await helperIgnore.execute(); - + const ignoreReceipt = this.helper.execute(); for (const selector of selectors) { - expectEvent(ignoreReceipt, 'AccessManagerIgnoredSet', { - target: other, - selector, - ignored: true, - }); - expect(await this.mock.isAccessManagerIgnored(other, selector)).to.be.true; + await expect(ignoreReceipt) + .to.emit(this.mock, 'AccessManagerIgnoredSet') + .withArgs(this.other.address, selector, true); + expect(await this.mock.isAccessManagerIgnored(this.other, selector)).to.be.true; } // Unignore - const helperUnignore = new GovernorHelper(this.mock, mode); - await helperUnignore.setProposal( + await this.helper.setProposal( [ { - target: this.mock.address, - value: '0', - data: this.mock.contract.methods.setAccessManagerIgnored(other, selectors, false).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [ + this.other.address, + selectors, + false, + ]), }, ], 'descr', ); - await helperUnignore.propose(); - await helperUnignore.waitForSnapshot(); - await helperUnignore.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await helperUnignore.waitForDeadline(); - const unignoreReceipt = await helperUnignore.execute(); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(); + const unignoreReceipt = this.helper.execute(); for (const selector of selectors) { - expectEvent(unignoreReceipt, 'AccessManagerIgnoredSet', { - target: other, - selector, - ignored: false, - }); - expect(await this.mock.isAccessManagerIgnored(other, selector)).to.be.false; + await expect(unignoreReceipt) + .to.emit(this.mock, 'AccessManagerIgnoredSet') + .withArgs(this.other.address, selector, false); + expect(await this.mock.isAccessManagerIgnored(this.other, selector)).to.be.false; } }); it('sets access manager ignored when target is the governor', async function () { - const other = this.mock.address; const selectors = ['0x12345678', '0x87654321', '0xabcdef01']; await this.helper.setProposal( [ { - target: this.mock.address, - value: '0', - data: this.mock.contract.methods.setAccessManagerIgnored(other, selectors, true).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [ + this.mock.target, + selectors, + true, + ]), }, ], 'descr', @@ -230,154 +213,141 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - const receipt = await this.helper.execute(); + const tx = this.helper.execute(); for (const selector of selectors) { - expectEvent(receipt, 'AccessManagerIgnoredSet', { - target: other, - selector, - ignored: true, - }); - expect(await this.mock.isAccessManagerIgnored(other, selector)).to.be.true; + await expect(tx).to.emit(this.mock, 'AccessManagerIgnoredSet').withArgs(this.mock.target, selector, true); + expect(await this.mock.isAccessManagerIgnored(this.mock, selector)).to.be.true; } }); it('does not need to queue proposals with no delay', async function () { - const roleId = '1'; - - const executionDelay = web3.utils.toBN(0); - const baseDelay = web3.utils.toBN(0); + const roleId = 1n; + const executionDelay = 0n; + const baseDelay = 0n; // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - this.proposal = await this.helper.setProposal([this.restricted.operation], 'descr'); + await this.helper.setProposal([this.restricted.operation], 'descr'); await this.helper.propose(); expect(await this.mock.proposalNeedsQueuing(this.helper.currentProposal.id)).to.be.false; }); it('needs to queue proposals with any delay', async function () { - const roleId = '1'; - + const roleId = 1n; const delays = [ - [time.duration.hours(1), time.duration.hours(2)], - [time.duration.hours(2), time.duration.hours(1)], + [time.duration.hours(1n), time.duration.hours(2n)], + [time.duration.hours(2n), time.duration.hours(1n)], ]; for (const [executionDelay, baseDelay] of delays) { // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - const helper = new GovernorHelper(this.mock, mode); - this.proposal = await helper.setProposal( + await this.helper.setProposal( [this.restricted.operation], `executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`, ); - await helper.propose(); - expect(await this.mock.proposalNeedsQueuing(helper.currentProposal.id)).to.be.true; + await this.helper.propose(); + expect(await this.mock.proposalNeedsQueuing(this.helper.currentProposal.id)).to.be.true; } }); describe('execution plan', function () { it('returns plan for delayed operations', async function () { - const roleId = '1'; - + const roleId = 1n; const delays = [ - [time.duration.hours(1), time.duration.hours(2)], - [time.duration.hours(2), time.duration.hours(1)], + [time.duration.hours(1n), time.duration.hours(2n)], + [time.duration.hours(2n), time.duration.hours(1n)], ]; for (const [executionDelay, baseDelay] of delays) { // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - const helper = new GovernorHelper(this.mock, mode); - this.proposal = await helper.setProposal( + this.proposal = await this.helper.setProposal( [this.restricted.operation], `executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`, ); - await helper.propose(); - const { delay: planDelay, indirect, withDelay } = await this.mock.proposalExecutionPlan(this.proposal.id); - const maxDelay = web3.utils.toBN(Math.max(baseDelay.toNumber(), executionDelay.toNumber())); - expect(planDelay).to.be.bignumber.eq(maxDelay); - expect(indirect).to.deep.eq([true]); - expect(withDelay).to.deep.eq([true]); + await this.helper.propose(); + + expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([ + max(baseDelay, executionDelay), + [true], + [true], + ]); } }); it('returns plan for not delayed operations', async function () { - const roleId = '1'; - - const executionDelay = web3.utils.toBN(0); - const baseDelay = web3.utils.toBN(0); + const roleId = 1n; + const executionDelay = 0n; + const baseDelay = 0n; // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); this.proposal = await this.helper.setProposal([this.restricted.operation], `descr`); await this.helper.propose(); - const { delay: planDelay, indirect, withDelay } = await this.mock.proposalExecutionPlan(this.proposal.id); - expect(planDelay).to.be.bignumber.eq(web3.utils.toBN(0)); - expect(indirect).to.deep.eq([true]); - expect(withDelay).to.deep.eq([false]); + + expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([0n, [true], [false]]); }); it('returns plan for an operation ignoring the manager', async function () { - await this.mock.$_setAccessManagerIgnored(this.receiver.address, this.restricted.selector, true); - - const roleId = '1'; + await this.mock.$_setAccessManagerIgnored(this.receiver, this.restricted.selector, true); + const roleId = 1n; const delays = [ - [time.duration.hours(1), time.duration.hours(2)], - [time.duration.hours(2), time.duration.hours(1)], + [time.duration.hours(1n), time.duration.hours(2n)], + [time.duration.hours(2n), time.duration.hours(1n)], ]; for (const [executionDelay, baseDelay] of delays) { // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - const helper = new GovernorHelper(this.mock, mode); - this.proposal = await helper.setProposal( + this.proposal = await this.helper.setProposal( [this.restricted.operation], `executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`, ); - await helper.propose(); - const { delay: planDelay, indirect, withDelay } = await this.mock.proposalExecutionPlan(this.proposal.id); - expect(planDelay).to.be.bignumber.eq(baseDelay); - expect(indirect).to.deep.eq([false]); - expect(withDelay).to.deep.eq([false]); + await this.helper.propose(); + + expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([ + baseDelay, + [false], + [false], + ]); } }); }); @@ -394,49 +364,47 @@ contract('GovernorTimelockAccess', function (accounts) { this.proposal = await this.helper.setProposal([this.unrestricted.operation], 'descr'); await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); if (await this.mock.proposalNeedsQueuing(this.proposal.id)) { - const txQueue = await this.helper.queue(); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + expect(await this.helper.queue()) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id); } if (delay > 0) { await this.helper.waitForEta(); } - const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - expectEvent.inTransaction(txExecute, this.receiver, 'CalledUnrestricted'); + expect(await this.helper.execute()) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.not.emit(this.receiver, 'CalledUnrestricted'); }); } }); it('reverts when an operation is executed before eta', async function () { - const delay = time.duration.hours(2); + const delay = time.duration.hours(2n); await this.mock.$_setBaseDelaySeconds(delay); this.proposal = await this.helper.setProposal([this.unrestricted.operation], 'descr'); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnmetDelay', [ - this.proposal.id, - await this.mock.proposalEta(this.proposal.id), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnmetDelay') + .withArgs(this.proposal.id, await this.mock.proposalEta(this.proposal.id)); }); it('reverts with a proposal including multiple operations but one of those was cancelled in the manager', async function () { - const delay = time.duration.hours(2); - const roleId = '1'; + const delay = time.duration.hours(2n); + const roleId = 1n; - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, delay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay); // Set proposals const original = new GovernorHelper(this.mock, mode); @@ -445,83 +413,79 @@ contract('GovernorTimelockAccess', function (accounts) { // Go through all the governance process await original.propose(); await original.waitForSnapshot(); - await original.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await original.connect(this.voter1).vote({ support: Enums.VoteType.For }); await original.waitForDeadline(); await original.queue(); await original.waitForEta(); // Suddenly cancel one of the proposed operations in the manager - await this.manager.cancel(this.mock.address, this.restricted.operation.target, this.restricted.operation.data, { - from: admin, - }); + await this.manager + .connect(this.admin) + .cancel(this.mock, this.restricted.operation.target, this.restricted.operation.data); // Reschedule the same operation in a different proposal to avoid "AccessManagerNotScheduled" error const rescheduled = new GovernorHelper(this.mock, mode); await rescheduled.setProposal([this.restricted.operation], 'descr'); await rescheduled.propose(); await rescheduled.waitForSnapshot(); - await rescheduled.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await rescheduled.connect(this.voter1).vote({ support: Enums.VoteType.For }); await rescheduled.waitForDeadline(); await rescheduled.queue(); // This will schedule it again in the manager await rescheduled.waitForEta(); // Attempt to execute - await expectRevertCustomError(original.execute(), 'GovernorMismatchedNonce', [ - original.currentProposal.id, - 1, - 2, - ]); + await expect(original.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorMismatchedNonce') + .withArgs(original.currentProposal.id, 1, 2); }); it('single operation with access manager delay', async function () { - const delay = 1000; - const roleId = '1'; + const delay = 1000n; + const roleId = 1n; - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, delay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay); this.proposal = await this.helper.setProposal([this.restricted.operation], 'descr'); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); 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.manager, 'OperationScheduled', { - operationId: this.restricted.operationId, - nonce: '1', - schedule: web3.utils.toBN(await clockFromReceipt.timestamp(txQueue.receipt)).addn(delay), - caller: this.mock.address, - target: this.restricted.operation.target, - data: this.restricted.operation.data, - }); + await expect(txQueue) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, anyValue) + .to.emit(this.manager, 'OperationScheduled') + .withArgs( + this.restricted.id, + 1n, + (await time.clockFromReceipt.timestamp(txQueue)) + delay, + this.mock.target, + this.restricted.operation.target, + this.restricted.operation.data, + ); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.manager, 'OperationExecuted', { - operationId: this.restricted.operationId, - nonce: '1', - }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'CalledRestricted'); + await expect(txExecute) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.manager, 'OperationExecuted') + .withArgs(this.restricted.id, 1n) + .to.emit(this.receiver, 'CalledRestricted'); }); it('bundle of varied operations', async function () { - const managerDelay = 1000; - const roleId = '1'; - - const baseDelay = managerDelay * 2; + const managerDelay = 1000n; + const roleId = 1n; + const baseDelay = managerDelay * 2n; await this.mock.$_setBaseDelaySeconds(baseDelay); - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, managerDelay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, managerDelay); this.proposal = await this.helper.setProposal( [this.restricted.operation, this.unrestricted.operation, this.fallback.operation], @@ -530,41 +494,44 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); 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.manager, 'OperationScheduled', { - operationId: this.restricted.operationId, - nonce: '1', - schedule: web3.utils.toBN(await clockFromReceipt.timestamp(txQueue.receipt)).addn(baseDelay), - caller: this.mock.address, - target: this.restricted.operation.target, - data: this.restricted.operation.data, - }); + await expect(txQueue) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, anyValue) + .to.emit(this.manager, 'OperationScheduled') + .withArgs( + this.restricted.id, + 1n, + (await time.clockFromReceipt.timestamp(txQueue)) + baseDelay, + this.mock.target, + this.restricted.operation.target, + this.restricted.operation.data, + ); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.manager, 'OperationExecuted', { - operationId: this.restricted.operationId, - nonce: '1', - }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'CalledRestricted'); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'CalledUnrestricted'); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'CalledFallback'); + await expect(txExecute) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.manager, 'OperationExecuted') + .withArgs(this.restricted.id, 1n) + .to.emit(this.receiver, 'CalledRestricted') + .to.emit(this.receiver, 'CalledUnrestricted') + .to.emit(this.receiver, 'CalledFallback'); }); describe('cancel', function () { - const delay = 1000; - const roleId = '1'; + const delay = 1000n; + const roleId = 1n; beforeEach(async function () { - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, delay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay); }); it('cancels restricted with delay after queue (internal)', async function () { @@ -572,23 +539,25 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - const txCancel = await this.helper.cancel('internal'); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txCancel.tx, this.manager, 'OperationCanceled', { - operationId: this.restricted.operationId, - nonce: '1', - }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id) + .to.emit(this.manager, 'OperationCanceled') + .withArgs(this.restricted.id, 1n); await this.helper.waitForEta(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancels restricted with queueing if the same operation is part of a more recent proposal (internal)', async function () { @@ -599,17 +568,14 @@ contract('GovernorTimelockAccess', function (accounts) { // Go through all the governance process await original.propose(); await original.waitForSnapshot(); - await original.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await original.connect(this.voter1).vote({ support: Enums.VoteType.For }); await original.waitForDeadline(); await original.queue(); // Cancel the operation in the manager - await this.manager.cancel( - this.mock.address, - this.restricted.operation.target, - this.restricted.operation.data, - { from: admin }, - ); + await this.manager + .connect(this.admin) + .cancel(this.mock, this.restricted.operation.target, this.restricted.operation.data); // Another proposal is added with the same operation const rescheduled = new GovernorHelper(this.mock, mode); @@ -618,21 +584,26 @@ contract('GovernorTimelockAccess', function (accounts) { // Queue the new proposal await rescheduled.propose(); await rescheduled.waitForSnapshot(); - await rescheduled.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await rescheduled.connect(this.voter1).vote({ support: Enums.VoteType.For }); await rescheduled.waitForDeadline(); await rescheduled.queue(); // This will schedule it again in the manager // Cancel const eta = await this.mock.proposalEta(rescheduled.currentProposal.id); - const txCancel = await original.cancel('internal'); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: original.currentProposal.id }); - - await time.increase(eta); // waitForEta() - await expectRevertCustomError(original.execute(), 'GovernorUnexpectedProposalState', [ - original.currentProposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(original.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(original.currentProposal.id); + + await time.clock.timestamp().then(clock => time.forward.timestamp(max(clock + 1n, eta))); + + await expect(original.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + original.currentProposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancels unrestricted with queueing (internal)', async function () { @@ -640,20 +611,25 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); const eta = await this.mock.proposalEta(this.proposal.id); - const txCancel = await this.helper.cancel('internal'); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id }); - - await time.increase(eta); // waitForEta() - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + + await time.clock.timestamp().then(clock => time.forward.timestamp(max(clock + 1n, eta))); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancels unrestricted without queueing (internal)', async function () { @@ -661,28 +637,31 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); // await this.helper.queue(); // const eta = await this.mock.proposalEta(this.proposal.id); - const txCancel = await this.helper.cancel('internal'); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id }); - - // await time.increase(eta); // waitForEta() - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + + // await time.forward.timestamp(eta); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancels calls already canceled by guardian', async function () { - const operationA = { target: this.receiver.address, data: this.restricted.selector + '00' }; - const operationB = { target: this.receiver.address, data: this.restricted.selector + '01' }; - const operationC = { target: this.receiver.address, data: this.restricted.selector + '02' }; - const operationAId = hashOperation(this.mock.address, operationA.target, operationA.data); - const operationBId = hashOperation(this.mock.address, operationB.target, operationB.data); + const operationA = { target: this.receiver.target, data: this.restricted.selector + '00' }; + const operationB = { target: this.receiver.target, data: this.restricted.selector + '01' }; + const operationC = { target: this.receiver.target, data: this.restricted.selector + '02' }; + const operationAId = hashOperation(this.mock.target, operationA.target, operationA.data); + const operationBId = hashOperation(this.mock.target, operationB.target, operationB.data); const proposal1 = new GovernorHelper(this.mock, mode); const proposal2 = new GovernorHelper(this.mock, mode); @@ -692,7 +671,7 @@ contract('GovernorTimelockAccess', function (accounts) { for (const p of [proposal1, proposal2]) { await p.propose(); await p.waitForSnapshot(); - await p.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await p.connect(this.voter1).vote({ support: Enums.VoteType.For }); await p.waitForDeadline(); } @@ -700,18 +679,24 @@ contract('GovernorTimelockAccess', function (accounts) { await proposal1.queue(); // Cannot queue the second proposal: operation A already scheduled with delay - await expectRevertCustomError(proposal2.queue(), 'AccessManagerAlreadyScheduled', [operationAId]); + await expect(proposal2.queue()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerAlreadyScheduled') + .withArgs(operationAId); // Admin cancels operation B on the manager - await this.manager.cancel(this.mock.address, operationB.target, operationB.data, { from: admin }); + await this.manager.connect(this.admin).cancel(this.mock, operationB.target, operationB.data); // Still cannot queue the second proposal: operation A already scheduled with delay - await expectRevertCustomError(proposal2.queue(), 'AccessManagerAlreadyScheduled', [operationAId]); + await expect(proposal2.queue()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerAlreadyScheduled') + .withArgs(operationAId); await proposal1.waitForEta(); // Cannot execute first proposal: operation B has been canceled - await expectRevertCustomError(proposal1.execute(), 'AccessManagerNotScheduled', [operationBId]); + await expect(proposal1.execute()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled') + .withArgs(operationBId); // Cancel the first proposal to release operation A await proposal1.cancel('internal'); @@ -728,43 +713,41 @@ contract('GovernorTimelockAccess', function (accounts) { describe('ignore AccessManager', function () { it('defaults', async function () { - expect(await this.mock.isAccessManagerIgnored(this.receiver.address, this.restricted.selector)).to.equal( - false, - ); - expect(await this.mock.isAccessManagerIgnored(this.mock.address, '0x12341234')).to.equal(true); + expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.false; + expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.true; }); it('internal setter', async function () { - const p1 = { target: this.receiver.address, selector: this.restricted.selector, ignored: true }; - const tx1 = await this.mock.$_setAccessManagerIgnored(p1.target, p1.selector, p1.ignored); - expect(await this.mock.isAccessManagerIgnored(p1.target, p1.selector)).to.equal(p1.ignored); - expectEvent(tx1, 'AccessManagerIgnoredSet', p1); - - const p2 = { target: this.mock.address, selector: '0x12341234', ignored: false }; - const tx2 = await this.mock.$_setAccessManagerIgnored(p2.target, p2.selector, p2.ignored); - expect(await this.mock.isAccessManagerIgnored(p2.target, p2.selector)).to.equal(p2.ignored); - expectEvent(tx2, 'AccessManagerIgnoredSet', p2); + await expect(this.mock.$_setAccessManagerIgnored(this.receiver, this.restricted.selector, true)) + .to.emit(this.mock, 'AccessManagerIgnoredSet') + .withArgs(this.receiver.target, this.restricted.selector, true); + + expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.true; + + await expect(this.mock.$_setAccessManagerIgnored(this.mock, '0x12341234', false)) + .to.emit(this.mock, 'AccessManagerIgnoredSet') + .withArgs(this.mock.target, '0x12341234', false); + + expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.false; }); it('external setter', async function () { const setAccessManagerIgnored = (...args) => - this.mock.contract.methods.setAccessManagerIgnored(...args).encodeABI(); + this.mock.interface.encodeFunctionData('setAccessManagerIgnored', args); await this.helper.setProposal( [ { - target: this.mock.address, + target: this.mock.target, data: setAccessManagerIgnored( - this.receiver.address, + this.receiver.target, [this.restricted.selector, this.unrestricted.selector], true, ), - value: '0', }, { - target: this.mock.address, - data: setAccessManagerIgnored(this.mock.address, ['0x12341234', '0x67896789'], false), - value: '0', + target: this.mock.target, + data: setAccessManagerIgnored(this.mock.target, ['0x12341234', '0x67896789'], false), }, ], 'descr', @@ -772,65 +755,59 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - const tx = await this.helper.execute(); - expectEvent(tx, 'AccessManagerIgnoredSet'); + await expect(this.helper.execute()).to.emit(this.mock, 'AccessManagerIgnoredSet'); - expect(await this.mock.isAccessManagerIgnored(this.receiver.address, this.restricted.selector)).to.equal( - true, - ); - expect(await this.mock.isAccessManagerIgnored(this.receiver.address, this.unrestricted.selector)).to.equal( - true, - ); - - expect(await this.mock.isAccessManagerIgnored(this.mock.address, '0x12341234')).to.equal(false); - expect(await this.mock.isAccessManagerIgnored(this.mock.address, '0x67896789')).to.equal(false); + expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.true; + expect(await this.mock.isAccessManagerIgnored(this.receiver, this.unrestricted.selector)).to.be.true; + expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.false; + expect(await this.mock.isAccessManagerIgnored(this.mock, '0x67896789')).to.be.false; }); it('locked function', async function () { const setAccessManagerIgnored = selector('setAccessManagerIgnored(address,bytes4[],bool)'); - await expectRevertCustomError( - this.mock.$_setAccessManagerIgnored(this.mock.address, setAccessManagerIgnored, true), - 'GovernorLockedIgnore', - [], - ); - await this.mock.$_setAccessManagerIgnored(this.receiver.address, setAccessManagerIgnored, true); + + await expect( + this.mock.$_setAccessManagerIgnored(this.mock, setAccessManagerIgnored, true), + ).to.be.revertedWithCustomError(this.mock, 'GovernorLockedIgnore'); + + await this.mock.$_setAccessManagerIgnored(this.receiver, setAccessManagerIgnored, true); }); it('ignores access manager', async function () { - const amount = 100; - - const target = this.token.address; - const data = this.token.contract.methods.transfer(voter4, amount).encodeABI(); + const amount = 100n; + const target = this.token.target; + const data = this.token.interface.encodeFunctionData('transfer', [this.voter4.address, amount]); const selector = data.slice(0, 10); - await this.token.$_mint(this.mock.address, amount); + await this.token.$_mint(this.mock, amount); - const roleId = '1'; - await this.manager.setTargetFunctionRole(target, [selector], roleId, { from: admin }); - await this.manager.grantRole(roleId, this.mock.address, 0, { from: admin }); + const roleId = 1n; + await this.manager.connect(this.admin).setTargetFunctionRole(target, [selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, 0); - this.proposal = await this.helper.setProposal([{ target, data, value: '0' }], '1'); + await this.helper.setProposal([{ target, data }], 'descr #1'); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'ERC20InsufficientBalance', [ - this.manager.address, - 0, - amount, - ]); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.manager.target, 0n, amount); await this.mock.$_setAccessManagerIgnored(target, selector, true); - await this.helper.setProposal([{ target, data, value: '0' }], '2'); + await this.helper.setProposal([{ target, data }], 'descr #2'); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - const tx = await this.helper.execute(); - expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.mock.address }); + + await expect(this.helper.execute()) + .to.emit(this.token, 'Transfer') + .withArgs(this.mock.target, this.voter4.address, amount); }); }); @@ -838,32 +815,29 @@ contract('GovernorTimelockAccess', function (accounts) { const method = selector('$_checkOwner()'); beforeEach(async function () { - this.ownable = await Ownable.new(this.manager.address); + this.ownable = await ethers.deployContract('$Ownable', [this.manager]); this.operation = { - target: this.ownable.address, - value: '0', - data: this.ownable.contract.methods.$_checkOwner().encodeABI(), + target: this.ownable.target, + data: this.ownable.interface.encodeFunctionData('$_checkOwner'), }; }); it('succeeds with delay', async function () { - const roleId = '1'; - const executionDelay = time.duration.hours(2); - const baseDelay = time.duration.hours(1); + const roleId = 1n; + const executionDelay = time.duration.hours(2n); + const baseDelay = time.duration.hours(1n); // Set execution delay - await this.manager.setTargetFunctionRole(this.ownable.address, [method], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.ownable, [method], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - this.proposal = await this.helper.setProposal([this.operation], `descr`); + await this.helper.setProposal([this.operation], `descr`); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); @@ -871,23 +845,21 @@ contract('GovernorTimelockAccess', function (accounts) { }); it('succeeds without delay', async function () { - const roleId = '1'; - const executionDelay = web3.utils.toBN(0); - const baseDelay = web3.utils.toBN(0); + const roleId = 1n; + const executionDelay = 0n; + const baseDelay = 0n; // Set execution delay - await this.manager.setTargetFunctionRole(this.ownable.address, [method], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.ownable, [method], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - this.proposal = await this.helper.setProposal([this.operation], `descr`); + await this.helper.setProposal([this.operation], `descr`); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); // Don't revert }); diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 56191eb5056..ecc71dc0907 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -1,77 +1,71 @@ -const { ethers } = require('ethers'); -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); -const Enums = require('../../helpers/enums'); -const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { clockFromReceipt } = require('../../helpers/time'); - -const Timelock = artifacts.require('CompTimelock'); -const Governor = artifacts.require('$GovernorTimelockCompoundMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); -const ERC721 = artifacts.require('$ERC721'); -const ERC1155 = artifacts.require('$ERC1155'); +const { GovernorHelper } = require('../../helpers/governance'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorTimelockCompound', function (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 = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - const defaultDelay = 2 * 86400; - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); +const defaultDelay = time.duration.days(2n); + +describe('GovernorTimelockCompound', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const predictGovernor = await deployer + .getNonce() + .then(nonce => ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 })); + const timelock = await ethers.deployContract('CompTimelock', [predictGovernor, defaultDelay]); + const mock = await ethers.deployContract('$GovernorTimelockCompoundMock', [ + name, + votingDelay, + votingPeriod, + 0n, + timelock, + token, + 0n, + ]); + + await owner.sendTransaction({ to: timelock, value }); + await token.$_mint(owner, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { deployer, owner, voter1, voter2, voter3, voter4, other, receiver, token, mock, timelock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); - - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - - // Need to predict governance address to set it as timelock admin with a delayed transfer - const nonce = await web3.eth.getTransactionCount(deployer); - const predictGovernor = ethers.getCreateAddress({ from: deployer, nonce: nonce + 1 }); - - this.timelock = await Timelock.new(predictGovernor, defaultDelay); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.timelock.address, - this.token.address, - 0, - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - 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 }); + Object.assign(this, await loadFixture(fixture)); // default proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + data: this.receiver.interface.encodeFunctionData('mockFunction'), }, ], '', @@ -79,46 +73,55 @@ contract('GovernorTimelockCompound', function (accounts) { }); it("doesn't accept ether transfers", async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); + await expect(this.owner.sendTransaction({ to: this.mock, value: 1n })).to.be.revertedWithCustomError( + this.mock, + 'GovernorDisabledDeposit', + ); }); 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(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); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0n)).to.equal(0n); + + expect(await this.mock.timelock()).to.equal(this.timelock.target); + expect(await this.timelock.admin()).to.equal(this.mock.target); }); it('nominal', async function () { - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0'); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(true); + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true; 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); const txQueue = await this.helper.queue(); - const eta = web3.utils.toBN(await clockFromReceipt.timestamp(txQueue.receipt)).addn(defaultDelay); - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal(eta); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(true); + const eta = (await time.clockFromReceipt.timestamp(txQueue)) + defaultDelay; + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(eta); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true; 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'); + await expect(txQueue) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, eta) + .to.emit(this.timelock, 'QueueTransaction') + .withArgs(...Array(5).fill(anyValue), eta); + + await expect(txExecute) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.timelock, 'ExecuteTransaction') + .withArgs(...Array(5).fill(anyValue), eta) + .to.emit(this.receiver, 'MockFunctionCalled'); }); describe('should revert', function () { @@ -126,29 +129,35 @@ contract('GovernorTimelockCompound', function (accounts) { 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Queued, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Queued, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ); }); 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(), + target: this.token.target, + data: this.token.interface.encodeFunctionData('approve', [this.receiver.target, ethers.MaxUint256]), }; const { id } = this.helper.setProposal([action, action], ''); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.queue(), 'GovernorAlreadyQueuedProposal', [id]); - await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [id]); + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyQueuedProposal') + .withArgs(id); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal') + .withArgs(id); }); }); @@ -156,25 +165,26 @@ contract('GovernorTimelockCompound', function (accounts) { 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 this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(1n); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Succeeded); - await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [this.proposal.id]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal') + .withArgs(this.proposal.id); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Queued); - await expectRevert( - this.helper.execute(), + await expect(this.helper.execute()).to.be.rejectedWith( "Timelock::executeTransaction: Transaction hasn't surpassed time lock", ); }); @@ -182,96 +192,86 @@ contract('GovernorTimelockCompound', function (accounts) { 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - await this.helper.waitForEta(+30 * 86400); + await this.helper.waitForEta(time.duration.days(30)); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Expired); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Expired, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Expired, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); await this.helper.execute(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); }); describe('on safe receive', function () { describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = web3.utils.toBN(1); + const tokenId = 1n; beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(owner, tokenId); + this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']); + await this.token.$_mint(this.owner, tokenId); }); it("can't receive an ERC721 safeTransfer", async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), - 'GovernorDisabledDeposit', - [], - ); + await expect( + this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId), + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); }); describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { - 1: web3.utils.toBN(1000), - 2: web3.utils.toBN(2000), - 3: web3.utils.toBN(3000), + 1: 1000n, + 2: 2000n, + 3: 3000n, }; beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it("can't receive ERC1155 safeTransfer", async function () { - await expectRevertCustomError( - this.token.safeTransferFrom( - owner, - this.mock.address, + await expect( + this.token.connect(this.owner).safeTransferFrom( + this.owner, + this.mock, ...Object.entries(tokenIds)[0], // id + amount '0x', - { from: owner }, ), - 'GovernorDisabledDeposit', - [], - ); + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); it("can't receive ERC1155 safeBatchTransfer", async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - owner, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: owner }, - ), - 'GovernorDisabledDeposit', - [], - ); + await expect( + this.token + .connect(this.owner) + .safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'), + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); }); }); @@ -281,111 +281,114 @@ contract('GovernorTimelockCompound', function (accounts) { 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); + + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); }); describe('onlyGovernance', function () { describe('relay', function () { beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); + await this.token.$_mint(this.mock, 1); }); it('is protected', async function () { - await expectRevertCustomError( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { - from: owner, - }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect( + this.mock + .connect(this.owner) + .relay(this.token, 0, this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n])), + ) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); 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(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('relay', [ + this.token.target, + 0n, + this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n]), + ]), }, ], '', ); - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); - await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); 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); + const txExecute = this.helper.execute(); - await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', - }); + await expect(txExecute).to.changeTokenBalances(this.token, [this.mock, this.other], [-1n, 1n]); + + await expect(txExecute).to.emit(this.token, 'Transfer').withArgs(this.mock.target, this.other.address, 1n); }); }); describe('updateTimelock', function () { beforeEach(async function () { - this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400); + this.newTimelock = await ethers.deployContract('CompTimelock', [this.mock, time.duration.days(7n)]); }); it('is protected', async function () { - await expectRevertCustomError( - this.mock.updateTimelock(this.newTimelock.address, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).updateTimelock(this.newTimelock)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); 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.timelock.target, + data: this.timelock.interface.encodeFunctionData('setPendingAdmin', [this.owner.address]), }, { - target: this.mock.address, - data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('updateTimelock', [this.newTimelock.target]), }, ], '', @@ -393,28 +396,35 @@ contract('GovernorTimelockCompound', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); + await expect(this.helper.execute()) + .to.emit(this.mock, 'TimelockChange') + .withArgs(this.timelock.target, this.newTimelock.target); - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + expect(await this.mock.timelock()).to.equal(this.newTimelock.target); }); }); it('can transfer timelock to new governor', async function () { - const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0); + const newGovernor = await ethers.deployContract('$GovernorTimelockCompoundMock', [ + name, + 8n, + 32n, + 0n, + this.timelock, + this.token, + 0n, + ]); + this.helper.setProposal( [ { - target: this.timelock.address, - data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(), + target: this.timelock.target, + data: this.timelock.interface.encodeFunctionData('setPendingAdmin', [newGovernor.target]), }, ], '', @@ -422,18 +432,15 @@ contract('GovernorTimelockCompound', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); 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 expect(this.helper.execute()).to.emit(this.timelock, 'NewPendingAdmin').withArgs(newGovernor.target); await newGovernor.__acceptAdmin(); - expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address); + expect(await this.timelock.admin()).to.equal(newGovernor.target); }); }); }); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index ec03d614475..9f6bceb5baa 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -1,88 +1,79 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); -const Enums = require('../../helpers/enums'); -const { GovernorHelper, proposalStatesToBitMap, timelockSalt } = require('../../helpers/governance'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { clockFromReceipt } = require('../../helpers/time'); - -const Timelock = artifacts.require('TimelockController'); -const Governor = artifacts.require('$GovernorTimelockControlMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); -const ERC721 = artifacts.require('$ERC721'); -const ERC1155 = artifacts.require('$ERC1155'); +const { GovernorHelper, timelockSalt } = require('../../helpers/governance'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorTimelockControl', function (accounts) { - const [owner, voter1, voter2, voter3, voter4, other] = accounts; - - const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE'); - const EXECUTOR_ROLE = web3.utils.soliditySha3('EXECUTOR_ROLE'); - const CANCELLER_ROLE = web3.utils.soliditySha3('CANCELLER_ROLE'); - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - const delay = 3600; - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; +const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE'); +const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE'); +const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE'); + +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); +const delay = time.duration.hours(1n); + +describe('GovernorTimelockControl', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]); + const mock = await ethers.deployContract('$GovernorTimelockControlMock', [ + name, + votingDelay, + votingPeriod, + 0n, + timelock, + token, + 0n, + ]); + + await owner.sendTransaction({ to: timelock, value }); + await token.$_mint(owner, tokenSupply); + await timelock.grantRole(PROPOSER_ROLE, mock); + await timelock.grantRole(PROPOSER_ROLE, owner); + await timelock.grantRole(CANCELLER_ROLE, mock); + await timelock.grantRole(CANCELLER_ROLE, owner); + await timelock.grantRole(EXECUTOR_ROLE, ethers.ZeroAddress); + await timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { deployer, owner, voter1, voter2, voter3, voter4, other, receiver, token, mock, timelock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); - - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.timelock = await Timelock.new(delay, [], [], deployer); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.timelock.address, - this.token.address, - 0, - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - 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, owner); - await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); - await this.timelock.grantRole(CANCELLER_ROLE, owner); - await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); - await this.timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); - - 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 }); + Object.assign(this, await loadFixture(fixture)); // default proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + data: this.receiver.interface.encodeFunctionData('mockFunction'), }, ], '', @@ -90,54 +81,63 @@ contract('GovernorTimelockControl', function (accounts) { this.proposal.timelockid = await this.timelock.hashOperationBatch( ...this.proposal.shortProposal.slice(0, 3), - '0x0', - timelockSalt(this.mock.address, this.proposal.shortProposal[3]), + ethers.ZeroHash, + timelockSalt(this.mock.target, this.proposal.shortProposal[3]), ); }); it("doesn't accept ether transfers", async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); + await expect(this.owner.sendTransaction({ to: this.mock, value: 1n })).to.be.revertedWithCustomError( + this.mock, + 'GovernorDisabledDeposit', + ); }); 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(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.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0n)).to.equal(0n); - expect(await this.mock.timelock()).to.be.equal(this.timelock.address); + expect(await this.mock.timelock()).to.equal(this.timelock.target); }); it('nominal', async function () { - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0'); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(true); + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true; 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); - const txQueue = await this.helper.queue(); - await this.helper.waitForEta(); - - const eta = web3.utils.toBN(await clockFromReceipt.timestamp(txQueue.receipt)).addn(delay); - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal(eta); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(true); - const txExecute = await this.helper.execute(); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true; + const txQueue = await this.helper.queue(); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', { - id: this.proposal.timelockid, - }); + const eta = (await time.clockFromReceipt.timestamp(txQueue)) + delay; + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(eta); + await this.helper.waitForEta(); - 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'); + const txExecute = this.helper.execute(); + + await expect(txQueue) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, anyValue) + .to.emit(this.timelock, 'CallScheduled') + .withArgs(this.proposal.timelockid, ...Array(6).fill(anyValue)) + .to.emit(this.timelock, 'CallSalt') + .withArgs(this.proposal.timelockid, anyValue); + + await expect(txExecute) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.timelock, 'CallExecuted') + .withArgs(this.proposal.timelockid, ...Array(4).fill(anyValue)) + .to.emit(this.receiver, 'MockFunctionCalled'); }); describe('should revert', function () { @@ -145,14 +145,16 @@ contract('GovernorTimelockControl', function (accounts) { 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Queued, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Queued, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ); }); }); @@ -160,66 +162,69 @@ contract('GovernorTimelockControl', function (accounts) { 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 this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(1n); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Succeeded); - await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ - this.proposal.timelockid, - proposalStatesToBitMap(Enums.OperationState.Ready), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.timelock, 'TimelockUnexpectedOperationState') + .withArgs(this.proposal.timelockid, GovernorHelper.proposalStatesToBitMap(Enums.OperationState.Ready)); }); it('if too early', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Queued); - await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ - this.proposal.timelockid, - proposalStatesToBitMap(Enums.OperationState.Ready), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.timelock, 'TimelockUnexpectedOperationState') + .withArgs(this.proposal.timelockid, GovernorHelper.proposalStatesToBitMap(Enums.OperationState.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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); await this.helper.execute(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); await this.timelock.executeBatch( ...this.proposal.shortProposal.slice(0, 3), - '0x0', - timelockSalt(this.mock.address, this.proposal.shortProposal[3]), + ethers.ZeroHash, + timelockSalt(this.mock.target, this.proposal.shortProposal[3]), ); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); }); }); @@ -228,178 +233,179 @@ contract('GovernorTimelockControl', function (accounts) { 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Queued); - expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { - id: this.proposal.timelockid, - }); + await expect(this.timelock.connect(this.owner).cancel(this.proposal.timelockid)) + .to.emit(this.timelock, 'Cancelled') + .withArgs(this.proposal.timelockid); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); }); }); describe('onlyGovernance', function () { describe('relay', function () { beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); + await this.token.$_mint(this.mock, 1); }); it('is protected', async function () { - await expectRevertCustomError( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { - from: owner, - }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect( + this.mock + .connect(this.owner) + .relay(this.token, 0n, this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n])), + ) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); 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(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('relay', [ + this.token.target, + 0n, + this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n]), + ]), }, ], '', ); - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); - await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); 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); + await expect(txExecute).to.changeTokenBalances(this.token, [this.mock, this.other], [-1n, 1n]); - await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', - }); + await expect(txExecute).to.emit(this.token, 'Transfer').withArgs(this.mock.target, this.other.address, 1n); }); it('is payable and can transfer eth to EOA', async function () { - const t2g = web3.utils.toBN(128); // timelock to governor - const g2o = web3.utils.toBN(100); // governor to eoa (other) + const t2g = 128n; // timelock to governor + const g2o = 100n; // governor to eoa (other) this.helper.setProposal( [ { - target: this.mock.address, + target: this.mock.target, value: t2g, - data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), + data: this.mock.interface.encodeFunctionData('relay', [this.other.address, g2o, '0x']), }, ], '', ); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN); - const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN); - await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - await this.helper.execute(); - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(timelockBalance.sub(t2g)); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o)); - expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o)); + await expect(this.helper.execute()).to.changeEtherBalances( + [this.timelock, this.mock, this.other], + [-t2g, t2g - g2o, g2o], + ); }); it('protected against other proposers', async function () { - const target = this.mock.address; - const value = web3.utils.toWei('0'); - const data = this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(); - const predecessor = constants.ZERO_BYTES32; - const salt = constants.ZERO_BYTES32; - - await this.timelock.schedule(target, value, data, predecessor, salt, delay, { from: owner }); - - await time.increase(delay); - - await expectRevertCustomError( - this.timelock.execute(target, value, data, predecessor, salt, { from: owner }), - 'QueueEmpty', // Bubbled up from Governor - [], + const call = [ + this.mock, + 0n, + this.mock.interface.encodeFunctionData('relay', [ethers.ZeroAddress, 0n, '0x']), + ethers.ZeroHash, + ethers.ZeroHash, + ]; + + await this.timelock.connect(this.owner).schedule(...call, delay); + + await time.clock.timestamp().then(clock => time.forward.timestamp(clock + delay)); + + // Error bubbled up from Governor + await expect(this.timelock.connect(this.owner).execute(...call)).to.be.revertedWithCustomError( + this.mock, + 'QueueEmpty', ); }); }); describe('updateTimelock', function () { beforeEach(async function () { - this.newTimelock = await Timelock.new( + this.newTimelock = await ethers.deployContract('TimelockController', [ delay, - [this.mock.address], - [this.mock.address], - constants.ZERO_ADDRESS, - ); + [this.mock], + [this.mock], + ethers.ZeroAddress, + ]); }); it('is protected', async function () { - await expectRevertCustomError( - this.mock.updateTimelock(this.newTimelock.address, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).updateTimelock(this.newTimelock)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); 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(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('updateTimelock', [this.newTimelock.target]), }, ], '', @@ -407,81 +413,64 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); + await expect(this.helper.execute()) + .to.emit(this.mock, 'TimelockChange') + .withArgs(this.timelock.target, this.newTimelock.target); - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + expect(await this.mock.timelock()).to.equal(this.newTimelock.target); }); }); describe('on safe receive', function () { describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = web3.utils.toBN(1); + const tokenId = 1n; beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(owner, tokenId); + this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']); + await this.token.$_mint(this.owner, tokenId); }); it("can't receive an ERC721 safeTransfer", async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), - 'GovernorDisabledDeposit', - [], - ); + await expect( + this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId), + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); }); describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { - 1: web3.utils.toBN(1000), - 2: web3.utils.toBN(2000), - 3: web3.utils.toBN(3000), + 1: 1000n, + 2: 2000n, + 3: 3000n, }; beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it("can't receive ERC1155 safeTransfer", async function () { - await expectRevertCustomError( - this.token.safeTransferFrom( - owner, - this.mock.address, + await expect( + this.token.connect(this.owner).safeTransferFrom( + this.owner, + this.mock, ...Object.entries(tokenIds)[0], // id + amount '0x', - { from: owner }, ), - 'GovernorDisabledDeposit', - [], - ); + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); it("can't receive ERC1155 safeBatchTransfer", async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - owner, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: owner }, - ), - 'GovernorDisabledDeposit', - [], - ); + await expect( + this.token + .connect(this.owner) + .safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'), + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); }); }); @@ -491,8 +480,8 @@ contract('GovernorTimelockControl', function (accounts) { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('nonGovernanceFunction'), }, ], '', @@ -500,7 +489,7 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); diff --git a/test/governance/extensions/GovernorVotesQuorumFraction.test.js b/test/governance/extensions/GovernorVotesQuorumFraction.test.js index ece9c78d6f1..cae4c76f0fa 100644 --- a/test/governance/extensions/GovernorVotesQuorumFraction.test.js +++ b/test/governance/extensions/GovernorVotesQuorumFraction.test.js @@ -1,58 +1,60 @@ -const { expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../../helpers/enums'); -const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); -const { clock } = require('../../helpers/time'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const Governor = artifacts.require('$GovernorMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); +const { GovernorHelper } = require('../../helpers/governance'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorVotesQuorumFraction', function (accounts) { - const [owner, voter1, voter2, voter3, voter4] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toBN(web3.utils.toWei('100')); - const ratio = web3.utils.toBN(8); // percents - const newRatio = web3.utils.toBN(6); // percents - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio); - this.receiver = await CallReceiver.new(); +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const ratio = 8n; // percents +const newRatio = 6n; // percents +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +describe('GovernorVotesQuorumFraction', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, voter1, voter2, voter3, voter4] = await ethers.getSigners(); + + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorMock', [name, votingDelay, votingPeriod, 0n, token, ratio]); - this.helper = new GovernorHelper(this.mock, mode); + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); - 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 }); + return { owner, voter1, voter2, voter3, voter4, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); // default proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + data: this.receiver.interface.encodeFunctionData('mockFunction'), }, ], '', @@ -60,22 +62,22 @@ contract('GovernorVotesQuorumFraction', 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(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'); - expect(await clock[mode]().then(timepoint => this.mock.quorum(timepoint - 1))).to.be.bignumber.equal( - tokenSupply.mul(ratio).divn(100), + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.equal(0n); + expect(await this.mock.quorumNumerator()).to.equal(ratio); + expect(await this.mock.quorumDenominator()).to.equal(100n); + expect(await time.clock[mode]().then(clock => this.mock.quorum(clock - 1n))).to.equal( + (tokenSupply * ratio) / 100n, ); }); 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.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); }); @@ -83,30 +85,30 @@ contract('GovernorVotesQuorumFraction', function (accounts) { 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.connect(this.voter2).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Defeated, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Defeated, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); describe('onlyGovernance updates', function () { it('updateQuorumNumerator is protected', async function () { - await expectRevertCustomError( - this.mock.updateQuorumNumerator(newRatio, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).updateQuorumNumerator(newRatio)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can updateQuorumNumerator through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('updateQuorumNumerator', [newRatio]), }, ], '', @@ -114,36 +116,33 @@ contract('GovernorVotesQuorumFraction', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'QuorumNumeratorUpdated', { - oldQuorumNumerator: ratio, - newQuorumNumerator: newRatio, - }); + await expect(this.helper.execute()).to.emit(this.mock, 'QuorumNumeratorUpdated').withArgs(ratio, newRatio); - expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio); - expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + expect(await this.mock.quorumNumerator()).to.equal(newRatio); + expect(await this.mock.quorumDenominator()).to.equal(100n); // it takes one block for the new quorum to take effect - expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal( - tokenSupply.mul(ratio).divn(100), + expect(await time.clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1n))).to.equal( + (tokenSupply * ratio) / 100n, ); - await time.advanceBlock(); + await mine(); - expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal( - tokenSupply.mul(newRatio).divn(100), + expect(await time.clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1n))).to.equal( + (tokenSupply * newRatio) / 100n, ); }); it('cannot updateQuorumNumerator over the maximum', async function () { - const quorumNumerator = 101; + const quorumNumerator = 101n; this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator(quorumNumerator).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('updateQuorumNumerator', [quorumNumerator]), }, ], '', @@ -151,15 +150,14 @@ contract('GovernorVotesQuorumFraction', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); const quorumDenominator = await this.mock.quorumDenominator(); - await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidQuorumFraction', [ - quorumNumerator, - quorumDenominator, - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidQuorumFraction') + .withArgs(quorumNumerator, quorumDenominator); }); }); }); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index bbac688a23c..194a8aa6f1e 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -1,66 +1,62 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../../helpers/enums'); -const { getDomain, domainType, ExtendedBallot } = require('../../helpers/eip712'); const { GovernorHelper } = require('../../helpers/governance'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const Governor = artifacts.require('$GovernorWithParamsMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); -const ERC1271WalletMock = artifacts.require('ERC1271WalletMock'); - -const rawParams = { - uintParam: web3.utils.toBN('42'), - strParam: 'These are my params', -}; - -const encodedParams = web3.eth.abi.encodeParameters(['uint256', 'string'], Object.values(rawParams)); +const { bigint: Enums } = require('../../helpers/enums'); +const { getDomain, ExtendedBallot } = require('../../helpers/eip712'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorWithParams', function (accounts) { - const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +const params = { + decoded: [42n, 'These are my params'], + encoded: ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'string'], [42n, 'These are my params']), +}; - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); +describe('GovernorWithParams', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { - beforeEach(async function () { - this.chainId = await web3.eth.getChainId(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorWithParamsMock', [name, token]); - this.helper = new GovernorHelper(this.mock, mode); + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); - 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 }); + return { owner, proposer, voter1, voter2, voter3, voter4, other, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); // default proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + data: this.receiver.interface.encodeFunctionData('mockFunction'), }, ], '', @@ -68,201 +64,180 @@ contract('GovernorWithParams', 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(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); }); it('nominal is unaffected', async function () { - await this.helper.propose({ from: proposer }); + await this.helper.connect(this.proposer).propose(); 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.connect(this.voter1).vote({ support: Enums.VoteType.For, reason: 'This is nice' }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); 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); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.receiver)).to.equal(value); }); it('Voting with params is properly supported', async function () { - await this.helper.propose({ from: proposer }); + await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); - const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); + const weight = ethers.parseEther('7') - params.decoded[0]; - const tx = await this.helper.vote( - { + await expect( + this.helper.connect(this.voter2).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, - }); + params: params.encoded, + }), + ) + .to.emit(this.mock, 'CountParams') + .withArgs(...params.decoded) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs( + this.voter2.address, + this.proposal.id, + Enums.VoteType.For, + weight, + 'no particular reason', + params.encoded, + ); - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]); }); describe('voting by signature', function () { - beforeEach(async function () { - this.voterBySig = Wallet.generate(); - this.voterBySig.address = web3.utils.toChecksumAddress(this.voterBySig.getAddressString()); - - this.data = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'ExtendedBallot', - types: { - EIP712Domain: domainType(domain), - ExtendedBallot, - }, - domain, - message, - })); - - this.sign = privateKey => async (contract, message) => - ethSigUtil.signTypedMessage(privateKey, { data: await this.data(contract, message) }); - }); - it('supports EOA signatures', async function () { - await this.token.delegate(this.voterBySig.address, { from: voter2 }); - - const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); - - const nonce = await this.mock.nonces(this.voterBySig.address); + await this.token.connect(this.voter2).delegate(this.other); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - const tx = await this.helper.vote({ - support: Enums.VoteType.For, - voter: this.voterBySig.address, - nonce, - reason: 'no particular reason', - params: encodedParams, - signature: this.sign(this.voterBySig.getPrivateKey()), - }); - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: this.voterBySig.address, + // Prepare vote + const weight = ethers.parseEther('7') - params.decoded[0]; + const nonce = await this.mock.nonces(this.other); + const data = { proposalId: this.proposal.id, support: Enums.VoteType.For, - weight, + voter: this.other.address, + nonce, reason: 'no particular reason', - params: encodedParams, - }); + params: params.encoded, + signature: (contract, message) => + getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)), + }; + + // Vote + await expect(this.helper.vote(data)) + .to.emit(this.mock, 'CountParams') + .withArgs(...params.decoded) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs(data.voter, data.proposalId, data.support, weight, data.reason, data.params); - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); - expect(await this.mock.nonces(this.voterBySig.address)).to.be.bignumber.equal(nonce.addn(1)); + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]); + expect(await this.mock.nonces(this.other)).to.equal(nonce + 1n); }); it('supports EIP-1271 signature signatures', async function () { - const ERC1271WalletOwner = Wallet.generate(); - ERC1271WalletOwner.address = web3.utils.toChecksumAddress(ERC1271WalletOwner.getAddressString()); - - const wallet = await ERC1271WalletMock.new(ERC1271WalletOwner.address); - - await this.token.delegate(wallet.address, { from: voter2 }); - - const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); - - const nonce = await this.mock.nonces(wallet.address); + const wallet = await ethers.deployContract('ERC1271WalletMock', [this.other]); + await this.token.connect(this.voter2).delegate(wallet); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - const tx = await this.helper.vote({ - support: Enums.VoteType.For, - voter: wallet.address, - nonce, - reason: 'no particular reason', - params: encodedParams, - signature: this.sign(ERC1271WalletOwner.getPrivateKey()), - }); - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: wallet.address, + // Prepare vote + const weight = ethers.parseEther('7') - params.decoded[0]; + const nonce = await this.mock.nonces(this.other); + const data = { proposalId: this.proposal.id, support: Enums.VoteType.For, - weight, + voter: wallet.target, + nonce, reason: 'no particular reason', - params: encodedParams, - }); + params: params.encoded, + signature: (contract, message) => + getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)), + }; + + // Vote + await expect(this.helper.vote(data)) + .to.emit(this.mock, 'CountParams') + .withArgs(...params.decoded) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs(data.voter, data.proposalId, data.support, weight, data.reason, data.params); - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); - expect(await this.mock.nonces(wallet.address)).to.be.bignumber.equal(nonce.addn(1)); + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]); + expect(await this.mock.nonces(wallet)).to.equal(nonce + 1n); }); it('reverts if signature does not match signer', async function () { - await this.token.delegate(this.voterBySig.address, { from: voter2 }); - - const nonce = await this.mock.nonces(this.voterBySig.address); - - const signature = this.sign(this.voterBySig.getPrivateKey()); + await this.token.connect(this.voter2).delegate(this.other); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - const voteParams = { + + // Prepare vote + const nonce = await this.mock.nonces(this.other); + const data = { + proposalId: this.proposal.id, support: Enums.VoteType.For, - voter: this.voterBySig.address, + voter: this.other.address, nonce, - signature: async (...params) => { - const sig = await signature(...params); - const tamperedSig = web3.utils.hexToBytes(sig); - tamperedSig[42] ^= 0xff; - return web3.utils.bytesToHex(tamperedSig); - }, reason: 'no particular reason', - params: encodedParams, + params: params.encoded, + // tampered signature + signature: (contract, message) => + getDomain(contract) + .then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)) + .then(signature => { + const tamperedSig = ethers.toBeArray(signature); + tamperedSig[42] ^= 0xff; + return ethers.hexlify(tamperedSig); + }), }; - await expectRevertCustomError(this.helper.vote(voteParams), 'GovernorInvalidSignature', [voteParams.voter]); + // Vote + await expect(this.helper.vote(data)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(data.voter); }); it('reverts if vote nonce is incorrect', async function () { - await this.token.delegate(this.voterBySig.address, { from: voter2 }); - - const nonce = await this.mock.nonces(this.voterBySig.address); + await this.token.connect(this.voter2).delegate(this.other); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - const voteParams = { + + // Prepare vote + const nonce = await this.mock.nonces(this.other); + const data = { + proposalId: this.proposal.id, support: Enums.VoteType.For, - voter: this.voterBySig.address, - nonce: nonce.addn(1), - signature: this.sign(this.voterBySig.getPrivateKey()), + voter: this.other.address, + nonce: nonce + 1n, reason: 'no particular reason', - params: encodedParams, + params: params.encoded, + signature: (contract, message) => + getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)), }; - await expectRevertCustomError( - this.helper.vote(voteParams), - // The signature check implies the nonce can't be tampered without changing the signer - 'GovernorInvalidSignature', - [voteParams.voter], - ); + // Vote + await expect(this.helper.vote(data)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(data.voter); }); }); }); diff --git a/test/governance/utils/ERC6372.behavior.js b/test/governance/utils/ERC6372.behavior.js index 5e8633f01cd..b5a6cb13c0b 100644 --- a/test/governance/utils/ERC6372.behavior.js +++ b/test/governance/utils/ERC6372.behavior.js @@ -1,19 +1,19 @@ -const { clock } = require('../../helpers/time'); +const { bigint: time } = require('../../helpers/time'); function shouldBehaveLikeERC6372(mode = 'blocknumber') { - describe('should implement ERC6372', function () { + describe('should implement ERC-6372', function () { beforeEach(async function () { this.mock = this.mock ?? this.token ?? this.votes; }); it('clock is correct', async function () { - expect(await this.mock.clock()).to.be.bignumber.equal(await clock[mode]().then(web3.utils.toBN)); + expect(await this.mock.clock()).to.equal(await time.clock[mode]()); }); it('CLOCK_MODE is correct', async function () { const params = new URLSearchParams(await this.mock.CLOCK_MODE()); - expect(params.get('mode')).to.be.equal(mode); - expect(params.get('from')).to.be.equal(mode == 'blocknumber' ? 'default' : null); + expect(params.get('mode')).to.equal(mode); + expect(params.get('from')).to.equal(mode == 'blocknumber' ? 'default' : null); }); }); } diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 6243cf4e447..a08f184c8ac 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -1,303 +1,277 @@ -const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { mine } = require('@nomicfoundation/hardhat-network-helpers'); -const { MAX_UINT256, ZERO_ADDRESS } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; +const { bigint: time } = require('../../helpers/time'); +const { getDomain, Delegation } = require('../../helpers/eip712'); const { shouldBehaveLikeERC6372 } = require('./ERC6372.behavior'); -const { getDomain, domainType, Delegation } = require('../../helpers/eip712'); -const { clockFromReceipt } = require('../../helpers/time'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const buildAndSignDelegation = (contract, message, pk) => - getDomain(contract) - .then(domain => ({ - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message, - })) - .then(data => fromRpcSig(ethSigUtil.signTypedMessage(pk, { data }))); - -function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungible = true }) { + +function shouldBehaveLikeVotes(tokens, { mode = 'blocknumber', fungible = true }) { + beforeEach(async function () { + [this.delegator, this.delegatee, this.alice, this.bob, this.other] = this.accounts; + this.domain = await getDomain(this.votes); + }); + shouldBehaveLikeERC6372(mode); - const getWeight = token => web3.utils.toBN(fungible ? token : 1); + const getWeight = token => (fungible ? token : 1n); describe('run votes workflow', function () { it('initial nonce is 0', async function () { - expect(await this.votes.nonces(accounts[0])).to.be.bignumber.equal('0'); + expect(await this.votes.nonces(this.alice)).to.equal(0n); }); describe('delegation with signature', function () { const token = tokens[0]; it('delegation without tokens', async function () { - expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS); + expect(await this.votes.delegates(this.alice)).to.equal(ethers.ZeroAddress); - const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] }); - expectEvent(receipt, 'DelegateChanged', { - delegator: accounts[1], - fromDelegate: ZERO_ADDRESS, - toDelegate: accounts[1], - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + await expect(this.votes.connect(this.alice).delegate(this.alice)) + .to.emit(this.votes, 'DelegateChanged') + .withArgs(this.alice.address, ethers.ZeroAddress, this.alice.address) + .to.not.emit(this.votes, 'DelegateVotesChanged'); - expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]); + expect(await this.votes.delegates(this.alice)).to.equal(this.alice.address); }); it('delegation with tokens', async function () { - await this.votes.$_mint(accounts[1], token); + await this.votes.$_mint(this.alice, token); const weight = getWeight(token); - expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS); + expect(await this.votes.delegates(this.alice)).to.equal(ethers.ZeroAddress); - const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] }); - const timepoint = await clockFromReceipt[mode](receipt); + const tx = await this.votes.connect(this.alice).delegate(this.alice); + const timepoint = await time.clockFromReceipt[mode](tx); - expectEvent(receipt, 'DelegateChanged', { - delegator: accounts[1], - fromDelegate: ZERO_ADDRESS, - toDelegate: accounts[1], - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: accounts[1], - previousVotes: '0', - newVotes: weight, - }); + await expect(tx) + .to.emit(this.votes, 'DelegateChanged') + .withArgs(this.alice.address, ethers.ZeroAddress, this.alice.address) + .to.emit(this.votes, 'DelegateVotesChanged') + .withArgs(this.alice.address, 0n, weight); - expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]); - expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(accounts[1], timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(accounts[1], timepoint)).to.be.bignumber.equal(weight); + expect(await this.votes.delegates(this.alice)).to.equal(this.alice.address); + expect(await this.votes.getVotes(this.alice)).to.equal(weight); + expect(await this.votes.getPastVotes(this.alice, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.votes.getPastVotes(this.alice, timepoint)).to.equal(weight); }); it('delegation update', async function () { - await this.votes.delegate(accounts[1], { from: accounts[1] }); - await this.votes.$_mint(accounts[1], token); + await this.votes.connect(this.alice).delegate(this.alice); + await this.votes.$_mint(this.alice, token); const weight = getWeight(token); - expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]); - expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight); - expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal('0'); - - const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: accounts[1], - fromDelegate: accounts[1], - toDelegate: accounts[2], - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: accounts[1], - previousVotes: weight, - newVotes: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: accounts[2], - previousVotes: '0', - newVotes: weight, - }); - - expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[2]); - expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal('0'); - expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal(weight); - - expect(await this.votes.getPastVotes(accounts[1], timepoint - 1)).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(accounts[2], timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(accounts[1], timepoint)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastVotes(accounts[2], timepoint)).to.be.bignumber.equal(weight); + expect(await this.votes.delegates(this.alice)).to.equal(this.alice.address); + expect(await this.votes.getVotes(this.alice)).to.equal(weight); + expect(await this.votes.getVotes(this.bob)).to.equal(0); + + const tx = await this.votes.connect(this.alice).delegate(this.bob); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.votes, 'DelegateChanged') + .withArgs(this.alice.address, this.alice.address, this.bob.address) + .to.emit(this.votes, 'DelegateVotesChanged') + .withArgs(this.alice.address, weight, 0) + .to.emit(this.votes, 'DelegateVotesChanged') + .withArgs(this.bob.address, 0, weight); + + expect(await this.votes.delegates(this.alice)).to.equal(this.bob.address); + expect(await this.votes.getVotes(this.alice)).to.equal(0n); + expect(await this.votes.getVotes(this.bob)).to.equal(weight); + + expect(await this.votes.getPastVotes(this.alice, timepoint - 1n)).to.equal(weight); + expect(await this.votes.getPastVotes(this.bob, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.votes.getPastVotes(this.alice, timepoint)).to.equal(0n); + expect(await this.votes.getPastVotes(this.bob, timepoint)).to.equal(weight); }); describe('with signature', function () { - const delegator = Wallet.generate(); - const [delegatee, other] = accounts; - const nonce = 0; - delegator.address = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0n; it('accept signed delegation', async function () { - await this.votes.$_mint(delegator.address, token); + await this.votes.$_mint(this.delegator.address, token); const weight = getWeight(token); - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce, - expiry: MAX_UINT256, - }, - delegator.getPrivateKey(), - ); - - expect(await this.votes.delegates(delegator.address)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: delegator.address, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatee, - previousVotes: '0', - newVotes: weight, - }); - - expect(await this.votes.delegates(delegator.address)).to.be.equal(delegatee); - expect(await this.votes.getVotes(delegator.address)).to.be.bignumber.equal('0'); - expect(await this.votes.getVotes(delegatee)).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(delegatee, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(delegatee, timepoint)).to.be.bignumber.equal(weight); + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + expect(await this.votes.delegates(this.delegator.address)).to.equal(ethers.ZeroAddress); + + const tx = await this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.votes, 'DelegateChanged') + .withArgs(this.delegator.address, ethers.ZeroAddress, this.delegatee.address) + .to.emit(this.votes, 'DelegateVotesChanged') + .withArgs(this.delegatee.address, 0, weight); + + expect(await this.votes.delegates(this.delegator.address)).to.equal(this.delegatee.address); + expect(await this.votes.getVotes(this.delegator.address)).to.equal(0n); + expect(await this.votes.getVotes(this.delegatee)).to.equal(weight); + expect(await this.votes.getPastVotes(this.delegatee, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.votes.getPastVotes(this.delegatee, timepoint)).to.equal(weight); }); it('rejects reused signature', async function () { - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce, - expiry: MAX_UINT256, - }, - delegator.getPrivateKey(), - ); - - await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s); - - await expectRevertCustomError( - this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), - 'InvalidAccountNonce', - [delegator.address, nonce + 1], - ); + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + await this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s); + + await expect(this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s)) + .to.be.revertedWithCustomError(this.votes, 'InvalidAccountNonce') + .withArgs(this.delegator.address, nonce + 1n); }); it('rejects bad delegatee', async function () { - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce, - expiry: MAX_UINT256, - }, - delegator.getPrivateKey(), + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + const tx = await this.votes.delegateBySig(this.other, nonce, ethers.MaxUint256, v, r, s); + const receipt = await tx.wait(); + + const [delegateChanged] = receipt.logs.filter( + log => this.votes.interface.parseLog(log)?.name === 'DelegateChanged', ); - - const receipt = await this.votes.delegateBySig(other, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event === 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegator.address); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(other); + const { args } = this.votes.interface.parseLog(delegateChanged); + expect(args.delegator).to.not.be.equal(this.delegator.address); + expect(args.fromDelegate).to.equal(ethers.ZeroAddress); + expect(args.toDelegate).to.equal(this.other.address); }); it('rejects bad nonce', async function () { - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce: nonce + 1, - expiry: MAX_UINT256, - }, - delegator.getPrivateKey(), - ); - - await expectRevertCustomError( - this.votes.delegateBySig(delegatee, nonce + 1, MAX_UINT256, v, r, s), - 'InvalidAccountNonce', - [delegator.address, 0], - ); + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce: nonce + 1n, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + await expect(this.votes.delegateBySig(this.delegatee, nonce + 1n, ethers.MaxUint256, v, r, s)) + .to.be.revertedWithCustomError(this.votes, 'InvalidAccountNonce') + .withArgs(this.delegator.address, 0); }); it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce, - expiry, - }, - delegator.getPrivateKey(), - ); - - await expectRevertCustomError( - this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), - 'VotesExpiredSignature', - [expiry], - ); + const expiry = (await time.clock.timestamp()) - 1n; + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce, + expiry, + }, + ) + .then(ethers.Signature.from); + + await expect(this.votes.delegateBySig(this.delegatee, nonce, expiry, v, r, s)) + .to.be.revertedWithCustomError(this.votes, 'VotesExpiredSignature') + .withArgs(expiry); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.votes.delegate(accounts[1], { from: accounts[1] }); + await this.votes.connect(this.alice).delegate(this.alice); }); it('reverts if block number >= current block', async function () { const timepoint = 5e10; const clock = await this.votes.clock(); - await expectRevertCustomError(this.votes.getPastTotalSupply(timepoint), 'ERC5805FutureLookup', [ - timepoint, - clock, - ]); + await expect(this.votes.getPastTotalSupply(timepoint)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(timepoint, clock); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.votes.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(0n)).to.equal(0n); }); it('returns the correct checkpointed total supply', async function () { const weight = tokens.map(token => getWeight(token)); // t0 = mint #0 - const t0 = await this.votes.$_mint(accounts[1], tokens[0]); - await time.advanceBlock(); + const t0 = await this.votes.$_mint(this.alice, tokens[0]); + await mine(); // t1 = mint #1 - const t1 = await this.votes.$_mint(accounts[1], tokens[1]); - await time.advanceBlock(); + const t1 = await this.votes.$_mint(this.alice, tokens[1]); + await mine(); // t2 = burn #1 - const t2 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[1]); - await time.advanceBlock(); + const t2 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[1]); + await mine(); // t3 = mint #2 - const t3 = await this.votes.$_mint(accounts[1], tokens[2]); - await time.advanceBlock(); + const t3 = await this.votes.$_mint(this.alice, tokens[2]); + await mine(); // t4 = burn #0 - const t4 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[0]); - await time.advanceBlock(); + const t4 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[0]); + await mine(); // t5 = burn #2 - const t5 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[2]); - await time.advanceBlock(); - - t0.timepoint = await clockFromReceipt[mode](t0.receipt); - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - t5.timepoint = await clockFromReceipt[mode](t5.receipt); - - expect(await this.votes.getPastTotalSupply(t0.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t0.timepoint)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(t0.timepoint + 1)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal(weight[0].add(weight[1])); - expect(await this.votes.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(weight[0].add(weight[1])); - expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal(weight[0].add(weight[2])); - expect(await this.votes.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(weight[0].add(weight[2])); - expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal(weight[2]); - expect(await this.votes.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(weight[2]); - expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.be.bignumber.equal('0'); - await expectRevertCustomError(this.votes.getPastTotalSupply(t5.timepoint + 1), 'ERC5805FutureLookup', [ - t5.timepoint + 1, // timepoint - t5.timepoint + 1, // clock - ]); + const t5 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[2]); + await mine(); + + t0.timepoint = await time.clockFromReceipt[mode](t0); + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); + t4.timepoint = await time.clockFromReceipt[mode](t4); + t5.timepoint = await time.clockFromReceipt[mode](t5); + + expect(await this.votes.getPastTotalSupply(t0.timepoint - 1n)).to.equal(0); + expect(await this.votes.getPastTotalSupply(t0.timepoint)).to.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t0.timepoint + 1n)).to.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.equal(weight[0] + weight[1]); + expect(await this.votes.getPastTotalSupply(t1.timepoint + 1n)).to.equal(weight[0] + weight[1]); + expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t2.timepoint + 1n)).to.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.equal(weight[0] + weight[2]); + expect(await this.votes.getPastTotalSupply(t3.timepoint + 1n)).to.equal(weight[0] + weight[2]); + expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.equal(weight[2]); + expect(await this.votes.getPastTotalSupply(t4.timepoint + 1n)).to.equal(weight[2]); + expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.equal(0); + await expect(this.votes.getPastTotalSupply(t5.timepoint + 1n)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(t5.timepoint + 1n, t5.timepoint + 1n); }); }); @@ -305,44 +279,41 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl // https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. describe('Compound test suite', function () { beforeEach(async function () { - await this.votes.$_mint(accounts[1], tokens[0]); - await this.votes.$_mint(accounts[1], tokens[1]); - await this.votes.$_mint(accounts[1], tokens[2]); + await this.votes.$_mint(this.alice, tokens[0]); + await this.votes.$_mint(this.alice, tokens[1]); + await this.votes.$_mint(this.alice, tokens[2]); }); describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { const clock = await this.votes.clock(); const timepoint = 5e10; // far in the future - await expectRevertCustomError(this.votes.getPastVotes(accounts[2], timepoint), 'ERC5805FutureLookup', [ - timepoint, - clock, - ]); + await expect(this.votes.getPastVotes(this.bob, timepoint)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(timepoint, clock); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.votes.getPastVotes(accounts[2], 0)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.bob, 0n)).to.equal(0n); }); it('returns the latest block if >= last checkpoint block', async function () { - const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - const latest = await this.votes.getVotes(accounts[2]); - expect(await this.votes.getPastVotes(accounts[2], timepoint)).to.be.bignumber.equal(latest); - expect(await this.votes.getPastVotes(accounts[2], timepoint + 1)).to.be.bignumber.equal(latest); + const delegate = await this.votes.connect(this.alice).delegate(this.bob); + const timepoint = await time.clockFromReceipt[mode](delegate); + await mine(2); + + const latest = await this.votes.getVotes(this.bob); + expect(await this.votes.getPastVotes(this.bob, timepoint)).to.equal(latest); + expect(await this.votes.getPastVotes(this.bob, timepoint + 1n)).to.equal(latest); }); it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); + await mine(); + const delegate = await this.votes.connect(this.alice).delegate(this.bob); + const timepoint = await time.clockFromReceipt[mode](delegate); + await mine(2); - expect(await this.votes.getPastVotes(accounts[2], timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.bob, timepoint - 1n)).to.equal(0n); }); }); }); diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index b2b80f9fe18..dda5e5c8251 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -1,90 +1,100 @@ -const { constants } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { clockFromReceipt } = require('../../helpers/time'); -const { BNsum } = require('../../helpers/math'); -const { expectRevertCustomError } = require('../../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -require('array.prototype.at/auto'); +const { bigint: time } = require('../../helpers/time'); +const { sum } = require('../../helpers/math'); +const { zip } = require('../../helpers/iterate'); const { shouldBehaveLikeVotes } = require('./Votes.behavior'); const MODES = { - blocknumber: artifacts.require('$VotesMock'), - timestamp: artifacts.require('$VotesTimestampMock'), + blocknumber: '$VotesMock', + timestamp: '$VotesTimestampMock', }; -contract('Votes', function (accounts) { - const [account1, account2, account3] = accounts; - const amounts = { - [account1]: web3.utils.toBN('10000000000000000000000000'), - [account2]: web3.utils.toBN('10'), - [account3]: web3.utils.toBN('20'), - }; - - const name = 'My Vote'; - const version = '1'; +const AMOUNTS = [ethers.parseEther('10000000'), 10n, 20n]; +describe('Votes', function () { for (const [mode, artifact] of Object.entries(MODES)) { + const fixture = async () => { + const accounts = await ethers.getSigners(); + + const amounts = Object.fromEntries( + zip( + accounts.slice(0, AMOUNTS.length).map(({ address }) => address), + AMOUNTS, + ), + ); + + const name = 'My Vote'; + const version = '1'; + const votes = await ethers.deployContract(artifact, [name, version]); + + return { accounts, amounts, votes, name, version }; + }; + describe(`vote with ${mode}`, function () { beforeEach(async function () { - this.votes = await artifact.new(name, version); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeVotes(accounts, Object.values(amounts), { mode, fungible: true }); + shouldBehaveLikeVotes(AMOUNTS, { mode, fungible: true }); it('starts with zero votes', async function () { - expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0'); + expect(await this.votes.getTotalSupply()).to.equal(0n); }); describe('performs voting operations', function () { beforeEach(async function () { this.txs = []; - for (const [account, amount] of Object.entries(amounts)) { + for (const [account, amount] of Object.entries(this.amounts)) { this.txs.push(await this.votes.$_mint(account, amount)); } }); it('reverts if block number >= current block', async function () { - const lastTxTimepoint = await clockFromReceipt[mode](this.txs.at(-1).receipt); + const lastTxTimepoint = await time.clockFromReceipt[mode](this.txs.at(-1)); const clock = await this.votes.clock(); - await expectRevertCustomError(this.votes.getPastTotalSupply(lastTxTimepoint + 1), 'ERC5805FutureLookup', [ - lastTxTimepoint + 1, - clock, - ]); + await expect(this.votes.getPastTotalSupply(lastTxTimepoint)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(lastTxTimepoint, clock); }); it('delegates', async function () { - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal('0'); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(constants.ZERO_ADDRESS); - expect(await this.votes.delegates(account2)).to.be.equal(constants.ZERO_ADDRESS); - - await this.votes.delegate(account1, account1); - - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account1]); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(account1); - expect(await this.votes.delegates(account2)).to.be.equal(constants.ZERO_ADDRESS); - - await this.votes.delegate(account2, account1); - - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account1].add(amounts[account2])); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(account1); - expect(await this.votes.delegates(account2)).to.be.equal(account1); + expect(await this.votes.getVotes(this.accounts[0])).to.equal(0n); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(ethers.ZeroAddress); + expect(await this.votes.delegates(this.accounts[1])).to.equal(ethers.ZeroAddress); + + await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[0])); + + expect(await this.votes.getVotes(this.accounts[0])).to.equal(this.amounts[this.accounts[0].address]); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(this.accounts[0].address); + expect(await this.votes.delegates(this.accounts[1])).to.equal(ethers.ZeroAddress); + + await this.votes.delegate(this.accounts[1], ethers.Typed.address(this.accounts[0])); + + expect(await this.votes.getVotes(this.accounts[0])).to.equal( + this.amounts[this.accounts[0].address] + this.amounts[this.accounts[1].address], + ); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(this.accounts[0].address); + expect(await this.votes.delegates(this.accounts[1])).to.equal(this.accounts[0].address); }); it('cross delegates', async function () { - await this.votes.delegate(account1, account2); - await this.votes.delegate(account2, account1); + await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[1].address)); + await this.votes.delegate(this.accounts[1], ethers.Typed.address(this.accounts[0].address)); - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account2]); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(amounts[account1]); + expect(await this.votes.getVotes(this.accounts[0])).to.equal(this.amounts[this.accounts[1].address]); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(this.amounts[this.accounts[0].address]); }); it('returns total amount of votes', async function () { - const totalSupply = BNsum(...Object.values(amounts)); - expect(await this.votes.getTotalSupply()).to.be.bignumber.equal(totalSupply); + const totalSupply = sum(...Object.values(this.amounts)); + expect(await this.votes.getTotalSupply()).to.equal(totalSupply); }); }); }); diff --git a/test/helpers/governance.js b/test/helpers/governance.js index fc4e30095a5..c2e79461a16 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -1,23 +1,10 @@ -const { web3 } = require('hardhat'); -const { forward } = require('../helpers/time'); +const { ethers } = require('hardhat'); +const { forward } = require('./time'); const { ProposalState } = require('./enums'); - -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; -} +const { unique } = require('./iterate'); const timelockSalt = (address, descriptionHash) => - '0x' + web3.utils.toBN(address).shln(96).xor(web3.utils.toBN(descriptionHash)).toString(16, 64); + ethers.toBeHex((ethers.toBigInt(address) << 96n) ^ ethers.toBigInt(descriptionHash), 32); class GovernorHelper { constructor(governor, mode = 'blocknumber') { @@ -25,229 +12,187 @@ class GovernorHelper { this.mode = mode; } - delegate(delegation = {}, opts = null) { + connect(account) { + this.governor = this.governor.connect(account); + return this; + } + + /// Setter and getters + /** + * Specify a proposal either as + * 1) an array of objects [{ target, value, data }] + * 2) an object of arrays { targets: [], values: [], data: [] } + */ + setProposal(actions, description) { + if (Array.isArray(actions)) { + this.targets = actions.map(a => a.target); + this.values = actions.map(a => a.value || 0n); + this.data = actions.map(a => a.data || '0x'); + } else { + ({ targets: this.targets, values: this.values, data: this.data } = actions); + } + this.description = description; + return this; + } + + get id() { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(['address[]', 'uint256[]', 'bytes[]', 'bytes32'], this.shortProposal), + ); + } + + // used for checking events + get signatures() { + return this.data.map(() => ''); + } + + get descriptionHash() { + return ethers.id(this.description); + } + + // condensed version for queueing end executing + get shortProposal() { + return [this.targets, this.values, this.data, this.descriptionHash]; + } + + // full version for proposing + get fullProposal() { + return [this.targets, this.values, this.data, this.description]; + } + + get currentProposal() { + return this; + } + + /// Proposal lifecycle + delegate(delegation) { 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.connect(delegation.to).delegate(delegation.to), + delegation.value === undefined || + delegation.token.connect(this.governor.runner).transfer(delegation.to, delegation.value), + delegation.tokenId === undefined || delegation.token .ownerOf(delegation.tokenId) .then(owner => - delegation.token.transferFrom(...concatOpts([owner, delegation.to, delegation.tokenId], opts)), + delegation.token.connect(this.governor.runner).transferFrom(owner, delegation.to, delegation.tokenId), ), ]); } - 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)); + propose() { + return this.governor.propose(...this.fullProposal); } - 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), - ); + queue() { + return this.governor.queue(...this.shortProposal); } - 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), - ); + execute() { + return this.governor.execute(...this.shortProposal); } - cancel(visibility = 'external', opts = null) { - const proposal = this.currentProposal; - + cancel(visibility = 'external') { switch (visibility) { case 'external': - if (proposal.useCompatibilityInterface) { - return this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts)); - } else { - return this.governor.methods['cancel(address[],uint256[],bytes[],bytes32)']( - ...concatOpts(proposal.shortProposal, opts), - ); - } + return this.governor.cancel(...this.shortProposal); + case 'internal': - return this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)']( - ...concatOpts(proposal.shortProposal, opts), - ); + return this.governor.$_cancel(...this.shortProposal); + default: throw new Error(`unsupported visibility "${visibility}"`); } } - vote(vote = {}, opts = null) { - const proposal = this.currentProposal; - - return vote.signature - ? // if signature, and either params or reason → - vote.params || vote.reason - ? this.sign(vote).then(signature => - this.governor.castVoteWithReasonAndParamsBySig( - ...concatOpts( - [proposal.id, vote.support, vote.voter, vote.reason || '', vote.params || '', signature], - opts, - ), - ), - ) - : this.sign(vote).then(signature => - this.governor.castVoteBySig(...concatOpts([proposal.id, vote.support, vote.voter, signature], opts)), - ) - : vote.params - ? // otherwise if params - this.governor.castVoteWithReasonAndParams( - ...concatOpts([proposal.id, vote.support, vote.reason || '', vote.params], opts), - ) - : vote.reason - ? // otherwise if reason - this.governor.castVoteWithReason(...concatOpts([proposal.id, vote.support, vote.reason], opts)) - : this.governor.castVote(...concatOpts([proposal.id, vote.support], opts)); - } - - sign(vote = {}) { - return vote.signature(this.governor, this.forgeMessage(vote)); - } - - forgeMessage(vote = {}) { - const proposal = this.currentProposal; - - const message = { proposalId: proposal.id, support: vote.support, voter: vote.voter, nonce: vote.nonce }; - - if (vote.params || vote.reason) { - message.reason = vote.reason || ''; - message.params = vote.params || ''; + async vote(vote = {}) { + let method = 'castVote'; // default + let args = [this.id, vote.support]; // base + + if (vote.signature) { + const sign = await vote.signature(this.governor, this.forgeMessage(vote)); + if (vote.params || vote.reason) { + method = 'castVoteWithReasonAndParamsBySig'; + args.push(vote.voter, vote.reason ?? '', vote.params ?? '0x', sign); + } else { + method = 'castVoteBySig'; + args.push(vote.voter, sign); + } + } else if (vote.params) { + method = 'castVoteWithReasonAndParams'; + args.push(vote.reason ?? '', vote.params); + } else if (vote.reason) { + method = 'castVoteWithReason'; + args.push(vote.reason); } - return message; + return await this.governor[method](...args); } - async waitForSnapshot(offset = 0) { - const proposal = this.currentProposal; - const timepoint = await this.governor.proposalSnapshot(proposal.id); - return forward[this.mode](timepoint.addn(offset)); + /// Clock helpers + async waitForSnapshot(offset = 0n) { + const timepoint = await this.governor.proposalSnapshot(this.id); + return forward[this.mode](timepoint + offset); } - async waitForDeadline(offset = 0) { - const proposal = this.currentProposal; - const timepoint = await this.governor.proposalDeadline(proposal.id); - return forward[this.mode](timepoint.addn(offset)); + async waitForDeadline(offset = 0n) { + const timepoint = await this.governor.proposalDeadline(this.id); + return forward[this.mode](timepoint + offset); } - async waitForEta(offset = 0) { - const proposal = this.currentProposal; - const timestamp = await this.governor.proposalEta(proposal.id); - return forward.timestamp(timestamp.addn(offset)); + async waitForEta(offset = 0n) { + const timestamp = await this.governor.proposalEta(this.id); + return forward.timestamp(timestamp + 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; + /// Other helpers + forgeMessage(vote = {}) { + const message = { proposalId: this.id, support: vote.support, voter: vote.voter, nonce: vote.nonce }; - 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); + if (vote.params || vote.reason) { + message.reason = vote.reason ?? ''; + message.params = vote.params ?? '0x'; } - 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 queueing 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; + return message; } -} -/** - * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to - * the underlying position in the `ProposalState` enum. For example: - * - * 0x000...10000 - * ^^^^^^------ ... - * ^----- Succeeded - * ^---- Defeated - * ^--- Canceled - * ^-- Active - * ^- Pending - */ -function proposalStatesToBitMap(proposalStates, options = {}) { - if (!Array.isArray(proposalStates)) { - proposalStates = [proposalStates]; - } - const statesCount = Object.keys(ProposalState).length; - let result = 0; - - const uniqueProposalStates = new Set(proposalStates.map(bn => bn.toNumber())); // Remove duplicates - for (const state of uniqueProposalStates) { - if (state < 0 || state >= statesCount) { - expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`); - } else { - result |= 1 << state; + /** + * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to + * the underlying position in the `ProposalState` enum. For example: + * + * 0x000...10000 + * ^^^^^^------ ... + * ^----- Succeeded + * ^---- Defeated + * ^--- Canceled + * ^-- Active + * ^- Pending + */ + static proposalStatesToBitMap(proposalStates, options = {}) { + if (!Array.isArray(proposalStates)) { + proposalStates = [proposalStates]; + } + const statesCount = BigInt(Object.keys(ProposalState).length); + let result = 0n; + + for (const state of unique(...proposalStates)) { + if (state < 0n || state >= statesCount) { + expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`); + } else { + result |= 1n << state; + } } - } - if (options.inverted) { - const mask = 2 ** statesCount - 1; - result = result ^ mask; - } + if (options.inverted) { + const mask = 2n ** statesCount - 1n; + result = result ^ mask; + } - const hex = web3.utils.numberToHex(result); - return web3.utils.padLeft(hex, 64); + return ethers.toBeHex(result, 32); + } } module.exports = { GovernorHelper, - proposalStatesToBitMap, timelockSalt, }; diff --git a/test/helpers/iterate.js b/test/helpers/iterate.js index 2a84dfbebdc..79d1c8c839e 100644 --- a/test/helpers/iterate.js +++ b/test/helpers/iterate.js @@ -3,8 +3,15 @@ const mapValues = (obj, fn) => Object.fromEntries(Object.entries(obj).map(([k, v // Cartesian product of a list of arrays const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [...ai, bi])), [[]]); +const unique = (...array) => array.filter((obj, i) => array.indexOf(obj) === i); +const zip = (...args) => + Array(Math.max(...args.map(array => array.length))) + .fill() + .map((_, i) => args.map(array => array[i])); module.exports = { mapValues, product, + unique, + zip, }; diff --git a/test/helpers/time.js b/test/helpers/time.js index 874713ee535..5f85b69158a 100644 --- a/test/helpers/time.js +++ b/test/helpers/time.js @@ -1,3 +1,4 @@ +const { ethers } = require('hardhat'); const { time, mineUpTo } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('./iterate'); @@ -8,9 +9,7 @@ module.exports = { }, clockFromReceipt: { blocknumber: receipt => Promise.resolve(receipt.blockNumber), - timestamp: receipt => web3.eth.getBlock(receipt.blockNumber).then(block => block.timestamp), - // TODO: update for ethers receipt - // timestamp: receipt => receipt.getBlock().then(block => block.timestamp), + timestamp: receipt => ethers.provider.getBlock(receipt.blockNumber).then(block => block.timestamp), }, forward: { blocknumber: mineUpTo, @@ -21,8 +20,8 @@ module.exports = { // TODO: deprecate the old version in favor of this one module.exports.bigint = { - clock: mapValues(module.exports.clock, fn => () => fn().then(BigInt)), - clockFromReceipt: mapValues(module.exports.clockFromReceipt, fn => receipt => fn(receipt).then(BigInt)), + clock: mapValues(module.exports.clock, fn => () => fn().then(ethers.toBigInt)), + clockFromReceipt: mapValues(module.exports.clockFromReceipt, fn => receipt => fn(receipt).then(ethers.toBigInt)), forward: module.exports.forward, - duration: mapValues(module.exports.duration, fn => n => BigInt(fn(n))), + duration: mapValues(module.exports.duration, fn => n => ethers.toBigInt(fn(ethers.toNumber(n)))), }; diff --git a/test/helpers/txpool.js b/test/helpers/txpool.js index ecdba546214..b6e960c1014 100644 --- a/test/helpers/txpool.js +++ b/test/helpers/txpool.js @@ -1,30 +1,20 @@ const { network } = require('hardhat'); -const { promisify } = require('util'); - -const queue = promisify(setImmediate); - -async function countPendingTransactions() { - return parseInt(await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending'])); -} +const { mine } = require('@nomicfoundation/hardhat-network-helpers'); +const { unique } = require('./iterate'); async function batchInBlock(txs) { try { // disable auto-mining await network.provider.send('evm_setAutomine', [false]); // send all transactions - const promises = txs.map(fn => fn()); - // wait for node to have all pending transactions - while (txs.length > (await countPendingTransactions())) { - await queue(); - } + const responses = await Promise.all(txs.map(fn => fn())); // mine one block - await network.provider.send('evm_mine'); + await mine(); // fetch receipts - const receipts = await Promise.all(promises); + const receipts = await Promise.all(responses.map(response => response.wait())); // Sanity check, all tx should be in the same block - const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); - expect(minedBlocks.size).to.equal(1); - + expect(unique(receipts.map(receipt => receipt.blockNumber))).to.have.lengthOf(1); + // return responses return receipts; } finally { // enable auto-mining @@ -33,6 +23,5 @@ async function batchInBlock(txs) { } module.exports = { - countPendingTransactions, batchInBlock, }; diff --git a/test/token/ERC20/extensions/ERC20Permit.test.js b/test/token/ERC20/extensions/ERC20Permit.test.js index e27a98239bb..538fa7d7f03 100644 --- a/test/token/ERC20/extensions/ERC20Permit.test.js +++ b/test/token/ERC20/extensions/ERC20Permit.test.js @@ -3,9 +3,7 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { getDomain, domainSeparator, Permit } = require('../../../helpers/eip712'); -const { - bigint: { clock, duration }, -} = require('../../../helpers/time'); +const { bigint: time } = require('../../../helpers/time'); const name = 'My Token'; const symbol = 'MTKN'; @@ -97,7 +95,7 @@ describe('ERC20Permit', function () { }); it('rejects expired permit', async function () { - const deadline = (await clock.timestamp()) - duration.weeks(1); + const deadline = (await time.clock.timestamp()) - time.duration.weeks(1); const { v, r, s } = await this.buildData(this.token, deadline) .then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message)) diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index 9ec1c09e935..165d08a1878 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -1,585 +1,544 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS } = constants; +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); + +const { getDomain, Delegation } = require('../../../helpers/eip712'); +const { batchInBlock } = require('../../../helpers/txpool'); +const { bigint: time } = require('../../../helpers/time'); const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; -const { batchInBlock } = require('../../../helpers/txpool'); -const { getDomain, domainType, Delegation } = require('../../../helpers/eip712'); -const { clock, clockFromReceipt } = require('../../../helpers/time'); -const { expectRevertCustomError } = require('../../../helpers/customError'); +const TOKENS = [ + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, +]; + +const name = 'My Token'; +const symbol = 'MTKN'; +const version = '1'; +const supply = ethers.parseEther('10000000'); -const MODES = { - blocknumber: artifacts.require('$ERC20Votes'), - timestamp: artifacts.require('$ERC20VotesTimestampMock'), -}; +describe('ERC20Votes', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + // accounts is required by shouldBehaveLikeVotes + const accounts = await ethers.getSigners(); + const [holder, recipient, delegatee, other1, other2] = accounts; -contract('ERC20Votes', function (accounts) { - const [holder, recipient, holderDelegatee, other1, other2] = accounts; + const token = await ethers.deployContract(Token, [name, symbol, name, version]); + const domain = await getDomain(token); - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - const supply = new BN('10000000000000000000000000'); + return { accounts, holder, recipient, delegatee, other1, other2, token, domain }; + }; - for (const [mode, artifact] of Object.entries(MODES)) { describe(`vote with ${mode}`, function () { beforeEach(async function () { - this.token = await artifact.new(name, symbol, name, version); + Object.assign(this, await loadFixture(fixture)); this.votes = this.token; }); // includes ERC6372 behavior check - shouldBehaveLikeVotes(accounts, [1, 17, 42], { mode, fungible: true }); + shouldBehaveLikeVotes([1, 17, 42], { mode, fungible: true }); it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + expect(await this.token.nonces(this.holder)).to.equal(0n); }); it('minting restriction', async function () { - const value = web3.utils.toBN(1).shln(208); - await expectRevertCustomError(this.token.$_mint(holder, value), 'ERC20ExceededSafeSupply', [ - value, - value.subn(1), - ]); + const value = 2n ** 208n; + await expect(this.token.$_mint(this.holder, value)) + .to.be.revertedWithCustomError(this.token, 'ERC20ExceededSafeSupply') + .withArgs(value, value - 1n); }); it('recent checkpoints', async function () { - await this.token.delegate(holder, { from: holder }); + await this.token.connect(this.holder).delegate(this.holder); for (let i = 0; i < 6; i++) { - await this.token.$_mint(holder, 1); + await this.token.$_mint(this.holder, 1n); } - const timepoint = await clock[mode](); - expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6'); + const timepoint = await time.clock[mode](); + expect(await this.token.numCheckpoints(this.holder)).to.equal(6n); // recent - expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('5'); + expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(5n); // non-recent - expect(await this.token.getPastVotes(holder, timepoint - 6)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(this.holder, timepoint - 6n)).to.equal(0n); }); describe('set delegation', function () { describe('call', function () { it('delegation with balance', async function () { - await this.token.$_mint(holder, supply); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousVotes: '0', - newVotes: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(supply); + await this.token.$_mint(this.holder, supply); + expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress); + + const tx = await this.token.connect(this.holder).delegate(this.holder); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.token, 'DelegateChanged') + .withArgs(this.holder.address, ethers.ZeroAddress, this.holder.address) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, 0n, supply); + + expect(await this.token.delegates(this.holder)).to.equal(this.holder.address); + expect(await this.token.getVotes(this.holder)).to.equal(supply); + expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(supply); }); it('delegation without balance', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress); - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + await expect(this.token.connect(this.holder).delegate(this.holder)) + .to.emit(this.token, 'DelegateChanged') + .withArgs(this.holder.address, ethers.ZeroAddress, this.holder.address) + .to.not.emit(this.token, 'DelegateVotesChanged'); - expect(await this.token.delegates(holder)).to.be.equal(holder); + expect(await this.token.delegates(this.holder)).to.equal(this.holder.address); }); }); describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message, - })); + const nonce = 0n; beforeEach(async function () { - await this.token.$_mint(delegatorAddress, supply); + await this.token.$_mint(this.holder, supply); }); it('accept signed delegation', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousVotes: '0', - newVotes: supply, - }); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply); + const { r, s, v } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress); + + const tx = await this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.token, 'DelegateChanged') + .withArgs(this.holder.address, ethers.ZeroAddress, this.holder.address) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, 0n, supply); + + expect(await this.token.delegates(this.holder)).to.equal(this.holder.address); + + expect(await this.token.getVotes(this.holder)).to.equal(supply); + expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(supply); }); it('rejects reused signature', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - - await expectRevertCustomError( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'InvalidAccountNonce', - [delegatorAddress, nonce + 1], - ); + const { r, s, v } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + await this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s); + + await expect(this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'InvalidAccountNonce') + .withArgs(this.holder.address, nonce + 1n); }); it('rejects bad delegatee', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); + const { r, s, v } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + const tx = await this.token.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s); + + const { args } = await tx + .wait() + .then(receipt => receipt.logs.find(event => event.fragment.name == 'DelegateChanged')); + expect(args[0]).to.not.equal(this.holder.address); + expect(args[1]).to.equal(ethers.ZeroAddress); + expect(args[2]).to.equal(this.delegatee.address); }); it('rejects bad nonce', async function () { - const sig = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })); - const { r, s, v } = fromRpcSig(sig); - - const domain = await getDomain(this.token); - const typedMessage = { - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message: { delegatee: delegatorAddress, nonce: nonce + 1, expiry: MAX_UINT256 }, - }; - - await expectRevertCustomError( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'InvalidAccountNonce', - [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), nonce], + const { r, s, v, serialized } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + const recovered = ethers.verifyTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce: nonce + 1n, + expiry: ethers.MaxUint256, + }, + serialized, ); + + await expect(this.token.delegateBySig(this.holder, nonce + 1n, ethers.MaxUint256, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'InvalidAccountNonce') + .withArgs(recovered, nonce); }); it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - await expectRevertCustomError( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'VotesExpiredSignature', - [expiry], - ); + const expiry = (await time.clock.timestamp()) - time.duration.weeks(1); + + const { r, s, v } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry, + }, + ) + .then(ethers.Signature.from); + + await expect(this.token.delegateBySig(this.holder, nonce, expiry, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'VotesExpiredSignature') + .withArgs(expiry); }); }); }); describe('change delegation', function () { beforeEach(async function () { - await this.token.$_mint(holder, supply); - await this.token.delegate(holder, { from: holder }); + await this.token.$_mint(this.holder, supply); + await this.token.connect(this.holder).delegate(this.holder); }); it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousVotes: supply, - newVotes: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousVotes: '0', - newVotes: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply); + expect(await this.token.delegates(this.holder)).to.equal(this.holder.address); + + const tx = await this.token.connect(this.holder).delegate(this.delegatee); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.token, 'DelegateChanged') + .withArgs(this.holder.address, this.holder.address, this.delegatee.address) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, supply, 0n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.delegatee.address, 0n, supply); + + expect(await this.token.delegates(this.holder)).to.equal(this.delegatee.address); + + expect(await this.token.getVotes(this.holder)).to.equal(0n); + expect(await this.token.getVotes(this.delegatee)).to.equal(supply); + expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(supply); + expect(await this.token.getPastVotes(this.delegatee, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(0n); + expect(await this.token.getPastVotes(this.delegatee, timepoint)).to.equal(supply); }); }); describe('transfers', function () { beforeEach(async function () { - await this.token.$_mint(holder, supply); + await this.token.$_mint(this.holder, supply); }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + await expect(this.token.connect(this.holder).transfer(this.recipient, 1n)) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, 1n) + .to.not.emit(this.token, 'DelegateVotesChanged'); - this.holderVotes = '0'; - this.recipientVotes = '0'; + this.holderVotes = 0n; + this.recipientVotes = 0n; }); it('sender delegation', async function () { - await this.token.delegate(holder, { from: holder }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousVotes: supply, - newVotes: supply.subn(1), - }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '0'; + await this.token.connect(this.holder).delegate(this.holder); + + const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, 1n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, supply, supply - 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = supply - 1n; + this.recipientVotes = 0n; }); it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousVotes: '0', newVotes: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = '0'; - this.recipientVotes = '1'; + await this.token.connect(this.recipient).delegate(this.recipient); + + const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, 1n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.recipient.address, 0n, 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = 0n; + this.recipientVotes = 1n; }); it('full delegation', async function () { - await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousVotes: supply, - newVotes: supply.subn(1), - }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousVotes: '0', newVotes: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '1'; + await this.token.connect(this.holder).delegate(this.holder); + await this.token.connect(this.recipient).delegate(this.recipient); + + const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, 1n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, supply, supply - 1n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.recipient.address, 0n, 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = supply - 1n; + this.recipientVotes = 1n; }); afterEach(async function () { - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + expect(await this.token.getVotes(this.holder)).to.equal(this.holderVotes); + expect(await this.token.getVotes(this.recipient)).to.equal(this.recipientVotes); // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const timepoint = await clock[mode](); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPastVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes); + const timepoint = await time.clock[mode](); + await mine(); + expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(this.holderVotes); + expect(await this.token.getPastVotes(this.recipient, timepoint)).to.equal(this.recipientVotes); }); }); // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. describe('Compound test suite', function () { beforeEach(async function () { - await this.token.$_mint(holder, supply); + await this.token.$_mint(this.holder, supply); }); describe('balanceOf', function () { it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.balanceOf(this.holder)).to.equal(supply); }); }); describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('100'); + await this.token.connect(this.holder).transfer(this.recipient, 100n); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(this.other1)).to.equal(0n); + + const t1 = await this.token.connect(this.recipient).delegate(this.other1); + t1.timepoint = await time.clockFromReceipt[mode](t1); + expect(await this.token.numCheckpoints(this.other1)).to.equal(1n); + + const t2 = await this.token.connect(this.recipient).transfer(this.other2, 10); + t2.timepoint = await time.clockFromReceipt[mode](t2); + expect(await this.token.numCheckpoints(this.other1)).to.equal(2n); + + const t3 = await this.token.connect(this.recipient).transfer(this.other2, 10); + t3.timepoint = await time.clockFromReceipt[mode](t3); + expect(await this.token.numCheckpoints(this.other1)).to.equal(3n); + + const t4 = await this.token.connect(this.holder).transfer(this.recipient, 20); + t4.timepoint = await time.clockFromReceipt[mode](t4); + expect(await this.token.numCheckpoints(this.other1)).to.equal(4n); + + expect(await this.token.checkpoints(this.other1, 0n)).to.deep.equal([t1.timepoint, 100n]); + expect(await this.token.checkpoints(this.other1, 1n)).to.deep.equal([t2.timepoint, 90n]); + expect(await this.token.checkpoints(this.other1, 2n)).to.deep.equal([t3.timepoint, 80n]); + expect(await this.token.checkpoints(this.other1, 3n)).to.deep.equal([t4.timepoint, 100n]); + await mine(); + expect(await this.token.getPastVotes(this.other1, t1.timepoint)).to.equal(100n); + expect(await this.token.getPastVotes(this.other1, t2.timepoint)).to.equal(90n); + expect(await this.token.getPastVotes(this.other1, t3.timepoint)).to.equal(80n); + expect(await this.token.getPastVotes(this.other1, t4.timepoint)).to.equal(100n); }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + await this.token.connect(this.holder).transfer(this.recipient, 100n); + expect(await this.token.numCheckpoints(this.other1)).to.equal(0n); const [t1, t2, t3] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }), + () => this.token.connect(this.recipient).delegate(this.other1, { gasLimit: 200000 }), + () => this.token.connect(this.recipient).transfer(this.other2, 10n, { gasLimit: 200000 }), + () => this.token.connect(this.recipient).transfer(this.other2, 10n, { gasLimit: 200000 }), ]); - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']); + expect(await this.token.numCheckpoints(this.other1)).to.equal(1); + expect(await this.token.checkpoints(this.other1, 0n)).to.be.deep.equal([t1.timepoint, 80n]); - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); + const t4 = await this.token.connect(this.holder).transfer(this.recipient, 20n); + t4.timepoint = await time.clockFromReceipt[mode](t4); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']); + expect(await this.token.numCheckpoints(this.other1)).to.equal(2n); + expect(await this.token.checkpoints(this.other1, 1n)).to.be.deep.equal([t4.timepoint, 100n]); }); }); describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { const clock = await this.token.clock(); - await expectRevertCustomError(this.token.getPastVotes(other1, 5e10), 'ERC5805FutureLookup', [5e10, clock]); + await expect(this.token.getPastVotes(this.other1, 50_000_000_000n)) + .to.be.revertedWithCustomError(this.token, 'ERC5805FutureLookup') + .withArgs(50_000_000_000n, clock); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(this.other1, 0n)).to.equal(0n); }); it('returns the latest block if >= last checkpoint block', async function () { - const { receipt } = await this.token.delegate(other1, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); + const tx = await this.token.connect(this.holder).delegate(this.other1); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); - expect(await this.token.getPastVotes(other1, timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + expect(await this.token.getPastVotes(this.other1, timepoint)).to.equal(supply); + expect(await this.token.getPastVotes(this.other1, timepoint + 1n)).to.equal(supply); }); it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const { receipt } = await this.token.delegate(other1, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + await mine(); + const tx = await this.token.connect(this.holder).delegate(this.other1); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); + + expect(await this.token.getPastVotes(this.other1, timepoint - 1n)).to.equal(0n); + expect(await this.token.getPastVotes(this.other1, timepoint + 1n)).to.equal(supply); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.token.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + const t1 = await this.token.connect(this.holder).delegate(this.other1); + await mine(2); + const t2 = await this.token.connect(this.holder).transfer(this.other2, 10); + await mine(2); + const t3 = await this.token.connect(this.holder).transfer(this.other2, 10); + await mine(2); + const t4 = await this.token.connect(this.other2).transfer(this.holder, 20); + await mine(2); + + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); + t4.timepoint = await time.clockFromReceipt[mode](t4); + + expect(await this.token.getPastVotes(this.other1, t1.timepoint - 1n)).to.equal(0n); + expect(await this.token.getPastVotes(this.other1, t1.timepoint)).to.equal(supply); + expect(await this.token.getPastVotes(this.other1, t1.timepoint + 1n)).to.equal(supply); + expect(await this.token.getPastVotes(this.other1, t2.timepoint)).to.equal(supply - 10n); + expect(await this.token.getPastVotes(this.other1, t2.timepoint + 1n)).to.equal(supply - 10n); + expect(await this.token.getPastVotes(this.other1, t3.timepoint)).to.equal(supply - 20n); + expect(await this.token.getPastVotes(this.other1, t3.timepoint + 1n)).to.equal(supply - 20n); + expect(await this.token.getPastVotes(this.other1, t4.timepoint)).to.equal(supply); + expect(await this.token.getPastVotes(this.other1, t4.timepoint + 1n)).to.equal(supply); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); + await this.token.connect(this.holder).delegate(this.holder); }); it('reverts if block number >= current block', async function () { const clock = await this.token.clock(); - await expectRevertCustomError(this.token.getPastTotalSupply(5e10), 'ERC5805FutureLookup', [5e10, clock]); + await expect(this.token.getPastTotalSupply(50_000_000_000n)) + .to.be.revertedWithCustomError(this.token, 'ERC5805FutureLookup') + .withArgs(50_000_000_000n, clock); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(0n)).to.equal(0n); }); it('returns the latest block if >= last checkpoint block', async function () { - const { receipt } = await this.token.$_mint(holder, supply); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); + const tx = await this.token.$_mint(this.holder, supply); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); - expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(timepoint)).to.equal(supply); + expect(await this.token.getPastTotalSupply(timepoint + 1n)).to.equal(supply); }); it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const { receipt } = await this.token.$_mint(holder, supply); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + await mine(); + const tx = await this.token.$_mint(this.holder, supply); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); + + expect(await this.token.getPastTotalSupply(timepoint - 1n)).to.equal(0n); + expect(await this.token.getPastTotalSupply(timepoint + 1n)).to.equal(supply); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.$_mint(holder, 20); - await time.advanceBlock(); - await time.advanceBlock(); - - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + const t1 = await this.token.$_mint(this.holder, supply); + await mine(2); + const t2 = await this.token.$_burn(this.holder, 10n); + await mine(2); + const t3 = await this.token.$_burn(this.holder, 10n); + await mine(2); + const t4 = await this.token.$_mint(this.holder, 20n); + await mine(2); + + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); + t4.timepoint = await time.clockFromReceipt[mode](t4); + + expect(await this.token.getPastTotalSupply(t1.timepoint - 1n)).to.equal(0n); + expect(await this.token.getPastTotalSupply(t1.timepoint)).to.equal(supply); + expect(await this.token.getPastTotalSupply(t1.timepoint + 1n)).to.equal(supply); + expect(await this.token.getPastTotalSupply(t2.timepoint)).to.equal(supply - 10n); + expect(await this.token.getPastTotalSupply(t2.timepoint + 1n)).to.equal(supply - 10n); + expect(await this.token.getPastTotalSupply(t3.timepoint)).to.equal(supply - 20n); + expect(await this.token.getPastTotalSupply(t3.timepoint + 1n)).to.equal(supply - 20n); + expect(await this.token.getPastTotalSupply(t4.timepoint)).to.equal(supply); + expect(await this.token.getPastTotalSupply(t4.timepoint + 1n)).to.equal(supply); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index ba9a2a8cb6c..f52e9ca95c9 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -1,181 +1,192 @@ -/* eslint-disable */ - -const { expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); -const { clock, clockFromReceipt } = require('../../../helpers/time'); +const { bigint: time } = require('../../../helpers/time'); const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); -const MODES = { - blocknumber: artifacts.require('$ERC721Votes'), +const TOKENS = [ + { Token: '$ERC721Votes', mode: 'blocknumber' }, // no timestamp mode for ERC721Votes yet -}; +]; + +const name = 'My Vote'; +const symbol = 'MTKN'; +const version = '1'; +const tokens = [ethers.parseEther('10000000'), 10n, 20n, 30n]; + +describe('ERC721Votes', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + // accounts is required by shouldBehaveLikeVotes + const accounts = await ethers.getSigners(); + const [holder, recipient, other1, other2] = accounts; -contract('ERC721Votes', function (accounts) { - const [account1, account2, other1, other2] = accounts; + const token = await ethers.deployContract(Token, [name, symbol, name, version]); - const name = 'My Vote'; - const symbol = 'MTKN'; - const version = '1'; - const tokens = ['10000000000000000000000000', '10', '20', '30'].map(n => web3.utils.toBN(n)); + return { accounts, holder, recipient, other1, other2, token }; + }; - for (const [mode, artifact] of Object.entries(MODES)) { describe(`vote with ${mode}`, function () { beforeEach(async function () { - this.votes = await artifact.new(name, symbol, name, version); + Object.assign(this, await loadFixture(fixture)); + this.votes = this.token; }); // includes ERC6372 behavior check - shouldBehaveLikeVotes(accounts, tokens, { mode, fungible: false }); + shouldBehaveLikeVotes(tokens, { mode, fungible: false }); describe('balanceOf', function () { beforeEach(async function () { - await this.votes.$_mint(account1, tokens[0]); - await this.votes.$_mint(account1, tokens[1]); - await this.votes.$_mint(account1, tokens[2]); - await this.votes.$_mint(account1, tokens[3]); + await this.votes.$_mint(this.holder, tokens[0]); + await this.votes.$_mint(this.holder, tokens[1]); + await this.votes.$_mint(this.holder, tokens[2]); + await this.votes.$_mint(this.holder, tokens[3]); }); it('grants to initial account', async function () { - expect(await this.votes.balanceOf(account1)).to.be.bignumber.equal('4'); + expect(await this.votes.balanceOf(this.holder)).to.equal(4n); }); }); describe('transfers', function () { beforeEach(async function () { - await this.votes.$_mint(account1, tokens[0]); + await this.votes.$_mint(this.holder, tokens[0]); }); it('no delegation', async function () { - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + await expect(this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0])) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, tokens[0]) + .to.not.emit(this.token, 'DelegateVotesChanged'); - this.account1Votes = '0'; - this.account2Votes = '0'; + this.holderVotes = 0n; + this.recipientVotes = 0n; }); it('sender delegation', async function () { - await this.votes.delegate(account1, { from: account1 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousVotes: '1', newVotes: '0' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '0'; + await this.votes.connect(this.holder).delegate(this.holder); + + const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, tokens[0]) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, 1n, 0n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = 0n; + this.recipientVotes = 0n; }); it('receiver delegation', async function () { - await this.votes.delegate(account2, { from: account2 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousVotes: '0', newVotes: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '1'; + await this.votes.connect(this.recipient).delegate(this.recipient); + + const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, tokens[0]) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.recipient.address, 0n, 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = 0n; + this.recipientVotes = 1n; }); it('full delegation', async function () { - await this.votes.delegate(account1, { from: account1 }); - await this.votes.delegate(account2, { from: account2 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousVotes: '1', newVotes: '0' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousVotes: '0', newVotes: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '1'; + await this.votes.connect(this.holder).delegate(this.holder); + await this.votes.connect(this.recipient).delegate(this.recipient); + + const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, tokens[0]) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, 1n, 0n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.recipient.address, 0n, 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = 0; + this.recipientVotes = 1n; }); it('returns the same total supply on transfers', async function () { - await this.votes.delegate(account1, { from: account1 }); + await this.votes.connect(this.holder).delegate(this.holder); - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - const timepoint = await clockFromReceipt[mode](receipt); + const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); + const timepoint = await time.clockFromReceipt[mode](tx); - await time.advanceBlock(); - await time.advanceBlock(); + await mine(2); - expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(timepoint - 1n)).to.equal(1n); + expect(await this.votes.getPastTotalSupply(timepoint + 1n)).to.equal(1n); - this.account1Votes = '0'; - this.account2Votes = '0'; + this.holderVotes = 0n; + this.recipientVotes = 0n; }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - await this.votes.$_mint(account1, tokens[1]); - await this.votes.$_mint(account1, tokens[2]); - await this.votes.$_mint(account1, tokens[3]); - - const total = await this.votes.balanceOf(account1); - - const t1 = await this.votes.delegate(other1, { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.votes.transferFrom(account1, other2, tokens[0], { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.votes.transferFrom(account1, other2, tokens[2], { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.votes.transferFrom(other2, account1, tokens[2], { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.votes.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal(total); - expect(await this.votes.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(total); - expect(await this.votes.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('2'); - expect(await this.votes.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal('2'); - expect(await this.votes.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal('3'); - - this.account1Votes = '0'; - this.account2Votes = '0'; + await this.votes.$_mint(this.holder, tokens[1]); + await this.votes.$_mint(this.holder, tokens[2]); + await this.votes.$_mint(this.holder, tokens[3]); + + const total = await this.votes.balanceOf(this.holder); + + const t1 = await this.votes.connect(this.holder).delegate(this.other1); + await mine(2); + const t2 = await this.votes.connect(this.holder).transferFrom(this.holder, this.other2, tokens[0]); + await mine(2); + const t3 = await this.votes.connect(this.holder).transferFrom(this.holder, this.other2, tokens[2]); + await mine(2); + const t4 = await this.votes.connect(this.other2).transferFrom(this.other2, this.holder, tokens[2]); + await mine(2); + + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); + t4.timepoint = await time.clockFromReceipt[mode](t4); + + expect(await this.votes.getPastVotes(this.other1, t1.timepoint - 1n)).to.equal(0n); + expect(await this.votes.getPastVotes(this.other1, t1.timepoint)).to.equal(total); + expect(await this.votes.getPastVotes(this.other1, t1.timepoint + 1n)).to.equal(total); + expect(await this.votes.getPastVotes(this.other1, t2.timepoint)).to.equal(3n); + expect(await this.votes.getPastVotes(this.other1, t2.timepoint + 1n)).to.equal(3n); + expect(await this.votes.getPastVotes(this.other1, t3.timepoint)).to.equal(2n); + expect(await this.votes.getPastVotes(this.other1, t3.timepoint + 1n)).to.equal(2n); + expect(await this.votes.getPastVotes(this.other1, t4.timepoint)).to.equal('3'); + expect(await this.votes.getPastVotes(this.other1, t4.timepoint + 1n)).to.equal(3n); + + this.holderVotes = 0n; + this.recipientVotes = 0n; }); afterEach(async function () { - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(this.account1Votes); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(this.account2Votes); + expect(await this.votes.getVotes(this.holder)).to.equal(this.holderVotes); + expect(await this.votes.getVotes(this.recipient)).to.equal(this.recipientVotes); // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const timepoint = await clock[mode](); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(account1, timepoint)).to.be.bignumber.equal(this.account1Votes); - expect(await this.votes.getPastVotes(account2, timepoint)).to.be.bignumber.equal(this.account2Votes); + const timepoint = await time.clock[mode](); + await mine(); + expect(await this.votes.getPastVotes(this.holder, timepoint)).to.equal(this.holderVotes); + expect(await this.votes.getPastVotes(this.recipient, timepoint)).to.equal(this.recipientVotes); }); }); });