Governor.test.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  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 { getDomain, Ballot } = require('../helpers/eip712');
  6. const { ProposalState, VoteType } = require('../helpers/enums');
  7. const time = require('../helpers/time');
  8. const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
  9. const { shouldBehaveLikeERC6372 } = require('./utils/ERC6372.behavior');
  10. const TOKENS = [
  11. { Token: '$ERC20Votes', mode: 'blocknumber' },
  12. { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
  13. { Token: '$ERC20VotesLegacyMock', mode: 'blocknumber' },
  14. ];
  15. const name = 'OZ-Governor';
  16. const version = '1';
  17. const tokenName = 'MockToken';
  18. const tokenSymbol = 'MTKN';
  19. const tokenSupply = ethers.parseEther('100');
  20. const votingDelay = 4n;
  21. const votingPeriod = 16n;
  22. const value = ethers.parseEther('1');
  23. const signBallot = account => (contract, message) =>
  24. getDomain(contract).then(domain => account.signTypedData(domain, { Ballot }, message));
  25. async function deployToken(contractName) {
  26. try {
  27. return await ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName, version]);
  28. } catch (error) {
  29. if (error.message == 'incorrect number of arguments to constructor') {
  30. // ERC20VotesLegacyMock has a different construction that uses version='1' by default.
  31. return ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName]);
  32. }
  33. throw error;
  34. }
  35. }
  36. describe('Governor', function () {
  37. for (const { Token, mode } of TOKENS) {
  38. const fixture = async () => {
  39. const [owner, proposer, voter1, voter2, voter3, voter4, userEOA] = await ethers.getSigners();
  40. const receiver = await ethers.deployContract('CallReceiverMock');
  41. const token = await deployToken(Token, [tokenName, tokenSymbol, version]);
  42. const mock = await ethers.deployContract('$GovernorMock', [
  43. name, // name
  44. votingDelay, // initialVotingDelay
  45. votingPeriod, // initialVotingPeriod
  46. 0n, // initialProposalThreshold
  47. token, // tokenAddress
  48. 10n, // quorumNumeratorValue
  49. ]);
  50. await owner.sendTransaction({ to: mock, value });
  51. await token.$_mint(owner, tokenSupply);
  52. const helper = new GovernorHelper(mock, mode);
  53. await helper.connect(owner).delegate({ token: token, to: voter1, value: ethers.parseEther('10') });
  54. await helper.connect(owner).delegate({ token: token, to: voter2, value: ethers.parseEther('7') });
  55. await helper.connect(owner).delegate({ token: token, to: voter3, value: ethers.parseEther('5') });
  56. await helper.connect(owner).delegate({ token: token, to: voter4, value: ethers.parseEther('2') });
  57. return {
  58. owner,
  59. proposer,
  60. voter1,
  61. voter2,
  62. voter3,
  63. voter4,
  64. userEOA,
  65. receiver,
  66. token,
  67. mock,
  68. helper,
  69. };
  70. };
  71. describe(`using ${Token}`, function () {
  72. beforeEach(async function () {
  73. Object.assign(this, await loadFixture(fixture));
  74. // initiate fresh proposal
  75. this.proposal = this.helper.setProposal(
  76. [
  77. {
  78. target: this.receiver.target,
  79. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  80. value,
  81. },
  82. ],
  83. '<proposal description>',
  84. );
  85. });
  86. shouldSupportInterfaces(['ERC1155Receiver', 'Governor', 'Governor_5_3']);
  87. shouldBehaveLikeERC6372(mode);
  88. it('deployment check', async function () {
  89. expect(await this.mock.name()).to.equal(name);
  90. expect(await this.mock.token()).to.equal(this.token);
  91. expect(await this.mock.votingDelay()).to.equal(votingDelay);
  92. expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
  93. expect(await this.mock.quorum(0)).to.equal(0n);
  94. expect(await this.mock.COUNTING_MODE()).to.equal('support=bravo&quorum=for,abstain');
  95. });
  96. it('nominal workflow', async function () {
  97. // Before
  98. expect(await this.mock.proposalProposer(this.proposal.id)).to.equal(ethers.ZeroAddress);
  99. expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
  100. expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.false;
  101. expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.false;
  102. expect(await ethers.provider.getBalance(this.mock)).to.equal(value);
  103. expect(await ethers.provider.getBalance(this.receiver)).to.equal(0n);
  104. expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n);
  105. expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.false;
  106. // Run proposal
  107. const txPropose = await this.helper.connect(this.proposer).propose();
  108. const timepoint = await time.clockFromReceipt[mode](txPropose);
  109. await expect(txPropose)
  110. .to.emit(this.mock, 'ProposalCreated')
  111. .withArgs(
  112. this.proposal.id,
  113. this.proposer,
  114. this.proposal.targets,
  115. this.proposal.values,
  116. this.proposal.signatures,
  117. this.proposal.data,
  118. timepoint + votingDelay,
  119. timepoint + votingDelay + votingPeriod,
  120. this.proposal.description,
  121. );
  122. await this.helper.waitForSnapshot();
  123. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For, reason: 'This is nice' }))
  124. .to.emit(this.mock, 'VoteCast')
  125. .withArgs(this.voter1, this.proposal.id, VoteType.For, ethers.parseEther('10'), 'This is nice');
  126. await expect(this.helper.connect(this.voter2).vote({ support: VoteType.For }))
  127. .to.emit(this.mock, 'VoteCast')
  128. .withArgs(this.voter2, this.proposal.id, VoteType.For, ethers.parseEther('7'), '');
  129. await expect(this.helper.connect(this.voter3).vote({ support: VoteType.Against }))
  130. .to.emit(this.mock, 'VoteCast')
  131. .withArgs(this.voter3, this.proposal.id, VoteType.Against, ethers.parseEther('5'), '');
  132. await expect(this.helper.connect(this.voter4).vote({ support: VoteType.Abstain }))
  133. .to.emit(this.mock, 'VoteCast')
  134. .withArgs(this.voter4, this.proposal.id, VoteType.Abstain, ethers.parseEther('2'), '');
  135. await this.helper.waitForDeadline();
  136. const txExecute = await this.helper.execute();
  137. await expect(txExecute).to.emit(this.mock, 'ProposalExecuted').withArgs(this.proposal.id);
  138. await expect(txExecute).to.emit(this.receiver, 'MockFunctionCalled');
  139. // After
  140. expect(await this.mock.proposalProposer(this.proposal.id)).to.equal(this.proposer);
  141. expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
  142. expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true;
  143. expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true;
  144. expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
  145. expect(await ethers.provider.getBalance(this.receiver)).to.equal(value);
  146. expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n);
  147. expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.false;
  148. });
  149. it('send ethers', async function () {
  150. this.helper.setProposal(
  151. [
  152. {
  153. target: this.userEOA.address,
  154. value,
  155. },
  156. ],
  157. '<proposal description>',
  158. );
  159. // Run proposal
  160. await expect(async () => {
  161. await this.helper.propose();
  162. await this.helper.waitForSnapshot();
  163. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  164. await this.helper.waitForDeadline();
  165. return this.helper.execute();
  166. }).to.changeEtherBalances([this.mock, this.userEOA], [-value, value]);
  167. });
  168. describe('vote with signature', function () {
  169. it('votes with an EOA signature on two proposals', async function () {
  170. await this.token.connect(this.voter1).delegate(this.userEOA);
  171. for (let i = 0; i < 2; i++) {
  172. const nonce = await this.mock.nonces(this.userEOA);
  173. // Run proposal
  174. await this.helper.propose();
  175. await this.helper.waitForSnapshot();
  176. await expect(
  177. this.helper.vote({
  178. support: VoteType.For,
  179. voter: this.userEOA.address,
  180. nonce,
  181. signature: signBallot(this.userEOA),
  182. }),
  183. )
  184. .to.emit(this.mock, 'VoteCast')
  185. .withArgs(this.userEOA, this.proposal.id, VoteType.For, ethers.parseEther('10'), '');
  186. // After
  187. expect(await this.mock.hasVoted(this.proposal.id, this.userEOA)).to.be.true;
  188. expect(await this.mock.nonces(this.userEOA)).to.equal(nonce + 1n);
  189. // Update proposal to allow for re-propose
  190. this.helper.description += ' - updated';
  191. }
  192. await expect(this.mock.nonces(this.userEOA)).to.eventually.equal(2n);
  193. });
  194. it('votes with a valid EIP-1271 signature', async function () {
  195. const wallet = await ethers.deployContract('ERC1271WalletMock', [this.userEOA]);
  196. await this.token.connect(this.voter1).delegate(wallet);
  197. const nonce = await this.mock.nonces(this.userEOA);
  198. // Run proposal
  199. await this.helper.propose();
  200. await this.helper.waitForSnapshot();
  201. await expect(
  202. this.helper.vote({
  203. support: VoteType.For,
  204. voter: wallet.target,
  205. nonce,
  206. signature: signBallot(this.userEOA),
  207. }),
  208. )
  209. .to.emit(this.mock, 'VoteCast')
  210. .withArgs(wallet, this.proposal.id, VoteType.For, ethers.parseEther('10'), '');
  211. await this.helper.waitForDeadline();
  212. await this.helper.execute();
  213. // After
  214. expect(await this.mock.hasVoted(this.proposal.id, wallet)).to.be.true;
  215. expect(await this.mock.nonces(wallet)).to.equal(nonce + 1n);
  216. });
  217. afterEach('no other votes are cast', async function () {
  218. expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
  219. expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.false;
  220. expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.false;
  221. });
  222. });
  223. describe('should revert', function () {
  224. describe('on propose', function () {
  225. it('if proposal already exists', async function () {
  226. await this.helper.propose();
  227. await expect(this.helper.propose())
  228. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  229. .withArgs(this.proposal.id, ProposalState.Pending, ethers.ZeroHash);
  230. });
  231. it('if proposer has below threshold votes', async function () {
  232. const votes = ethers.parseEther('10');
  233. const threshold = ethers.parseEther('1000');
  234. await this.mock.$_setProposalThreshold(threshold);
  235. await expect(this.helper.connect(this.voter1).propose())
  236. .to.be.revertedWithCustomError(this.mock, 'GovernorInsufficientProposerVotes')
  237. .withArgs(this.voter1, votes, threshold);
  238. });
  239. });
  240. describe('on vote', function () {
  241. it('if proposal does not exist', async function () {
  242. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  243. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  244. .withArgs(this.proposal.id);
  245. });
  246. it('if voting has not started', async function () {
  247. await this.helper.propose();
  248. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  249. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  250. .withArgs(
  251. this.proposal.id,
  252. ProposalState.Pending,
  253. GovernorHelper.proposalStatesToBitMap([ProposalState.Active]),
  254. );
  255. });
  256. it('if support value is invalid', async function () {
  257. await this.helper.propose();
  258. await this.helper.waitForSnapshot();
  259. await expect(this.helper.vote({ support: 255 })).to.be.revertedWithCustomError(
  260. this.mock,
  261. 'GovernorInvalidVoteType',
  262. );
  263. });
  264. it('if vote was already casted', async function () {
  265. await this.helper.propose();
  266. await this.helper.waitForSnapshot();
  267. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  268. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  269. .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyCastVote')
  270. .withArgs(this.voter1);
  271. });
  272. it('if voting is over', async function () {
  273. await this.helper.propose();
  274. await this.helper.waitForDeadline();
  275. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  276. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  277. .withArgs(
  278. this.proposal.id,
  279. ProposalState.Defeated,
  280. GovernorHelper.proposalStatesToBitMap([ProposalState.Active]),
  281. );
  282. });
  283. });
  284. describe('on vote by signature', function () {
  285. beforeEach(async function () {
  286. await this.token.connect(this.voter1).delegate(this.userEOA);
  287. // Run proposal
  288. await this.helper.propose();
  289. await this.helper.waitForSnapshot();
  290. });
  291. it('if signature does not match signer', async function () {
  292. const nonce = await this.mock.nonces(this.userEOA);
  293. function tamper(str, index, mask) {
  294. const arrayStr = ethers.getBytes(str);
  295. arrayStr[index] ^= mask;
  296. return ethers.hexlify(arrayStr);
  297. }
  298. const voteParams = {
  299. support: VoteType.For,
  300. voter: this.userEOA.address,
  301. nonce,
  302. signature: (...args) => signBallot(this.userEOA)(...args).then(sig => tamper(sig, 42, 0xff)),
  303. };
  304. await expect(this.helper.vote(voteParams))
  305. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature')
  306. .withArgs(voteParams.voter);
  307. });
  308. it('if vote nonce is incorrect', async function () {
  309. const nonce = await this.mock.nonces(this.userEOA);
  310. const voteParams = {
  311. support: VoteType.For,
  312. voter: this.userEOA.address,
  313. nonce: nonce + 1n,
  314. signature: signBallot(this.userEOA),
  315. };
  316. await expect(this.helper.vote(voteParams))
  317. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature')
  318. .withArgs(voteParams.voter);
  319. });
  320. });
  321. describe('on queue', function () {
  322. it('always', async function () {
  323. await this.helper.connect(this.proposer).propose();
  324. await this.helper.waitForSnapshot();
  325. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  326. await this.helper.waitForDeadline();
  327. await expect(this.helper.queue()).to.be.revertedWithCustomError(this.mock, 'GovernorQueueNotImplemented');
  328. });
  329. });
  330. describe('on execute', function () {
  331. it('if proposal does not exist', async function () {
  332. await expect(this.helper.execute())
  333. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  334. .withArgs(this.proposal.id);
  335. });
  336. it('if quorum is not reached', async function () {
  337. await this.helper.propose();
  338. await this.helper.waitForSnapshot();
  339. await this.helper.connect(this.voter3).vote({ support: VoteType.For });
  340. await expect(this.helper.execute())
  341. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  342. .withArgs(
  343. this.proposal.id,
  344. ProposalState.Active,
  345. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  346. );
  347. });
  348. it('if score not reached', async function () {
  349. await this.helper.propose();
  350. await this.helper.waitForSnapshot();
  351. await this.helper.connect(this.voter1).vote({ support: VoteType.Against });
  352. await expect(this.helper.execute())
  353. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  354. .withArgs(
  355. this.proposal.id,
  356. ProposalState.Active,
  357. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  358. );
  359. });
  360. it('if voting is not over', async function () {
  361. await this.helper.propose();
  362. await this.helper.waitForSnapshot();
  363. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  364. await expect(this.helper.execute())
  365. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  366. .withArgs(
  367. this.proposal.id,
  368. ProposalState.Active,
  369. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  370. );
  371. });
  372. it('if receiver revert without reason', async function () {
  373. this.helper.setProposal(
  374. [
  375. {
  376. target: this.receiver.target,
  377. data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
  378. },
  379. ],
  380. '<proposal description>',
  381. );
  382. await this.helper.propose();
  383. await this.helper.waitForSnapshot();
  384. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  385. await this.helper.waitForDeadline();
  386. await expect(this.helper.execute()).to.be.revertedWithCustomError(this.mock, 'FailedCall');
  387. });
  388. it('if receiver revert with reason', async function () {
  389. this.helper.setProposal(
  390. [
  391. {
  392. target: this.receiver.target,
  393. data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsReason'),
  394. },
  395. ],
  396. '<proposal description>',
  397. );
  398. await this.helper.propose();
  399. await this.helper.waitForSnapshot();
  400. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  401. await this.helper.waitForDeadline();
  402. await expect(this.helper.execute()).to.be.revertedWith('CallReceiverMock: reverting');
  403. });
  404. it('if proposal was already executed', async function () {
  405. await this.helper.propose();
  406. await this.helper.waitForSnapshot();
  407. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  408. await this.helper.waitForDeadline();
  409. await this.helper.execute();
  410. await expect(this.helper.execute())
  411. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  412. .withArgs(
  413. this.proposal.id,
  414. ProposalState.Executed,
  415. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  416. );
  417. });
  418. });
  419. });
  420. describe('state', function () {
  421. it('Unset', async function () {
  422. await expect(this.mock.state(this.proposal.id))
  423. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  424. .withArgs(this.proposal.id);
  425. });
  426. it('Pending & Active', async function () {
  427. await this.helper.propose();
  428. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Pending);
  429. await this.helper.waitForSnapshot();
  430. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Pending);
  431. await this.helper.waitForSnapshot(1n);
  432. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Active);
  433. });
  434. it('Defeated', async function () {
  435. await this.helper.propose();
  436. await this.helper.waitForDeadline();
  437. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Active);
  438. await this.helper.waitForDeadline(1n);
  439. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Defeated);
  440. });
  441. it('Succeeded', async function () {
  442. await this.helper.propose();
  443. await this.helper.waitForSnapshot();
  444. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  445. await this.helper.waitForDeadline();
  446. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Active);
  447. await this.helper.waitForDeadline(1n);
  448. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Succeeded);
  449. });
  450. it('Executed', async function () {
  451. await this.helper.propose();
  452. await this.helper.waitForSnapshot();
  453. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  454. await this.helper.waitForDeadline();
  455. await this.helper.execute();
  456. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Executed);
  457. });
  458. });
  459. describe('cancel', function () {
  460. describe('internal', function () {
  461. it('before proposal', async function () {
  462. await expect(this.helper.cancel('internal'))
  463. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  464. .withArgs(this.proposal.id);
  465. });
  466. it('after proposal', async function () {
  467. await this.helper.propose();
  468. await this.helper.cancel('internal');
  469. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
  470. await this.helper.waitForSnapshot();
  471. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  472. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  473. .withArgs(
  474. this.proposal.id,
  475. ProposalState.Canceled,
  476. GovernorHelper.proposalStatesToBitMap([ProposalState.Active]),
  477. );
  478. });
  479. it('after vote', async function () {
  480. await this.helper.propose();
  481. await this.helper.waitForSnapshot();
  482. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  483. await this.helper.cancel('internal');
  484. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
  485. await this.helper.waitForDeadline();
  486. await expect(this.helper.execute())
  487. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  488. .withArgs(
  489. this.proposal.id,
  490. ProposalState.Canceled,
  491. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  492. );
  493. });
  494. it('after deadline', async function () {
  495. await this.helper.propose();
  496. await this.helper.waitForSnapshot();
  497. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  498. await this.helper.waitForDeadline();
  499. await this.helper.cancel('internal');
  500. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
  501. await expect(this.helper.execute())
  502. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  503. .withArgs(
  504. this.proposal.id,
  505. ProposalState.Canceled,
  506. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  507. );
  508. });
  509. it('after execution', async function () {
  510. await this.helper.propose();
  511. await this.helper.waitForSnapshot();
  512. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  513. await this.helper.waitForDeadline();
  514. await this.helper.execute();
  515. await expect(this.helper.cancel('internal'))
  516. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  517. .withArgs(
  518. this.proposal.id,
  519. ProposalState.Executed,
  520. GovernorHelper.proposalStatesToBitMap(
  521. [ProposalState.Canceled, ProposalState.Expired, ProposalState.Executed],
  522. { inverted: true },
  523. ),
  524. );
  525. });
  526. });
  527. describe('public', function () {
  528. it('before proposal', async function () {
  529. await expect(this.helper.cancel('external'))
  530. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  531. .withArgs(this.proposal.id);
  532. });
  533. it('after proposal', async function () {
  534. await this.helper.propose();
  535. await this.helper.cancel('external');
  536. });
  537. it('after proposal - restricted to proposer', async function () {
  538. await this.helper.connect(this.proposer).propose();
  539. await expect(this.helper.connect(this.owner).cancel('external'))
  540. .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel')
  541. .withArgs(this.proposal.id, this.owner);
  542. });
  543. it('after vote started', async function () {
  544. await this.helper.propose();
  545. await this.helper.waitForSnapshot(1n); // snapshot + 1 block
  546. await expect(this.helper.cancel('external'))
  547. .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel')
  548. .withArgs(this.proposal.id, this.owner);
  549. });
  550. it('after vote', async function () {
  551. await this.helper.propose();
  552. await this.helper.waitForSnapshot();
  553. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  554. await expect(this.helper.cancel('external'))
  555. .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel')
  556. .withArgs(this.proposal.id, this.voter1);
  557. });
  558. it('after deadline', async function () {
  559. await this.helper.propose();
  560. await this.helper.waitForSnapshot();
  561. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  562. await this.helper.waitForDeadline();
  563. await expect(this.helper.cancel('external'))
  564. .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel')
  565. .withArgs(this.proposal.id, this.voter1);
  566. });
  567. it('after execution', async function () {
  568. await this.helper.propose();
  569. await this.helper.waitForSnapshot();
  570. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  571. await this.helper.waitForDeadline();
  572. await this.helper.execute();
  573. await expect(this.helper.cancel('external'))
  574. .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel')
  575. .withArgs(this.proposal.id, this.voter1);
  576. });
  577. });
  578. });
  579. describe('proposal length', function () {
  580. it('empty', async function () {
  581. this.helper.setProposal([], '<proposal description>');
  582. await expect(this.helper.propose())
  583. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength')
  584. .withArgs(0n, 0n, 0n);
  585. });
  586. it('mismatch #1', async function () {
  587. this.helper.setProposal(
  588. {
  589. targets: [],
  590. values: [0n],
  591. data: [this.receiver.interface.encodeFunctionData('mockFunction')],
  592. },
  593. '<proposal description>',
  594. );
  595. await expect(this.helper.propose())
  596. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength')
  597. .withArgs(0n, 1n, 1n);
  598. });
  599. it('mismatch #2', async function () {
  600. this.helper.setProposal(
  601. {
  602. targets: [this.receiver.target],
  603. values: [],
  604. data: [this.receiver.interface.encodeFunctionData('mockFunction')],
  605. },
  606. '<proposal description>',
  607. );
  608. await expect(this.helper.propose())
  609. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength')
  610. .withArgs(1n, 1n, 0n);
  611. });
  612. it('mismatch #3', async function () {
  613. this.helper.setProposal(
  614. {
  615. targets: [this.receiver.target],
  616. values: [0n],
  617. data: [],
  618. },
  619. '<proposal description>',
  620. );
  621. await expect(this.helper.propose())
  622. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength')
  623. .withArgs(1n, 0n, 1n);
  624. });
  625. });
  626. describe('frontrun protection using description suffix', function () {
  627. function shouldPropose() {
  628. it('proposer can propose', async function () {
  629. const txPropose = await this.helper.connect(this.proposer).propose();
  630. await expect(txPropose)
  631. .to.emit(this.mock, 'ProposalCreated')
  632. .withArgs(
  633. this.proposal.id,
  634. this.proposer,
  635. this.proposal.targets,
  636. this.proposal.values,
  637. this.proposal.signatures,
  638. this.proposal.data,
  639. (await time.clockFromReceipt[mode](txPropose)) + votingDelay,
  640. (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod,
  641. this.proposal.description,
  642. );
  643. });
  644. it('someone else can propose', async function () {
  645. const txPropose = await this.helper.connect(this.voter1).propose();
  646. await expect(txPropose)
  647. .to.emit(this.mock, 'ProposalCreated')
  648. .withArgs(
  649. this.proposal.id,
  650. this.voter1,
  651. this.proposal.targets,
  652. this.proposal.values,
  653. this.proposal.signatures,
  654. this.proposal.data,
  655. (await time.clockFromReceipt[mode](txPropose)) + votingDelay,
  656. (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod,
  657. this.proposal.description,
  658. );
  659. });
  660. }
  661. describe('without protection', function () {
  662. describe('without suffix', function () {
  663. shouldPropose();
  664. });
  665. describe('with different suffix', function () {
  666. beforeEach(function () {
  667. this.proposal = this.helper.setProposal(
  668. [
  669. {
  670. target: this.receiver.target,
  671. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  672. value,
  673. },
  674. ],
  675. `<proposal description>#wrong-suffix=${this.proposer}`,
  676. );
  677. });
  678. shouldPropose();
  679. });
  680. describe('with proposer suffix but bad address part', function () {
  681. beforeEach(function () {
  682. this.proposal = this.helper.setProposal(
  683. [
  684. {
  685. target: this.receiver.target,
  686. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  687. value,
  688. },
  689. ],
  690. `<proposal description>#proposer=0x3C44CdDdB6a900fa2b585dd299e03d12FA429XYZ`, // XYZ are not a valid hex char
  691. );
  692. });
  693. shouldPropose();
  694. });
  695. });
  696. describe('with protection via proposer suffix', function () {
  697. beforeEach(function () {
  698. this.proposal = this.helper.setProposal(
  699. [
  700. {
  701. target: this.receiver.target,
  702. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  703. value,
  704. },
  705. ],
  706. `<proposal description>#proposer=${this.proposer}`,
  707. );
  708. });
  709. shouldPropose();
  710. });
  711. });
  712. describe('onlyGovernance updates', function () {
  713. it('setVotingDelay is protected', async function () {
  714. await expect(this.mock.connect(this.owner).setVotingDelay(0n))
  715. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
  716. .withArgs(this.owner);
  717. });
  718. it('setVotingPeriod is protected', async function () {
  719. await expect(this.mock.connect(this.owner).setVotingPeriod(32n))
  720. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
  721. .withArgs(this.owner);
  722. });
  723. it('setProposalThreshold is protected', async function () {
  724. await expect(this.mock.connect(this.owner).setProposalThreshold(1_000_000_000_000_000_000n))
  725. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
  726. .withArgs(this.owner);
  727. });
  728. it('can setVotingDelay through governance', async function () {
  729. this.helper.setProposal(
  730. [
  731. {
  732. target: this.mock.target,
  733. data: this.mock.interface.encodeFunctionData('setVotingDelay', [0n]),
  734. },
  735. ],
  736. '<proposal description>',
  737. );
  738. await this.helper.propose();
  739. await this.helper.waitForSnapshot();
  740. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  741. await this.helper.waitForDeadline();
  742. await expect(this.helper.execute()).to.emit(this.mock, 'VotingDelaySet').withArgs(4n, 0n);
  743. expect(await this.mock.votingDelay()).to.equal(0n);
  744. });
  745. it('can setVotingPeriod through governance', async function () {
  746. this.helper.setProposal(
  747. [
  748. {
  749. target: this.mock.target,
  750. data: this.mock.interface.encodeFunctionData('setVotingPeriod', [32n]),
  751. },
  752. ],
  753. '<proposal description>',
  754. );
  755. await this.helper.propose();
  756. await this.helper.waitForSnapshot();
  757. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  758. await this.helper.waitForDeadline();
  759. await expect(this.helper.execute()).to.emit(this.mock, 'VotingPeriodSet').withArgs(16n, 32n);
  760. expect(await this.mock.votingPeriod()).to.equal(32n);
  761. });
  762. it('cannot setVotingPeriod to 0 through governance', async function () {
  763. const votingPeriod = 0n;
  764. this.helper.setProposal(
  765. [
  766. {
  767. target: this.mock.target,
  768. data: this.mock.interface.encodeFunctionData('setVotingPeriod', [votingPeriod]),
  769. },
  770. ],
  771. '<proposal description>',
  772. );
  773. await this.helper.propose();
  774. await this.helper.waitForSnapshot();
  775. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  776. await this.helper.waitForDeadline();
  777. await expect(this.helper.execute())
  778. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVotingPeriod')
  779. .withArgs(votingPeriod);
  780. });
  781. it('can setProposalThreshold to 0 through governance', async function () {
  782. this.helper.setProposal(
  783. [
  784. {
  785. target: this.mock.target,
  786. data: this.mock.interface.encodeFunctionData('setProposalThreshold', [1_000_000_000_000_000_000n]),
  787. },
  788. ],
  789. '<proposal description>',
  790. );
  791. await this.helper.propose();
  792. await this.helper.waitForSnapshot();
  793. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  794. await this.helper.waitForDeadline();
  795. await expect(this.helper.execute())
  796. .to.emit(this.mock, 'ProposalThresholdSet')
  797. .withArgs(0n, 1_000_000_000_000_000_000n);
  798. expect(await this.mock.proposalThreshold()).to.equal(1_000_000_000_000_000_000n);
  799. });
  800. });
  801. describe('safe receive', function () {
  802. describe('ERC721', function () {
  803. const tokenId = 1n;
  804. beforeEach(async function () {
  805. this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']);
  806. await this.token.$_mint(this.owner, tokenId);
  807. });
  808. it('can receive an ERC721 safeTransfer', async function () {
  809. await this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId);
  810. });
  811. });
  812. describe('ERC1155', function () {
  813. const tokenIds = {
  814. 1: 1000n,
  815. 2: 2000n,
  816. 3: 3000n,
  817. };
  818. beforeEach(async function () {
  819. this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']);
  820. await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  821. });
  822. it('can receive ERC1155 safeTransfer', async function () {
  823. await this.token.connect(this.owner).safeTransferFrom(
  824. this.owner,
  825. this.mock,
  826. ...Object.entries(tokenIds)[0], // id + amount
  827. '0x',
  828. );
  829. });
  830. it('can receive ERC1155 safeBatchTransfer', async function () {
  831. await this.token
  832. .connect(this.owner)
  833. .safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  834. });
  835. });
  836. });
  837. });
  838. }
  839. });