GovernorSequentialProposalId.test.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');
  5. const { GovernorHelper } = require('../../helpers/governance');
  6. const { VoteType } = require('../../helpers/enums');
  7. const iterate = require('../../helpers/iterate');
  8. const TOKENS = [
  9. { Token: '$ERC20Votes', mode: 'blocknumber' },
  10. { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
  11. ];
  12. const name = 'OZ-Governor';
  13. const version = '1';
  14. const tokenName = 'MockToken';
  15. const tokenSymbol = 'MTKN';
  16. const tokenSupply = ethers.parseEther('100');
  17. const votingDelay = 4n;
  18. const votingPeriod = 16n;
  19. const value = ethers.parseEther('1');
  20. async function deployToken(contractName) {
  21. try {
  22. return await ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName, version]);
  23. } catch (error) {
  24. if (error.message == 'incorrect number of arguments to constructor') {
  25. // ERC20VotesLegacyMock has a different construction that uses version='1' by default.
  26. return ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName]);
  27. }
  28. throw error;
  29. }
  30. }
  31. describe('GovernorSequentialProposalId', function () {
  32. for (const { Token, mode } of TOKENS) {
  33. const fixture = async () => {
  34. const [owner, proposer, voter1, voter2, voter3, voter4, userEOA] = await ethers.getSigners();
  35. const receiver = await ethers.deployContract('CallReceiverMock');
  36. const token = await deployToken(Token, [tokenName, tokenSymbol, version]);
  37. const mock = await ethers.deployContract('$GovernorSequentialProposalIdMock', [
  38. name, // name
  39. votingDelay, // initialVotingDelay
  40. votingPeriod, // initialVotingPeriod
  41. 0n, // initialProposalThreshold
  42. token, // tokenAddress
  43. 10n, // quorumNumeratorValue
  44. ]);
  45. await owner.sendTransaction({ to: mock, value });
  46. await token.$_mint(owner, tokenSupply);
  47. const helper = new GovernorHelper(mock, mode);
  48. await helper.connect(owner).delegate({ token: token, to: voter1, value: ethers.parseEther('10') });
  49. await helper.connect(owner).delegate({ token: token, to: voter2, value: ethers.parseEther('7') });
  50. await helper.connect(owner).delegate({ token: token, to: voter3, value: ethers.parseEther('5') });
  51. await helper.connect(owner).delegate({ token: token, to: voter4, value: ethers.parseEther('2') });
  52. return {
  53. owner,
  54. proposer,
  55. voter1,
  56. voter2,
  57. voter3,
  58. voter4,
  59. userEOA,
  60. receiver,
  61. token,
  62. mock,
  63. helper,
  64. };
  65. };
  66. describe(`using ${Token}`, function () {
  67. beforeEach(async function () {
  68. Object.assign(this, await loadFixture(fixture));
  69. this.proposal = this.helper.setProposal(
  70. [
  71. {
  72. target: this.receiver.target,
  73. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  74. value,
  75. },
  76. ],
  77. '<proposal description>',
  78. );
  79. });
  80. it('sequential proposal ids', async function () {
  81. for (const i of iterate.range(1, 10)) {
  82. this.proposal.description = `<proposal description #${i}>`;
  83. await expect(this.mock.hashProposal(...this.proposal.shortProposal)).to.eventually.equal(this.proposal.hash);
  84. await expect(this.mock.getProposalId(...this.proposal.shortProposal)).revertedWithCustomError(
  85. this.mock,
  86. 'GovernorNonexistentProposal',
  87. );
  88. await expect(this.mock.latestProposalId()).to.eventually.equal(i - 1);
  89. await expect(this.helper.connect(this.proposer).propose())
  90. .to.emit(this.mock, 'ProposalCreated')
  91. .withArgs(
  92. i,
  93. this.proposer,
  94. this.proposal.targets,
  95. this.proposal.values,
  96. this.proposal.signatures,
  97. this.proposal.data,
  98. anyValue,
  99. anyValue,
  100. this.proposal.description,
  101. );
  102. await expect(this.mock.hashProposal(...this.proposal.shortProposal)).to.eventually.equal(this.proposal.hash);
  103. await expect(this.mock.getProposalId(...this.proposal.shortProposal)).to.eventually.equal(i);
  104. await expect(this.mock.latestProposalId()).to.eventually.equal(i);
  105. }
  106. });
  107. it('sequential proposal ids with offset start', async function () {
  108. const offset = 69420;
  109. await this.mock.$_initializeLatestProposalId(offset);
  110. for (const i of iterate.range(offset + 1, offset + 10)) {
  111. this.proposal.description = `<proposal description #${i}>`;
  112. await expect(this.mock.hashProposal(...this.proposal.shortProposal)).to.eventually.equal(this.proposal.hash);
  113. await expect(this.mock.getProposalId(...this.proposal.shortProposal)).revertedWithCustomError(
  114. this.mock,
  115. 'GovernorNonexistentProposal',
  116. );
  117. await expect(this.mock.latestProposalId()).to.eventually.equal(i - 1);
  118. await expect(this.helper.connect(this.proposer).propose())
  119. .to.emit(this.mock, 'ProposalCreated')
  120. .withArgs(
  121. i,
  122. this.proposer,
  123. this.proposal.targets,
  124. this.proposal.values,
  125. this.proposal.signatures,
  126. this.proposal.data,
  127. anyValue,
  128. anyValue,
  129. this.proposal.description,
  130. );
  131. await expect(this.mock.hashProposal(...this.proposal.shortProposal)).to.eventually.equal(this.proposal.hash);
  132. await expect(this.mock.getProposalId(...this.proposal.shortProposal)).to.eventually.equal(i);
  133. await expect(this.mock.latestProposalId()).to.eventually.equal(i);
  134. }
  135. });
  136. it('can only initialize latest proposal id from 0', async function () {
  137. await this.helper.propose();
  138. await expect(this.mock.latestProposalId()).to.eventually.equal(1);
  139. await expect(this.mock.$_initializeLatestProposalId(2)).to.be.revertedWithCustomError(
  140. this.mock,
  141. 'GovernorAlreadyInitializedLatestProposalId',
  142. );
  143. });
  144. it('cannot repropose same proposal', async function () {
  145. await this.helper.connect(this.proposer).propose();
  146. await expect(this.helper.connect(this.proposer).propose())
  147. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  148. .withArgs(await this.proposal.id, 0, ethers.ZeroHash);
  149. });
  150. it('nominal workflow', async function () {
  151. await this.helper.connect(this.proposer).propose();
  152. await this.helper.waitForSnapshot();
  153. await expect(this.mock.connect(this.voter1).castVote(1, VoteType.For))
  154. .to.emit(this.mock, 'VoteCast')
  155. .withArgs(this.voter1, 1, VoteType.For, ethers.parseEther('10'), '');
  156. await expect(this.mock.connect(this.voter2).castVote(1, VoteType.For))
  157. .to.emit(this.mock, 'VoteCast')
  158. .withArgs(this.voter2, 1, VoteType.For, ethers.parseEther('7'), '');
  159. await expect(this.mock.connect(this.voter3).castVote(1, VoteType.For))
  160. .to.emit(this.mock, 'VoteCast')
  161. .withArgs(this.voter3, 1, VoteType.For, ethers.parseEther('5'), '');
  162. await expect(this.mock.connect(this.voter4).castVote(1, VoteType.Abstain))
  163. .to.emit(this.mock, 'VoteCast')
  164. .withArgs(this.voter4, 1, VoteType.Abstain, ethers.parseEther('2'), '');
  165. await this.helper.waitForDeadline();
  166. await expect(this.helper.execute())
  167. .to.eventually.emit(this.mock, 'ProposalExecuted')
  168. .withArgs(1)
  169. .emit(this.receiver, 'MockFunctionCalled');
  170. });
  171. });
  172. }
  173. });