AccessManager.predicate.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { setStorageAt } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { EXECUTION_ID_STORAGE_SLOT, EXPIRATION, prepareOperation } = require('../../helpers/access-manager');
  5. const { impersonate } = require('../../helpers/account');
  6. const time = require('../../helpers/time');
  7. // ============ COMMON PREDICATES ============
  8. const LIKE_COMMON_IS_EXECUTING = {
  9. executing() {
  10. it('succeeds', async function () {
  11. await this.caller.sendTransaction({ to: this.target, data: this.calldata });
  12. });
  13. },
  14. notExecuting() {
  15. it('reverts as AccessManagerUnauthorizedAccount', async function () {
  16. await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
  17. .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount')
  18. .withArgs(this.caller, this.role.id);
  19. });
  20. },
  21. };
  22. const LIKE_COMMON_GET_ACCESS = {
  23. requiredRoleIsGranted: {
  24. roleGrantingIsDelayed: {
  25. callerHasAnExecutionDelay: {
  26. beforeGrantDelay() {
  27. it('reverts as AccessManagerUnauthorizedAccount', async function () {
  28. await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
  29. .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount')
  30. .withArgs(this.caller, this.role.id);
  31. });
  32. },
  33. afterGrantDelay: undefined, // Diverges if there's an operation delay or not
  34. },
  35. callerHasNoExecutionDelay: {
  36. beforeGrantDelay() {
  37. it('reverts as AccessManagerUnauthorizedAccount', async function () {
  38. await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
  39. .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount')
  40. .withArgs(this.caller, this.role.id);
  41. });
  42. },
  43. afterGrantDelay() {
  44. it('succeeds called directly', async function () {
  45. await this.caller.sendTransaction({ to: this.target, data: this.calldata });
  46. });
  47. it('succeeds via execute', async function () {
  48. await this.manager.connect(this.caller).execute(this.target, this.calldata);
  49. });
  50. },
  51. },
  52. },
  53. roleGrantingIsNotDelayed: {
  54. callerHasAnExecutionDelay: undefined, // Diverges if there's an operation to schedule or not
  55. callerHasNoExecutionDelay() {
  56. it('succeeds called directly', async function () {
  57. await this.caller.sendTransaction({ to: this.target, data: this.calldata });
  58. });
  59. it('succeeds via execute', async function () {
  60. await this.manager.connect(this.caller).execute(this.target, this.calldata);
  61. });
  62. },
  63. },
  64. },
  65. requiredRoleIsNotGranted() {
  66. it('reverts as AccessManagerUnauthorizedAccount', async function () {
  67. await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
  68. .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount')
  69. .withArgs(this.caller, this.role.id);
  70. });
  71. },
  72. };
  73. const LIKE_COMMON_SCHEDULABLE = {
  74. scheduled: {
  75. before() {
  76. it('reverts as AccessManagerNotReady', async function () {
  77. await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
  78. .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotReady')
  79. .withArgs(this.operationId);
  80. });
  81. },
  82. after() {
  83. it('succeeds called directly', async function () {
  84. await this.caller.sendTransaction({ to: this.target, data: this.calldata });
  85. });
  86. it('succeeds via execute', async function () {
  87. await this.manager.connect(this.caller).execute(this.target, this.calldata);
  88. });
  89. },
  90. expired() {
  91. it('reverts as AccessManagerExpired', async function () {
  92. await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
  93. .to.be.revertedWithCustomError(this.manager, 'AccessManagerExpired')
  94. .withArgs(this.operationId);
  95. });
  96. },
  97. },
  98. notScheduled() {
  99. it('reverts as AccessManagerNotScheduled', async function () {
  100. await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
  101. .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled')
  102. .withArgs(this.operationId);
  103. });
  104. },
  105. };
  106. // ============ MODE ============
  107. /**
  108. * @requires this.{manager,target}
  109. */
  110. function testAsClosable({ closed, open }) {
  111. describe('when the manager is closed', function () {
  112. beforeEach('close', async function () {
  113. await this.manager.$_setTargetClosed(this.target, true);
  114. });
  115. closed();
  116. });
  117. describe('when the manager is open', function () {
  118. beforeEach('open', async function () {
  119. await this.manager.$_setTargetClosed(this.target, false);
  120. });
  121. open();
  122. });
  123. }
  124. // ============ DELAY ============
  125. /**
  126. * @requires this.{delay}
  127. */
  128. function testAsDelay(type, { before, after }) {
  129. beforeEach('define timestamp when delay takes effect', async function () {
  130. const timestamp = await time.clock.timestamp();
  131. this.delayEffect = timestamp + this.delay;
  132. });
  133. describe(`when ${type} delay has not taken effect yet`, function () {
  134. beforeEach(`set next block timestamp before ${type} takes effect`, async function () {
  135. await time.increaseTo.timestamp(this.delayEffect - 1n, !!before.mineDelay);
  136. });
  137. before();
  138. });
  139. describe(`when ${type} delay has taken effect`, function () {
  140. beforeEach(`set next block timestamp when ${type} takes effect`, async function () {
  141. await time.increaseTo.timestamp(this.delayEffect, !!after.mineDelay);
  142. });
  143. after();
  144. });
  145. }
  146. // ============ OPERATION ============
  147. /**
  148. * @requires this.{manager,scheduleIn,caller,target,calldata}
  149. */
  150. function testAsSchedulableOperation({ scheduled: { before, after, expired }, notScheduled }) {
  151. describe('when operation is scheduled', function () {
  152. beforeEach('schedule operation', async function () {
  153. if (this.caller.target) {
  154. await impersonate(this.caller.target);
  155. this.caller = await ethers.getSigner(this.caller.target);
  156. }
  157. const { operationId, schedule } = await prepareOperation(this.manager, {
  158. caller: this.caller,
  159. target: this.target,
  160. calldata: this.calldata,
  161. delay: this.scheduleIn,
  162. });
  163. await schedule();
  164. this.operationId = operationId;
  165. });
  166. describe('when operation is not ready for execution', function () {
  167. beforeEach('set next block time before operation is ready', async function () {
  168. this.scheduledAt = await time.clock.timestamp();
  169. const schedule = await this.manager.getSchedule(this.operationId);
  170. await time.increaseTo.timestamp(schedule - 1n, !!before.mineDelay);
  171. });
  172. before();
  173. });
  174. describe('when operation is ready for execution', function () {
  175. beforeEach('set next block time when operation is ready for execution', async function () {
  176. this.scheduledAt = await time.clock.timestamp();
  177. const schedule = await this.manager.getSchedule(this.operationId);
  178. await time.increaseTo.timestamp(schedule, !!after.mineDelay);
  179. });
  180. after();
  181. });
  182. describe('when operation has expired', function () {
  183. beforeEach('set next block time when operation expired', async function () {
  184. this.scheduledAt = await time.clock.timestamp();
  185. const schedule = await this.manager.getSchedule(this.operationId);
  186. await time.increaseTo.timestamp(schedule + EXPIRATION, !!expired.mineDelay);
  187. });
  188. expired();
  189. });
  190. });
  191. describe('when operation is not scheduled', function () {
  192. beforeEach('set expected operationId', async function () {
  193. this.operationId = await this.manager.hashOperation(this.caller, this.target, this.calldata);
  194. // Assert operation is not scheduled
  195. expect(await this.manager.getSchedule(this.operationId)).to.equal(0n);
  196. });
  197. notScheduled();
  198. });
  199. }
  200. /**
  201. * @requires this.{manager,roles,target,calldata}
  202. */
  203. function testAsRestrictedOperation({ callerIsTheManager: { executing, notExecuting }, callerIsNotTheManager }) {
  204. describe('when the call comes from the manager (msg.sender == manager)', function () {
  205. beforeEach('define caller as manager', async function () {
  206. this.caller = this.manager;
  207. if (this.caller.target) {
  208. await impersonate(this.caller.target);
  209. this.caller = await ethers.getSigner(this.caller.target);
  210. }
  211. });
  212. describe.skip('when _executionId is in storage for target and selector', function () {
  213. beforeEach('set _executionId flag from calldata and target', async function () {
  214. const executionId = ethers.keccak256(
  215. ethers.AbiCoder.defaultAbiCoder().encode(
  216. ['address', 'bytes4'],
  217. [this.target.target, this.calldata.substring(0, 10)],
  218. ),
  219. );
  220. // Note: this testing methods doesn't work with execution id in temporary storage
  221. await setStorageAt(this.manager.target, EXECUTION_ID_STORAGE_SLOT, executionId);
  222. });
  223. executing();
  224. });
  225. describe('when _executionId does not match target and selector', notExecuting);
  226. });
  227. describe('when the call does not come from the manager (msg.sender != manager)', function () {
  228. beforeEach('define non manager caller', function () {
  229. this.caller = this.roles.SOME.members[0];
  230. });
  231. callerIsNotTheManager();
  232. });
  233. }
  234. /**
  235. * @requires this.{manager,scheduleIn,caller,target,calldata,executionDelay}
  236. */
  237. function testAsDelayedOperation() {
  238. describe('with operation delay', function () {
  239. describe('when operation delay is greater than execution delay', function () {
  240. beforeEach('set operation delay', async function () {
  241. this.operationDelay = this.executionDelay + time.duration.hours(1);
  242. await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
  243. this.scheduleIn = this.operationDelay; // For testAsSchedulableOperation
  244. });
  245. testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
  246. });
  247. describe('when operation delay is shorter than execution delay', function () {
  248. beforeEach('set operation delay', async function () {
  249. this.operationDelay = this.executionDelay - time.duration.hours(1);
  250. await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
  251. this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
  252. });
  253. testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
  254. });
  255. });
  256. describe('without operation delay', function () {
  257. beforeEach('set operation delay', async function () {
  258. this.operationDelay = 0n;
  259. await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
  260. this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
  261. });
  262. testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
  263. });
  264. }
  265. // ============ METHOD ============
  266. /**
  267. * @requires this.{manager,roles,role,target,calldata}
  268. */
  269. function testAsCanCall({
  270. closed,
  271. open: {
  272. callerIsTheManager,
  273. callerIsNotTheManager: { publicRoleIsRequired, specificRoleIsRequired },
  274. },
  275. }) {
  276. testAsClosable({
  277. closed,
  278. open() {
  279. testAsRestrictedOperation({
  280. callerIsTheManager,
  281. callerIsNotTheManager() {
  282. testAsHasRole({
  283. publicRoleIsRequired,
  284. specificRoleIsRequired,
  285. });
  286. },
  287. });
  288. },
  289. });
  290. }
  291. /**
  292. * @requires this.{target,calldata,roles,role}
  293. */
  294. function testAsHasRole({ publicRoleIsRequired, specificRoleIsRequired }) {
  295. describe('when the function requires the caller to be granted with the PUBLIC_ROLE', function () {
  296. beforeEach('set target function role as PUBLIC_ROLE', async function () {
  297. this.role = this.roles.PUBLIC;
  298. await this.manager
  299. .connect(this.roles.ADMIN.members[0])
  300. .$_setTargetFunctionRole(this.target, this.calldata.substring(0, 10), this.role.id);
  301. });
  302. publicRoleIsRequired();
  303. });
  304. describe('when the function requires the caller to be granted with a role other than PUBLIC_ROLE', function () {
  305. beforeEach('set target function role as PUBLIC_ROLE', async function () {
  306. await this.manager
  307. .connect(this.roles.ADMIN.members[0])
  308. .$_setTargetFunctionRole(this.target, this.calldata.substring(0, 10), this.role.id);
  309. });
  310. testAsGetAccess(specificRoleIsRequired);
  311. });
  312. }
  313. /**
  314. * @requires this.{manager,role,caller}
  315. */
  316. function testAsGetAccess({
  317. requiredRoleIsGranted: {
  318. roleGrantingIsDelayed: {
  319. // Because both grant and execution delay are set within the same $_grantRole call
  320. // it's not possible to create a set of tests that diverge between grant and execution delay.
  321. // Therefore, the testAsDelay arguments are renamed for clarity:
  322. // before => beforeGrantDelay
  323. // after => afterGrantDelay
  324. callerHasAnExecutionDelay: { beforeGrantDelay: case1, afterGrantDelay: case2 },
  325. callerHasNoExecutionDelay: { beforeGrantDelay: case3, afterGrantDelay: case4 },
  326. },
  327. roleGrantingIsNotDelayed: { callerHasAnExecutionDelay: case5, callerHasNoExecutionDelay: case6 },
  328. },
  329. requiredRoleIsNotGranted,
  330. }) {
  331. describe('when the required role is granted to the caller', function () {
  332. describe('when role granting is delayed', function () {
  333. beforeEach('define delay', function () {
  334. this.grantDelay = time.duration.minutes(3);
  335. this.delay = this.grantDelay; // For testAsDelay
  336. });
  337. describe('when caller has an execution delay', function () {
  338. beforeEach('set role and delay', async function () {
  339. this.executionDelay = time.duration.hours(10);
  340. this.delay = this.grantDelay;
  341. await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
  342. });
  343. testAsDelay('grant', { before: case1, after: case2 });
  344. });
  345. describe('when caller has no execution delay', function () {
  346. beforeEach('set role and delay', async function () {
  347. this.executionDelay = 0n;
  348. await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
  349. });
  350. testAsDelay('grant', { before: case3, after: case4 });
  351. });
  352. });
  353. describe('when role granting is not delayed', function () {
  354. beforeEach('define delay', function () {
  355. this.grantDelay = 0n;
  356. });
  357. describe('when caller has an execution delay', function () {
  358. beforeEach('set role and delay', async function () {
  359. this.executionDelay = time.duration.hours(10);
  360. await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
  361. });
  362. case5();
  363. });
  364. describe('when caller has no execution delay', function () {
  365. beforeEach('set role and delay', async function () {
  366. this.executionDelay = 0n;
  367. await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
  368. });
  369. case6();
  370. });
  371. });
  372. });
  373. describe('when role is not granted', function () {
  374. // Because this helper can be composed with other helpers, it's possible
  375. // that role has been set already by another helper.
  376. // Although this is highly unlikely, we check for it here to avoid false positives.
  377. beforeEach('assert role is unset', async function () {
  378. const { since } = await this.manager.getAccess(this.role.id, this.caller);
  379. expect(since).to.equal(0n);
  380. });
  381. requiredRoleIsNotGranted();
  382. });
  383. }
  384. module.exports = {
  385. LIKE_COMMON_IS_EXECUTING,
  386. LIKE_COMMON_GET_ACCESS,
  387. LIKE_COMMON_SCHEDULABLE,
  388. testAsClosable,
  389. testAsDelay,
  390. testAsSchedulableOperation,
  391. testAsRestrictedOperation,
  392. testAsDelayedOperation,
  393. testAsCanCall,
  394. testAsHasRole,
  395. testAsGetAccess,
  396. };