GovernorTimelockCompound.test.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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 { ProposalState, VoteType } = require('../../helpers/enums');
  7. const time = require('../../helpers/time');
  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. const defaultDelay = time.duration.days(2n);
  21. describe('GovernorTimelockCompound', function () {
  22. for (const { Token, mode } of TOKENS) {
  23. const fixture = async () => {
  24. const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
  25. const receiver = await ethers.deployContract('CallReceiverMock');
  26. const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
  27. const predictGovernor = await deployer
  28. .getNonce()
  29. .then(nonce => ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 }));
  30. const timelock = await ethers.deployContract('CompTimelock', [predictGovernor, defaultDelay]);
  31. const mock = await ethers.deployContract('$GovernorTimelockCompoundMock', [
  32. name,
  33. votingDelay,
  34. votingPeriod,
  35. 0n,
  36. timelock,
  37. token,
  38. 0n,
  39. ]);
  40. await owner.sendTransaction({ to: timelock, value });
  41. await token.$_mint(owner, tokenSupply);
  42. const helper = new GovernorHelper(mock, mode);
  43. await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') });
  44. await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
  45. await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
  46. await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
  47. return { deployer, owner, voter1, voter2, voter3, voter4, other, receiver, token, mock, timelock, helper };
  48. };
  49. describe(`using ${Token}`, function () {
  50. beforeEach(async function () {
  51. Object.assign(this, await loadFixture(fixture));
  52. // default proposal
  53. this.proposal = this.helper.setProposal(
  54. [
  55. {
  56. target: this.receiver.target,
  57. value,
  58. data: this.receiver.interface.encodeFunctionData('mockFunction'),
  59. },
  60. ],
  61. '<proposal description>',
  62. );
  63. });
  64. it("doesn't accept ether transfers", async function () {
  65. await expect(this.owner.sendTransaction({ to: this.mock, value: 1n })).to.be.revertedWithCustomError(
  66. this.mock,
  67. 'GovernorDisabledDeposit',
  68. );
  69. });
  70. it('post deployment check', async function () {
  71. expect(await this.mock.name()).to.equal(name);
  72. expect(await this.mock.token()).to.equal(this.token);
  73. expect(await this.mock.votingDelay()).to.equal(votingDelay);
  74. expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
  75. expect(await this.mock.quorum(0n)).to.equal(0n);
  76. expect(await this.mock.timelock()).to.equal(this.timelock);
  77. expect(await this.timelock.admin()).to.equal(this.mock);
  78. });
  79. it('nominal', async function () {
  80. expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n);
  81. expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true;
  82. await this.helper.propose();
  83. await this.helper.waitForSnapshot();
  84. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  85. await this.helper.connect(this.voter2).vote({ support: VoteType.For });
  86. await this.helper.connect(this.voter3).vote({ support: VoteType.Against });
  87. await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain });
  88. await this.helper.waitForDeadline();
  89. const txQueue = await this.helper.queue();
  90. const eta = (await time.clockFromReceipt.timestamp(txQueue)) + defaultDelay;
  91. expect(await this.mock.proposalEta(this.proposal.id)).to.equal(eta);
  92. expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true;
  93. await this.helper.waitForEta();
  94. const txExecute = await this.helper.execute();
  95. await expect(txQueue)
  96. .to.emit(this.mock, 'ProposalQueued')
  97. .withArgs(this.proposal.id, eta)
  98. .to.emit(this.timelock, 'QueueTransaction')
  99. .withArgs(...Array(5).fill(anyValue), eta);
  100. await expect(txExecute)
  101. .to.emit(this.mock, 'ProposalExecuted')
  102. .withArgs(this.proposal.id)
  103. .to.emit(this.timelock, 'ExecuteTransaction')
  104. .withArgs(...Array(5).fill(anyValue), eta)
  105. .to.emit(this.receiver, 'MockFunctionCalled');
  106. });
  107. describe('should revert', function () {
  108. describe('on queue', function () {
  109. it('if already queued', async function () {
  110. await this.helper.propose();
  111. await this.helper.waitForSnapshot();
  112. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  113. await this.helper.waitForDeadline();
  114. await this.helper.queue();
  115. await expect(this.helper.queue())
  116. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  117. .withArgs(
  118. this.proposal.id,
  119. ProposalState.Queued,
  120. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
  121. );
  122. });
  123. it('if proposal contains duplicate calls', async function () {
  124. const action = {
  125. target: this.token.target,
  126. data: this.token.interface.encodeFunctionData('approve', [this.receiver.target, ethers.MaxUint256]),
  127. };
  128. const { id } = this.helper.setProposal([action, action], '<proposal description>');
  129. await this.helper.propose();
  130. await this.helper.waitForSnapshot();
  131. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  132. await this.helper.waitForDeadline();
  133. await expect(this.helper.queue())
  134. .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyQueuedProposal')
  135. .withArgs(id);
  136. await expect(this.helper.execute())
  137. .to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal')
  138. .withArgs(id);
  139. });
  140. });
  141. describe('on execute', function () {
  142. it('if not queued', async function () {
  143. await this.helper.propose();
  144. await this.helper.waitForSnapshot();
  145. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  146. await this.helper.waitForDeadline(1n);
  147. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Succeeded);
  148. await expect(this.helper.execute())
  149. .to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal')
  150. .withArgs(this.proposal.id);
  151. });
  152. it('if too early', async function () {
  153. await this.helper.propose();
  154. await this.helper.waitForSnapshot();
  155. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  156. await this.helper.waitForDeadline();
  157. await this.helper.queue();
  158. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Queued);
  159. await expect(this.helper.execute()).to.be.rejectedWith(
  160. "Timelock::executeTransaction: Transaction hasn't surpassed time lock",
  161. );
  162. });
  163. it('if too late', async function () {
  164. await this.helper.propose();
  165. await this.helper.waitForSnapshot();
  166. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  167. await this.helper.waitForDeadline();
  168. await this.helper.queue();
  169. await this.helper.waitForEta(time.duration.days(30));
  170. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Expired);
  171. await expect(this.helper.execute())
  172. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  173. .withArgs(
  174. this.proposal.id,
  175. ProposalState.Expired,
  176. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  177. );
  178. });
  179. it('if already executed', async function () {
  180. await this.helper.propose();
  181. await this.helper.waitForSnapshot();
  182. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  183. await this.helper.waitForDeadline();
  184. await this.helper.queue();
  185. await this.helper.waitForEta();
  186. await this.helper.execute();
  187. await expect(this.helper.execute())
  188. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  189. .withArgs(
  190. this.proposal.id,
  191. ProposalState.Executed,
  192. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  193. );
  194. });
  195. });
  196. describe('on safe receive', function () {
  197. describe('ERC721', function () {
  198. const tokenId = 1n;
  199. beforeEach(async function () {
  200. this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']);
  201. await this.token.$_mint(this.owner, tokenId);
  202. });
  203. it("can't receive an ERC721 safeTransfer", async function () {
  204. await expect(
  205. this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId),
  206. ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
  207. });
  208. });
  209. describe('ERC1155', function () {
  210. const tokenIds = {
  211. 1: 1000n,
  212. 2: 2000n,
  213. 3: 3000n,
  214. };
  215. beforeEach(async function () {
  216. this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']);
  217. await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  218. });
  219. it("can't receive ERC1155 safeTransfer", async function () {
  220. await expect(
  221. this.token.connect(this.owner).safeTransferFrom(
  222. this.owner,
  223. this.mock,
  224. ...Object.entries(tokenIds)[0], // id + amount
  225. '0x',
  226. ),
  227. ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
  228. });
  229. it("can't receive ERC1155 safeBatchTransfer", async function () {
  230. await expect(
  231. this.token
  232. .connect(this.owner)
  233. .safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'),
  234. ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit');
  235. });
  236. });
  237. });
  238. });
  239. describe('cancel', function () {
  240. it('cancel before queue prevents scheduling', async function () {
  241. await this.helper.propose();
  242. await this.helper.waitForSnapshot();
  243. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  244. await this.helper.waitForDeadline();
  245. await expect(this.helper.cancel('internal'))
  246. .to.emit(this.mock, 'ProposalCanceled')
  247. .withArgs(this.proposal.id);
  248. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
  249. await expect(this.helper.queue())
  250. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  251. .withArgs(
  252. this.proposal.id,
  253. ProposalState.Canceled,
  254. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded]),
  255. );
  256. });
  257. it('cancel after queue prevents executing', async function () {
  258. await this.helper.propose();
  259. await this.helper.waitForSnapshot();
  260. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  261. await this.helper.waitForDeadline();
  262. await this.helper.queue();
  263. await expect(this.helper.cancel('internal'))
  264. .to.emit(this.mock, 'ProposalCanceled')
  265. .withArgs(this.proposal.id);
  266. expect(await this.mock.state(this.proposal.id)).to.equal(ProposalState.Canceled);
  267. await expect(this.helper.execute())
  268. .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
  269. .withArgs(
  270. this.proposal.id,
  271. ProposalState.Canceled,
  272. GovernorHelper.proposalStatesToBitMap([ProposalState.Succeeded, ProposalState.Queued]),
  273. );
  274. });
  275. });
  276. describe('onlyGovernance', function () {
  277. describe('relay', function () {
  278. beforeEach(async function () {
  279. await this.token.$_mint(this.mock, 1);
  280. });
  281. it('is protected', async function () {
  282. await expect(
  283. this.mock
  284. .connect(this.owner)
  285. .relay(this.token, 0, this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n])),
  286. )
  287. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
  288. .withArgs(this.owner);
  289. });
  290. it('can be executed through governance', async function () {
  291. this.helper.setProposal(
  292. [
  293. {
  294. target: this.mock.target,
  295. data: this.mock.interface.encodeFunctionData('relay', [
  296. this.token.target,
  297. 0n,
  298. this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n]),
  299. ]),
  300. },
  301. ],
  302. '<proposal description>',
  303. );
  304. await this.helper.propose();
  305. await this.helper.waitForSnapshot();
  306. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  307. await this.helper.waitForDeadline();
  308. await this.helper.queue();
  309. await this.helper.waitForEta();
  310. const txExecute = this.helper.execute();
  311. await expect(txExecute).to.changeTokenBalances(this.token, [this.mock, this.other], [-1n, 1n]);
  312. await expect(txExecute).to.emit(this.token, 'Transfer').withArgs(this.mock, this.other, 1n);
  313. });
  314. });
  315. describe('updateTimelock', function () {
  316. beforeEach(async function () {
  317. this.newTimelock = await ethers.deployContract('CompTimelock', [this.mock, time.duration.days(7n)]);
  318. });
  319. it('is protected', async function () {
  320. await expect(this.mock.connect(this.owner).updateTimelock(this.newTimelock))
  321. .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor')
  322. .withArgs(this.owner);
  323. });
  324. it('can be executed through governance to', async function () {
  325. this.helper.setProposal(
  326. [
  327. {
  328. target: this.timelock.target,
  329. data: this.timelock.interface.encodeFunctionData('setPendingAdmin', [this.owner.address]),
  330. },
  331. {
  332. target: this.mock.target,
  333. data: this.mock.interface.encodeFunctionData('updateTimelock', [this.newTimelock.target]),
  334. },
  335. ],
  336. '<proposal description>',
  337. );
  338. await this.helper.propose();
  339. await this.helper.waitForSnapshot();
  340. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  341. await this.helper.waitForDeadline();
  342. await this.helper.queue();
  343. await this.helper.waitForEta();
  344. await expect(this.helper.execute())
  345. .to.emit(this.mock, 'TimelockChange')
  346. .withArgs(this.timelock, this.newTimelock);
  347. expect(await this.mock.timelock()).to.equal(this.newTimelock);
  348. });
  349. });
  350. it('can transfer timelock to new governor', async function () {
  351. const newGovernor = await ethers.deployContract('$GovernorTimelockCompoundMock', [
  352. name,
  353. 8n,
  354. 32n,
  355. 0n,
  356. this.timelock,
  357. this.token,
  358. 0n,
  359. ]);
  360. this.helper.setProposal(
  361. [
  362. {
  363. target: this.timelock.target,
  364. data: this.timelock.interface.encodeFunctionData('setPendingAdmin', [newGovernor.target]),
  365. },
  366. ],
  367. '<proposal description>',
  368. );
  369. await this.helper.propose();
  370. await this.helper.waitForSnapshot();
  371. await this.helper.connect(this.voter1).vote({ support: VoteType.For });
  372. await this.helper.waitForDeadline();
  373. await this.helper.queue();
  374. await this.helper.waitForEta();
  375. await expect(this.helper.execute()).to.emit(this.timelock, 'NewPendingAdmin').withArgs(newGovernor);
  376. await newGovernor.__acceptAdmin();
  377. expect(await this.timelock.admin()).to.equal(newGovernor);
  378. });
  379. });
  380. });
  381. }
  382. });