Governor.test.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992
  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']);
  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', async function () {
  170. await this.token.connect(this.voter1).delegate(this.userEOA);
  171. const nonce = await this.mock.nonces(this.userEOA);
  172. // Run proposal
  173. await this.helper.propose();
  174. await this.helper.waitForSnapshot();
  175. await expect(
  176. this.helper.vote({
  177. support: VoteType.For,
  178. voter: this.userEOA.address,
  179. nonce,
  180. signature: signBallot(this.userEOA),
  181. }),
  182. )
  183. .to.emit(this.mock, 'VoteCast')
  184. .withArgs(this.userEOA, this.proposal.id, VoteType.For, ethers.parseEther('10'), '');
  185. await this.helper.waitForDeadline();
  186. await this.helper.execute();
  187. // After
  188. expect(await this.mock.hasVoted(this.proposal.id, this.userEOA)).to.be.true;
  189. expect(await this.mock.nonces(this.userEOA)).to.equal(nonce + 1n);
  190. });
  191. it('votes with a valid EIP-1271 signature', async function () {
  192. const wallet = await ethers.deployContract('ERC1271WalletMock', [this.userEOA]);
  193. await this.token.connect(this.voter1).delegate(wallet);
  194. const nonce = await this.mock.nonces(this.userEOA);
  195. // Run proposal
  196. await this.helper.propose();
  197. await this.helper.waitForSnapshot();
  198. await expect(
  199. this.helper.vote({
  200. support: VoteType.For,
  201. voter: wallet.target,
  202. nonce,
  203. signature: signBallot(this.userEOA),
  204. }),
  205. )
  206. .to.emit(this.mock, 'VoteCast')
  207. .withArgs(wallet, this.proposal.id, VoteType.For, ethers.parseEther('10'), '');
  208. await this.helper.waitForDeadline();
  209. await this.helper.execute();
  210. // After
  211. expect(await this.mock.hasVoted(this.proposal.id, wallet)).to.be.true;
  212. expect(await this.mock.nonces(wallet)).to.equal(nonce + 1n);
  213. });
  214. afterEach('no other votes are cast', async function () {
  215. expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
  216. expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.false;
  217. expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.false;
  218. });
  219. });
  220. describe('should revert', function () {
  221. describe('on propose', function () {
  222. it('if proposal already exists', async function () {
  223. await this.helper.propose();
  224. await expect(this.helper.propose())
  225. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  226. .withArgs(this.proposal.id, ProposalState.Pending, ethers.ZeroHash);
  227. });
  228. it('if proposer has below threshold votes', async function () {
  229. const votes = ethers.parseEther('10');
  230. const threshold = ethers.parseEther('1000');
  231. await this.mock.$_setProposalThreshold(threshold);
  232. await expect(this.helper.connect(this.voter1).propose())
  233. .to.be.revertedWithCustomError(this.mock, 'GovernorInsufficientProposerVotes')
  234. .withArgs(this.voter1, votes, threshold);
  235. });
  236. });
  237. describe('on vote', function () {
  238. it('if proposal does not exist', async function () {
  239. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  240. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  241. .withArgs(this.proposal.id);
  242. });
  243. it('if voting has not started', async function () {
  244. await this.helper.propose();
  245. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  246. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  247. .withArgs(
  248. this.proposal.id,
  249. ProposalState.Pending,
  250. GovernorHelper.proposalStatesToBitMap([ProposalState.Active]),
  251. );
  252. });
  253. it('if support value is invalid', async function () {
  254. await this.helper.propose();
  255. await this.helper.waitForSnapshot();
  256. await expect(this.helper.vote({ support: 255 })).to.be.revertedWithCustomError(
  257. this.mock,
  258. 'GovernorInvalidVoteType',
  259. );
  260. });
  261. it('if vote was already casted', async function () {
  262. await this.helper.propose();
  263. await this.helper.waitForSnapshot();
  264. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  265. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  266. .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyCastVote')
  267. .withArgs(this.voter1);
  268. });
  269. it('if voting is over', async function () {
  270. await this.helper.propose();
  271. await this.helper.waitForDeadline();
  272. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  273. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  274. .withArgs(
  275. this.proposal.id,
  276. ProposalState.Defeated,
  277. GovernorHelper.proposalStatesToBitMap([ProposalState.Active]),
  278. );
  279. });
  280. });
  281. describe('on vote by signature', function () {
  282. beforeEach(async function () {
  283. await this.token.connect(this.voter1).delegate(this.userEOA);
  284. // Run proposal
  285. await this.helper.propose();
  286. await this.helper.waitForSnapshot();
  287. });
  288. it('if signature does not match signer', async function () {
  289. const nonce = await this.mock.nonces(this.userEOA);
  290. function tamper(str, index, mask) {
  291. const arrayStr = ethers.getBytes(str);
  292. arrayStr[index] ^= mask;
  293. return ethers.hexlify(arrayStr);
  294. }
  295. const voteParams = {
  296. support: VoteType.For,
  297. voter: this.userEOA.address,
  298. nonce,
  299. signature: (...args) => signBallot(this.userEOA)(...args).then(sig => tamper(sig, 42, 0xff)),
  300. };
  301. await expect(this.helper.vote(voteParams))
  302. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature')
  303. .withArgs(voteParams.voter);
  304. });
  305. it('if vote nonce is incorrect', async function () {
  306. const nonce = await this.mock.nonces(this.userEOA);
  307. const voteParams = {
  308. support: VoteType.For,
  309. voter: this.userEOA.address,
  310. nonce: nonce + 1n,
  311. signature: signBallot(this.userEOA),
  312. };
  313. await expect(this.helper.vote(voteParams))
  314. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature')
  315. .withArgs(voteParams.voter);
  316. });
  317. });
  318. describe('on queue', function () {
  319. it('always', async function () {
  320. await this.helper.connect(this.proposer).propose();
  321. await this.helper.waitForSnapshot();
  322. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  323. await this.helper.waitForDeadline();
  324. await expect(this.helper.queue()).to.be.revertedWithCustomError(this.mock, 'GovernorQueueNotImplemented');
  325. });
  326. });
  327. describe('on execute', function () {
  328. it('if proposal does not exist', async function () {
  329. await expect(this.helper.execute())
  330. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  331. .withArgs(this.proposal.id);
  332. });
  333. it('if quorum is not reached', async function () {
  334. await this.helper.propose();
  335. await this.helper.waitForSnapshot();
  336. await this.helper.connect(this.voter3).vote({ support: VoteType.For });
  337. await expect(this.helper.execute())
  338. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  339. .withArgs(
  340. this.proposal.id,
  341. ProposalState.Active,
  342. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  343. );
  344. });
  345. it('if score not reached', async function () {
  346. await this.helper.propose();
  347. await this.helper.waitForSnapshot();
  348. await this.helper.connect(this.voter1).vote({ support: VoteType.Against });
  349. await expect(this.helper.execute())
  350. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  351. .withArgs(
  352. this.proposal.id,
  353. ProposalState.Active,
  354. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  355. );
  356. });
  357. it('if voting is not over', async function () {
  358. await this.helper.propose();
  359. await this.helper.waitForSnapshot();
  360. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  361. await expect(this.helper.execute())
  362. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  363. .withArgs(
  364. this.proposal.id,
  365. ProposalState.Active,
  366. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  367. );
  368. });
  369. it('if receiver revert without reason', async function () {
  370. this.helper.setProposal(
  371. [
  372. {
  373. target: this.receiver.target,
  374. data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
  375. },
  376. ],
  377. '<proposal description>',
  378. );
  379. await this.helper.propose();
  380. await this.helper.waitForSnapshot();
  381. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  382. await this.helper.waitForDeadline();
  383. await expect(this.helper.execute()).to.be.revertedWithCustomError(this.mock, 'FailedCall');
  384. });
  385. it('if receiver revert with reason', async function () {
  386. this.helper.setProposal(
  387. [
  388. {
  389. target: this.receiver.target,
  390. data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsReason'),
  391. },
  392. ],
  393. '<proposal description>',
  394. );
  395. await this.helper.propose();
  396. await this.helper.waitForSnapshot();
  397. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  398. await this.helper.waitForDeadline();
  399. await expect(this.helper.execute()).to.be.revertedWith('CallReceiverMock: reverting');
  400. });
  401. it('if proposal was already executed', async function () {
  402. await this.helper.propose();
  403. await this.helper.waitForSnapshot();
  404. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  405. await this.helper.waitForDeadline();
  406. await this.helper.execute();
  407. await expect(this.helper.execute())
  408. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  409. .withArgs(
  410. this.proposal.id,
  411. ProposalState.Executed,
  412. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  413. );
  414. });
  415. });
  416. });
  417. describe('state', function () {
  418. it('Unset', async function () {
  419. await expect(this.mock.state(this.proposal.id))
  420. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  421. .withArgs(this.proposal.id);
  422. });
  423. it('Pending & Active', async function () {
  424. await this.helper.propose();
  425. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Pending);
  426. await this.helper.waitForSnapshot();
  427. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Pending);
  428. await this.helper.waitForSnapshot(1n);
  429. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Active);
  430. });
  431. it('Defeated', async function () {
  432. await this.helper.propose();
  433. await this.helper.waitForDeadline();
  434. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Active);
  435. await this.helper.waitForDeadline(1n);
  436. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Defeated);
  437. });
  438. it('Succeeded', async function () {
  439. await this.helper.propose();
  440. await this.helper.waitForSnapshot();
  441. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  442. await this.helper.waitForDeadline();
  443. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Active);
  444. await this.helper.waitForDeadline(1n);
  445. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Succeeded);
  446. });
  447. it('Executed', async function () {
  448. await this.helper.propose();
  449. await this.helper.waitForSnapshot();
  450. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  451. await this.helper.waitForDeadline();
  452. await this.helper.execute();
  453. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Executed);
  454. });
  455. });
  456. describe('cancel', function () {
  457. describe('internal', function () {
  458. it('before proposal', async function () {
  459. await expect(this.helper.cancel('internal'))
  460. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  461. .withArgs(this.proposal.id);
  462. });
  463. it('after proposal', async function () {
  464. await this.helper.propose();
  465. await this.helper.cancel('internal');
  466. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
  467. await this.helper.waitForSnapshot();
  468. await expect(this.helper.connect(this.voter1).vote({ support: VoteType.For }))
  469. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  470. .withArgs(
  471. this.proposal.id,
  472. ProposalState.Canceled,
  473. GovernorHelper.proposalStatesToBitMap([ProposalState.Active]),
  474. );
  475. });
  476. it('after vote', async function () {
  477. await this.helper.propose();
  478. await this.helper.waitForSnapshot();
  479. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  480. await this.helper.cancel('internal');
  481. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
  482. await this.helper.waitForDeadline();
  483. await expect(this.helper.execute())
  484. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  485. .withArgs(
  486. this.proposal.id,
  487. ProposalState.Canceled,
  488. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  489. );
  490. });
  491. it('after deadline', async function () {
  492. await this.helper.propose();
  493. await this.helper.waitForSnapshot();
  494. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  495. await this.helper.waitForDeadline();
  496. await this.helper.cancel('internal');
  497. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
  498. await expect(this.helper.execute())
  499. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  500. .withArgs(
  501. this.proposal.id,
  502. ProposalState.Canceled,
  503. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  504. );
  505. });
  506. it('after execution', async function () {
  507. await this.helper.propose();
  508. await this.helper.waitForSnapshot();
  509. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  510. await this.helper.waitForDeadline();
  511. await this.helper.execute();
  512. await expect(this.helper.cancel('internal'))
  513. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  514. .withArgs(
  515. this.proposal.id,
  516. ProposalState.Executed,
  517. GovernorHelper.proposalStatesToBitMap(
  518. [ProposalState.Canceled, ProposalState.Expired, ProposalState.Executed],
  519. { inverted: true },
  520. ),
  521. );
  522. });
  523. });
  524. describe('public', function () {
  525. it('before proposal', async function () {
  526. await expect(this.helper.cancel('external'))
  527. .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal')
  528. .withArgs(this.proposal.id);
  529. });
  530. it('after proposal', async function () {
  531. await this.helper.propose();
  532. await this.helper.cancel('external');
  533. });
  534. it('after proposal - restricted to proposer', async function () {
  535. await this.helper.connect(this.proposer).propose();
  536. await expect(this.helper.connect(this.owner).cancel('external'))
  537. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyProposer')
  538. .withArgs(this.owner);
  539. });
  540. it('after vote started', async function () {
  541. await this.helper.propose();
  542. await this.helper.waitForSnapshot(1n); // snapshot + 1 block
  543. await expect(this.helper.cancel('external'))
  544. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  545. .withArgs(
  546. this.proposal.id,
  547. ProposalState.Active,
  548. GovernorHelper.proposalStatesToBitMap([ProposalState.Pending]),
  549. );
  550. });
  551. it('after vote', async function () {
  552. await this.helper.propose();
  553. await this.helper.waitForSnapshot();
  554. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  555. await expect(this.helper.cancel('external'))
  556. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  557. .withArgs(
  558. this.proposal.id,
  559. ProposalState.Active,
  560. GovernorHelper.proposalStatesToBitMap([ProposalState.Pending]),
  561. );
  562. });
  563. it('after deadline', async function () {
  564. await this.helper.propose();
  565. await this.helper.waitForSnapshot();
  566. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  567. await this.helper.waitForDeadline();
  568. await expect(this.helper.cancel('external'))
  569. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  570. .withArgs(
  571. this.proposal.id,
  572. ProposalState.Succeeded,
  573. GovernorHelper.proposalStatesToBitMap([ProposalState.Pending]),
  574. );
  575. });
  576. it('after execution', async function () {
  577. await this.helper.propose();
  578. await this.helper.waitForSnapshot();
  579. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  580. await this.helper.waitForDeadline();
  581. await this.helper.execute();
  582. await expect(this.helper.cancel('external'))
  583. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  584. .withArgs(
  585. this.proposal.id,
  586. ProposalState.Executed,
  587. GovernorHelper.proposalStatesToBitMap([ProposalState.Pending]),
  588. );
  589. });
  590. });
  591. });
  592. describe('proposal length', function () {
  593. it('empty', async function () {
  594. this.helper.setProposal([], '<proposal description>');
  595. await expect(this.helper.propose())
  596. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength')
  597. .withArgs(0, 0, 0);
  598. });
  599. it('mismatch #1', async function () {
  600. this.helper.setProposal(
  601. {
  602. targets: [],
  603. values: [0n],
  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(0, 1, 1);
  611. });
  612. it('mismatch #2', async function () {
  613. this.helper.setProposal(
  614. {
  615. targets: [this.receiver.target],
  616. values: [],
  617. data: [this.receiver.interface.encodeFunctionData('mockFunction')],
  618. },
  619. '<proposal description>',
  620. );
  621. await expect(this.helper.propose())
  622. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength')
  623. .withArgs(1, 1, 0);
  624. });
  625. it('mismatch #3', async function () {
  626. this.helper.setProposal(
  627. {
  628. targets: [this.receiver.target],
  629. values: [0n],
  630. data: [],
  631. },
  632. '<proposal description>',
  633. );
  634. await expect(this.helper.propose())
  635. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength')
  636. .withArgs(1, 0, 1);
  637. });
  638. });
  639. describe('frontrun protection using description suffix', function () {
  640. function shouldPropose() {
  641. it('proposer can propose', async function () {
  642. const txPropose = await this.helper.connect(this.proposer).propose();
  643. await expect(txPropose)
  644. .to.emit(this.mock, 'ProposalCreated')
  645. .withArgs(
  646. this.proposal.id,
  647. this.proposer,
  648. this.proposal.targets,
  649. this.proposal.values,
  650. this.proposal.signatures,
  651. this.proposal.data,
  652. (await time.clockFromReceipt[mode](txPropose)) + votingDelay,
  653. (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod,
  654. this.proposal.description,
  655. );
  656. });
  657. it('someone else can propose', async function () {
  658. const txPropose = await this.helper.connect(this.voter1).propose();
  659. await expect(txPropose)
  660. .to.emit(this.mock, 'ProposalCreated')
  661. .withArgs(
  662. this.proposal.id,
  663. this.voter1,
  664. this.proposal.targets,
  665. this.proposal.values,
  666. this.proposal.signatures,
  667. this.proposal.data,
  668. (await time.clockFromReceipt[mode](txPropose)) + votingDelay,
  669. (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod,
  670. this.proposal.description,
  671. );
  672. });
  673. }
  674. describe('without protection', function () {
  675. describe('without suffix', function () {
  676. shouldPropose();
  677. });
  678. describe('with different suffix', function () {
  679. beforeEach(function () {
  680. this.proposal = this.helper.setProposal(
  681. [
  682. {
  683. target: this.receiver.target,
  684. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  685. value,
  686. },
  687. ],
  688. `<proposal description>#wrong-suffix=${this.proposer}`,
  689. );
  690. });
  691. shouldPropose();
  692. });
  693. describe('with proposer suffix but bad address part', function () {
  694. beforeEach(function () {
  695. this.proposal = this.helper.setProposal(
  696. [
  697. {
  698. target: this.receiver.target,
  699. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  700. value,
  701. },
  702. ],
  703. `<proposal description>#proposer=0x3C44CdDdB6a900fa2b585dd299e03d12FA429XYZ`, // XYZ are not a valid hex char
  704. );
  705. });
  706. shouldPropose();
  707. });
  708. });
  709. describe('with protection via proposer suffix', function () {
  710. beforeEach(function () {
  711. this.proposal = this.helper.setProposal(
  712. [
  713. {
  714. target: this.receiver.target,
  715. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  716. value,
  717. },
  718. ],
  719. `<proposal description>#proposer=${this.proposer}`,
  720. );
  721. });
  722. shouldPropose();
  723. });
  724. });
  725. describe('onlyGovernance updates', function () {
  726. it('setVotingDelay is protected', async function () {
  727. await expect(this.mock.connect(this.owner).setVotingDelay(0n))
  728. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
  729. .withArgs(this.owner);
  730. });
  731. it('setVotingPeriod is protected', async function () {
  732. await expect(this.mock.connect(this.owner).setVotingPeriod(32n))
  733. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
  734. .withArgs(this.owner);
  735. });
  736. it('setProposalThreshold is protected', async function () {
  737. await expect(this.mock.connect(this.owner).setProposalThreshold(1_000_000_000_000_000_000n))
  738. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
  739. .withArgs(this.owner);
  740. });
  741. it('can setVotingDelay through governance', async function () {
  742. this.helper.setProposal(
  743. [
  744. {
  745. target: this.mock.target,
  746. data: this.mock.interface.encodeFunctionData('setVotingDelay', [0n]),
  747. },
  748. ],
  749. '<proposal description>',
  750. );
  751. await this.helper.propose();
  752. await this.helper.waitForSnapshot();
  753. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  754. await this.helper.waitForDeadline();
  755. await expect(this.helper.execute()).to.emit(this.mock, 'VotingDelaySet').withArgs(4n, 0n);
  756. expect(await this.mock.votingDelay()).to.equal(0n);
  757. });
  758. it('can setVotingPeriod through governance', async function () {
  759. this.helper.setProposal(
  760. [
  761. {
  762. target: this.mock.target,
  763. data: this.mock.interface.encodeFunctionData('setVotingPeriod', [32n]),
  764. },
  765. ],
  766. '<proposal description>',
  767. );
  768. await this.helper.propose();
  769. await this.helper.waitForSnapshot();
  770. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  771. await this.helper.waitForDeadline();
  772. await expect(this.helper.execute()).to.emit(this.mock, 'VotingPeriodSet').withArgs(16n, 32n);
  773. expect(await this.mock.votingPeriod()).to.equal(32n);
  774. });
  775. it('cannot setVotingPeriod to 0 through governance', async function () {
  776. const votingPeriod = 0n;
  777. this.helper.setProposal(
  778. [
  779. {
  780. target: this.mock.target,
  781. data: this.mock.interface.encodeFunctionData('setVotingPeriod', [votingPeriod]),
  782. },
  783. ],
  784. '<proposal description>',
  785. );
  786. await this.helper.propose();
  787. await this.helper.waitForSnapshot();
  788. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  789. await this.helper.waitForDeadline();
  790. await expect(this.helper.execute())
  791. .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVotingPeriod')
  792. .withArgs(votingPeriod);
  793. });
  794. it('can setProposalThreshold to 0 through governance', async function () {
  795. this.helper.setProposal(
  796. [
  797. {
  798. target: this.mock.target,
  799. data: this.mock.interface.encodeFunctionData('setProposalThreshold', [1_000_000_000_000_000_000n]),
  800. },
  801. ],
  802. '<proposal description>',
  803. );
  804. await this.helper.propose();
  805. await this.helper.waitForSnapshot();
  806. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  807. await this.helper.waitForDeadline();
  808. await expect(this.helper.execute())
  809. .to.emit(this.mock, 'ProposalThresholdSet')
  810. .withArgs(0n, 1_000_000_000_000_000_000n);
  811. expect(await this.mock.proposalThreshold()).to.equal(1_000_000_000_000_000_000n);
  812. });
  813. });
  814. describe('safe receive', function () {
  815. describe('ERC721', function () {
  816. const tokenId = 1n;
  817. beforeEach(async function () {
  818. this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']);
  819. await this.token.$_mint(this.owner, tokenId);
  820. });
  821. it('can receive an ERC721 safeTransfer', async function () {
  822. await this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId);
  823. });
  824. });
  825. describe('ERC1155', function () {
  826. const tokenIds = {
  827. 1: 1000n,
  828. 2: 2000n,
  829. 3: 3000n,
  830. };
  831. beforeEach(async function () {
  832. this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']);
  833. await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  834. });
  835. it('can receive ERC1155 safeTransfer', async function () {
  836. await this.token.connect(this.owner).safeTransferFrom(
  837. this.owner,
  838. this.mock,
  839. ...Object.entries(tokenIds)[0], // id + amount
  840. '0x',
  841. );
  842. });
  843. it('can receive ERC1155 safeBatchTransfer', async function () {
  844. await this.token
  845. .connect(this.owner)
  846. .safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  847. });
  848. });
  849. });
  850. });
  851. }
  852. });