GovernorWithParams.test.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { GovernorHelper } = require('../../helpers/governance');
  5. const { VoteType } = require('../../helpers/enums');
  6. const { getDomain, ExtendedBallot } = require('../../helpers/eip712');
  7. const TOKENS = [
  8. { Token: '$ERC20Votes', mode: 'blocknumber' },
  9. { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
  10. ];
  11. const name = 'OZ-Governor';
  12. const version = '1';
  13. const tokenName = 'MockToken';
  14. const tokenSymbol = 'MTKN';
  15. const tokenSupply = ethers.parseEther('100');
  16. const votingDelay = 4n;
  17. const votingPeriod = 16n;
  18. const value = ethers.parseEther('1');
  19. const params = {
  20. decoded: [42n, 'These are my params'],
  21. encoded: ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'string'], [42n, 'These are my params']),
  22. };
  23. describe('GovernorWithParams', function () {
  24. for (const { Token, mode } of TOKENS) {
  25. const fixture = async () => {
  26. const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
  27. const receiver = await ethers.deployContract('CallReceiverMock');
  28. const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
  29. const mock = await ethers.deployContract('$GovernorWithParamsMock', [name, token]);
  30. await owner.sendTransaction({ to: mock, value });
  31. await token.$_mint(owner, tokenSupply);
  32. const helper = new GovernorHelper(mock, mode);
  33. await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') });
  34. await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
  35. await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
  36. await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
  37. return { owner, proposer, voter1, voter2, voter3, voter4, other, receiver, token, mock, helper };
  38. };
  39. describe(`using ${Token}`, function () {
  40. beforeEach(async function () {
  41. Object.assign(this, await loadFixture(fixture));
  42. // default proposal
  43. this.proposal = this.helper.setProposal(
  44. [
  45. {
  46. target: this.receiver.target,
  47. value,
  48. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  49. },
  50. ],
  51. '<proposal description>',
  52. );
  53. });
  54. it('deployment check', async function () {
  55. expect(await this.mock.name()).to.equal(name);
  56. expect(await this.mock.token()).to.equal(this.token);
  57. expect(await this.mock.votingDelay()).to.equal(votingDelay);
  58. expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
  59. });
  60. it('nominal is unaffected', async function () {
  61. await this.helper.connect(this.proposer).propose();
  62. await this.helper.waitForSnapshot();
  63. await this.helper.connect(this.voter1).vote({ support: VoteType.For, reason: 'This is nice' });
  64. await this.helper.connect(this.voter2).vote({ support: VoteType.For });
  65. await this.helper.connect(this.voter3).vote({ support: VoteType.Against });
  66. await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain });
  67. await this.helper.waitForDeadline();
  68. await this.helper.execute();
  69. expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
  70. expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true;
  71. expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true;
  72. expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
  73. expect(await ethers.provider.getBalance(this.receiver)).to.equal(value);
  74. });
  75. it('Voting with params is properly supported', async function () {
  76. await this.helper.connect(this.proposer).propose();
  77. await this.helper.waitForSnapshot();
  78. const weight = ethers.parseEther('7') - params.decoded[0];
  79. await expect(
  80. this.helper.connect(this.voter2).vote({
  81. support: VoteType.For,
  82. reason: 'no particular reason',
  83. params: params.encoded,
  84. }),
  85. )
  86. .to.emit(this.mock, 'CountParams')
  87. .withArgs(...params.decoded)
  88. .to.emit(this.mock, 'VoteCastWithParams')
  89. .withArgs(
  90. this.voter2.address,
  91. this.proposal.id,
  92. VoteType.For,
  93. weight,
  94. 'no particular reason',
  95. params.encoded,
  96. );
  97. expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]);
  98. });
  99. describe('voting by signature', function () {
  100. it('supports EOA signatures', async function () {
  101. await this.token.connect(this.voter2).delegate(this.other);
  102. // Run proposal
  103. await this.helper.propose();
  104. await this.helper.waitForSnapshot();
  105. // Prepare vote
  106. const weight = ethers.parseEther('7') - params.decoded[0];
  107. const nonce = await this.mock.nonces(this.other);
  108. const data = {
  109. proposalId: this.proposal.id,
  110. support: VoteType.For,
  111. voter: this.other.address,
  112. nonce,
  113. reason: 'no particular reason',
  114. params: params.encoded,
  115. signature: (contract, message) =>
  116. getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)),
  117. };
  118. // Vote
  119. await expect(this.helper.vote(data))
  120. .to.emit(this.mock, 'CountParams')
  121. .withArgs(...params.decoded)
  122. .to.emit(this.mock, 'VoteCastWithParams')
  123. .withArgs(data.voter, data.proposalId, data.support, weight, data.reason, data.params);
  124. expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]);
  125. expect(await this.mock.nonces(this.other)).to.equal(nonce + 1n);
  126. });
  127. it('supports EIP-1271 signature signatures', async function () {
  128. const wallet = await ethers.deployContract('ERC1271WalletMock', [this.other]);
  129. await this.token.connect(this.voter2).delegate(wallet);
  130. // Run proposal
  131. await this.helper.propose();
  132. await this.helper.waitForSnapshot();
  133. // Prepare vote
  134. const weight = ethers.parseEther('7') - params.decoded[0];
  135. const nonce = await this.mock.nonces(this.other);
  136. const data = {
  137. proposalId: this.proposal.id,
  138. support: VoteType.For,
  139. voter: wallet.target,
  140. nonce,
  141. reason: 'no particular reason',
  142. params: params.encoded,
  143. signature: (contract, message) =>
  144. getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)),
  145. };
  146. // Vote
  147. await expect(this.helper.vote(data))
  148. .to.emit(this.mock, 'CountParams')
  149. .withArgs(...params.decoded)
  150. .to.emit(this.mock, 'VoteCastWithParams')
  151. .withArgs(data.voter, data.proposalId, data.support, weight, data.reason, data.params);
  152. expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]);
  153. expect(await this.mock.nonces(wallet)).to.equal(nonce + 1n);
  154. });
  155. it('reverts if signature does not match signer', async function () {
  156. await this.token.connect(this.voter2).delegate(this.other);
  157. // Run proposal
  158. await this.helper.propose();
  159. await this.helper.waitForSnapshot();
  160. // Prepare vote
  161. const nonce = await this.mock.nonces(this.other);
  162. const data = {
  163. proposalId: this.proposal.id,
  164. support: VoteType.For,
  165. voter: this.other.address,
  166. nonce,
  167. reason: 'no particular reason',
  168. params: params.encoded,
  169. // tampered signature
  170. signature: (contract, message) =>
  171. getDomain(contract)
  172. .then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message))
  173. .then(signature => {
  174. const tamperedSig = ethers.toBeArray(signature);
  175. tamperedSig[42] ^= 0xff;
  176. return ethers.hexlify(tamperedSig);
  177. }),
  178. };
  179. // Vote
  180. await expect(this.helper.vote(data))
  181. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature')
  182. .withArgs(data.voter);
  183. });
  184. it('reverts if vote nonce is incorrect', async function () {
  185. await this.token.connect(this.voter2).delegate(this.other);
  186. // Run proposal
  187. await this.helper.propose();
  188. await this.helper.waitForSnapshot();
  189. // Prepare vote
  190. const nonce = await this.mock.nonces(this.other);
  191. const data = {
  192. proposalId: this.proposal.id,
  193. support: VoteType.For,
  194. voter: this.other.address,
  195. nonce: nonce + 1n,
  196. reason: 'no particular reason',
  197. params: params.encoded,
  198. signature: (contract, message) =>
  199. getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)),
  200. };
  201. // Vote
  202. await expect(this.helper.vote(data))
  203. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature')
  204. .withArgs(data.voter);
  205. });
  206. });
  207. });
  208. }
  209. });