TimelockController.test.js 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
  5. const { GovernorHelper } = require('../helpers/governance');
  6. const { bigint: time } = require('../helpers/time');
  7. const {
  8. bigint: { OperationState },
  9. } = require('../helpers/enums');
  10. const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
  11. const salt = '0x025e7b0be353a74631ad648c667493c0e1cd31caa4cc2d3520fdc171ea0cc726'; // a random value
  12. const MINDELAY = time.duration.days(1);
  13. const DEFAULT_ADMIN_ROLE = ethers.ZeroHash;
  14. const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE');
  15. const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE');
  16. const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE');
  17. const getAddress = obj => obj.address ?? obj.target ?? obj;
  18. function genOperation(target, value, data, predecessor, salt) {
  19. const id = ethers.keccak256(
  20. ethers.AbiCoder.defaultAbiCoder().encode(
  21. ['address', 'uint256', 'bytes', 'uint256', 'bytes32'],
  22. [getAddress(target), value, data, predecessor, salt],
  23. ),
  24. );
  25. return { id, target, value, data, predecessor, salt };
  26. }
  27. function genOperationBatch(targets, values, payloads, predecessor, salt) {
  28. const id = ethers.keccak256(
  29. ethers.AbiCoder.defaultAbiCoder().encode(
  30. ['address[]', 'uint256[]', 'bytes[]', 'uint256', 'bytes32'],
  31. [targets.map(getAddress), values, payloads, predecessor, salt],
  32. ),
  33. );
  34. return { id, targets, values, payloads, predecessor, salt };
  35. }
  36. async function fixture() {
  37. const [admin, proposer, canceller, executor, other] = await ethers.getSigners();
  38. const mock = await ethers.deployContract('TimelockController', [MINDELAY, [proposer], [executor], admin]);
  39. const callreceivermock = await ethers.deployContract('CallReceiverMock');
  40. const implementation2 = await ethers.deployContract('Implementation2');
  41. expect(await mock.hasRole(CANCELLER_ROLE, proposer)).to.be.true;
  42. await mock.connect(admin).revokeRole(CANCELLER_ROLE, proposer);
  43. await mock.connect(admin).grantRole(CANCELLER_ROLE, canceller);
  44. return {
  45. admin,
  46. proposer,
  47. canceller,
  48. executor,
  49. other,
  50. mock,
  51. callreceivermock,
  52. implementation2,
  53. };
  54. }
  55. describe('TimelockController', function () {
  56. beforeEach(async function () {
  57. Object.assign(this, await loadFixture(fixture));
  58. });
  59. shouldSupportInterfaces(['ERC1155Receiver']);
  60. it('initial state', async function () {
  61. expect(await this.mock.getMinDelay()).to.equal(MINDELAY);
  62. expect(await this.mock.DEFAULT_ADMIN_ROLE()).to.equal(DEFAULT_ADMIN_ROLE);
  63. expect(await this.mock.PROPOSER_ROLE()).to.equal(PROPOSER_ROLE);
  64. expect(await this.mock.EXECUTOR_ROLE()).to.equal(EXECUTOR_ROLE);
  65. expect(await this.mock.CANCELLER_ROLE()).to.equal(CANCELLER_ROLE);
  66. expect(
  67. await Promise.all(
  68. [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.proposer)),
  69. ),
  70. ).to.deep.equal([true, false, false]);
  71. expect(
  72. await Promise.all(
  73. [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.canceller)),
  74. ),
  75. ).to.deep.equal([false, true, false]);
  76. expect(
  77. await Promise.all(
  78. [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.executor)),
  79. ),
  80. ).to.deep.equal([false, false, true]);
  81. });
  82. it('optional admin', async function () {
  83. const mock = await ethers.deployContract('TimelockController', [
  84. MINDELAY,
  85. [this.proposer],
  86. [this.executor],
  87. ethers.ZeroAddress,
  88. ]);
  89. expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, this.admin)).to.be.false;
  90. expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, mock.target)).to.be.true;
  91. });
  92. describe('methods', function () {
  93. describe('operation hashing', function () {
  94. it('hashOperation', async function () {
  95. this.operation = genOperation(
  96. '0x29cebefe301c6ce1bb36b58654fea275e1cacc83',
  97. '0xf94fdd6e21da21d2',
  98. '0xa3bc5104',
  99. '0xba41db3be0a9929145cfe480bd0f1f003689104d275ae912099f925df424ef94',
  100. '0x60d9109846ab510ed75c15f979ae366a8a2ace11d34ba9788c13ac296db50e6e',
  101. );
  102. expect(
  103. await this.mock.hashOperation(
  104. this.operation.target,
  105. this.operation.value,
  106. this.operation.data,
  107. this.operation.predecessor,
  108. this.operation.salt,
  109. ),
  110. ).to.equal(this.operation.id);
  111. });
  112. it('hashOperationBatch', async function () {
  113. this.operation = genOperationBatch(
  114. Array(8).fill('0x2d5f21620e56531c1d59c2df9b8e95d129571f71'),
  115. Array(8).fill('0x2b993cfce932ccee'),
  116. Array(8).fill('0xcf51966b'),
  117. '0xce8f45069cc71d25f71ba05062de1a3974f9849b004de64a70998bca9d29c2e7',
  118. '0x8952d74c110f72bfe5accdf828c74d53a7dfb71235dfa8a1e8c75d8576b372ff',
  119. );
  120. expect(
  121. await this.mock.hashOperationBatch(
  122. this.operation.targets,
  123. this.operation.values,
  124. this.operation.payloads,
  125. this.operation.predecessor,
  126. this.operation.salt,
  127. ),
  128. ).to.equal(this.operation.id);
  129. });
  130. });
  131. describe('simple', function () {
  132. describe('schedule', function () {
  133. beforeEach(async function () {
  134. this.operation = genOperation(
  135. '0x31754f590B97fD975Eb86938f18Cc304E264D2F2',
  136. 0n,
  137. '0x3bf92ccc',
  138. ethers.ZeroHash,
  139. salt,
  140. );
  141. });
  142. it('proposer can schedule', async function () {
  143. const tx = await this.mock
  144. .connect(this.proposer)
  145. .schedule(
  146. this.operation.target,
  147. this.operation.value,
  148. this.operation.data,
  149. this.operation.predecessor,
  150. this.operation.salt,
  151. MINDELAY,
  152. );
  153. expect(tx)
  154. .to.emit(this.mock, 'CallScheduled')
  155. .withArgs(
  156. this.operation.id,
  157. 0n,
  158. this.operation.target,
  159. this.operation.value,
  160. this.operation.data,
  161. this.operation.predecessor,
  162. MINDELAY,
  163. )
  164. .to.emit(this.mock, 'CallSalt')
  165. .withArgs(this.operation.id, this.operation.salt);
  166. expect(await this.mock.getTimestamp(this.operation.id)).to.equal(
  167. (await time.clockFromReceipt.timestamp(tx)) + MINDELAY,
  168. );
  169. });
  170. it('prevent overwriting active operation', async function () {
  171. await this.mock
  172. .connect(this.proposer)
  173. .schedule(
  174. this.operation.target,
  175. this.operation.value,
  176. this.operation.data,
  177. this.operation.predecessor,
  178. this.operation.salt,
  179. MINDELAY,
  180. );
  181. await expect(
  182. this.mock
  183. .connect(this.proposer)
  184. .schedule(
  185. this.operation.target,
  186. this.operation.value,
  187. this.operation.data,
  188. this.operation.predecessor,
  189. this.operation.salt,
  190. MINDELAY,
  191. ),
  192. )
  193. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  194. .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Unset));
  195. });
  196. it('prevent non-proposer from committing', async function () {
  197. await expect(
  198. this.mock
  199. .connect(this.other)
  200. .schedule(
  201. this.operation.target,
  202. this.operation.value,
  203. this.operation.data,
  204. this.operation.predecessor,
  205. this.operation.salt,
  206. MINDELAY,
  207. ),
  208. )
  209. .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
  210. .withArgs(this.other.address, PROPOSER_ROLE);
  211. });
  212. it('enforce minimum delay', async function () {
  213. await expect(
  214. this.mock
  215. .connect(this.proposer)
  216. .schedule(
  217. this.operation.target,
  218. this.operation.value,
  219. this.operation.data,
  220. this.operation.predecessor,
  221. this.operation.salt,
  222. MINDELAY - 1n,
  223. ),
  224. )
  225. .to.be.revertedWithCustomError(this.mock, 'TimelockInsufficientDelay')
  226. .withArgs(MINDELAY - 1n, MINDELAY);
  227. });
  228. it('schedule operation with salt zero', async function () {
  229. await expect(
  230. this.mock
  231. .connect(this.proposer)
  232. .schedule(
  233. this.operation.target,
  234. this.operation.value,
  235. this.operation.data,
  236. this.operation.predecessor,
  237. ethers.ZeroHash,
  238. MINDELAY,
  239. ),
  240. ).to.not.emit(this.mock, 'CallSalt');
  241. });
  242. });
  243. describe('execute', function () {
  244. beforeEach(async function () {
  245. this.operation = genOperation(
  246. '0xAe22104DCD970750610E6FE15E623468A98b15f7',
  247. 0n,
  248. '0x13e414de',
  249. ethers.ZeroHash,
  250. '0xc1059ed2dc130227aa1d1d539ac94c641306905c020436c636e19e3fab56fc7f',
  251. );
  252. });
  253. it('revert if operation is not scheduled', async function () {
  254. await expect(
  255. this.mock
  256. .connect(this.executor)
  257. .execute(
  258. this.operation.target,
  259. this.operation.value,
  260. this.operation.data,
  261. this.operation.predecessor,
  262. this.operation.salt,
  263. ),
  264. )
  265. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  266. .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
  267. });
  268. describe('with scheduled operation', function () {
  269. beforeEach(async function () {
  270. await this.mock
  271. .connect(this.proposer)
  272. .schedule(
  273. this.operation.target,
  274. this.operation.value,
  275. this.operation.data,
  276. this.operation.predecessor,
  277. this.operation.salt,
  278. MINDELAY,
  279. );
  280. });
  281. it('revert if execution comes too early 1/2', async function () {
  282. await expect(
  283. this.mock
  284. .connect(this.executor)
  285. .execute(
  286. this.operation.target,
  287. this.operation.value,
  288. this.operation.data,
  289. this.operation.predecessor,
  290. this.operation.salt,
  291. ),
  292. )
  293. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  294. .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
  295. });
  296. it('revert if execution comes too early 2/2', async function () {
  297. // -1 is too tight, test sometime fails
  298. await this.mock.getTimestamp(this.operation.id).then(clock => time.increaseTo.timestamp(clock - 5n));
  299. await expect(
  300. this.mock
  301. .connect(this.executor)
  302. .execute(
  303. this.operation.target,
  304. this.operation.value,
  305. this.operation.data,
  306. this.operation.predecessor,
  307. this.operation.salt,
  308. ),
  309. )
  310. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  311. .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
  312. });
  313. describe('on time', function () {
  314. beforeEach(async function () {
  315. await this.mock.getTimestamp(this.operation.id).then(time.increaseTo.timestamp);
  316. });
  317. it('executor can reveal', async function () {
  318. await expect(
  319. this.mock
  320. .connect(this.executor)
  321. .execute(
  322. this.operation.target,
  323. this.operation.value,
  324. this.operation.data,
  325. this.operation.predecessor,
  326. this.operation.salt,
  327. ),
  328. )
  329. .to.emit(this.mock, 'CallExecuted')
  330. .withArgs(this.operation.id, 0n, this.operation.target, this.operation.value, this.operation.data);
  331. });
  332. it('prevent non-executor from revealing', async function () {
  333. await expect(
  334. this.mock
  335. .connect(this.other)
  336. .execute(
  337. this.operation.target,
  338. this.operation.value,
  339. this.operation.data,
  340. this.operation.predecessor,
  341. this.operation.salt,
  342. ),
  343. )
  344. .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
  345. .withArgs(this.other.address, EXECUTOR_ROLE);
  346. });
  347. it('prevents reentrancy execution', async function () {
  348. // Create operation
  349. const reentrant = await ethers.deployContract('$TimelockReentrant');
  350. const reentrantOperation = genOperation(
  351. reentrant,
  352. 0n,
  353. reentrant.interface.encodeFunctionData('reenter'),
  354. ethers.ZeroHash,
  355. salt,
  356. );
  357. // Schedule so it can be executed
  358. await this.mock
  359. .connect(this.proposer)
  360. .schedule(
  361. reentrantOperation.target,
  362. reentrantOperation.value,
  363. reentrantOperation.data,
  364. reentrantOperation.predecessor,
  365. reentrantOperation.salt,
  366. MINDELAY,
  367. );
  368. // Advance on time to make the operation executable
  369. await this.mock.getTimestamp(reentrantOperation.id).then(time.increaseTo.timestamp);
  370. // Grant executor role to the reentrant contract
  371. await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant);
  372. // Prepare reenter
  373. const data = this.mock.interface.encodeFunctionData('execute', [
  374. getAddress(reentrantOperation.target),
  375. reentrantOperation.value,
  376. reentrantOperation.data,
  377. reentrantOperation.predecessor,
  378. reentrantOperation.salt,
  379. ]);
  380. await reentrant.enableRentrancy(this.mock, data);
  381. // Expect to fail
  382. await expect(
  383. this.mock
  384. .connect(this.executor)
  385. .execute(
  386. reentrantOperation.target,
  387. reentrantOperation.value,
  388. reentrantOperation.data,
  389. reentrantOperation.predecessor,
  390. reentrantOperation.salt,
  391. ),
  392. )
  393. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  394. .withArgs(reentrantOperation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
  395. // Disable reentrancy
  396. await reentrant.disableReentrancy();
  397. const nonReentrantOperation = reentrantOperation; // Not anymore
  398. // Try again successfully
  399. await expect(
  400. this.mock
  401. .connect(this.executor)
  402. .execute(
  403. nonReentrantOperation.target,
  404. nonReentrantOperation.value,
  405. nonReentrantOperation.data,
  406. nonReentrantOperation.predecessor,
  407. nonReentrantOperation.salt,
  408. ),
  409. )
  410. .to.emit(this.mock, 'CallExecuted')
  411. .withArgs(
  412. nonReentrantOperation.id,
  413. 0n,
  414. getAddress(nonReentrantOperation.target),
  415. nonReentrantOperation.value,
  416. nonReentrantOperation.data,
  417. );
  418. });
  419. });
  420. });
  421. });
  422. });
  423. describe('batch', function () {
  424. describe('schedule', function () {
  425. beforeEach(async function () {
  426. this.operation = genOperationBatch(
  427. Array(8).fill('0xEd912250835c812D4516BBD80BdaEA1bB63a293C'),
  428. Array(8).fill(0n),
  429. Array(8).fill('0x2fcb7a88'),
  430. ethers.ZeroHash,
  431. '0x6cf9d042ade5de78bed9ffd075eb4b2a4f6b1736932c2dc8af517d6e066f51f5',
  432. );
  433. });
  434. it('proposer can schedule', async function () {
  435. const tx = this.mock
  436. .connect(this.proposer)
  437. .scheduleBatch(
  438. this.operation.targets,
  439. this.operation.values,
  440. this.operation.payloads,
  441. this.operation.predecessor,
  442. this.operation.salt,
  443. MINDELAY,
  444. );
  445. for (const i in this.operation.targets) {
  446. await expect(tx)
  447. .to.emit(this.mock, 'CallScheduled')
  448. .withArgs(
  449. this.operation.id,
  450. i,
  451. getAddress(this.operation.targets[i]),
  452. this.operation.values[i],
  453. this.operation.payloads[i],
  454. this.operation.predecessor,
  455. MINDELAY,
  456. )
  457. .to.emit(this.mock, 'CallSalt')
  458. .withArgs(this.operation.id, this.operation.salt);
  459. }
  460. expect(await this.mock.getTimestamp(this.operation.id)).to.equal(
  461. (await time.clockFromReceipt.timestamp(tx)) + MINDELAY,
  462. );
  463. });
  464. it('prevent overwriting active operation', async function () {
  465. await this.mock
  466. .connect(this.proposer)
  467. .scheduleBatch(
  468. this.operation.targets,
  469. this.operation.values,
  470. this.operation.payloads,
  471. this.operation.predecessor,
  472. this.operation.salt,
  473. MINDELAY,
  474. );
  475. await expect(
  476. this.mock
  477. .connect(this.proposer)
  478. .scheduleBatch(
  479. this.operation.targets,
  480. this.operation.values,
  481. this.operation.payloads,
  482. this.operation.predecessor,
  483. this.operation.salt,
  484. MINDELAY,
  485. ),
  486. )
  487. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  488. .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Unset));
  489. });
  490. it('length of batch parameter must match #1', async function () {
  491. await expect(
  492. this.mock
  493. .connect(this.proposer)
  494. .scheduleBatch(
  495. this.operation.targets,
  496. [],
  497. this.operation.payloads,
  498. this.operation.predecessor,
  499. this.operation.salt,
  500. MINDELAY,
  501. ),
  502. )
  503. .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
  504. .withArgs(this.operation.targets.length, this.operation.payloads.length, 0n);
  505. });
  506. it('length of batch parameter must match #1', async function () {
  507. await expect(
  508. this.mock
  509. .connect(this.proposer)
  510. .scheduleBatch(
  511. this.operation.targets,
  512. this.operation.values,
  513. [],
  514. this.operation.predecessor,
  515. this.operation.salt,
  516. MINDELAY,
  517. ),
  518. )
  519. .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
  520. .withArgs(this.operation.targets.length, 0n, this.operation.payloads.length);
  521. });
  522. it('prevent non-proposer from committing', async function () {
  523. await expect(
  524. this.mock
  525. .connect(this.other)
  526. .scheduleBatch(
  527. this.operation.targets,
  528. this.operation.values,
  529. this.operation.payloads,
  530. this.operation.predecessor,
  531. this.operation.salt,
  532. MINDELAY,
  533. ),
  534. )
  535. .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
  536. .withArgs(this.other.address, PROPOSER_ROLE);
  537. });
  538. it('enforce minimum delay', async function () {
  539. await expect(
  540. this.mock
  541. .connect(this.proposer)
  542. .scheduleBatch(
  543. this.operation.targets,
  544. this.operation.values,
  545. this.operation.payloads,
  546. this.operation.predecessor,
  547. this.operation.salt,
  548. MINDELAY - 1n,
  549. ),
  550. )
  551. .to.be.revertedWithCustomError(this.mock, 'TimelockInsufficientDelay')
  552. .withArgs(MINDELAY - 1n, MINDELAY);
  553. });
  554. });
  555. describe('execute', function () {
  556. beforeEach(async function () {
  557. this.operation = genOperationBatch(
  558. Array(8).fill('0x76E53CcEb05131Ef5248553bEBDb8F70536830b1'),
  559. Array(8).fill(0n),
  560. Array(8).fill('0x58a60f63'),
  561. ethers.ZeroHash,
  562. '0x9545eeabc7a7586689191f78a5532443698538e54211b5bd4d7dc0fc0102b5c7',
  563. );
  564. });
  565. it('revert if operation is not scheduled', async function () {
  566. await expect(
  567. this.mock
  568. .connect(this.executor)
  569. .executeBatch(
  570. this.operation.targets,
  571. this.operation.values,
  572. this.operation.payloads,
  573. this.operation.predecessor,
  574. this.operation.salt,
  575. ),
  576. )
  577. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  578. .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
  579. });
  580. describe('with scheduled operation', function () {
  581. beforeEach(async function () {
  582. await this.mock
  583. .connect(this.proposer)
  584. .scheduleBatch(
  585. this.operation.targets,
  586. this.operation.values,
  587. this.operation.payloads,
  588. this.operation.predecessor,
  589. this.operation.salt,
  590. MINDELAY,
  591. );
  592. });
  593. it('revert if execution comes too early 1/2', async function () {
  594. await expect(
  595. this.mock
  596. .connect(this.executor)
  597. .executeBatch(
  598. this.operation.targets,
  599. this.operation.values,
  600. this.operation.payloads,
  601. this.operation.predecessor,
  602. this.operation.salt,
  603. ),
  604. )
  605. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  606. .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
  607. });
  608. it('revert if execution comes too early 2/2', async function () {
  609. // -1 is to tight, test sometime fails
  610. await this.mock.getTimestamp(this.operation.id).then(clock => time.increaseTo.timestamp(clock - 5n));
  611. await expect(
  612. this.mock
  613. .connect(this.executor)
  614. .executeBatch(
  615. this.operation.targets,
  616. this.operation.values,
  617. this.operation.payloads,
  618. this.operation.predecessor,
  619. this.operation.salt,
  620. ),
  621. )
  622. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  623. .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
  624. });
  625. describe('on time', function () {
  626. beforeEach(async function () {
  627. await this.mock.getTimestamp(this.operation.id).then(time.increaseTo.timestamp);
  628. });
  629. it('executor can reveal', async function () {
  630. const tx = this.mock
  631. .connect(this.executor)
  632. .executeBatch(
  633. this.operation.targets,
  634. this.operation.values,
  635. this.operation.payloads,
  636. this.operation.predecessor,
  637. this.operation.salt,
  638. );
  639. for (const i in this.operation.targets) {
  640. expect(tx)
  641. .to.emit(this.mock, 'CallExecuted')
  642. .withArgs(
  643. this.operation.id,
  644. i,
  645. this.operation.targets[i],
  646. this.operation.values[i],
  647. this.operation.payloads[i],
  648. );
  649. }
  650. });
  651. it('prevent non-executor from revealing', async function () {
  652. await expect(
  653. this.mock
  654. .connect(this.other)
  655. .executeBatch(
  656. this.operation.targets,
  657. this.operation.values,
  658. this.operation.payloads,
  659. this.operation.predecessor,
  660. this.operation.salt,
  661. ),
  662. )
  663. .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
  664. .withArgs(this.other.address, EXECUTOR_ROLE);
  665. });
  666. it('length mismatch #1', async function () {
  667. await expect(
  668. this.mock
  669. .connect(this.executor)
  670. .executeBatch(
  671. [],
  672. this.operation.values,
  673. this.operation.payloads,
  674. this.operation.predecessor,
  675. this.operation.salt,
  676. ),
  677. )
  678. .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
  679. .withArgs(0, this.operation.payloads.length, this.operation.values.length);
  680. });
  681. it('length mismatch #2', async function () {
  682. await expect(
  683. this.mock
  684. .connect(this.executor)
  685. .executeBatch(
  686. this.operation.targets,
  687. [],
  688. this.operation.payloads,
  689. this.operation.predecessor,
  690. this.operation.salt,
  691. ),
  692. )
  693. .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
  694. .withArgs(this.operation.targets.length, this.operation.payloads.length, 0n);
  695. });
  696. it('length mismatch #3', async function () {
  697. await expect(
  698. this.mock
  699. .connect(this.executor)
  700. .executeBatch(
  701. this.operation.targets,
  702. this.operation.values,
  703. [],
  704. this.operation.predecessor,
  705. this.operation.salt,
  706. ),
  707. )
  708. .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength')
  709. .withArgs(this.operation.targets.length, 0n, this.operation.values.length);
  710. });
  711. it('prevents reentrancy execution', async function () {
  712. // Create operation
  713. const reentrant = await ethers.deployContract('$TimelockReentrant');
  714. const reentrantBatchOperation = genOperationBatch(
  715. [reentrant],
  716. [0n],
  717. [reentrant.interface.encodeFunctionData('reenter')],
  718. ethers.ZeroHash,
  719. salt,
  720. );
  721. // Schedule so it can be executed
  722. await this.mock
  723. .connect(this.proposer)
  724. .scheduleBatch(
  725. reentrantBatchOperation.targets,
  726. reentrantBatchOperation.values,
  727. reentrantBatchOperation.payloads,
  728. reentrantBatchOperation.predecessor,
  729. reentrantBatchOperation.salt,
  730. MINDELAY,
  731. );
  732. // Advance on time to make the operation executable
  733. await this.mock.getTimestamp(reentrantBatchOperation.id).then(time.increaseTo.timestamp);
  734. // Grant executor role to the reentrant contract
  735. await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant);
  736. // Prepare reenter
  737. const data = this.mock.interface.encodeFunctionData('executeBatch', [
  738. reentrantBatchOperation.targets.map(getAddress),
  739. reentrantBatchOperation.values,
  740. reentrantBatchOperation.payloads,
  741. reentrantBatchOperation.predecessor,
  742. reentrantBatchOperation.salt,
  743. ]);
  744. await reentrant.enableRentrancy(this.mock, data);
  745. // Expect to fail
  746. await expect(
  747. this.mock
  748. .connect(this.executor)
  749. .executeBatch(
  750. reentrantBatchOperation.targets,
  751. reentrantBatchOperation.values,
  752. reentrantBatchOperation.payloads,
  753. reentrantBatchOperation.predecessor,
  754. reentrantBatchOperation.salt,
  755. ),
  756. )
  757. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  758. .withArgs(reentrantBatchOperation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready));
  759. // Disable reentrancy
  760. await reentrant.disableReentrancy();
  761. const nonReentrantBatchOperation = reentrantBatchOperation; // Not anymore
  762. // Try again successfully
  763. const tx = this.mock
  764. .connect(this.executor)
  765. .executeBatch(
  766. nonReentrantBatchOperation.targets,
  767. nonReentrantBatchOperation.values,
  768. nonReentrantBatchOperation.payloads,
  769. nonReentrantBatchOperation.predecessor,
  770. nonReentrantBatchOperation.salt,
  771. );
  772. for (const i in nonReentrantBatchOperation.targets) {
  773. expect(tx)
  774. .to.emit(this.mock, 'CallExecuted')
  775. .withArgs(
  776. nonReentrantBatchOperation.id,
  777. i,
  778. nonReentrantBatchOperation.targets[i],
  779. nonReentrantBatchOperation.values[i],
  780. nonReentrantBatchOperation.payloads[i],
  781. );
  782. }
  783. });
  784. });
  785. });
  786. it('partial execution', async function () {
  787. const operation = genOperationBatch(
  788. [this.callreceivermock, this.callreceivermock, this.callreceivermock],
  789. [0n, 0n, 0n],
  790. [
  791. this.callreceivermock.interface.encodeFunctionData('mockFunction'),
  792. this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
  793. this.callreceivermock.interface.encodeFunctionData('mockFunction'),
  794. ],
  795. ethers.ZeroHash,
  796. '0x8ac04aa0d6d66b8812fb41d39638d37af0a9ab11da507afd65c509f8ed079d3e',
  797. );
  798. await this.mock
  799. .connect(this.proposer)
  800. .scheduleBatch(
  801. operation.targets,
  802. operation.values,
  803. operation.payloads,
  804. operation.predecessor,
  805. operation.salt,
  806. MINDELAY,
  807. );
  808. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  809. await expect(
  810. this.mock
  811. .connect(this.executor)
  812. .executeBatch(
  813. operation.targets,
  814. operation.values,
  815. operation.payloads,
  816. operation.predecessor,
  817. operation.salt,
  818. ),
  819. ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall');
  820. });
  821. });
  822. });
  823. describe('cancel', function () {
  824. beforeEach(async function () {
  825. this.operation = genOperation(
  826. '0xC6837c44AA376dbe1d2709F13879E040CAb653ca',
  827. 0n,
  828. '0x296e58dd',
  829. ethers.ZeroHash,
  830. '0xa2485763600634800df9fc9646fb2c112cf98649c55f63dd1d9c7d13a64399d9',
  831. );
  832. await this.mock
  833. .connect(this.proposer)
  834. .schedule(
  835. this.operation.target,
  836. this.operation.value,
  837. this.operation.data,
  838. this.operation.predecessor,
  839. this.operation.salt,
  840. MINDELAY,
  841. );
  842. });
  843. it('canceller can cancel', async function () {
  844. await expect(this.mock.connect(this.canceller).cancel(this.operation.id))
  845. .to.emit(this.mock, 'Cancelled')
  846. .withArgs(this.operation.id);
  847. });
  848. it('cannot cancel invalid operation', async function () {
  849. await expect(this.mock.connect(this.canceller).cancel(ethers.ZeroHash))
  850. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState')
  851. .withArgs(
  852. ethers.ZeroHash,
  853. GovernorHelper.proposalStatesToBitMap([OperationState.Waiting, OperationState.Ready]),
  854. );
  855. });
  856. it('prevent non-canceller from canceling', async function () {
  857. await expect(this.mock.connect(this.other).cancel(this.operation.id))
  858. .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount')
  859. .withArgs(this.other.address, CANCELLER_ROLE);
  860. });
  861. });
  862. });
  863. describe('maintenance', function () {
  864. it('prevent unauthorized maintenance', async function () {
  865. await expect(this.mock.connect(this.other).updateDelay(0n))
  866. .to.be.revertedWithCustomError(this.mock, 'TimelockUnauthorizedCaller')
  867. .withArgs(this.other.address);
  868. });
  869. it('timelock scheduled maintenance', async function () {
  870. const newDelay = time.duration.hours(6);
  871. const operation = genOperation(
  872. this.mock,
  873. 0n,
  874. this.mock.interface.encodeFunctionData('updateDelay', [newDelay]),
  875. ethers.ZeroHash,
  876. '0xf8e775b2c5f4d66fb5c7fa800f35ef518c262b6014b3c0aee6ea21bff157f108',
  877. );
  878. await this.mock
  879. .connect(this.proposer)
  880. .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
  881. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  882. await expect(
  883. this.mock
  884. .connect(this.executor)
  885. .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
  886. )
  887. .to.emit(this.mock, 'MinDelayChange')
  888. .withArgs(MINDELAY, newDelay);
  889. expect(await this.mock.getMinDelay()).to.equal(newDelay);
  890. });
  891. });
  892. describe('dependency', function () {
  893. beforeEach(async function () {
  894. this.operation1 = genOperation(
  895. '0xdE66bD4c97304200A95aE0AadA32d6d01A867E39',
  896. 0n,
  897. '0x01dc731a',
  898. ethers.ZeroHash,
  899. '0x64e932133c7677402ead2926f86205e2ca4686aebecf5a8077627092b9bb2feb',
  900. );
  901. this.operation2 = genOperation(
  902. '0x3c7944a3F1ee7fc8c5A5134ba7c79D11c3A1FCa3',
  903. 0n,
  904. '0x8f531849',
  905. this.operation1.id,
  906. '0x036e1311cac523f9548e6461e29fb1f8f9196b91910a41711ea22f5de48df07d',
  907. );
  908. await this.mock
  909. .connect(this.proposer)
  910. .schedule(
  911. this.operation1.target,
  912. this.operation1.value,
  913. this.operation1.data,
  914. this.operation1.predecessor,
  915. this.operation1.salt,
  916. MINDELAY,
  917. );
  918. await this.mock
  919. .connect(this.proposer)
  920. .schedule(
  921. this.operation2.target,
  922. this.operation2.value,
  923. this.operation2.data,
  924. this.operation2.predecessor,
  925. this.operation2.salt,
  926. MINDELAY,
  927. );
  928. await this.mock.getTimestamp(this.operation2.id).then(time.increaseTo.timestamp);
  929. });
  930. it('cannot execute before dependency', async function () {
  931. await expect(
  932. this.mock
  933. .connect(this.executor)
  934. .execute(
  935. this.operation2.target,
  936. this.operation2.value,
  937. this.operation2.data,
  938. this.operation2.predecessor,
  939. this.operation2.salt,
  940. ),
  941. )
  942. .to.be.revertedWithCustomError(this.mock, 'TimelockUnexecutedPredecessor')
  943. .withArgs(this.operation1.id);
  944. });
  945. it('can execute after dependency', async function () {
  946. await this.mock
  947. .connect(this.executor)
  948. .execute(
  949. this.operation1.target,
  950. this.operation1.value,
  951. this.operation1.data,
  952. this.operation1.predecessor,
  953. this.operation1.salt,
  954. );
  955. await this.mock
  956. .connect(this.executor)
  957. .execute(
  958. this.operation2.target,
  959. this.operation2.value,
  960. this.operation2.data,
  961. this.operation2.predecessor,
  962. this.operation2.salt,
  963. );
  964. });
  965. });
  966. describe('usage scenario', function () {
  967. this.timeout(10000);
  968. it('call', async function () {
  969. const operation = genOperation(
  970. this.implementation2,
  971. 0n,
  972. this.implementation2.interface.encodeFunctionData('setValue', [42n]),
  973. ethers.ZeroHash,
  974. '0x8043596363daefc89977b25f9d9b4d06c3910959ef0c4d213557a903e1b555e2',
  975. );
  976. await this.mock
  977. .connect(this.proposer)
  978. .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
  979. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  980. await this.mock
  981. .connect(this.executor)
  982. .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt);
  983. expect(await this.implementation2.getValue()).to.equal(42n);
  984. });
  985. it('call reverting', async function () {
  986. const operation = genOperation(
  987. this.callreceivermock,
  988. 0n,
  989. this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
  990. ethers.ZeroHash,
  991. '0xb1b1b276fdf1a28d1e00537ea73b04d56639128b08063c1a2f70a52e38cba693',
  992. );
  993. await this.mock
  994. .connect(this.proposer)
  995. .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
  996. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  997. await expect(
  998. this.mock
  999. .connect(this.executor)
  1000. .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
  1001. ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall');
  1002. });
  1003. it('call throw', async function () {
  1004. const operation = genOperation(
  1005. this.callreceivermock,
  1006. 0n,
  1007. this.callreceivermock.interface.encodeFunctionData('mockFunctionThrows'),
  1008. ethers.ZeroHash,
  1009. '0xe5ca79f295fc8327ee8a765fe19afb58f4a0cbc5053642bfdd7e73bc68e0fc67',
  1010. );
  1011. await this.mock
  1012. .connect(this.proposer)
  1013. .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
  1014. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  1015. // Targeted function reverts with a panic code (0x1) + the timelock bubble the panic code
  1016. await expect(
  1017. this.mock
  1018. .connect(this.executor)
  1019. .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
  1020. ).to.be.revertedWithPanic(PANIC_CODES.ASSERTION_ERROR);
  1021. });
  1022. it('call out of gas', async function () {
  1023. const operation = genOperation(
  1024. this.callreceivermock,
  1025. 0n,
  1026. this.callreceivermock.interface.encodeFunctionData('mockFunctionOutOfGas'),
  1027. ethers.ZeroHash,
  1028. '0xf3274ce7c394c5b629d5215723563a744b817e1730cca5587c567099a14578fd',
  1029. );
  1030. await this.mock
  1031. .connect(this.proposer)
  1032. .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
  1033. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  1034. await expect(
  1035. this.mock
  1036. .connect(this.executor)
  1037. .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, {
  1038. gasLimit: '100000',
  1039. }),
  1040. ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall');
  1041. });
  1042. it('call payable with eth', async function () {
  1043. const operation = genOperation(
  1044. this.callreceivermock,
  1045. 1,
  1046. this.callreceivermock.interface.encodeFunctionData('mockFunction'),
  1047. ethers.ZeroHash,
  1048. '0x5ab73cd33477dcd36c1e05e28362719d0ed59a7b9ff14939de63a43073dc1f44',
  1049. );
  1050. await this.mock
  1051. .connect(this.proposer)
  1052. .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
  1053. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  1054. expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
  1055. expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
  1056. await this.mock
  1057. .connect(this.executor)
  1058. .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, {
  1059. value: 1,
  1060. });
  1061. expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
  1062. expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(1n);
  1063. });
  1064. it('call nonpayable with eth', async function () {
  1065. const operation = genOperation(
  1066. this.callreceivermock,
  1067. 1,
  1068. this.callreceivermock.interface.encodeFunctionData('mockFunctionNonPayable'),
  1069. ethers.ZeroHash,
  1070. '0xb78edbd920c7867f187e5aa6294ae5a656cfbf0dea1ccdca3751b740d0f2bdf8',
  1071. );
  1072. await this.mock
  1073. .connect(this.proposer)
  1074. .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
  1075. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  1076. expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
  1077. expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
  1078. await expect(
  1079. this.mock
  1080. .connect(this.executor)
  1081. .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
  1082. ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall');
  1083. expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
  1084. expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
  1085. });
  1086. it('call reverting with eth', async function () {
  1087. const operation = genOperation(
  1088. this.callreceivermock,
  1089. 1,
  1090. this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
  1091. ethers.ZeroHash,
  1092. '0xdedb4563ef0095db01d81d3f2decf57cf83e4a72aa792af14c43a792b56f4de6',
  1093. );
  1094. await this.mock
  1095. .connect(this.proposer)
  1096. .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY);
  1097. await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp);
  1098. expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
  1099. expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
  1100. await expect(
  1101. this.mock
  1102. .connect(this.executor)
  1103. .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
  1104. ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall');
  1105. expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
  1106. expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
  1107. });
  1108. });
  1109. describe('safe receive', function () {
  1110. describe('ERC721', function () {
  1111. const tokenId = 1n;
  1112. beforeEach(async function () {
  1113. this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']);
  1114. await this.token.$_mint(this.other, tokenId);
  1115. });
  1116. it('can receive an ERC721 safeTransfer', async function () {
  1117. await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, tokenId);
  1118. });
  1119. });
  1120. describe('ERC1155', function () {
  1121. const tokenIds = {
  1122. 1: 1000n,
  1123. 2: 2000n,
  1124. 3: 3000n,
  1125. };
  1126. beforeEach(async function () {
  1127. this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']);
  1128. await this.token.$_mintBatch(this.other, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  1129. });
  1130. it('can receive ERC1155 safeTransfer', async function () {
  1131. await this.token.connect(this.other).safeTransferFrom(
  1132. this.other,
  1133. this.mock,
  1134. ...Object.entries(tokenIds)[0n], // id + amount
  1135. '0x',
  1136. );
  1137. });
  1138. it('can receive ERC1155 safeBatchTransfer', async function () {
  1139. await this.token
  1140. .connect(this.other)
  1141. .safeBatchTransferFrom(this.other, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x');
  1142. });
  1143. });
  1144. });
  1145. });