AccessManager.predicate.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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('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. await setStorageAt(this.manager.target, EXECUTION_ID_STORAGE_SLOT, executionId);
  221. });
  222. executing();
  223. });
  224. describe('when _executionId does not match target and selector', notExecuting);
  225. });
  226. describe('when the call does not come from the manager (msg.sender != manager)', function () {
  227. beforeEach('define non manager caller', function () {
  228. this.caller = this.roles.SOME.members[0];
  229. });
  230. callerIsNotTheManager();
  231. });
  232. }
  233. /**
  234. * @requires this.{manager,scheduleIn,caller,target,calldata,executionDelay}
  235. */
  236. function testAsDelayedOperation() {
  237. describe('with operation delay', function () {
  238. describe('when operation delay is greater than execution delay', function () {
  239. beforeEach('set operation delay', async function () {
  240. this.operationDelay = this.executionDelay + time.duration.hours(1);
  241. await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
  242. this.scheduleIn = this.operationDelay; // For testAsSchedulableOperation
  243. });
  244. testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
  245. });
  246. describe('when operation delay is shorter than execution delay', function () {
  247. beforeEach('set operation delay', async function () {
  248. this.operationDelay = this.executionDelay - time.duration.hours(1);
  249. await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
  250. this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
  251. });
  252. testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
  253. });
  254. });
  255. describe('without operation delay', function () {
  256. beforeEach('set operation delay', async function () {
  257. this.operationDelay = 0n;
  258. await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
  259. this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
  260. });
  261. testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
  262. });
  263. }
  264. // ============ METHOD ============
  265. /**
  266. * @requires this.{manager,roles,role,target,calldata}
  267. */
  268. function testAsCanCall({
  269. closed,
  270. open: {
  271. callerIsTheManager,
  272. callerIsNotTheManager: { publicRoleIsRequired, specificRoleIsRequired },
  273. },
  274. }) {
  275. testAsClosable({
  276. closed,
  277. open() {
  278. testAsRestrictedOperation({
  279. callerIsTheManager,
  280. callerIsNotTheManager() {
  281. testAsHasRole({
  282. publicRoleIsRequired,
  283. specificRoleIsRequired,
  284. });
  285. },
  286. });
  287. },
  288. });
  289. }
  290. /**
  291. * @requires this.{target,calldata,roles,role}
  292. */
  293. function testAsHasRole({ publicRoleIsRequired, specificRoleIsRequired }) {
  294. describe('when the function requires the caller to be granted with the PUBLIC_ROLE', function () {
  295. beforeEach('set target function role as PUBLIC_ROLE', async function () {
  296. this.role = this.roles.PUBLIC;
  297. await this.manager
  298. .connect(this.roles.ADMIN.members[0])
  299. .$_setTargetFunctionRole(this.target, this.calldata.substring(0, 10), this.role.id);
  300. });
  301. publicRoleIsRequired();
  302. });
  303. describe('when the function requires the caller to be granted with a role other than PUBLIC_ROLE', function () {
  304. beforeEach('set target function role as PUBLIC_ROLE', async function () {
  305. await this.manager
  306. .connect(this.roles.ADMIN.members[0])
  307. .$_setTargetFunctionRole(this.target, this.calldata.substring(0, 10), this.role.id);
  308. });
  309. testAsGetAccess(specificRoleIsRequired);
  310. });
  311. }
  312. /**
  313. * @requires this.{manager,role,caller}
  314. */
  315. function testAsGetAccess({
  316. requiredRoleIsGranted: {
  317. roleGrantingIsDelayed: {
  318. // Because both grant and execution delay are set within the same $_grantRole call
  319. // it's not possible to create a set of tests that diverge between grant and execution delay.
  320. // Therefore, the testAsDelay arguments are renamed for clarity:
  321. // before => beforeGrantDelay
  322. // after => afterGrantDelay
  323. callerHasAnExecutionDelay: { beforeGrantDelay: case1, afterGrantDelay: case2 },
  324. callerHasNoExecutionDelay: { beforeGrantDelay: case3, afterGrantDelay: case4 },
  325. },
  326. roleGrantingIsNotDelayed: { callerHasAnExecutionDelay: case5, callerHasNoExecutionDelay: case6 },
  327. },
  328. requiredRoleIsNotGranted,
  329. }) {
  330. describe('when the required role is granted to the caller', function () {
  331. describe('when role granting is delayed', function () {
  332. beforeEach('define delay', function () {
  333. this.grantDelay = time.duration.minutes(3);
  334. this.delay = this.grantDelay; // For testAsDelay
  335. });
  336. describe('when caller has an execution delay', function () {
  337. beforeEach('set role and delay', async function () {
  338. this.executionDelay = time.duration.hours(10);
  339. this.delay = this.grantDelay;
  340. await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
  341. });
  342. testAsDelay('grant', { before: case1, after: case2 });
  343. });
  344. describe('when caller has no execution delay', function () {
  345. beforeEach('set role and delay', async function () {
  346. this.executionDelay = 0n;
  347. await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
  348. });
  349. testAsDelay('grant', { before: case3, after: case4 });
  350. });
  351. });
  352. describe('when role granting is not delayed', function () {
  353. beforeEach('define delay', function () {
  354. this.grantDelay = 0n;
  355. });
  356. describe('when caller has an execution delay', function () {
  357. beforeEach('set role and delay', async function () {
  358. this.executionDelay = time.duration.hours(10);
  359. await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
  360. });
  361. case5();
  362. });
  363. describe('when caller has no execution delay', function () {
  364. beforeEach('set role and delay', async function () {
  365. this.executionDelay = 0n;
  366. await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
  367. });
  368. case6();
  369. });
  370. });
  371. });
  372. describe('when role is not granted', function () {
  373. // Because this helper can be composed with other helpers, it's possible
  374. // that role has been set already by another helper.
  375. // Although this is highly unlikely, we check for it here to avoid false positives.
  376. beforeEach('assert role is unset', async function () {
  377. const { since } = await this.manager.getAccess(this.role.id, this.caller);
  378. expect(since).to.equal(0n);
  379. });
  380. requiredRoleIsNotGranted();
  381. });
  382. }
  383. module.exports = {
  384. LIKE_COMMON_IS_EXECUTING,
  385. LIKE_COMMON_GET_ACCESS,
  386. LIKE_COMMON_SCHEDULABLE,
  387. testAsClosable,
  388. testAsDelay,
  389. testAsSchedulableOperation,
  390. testAsRestrictedOperation,
  391. testAsDelayedOperation,
  392. testAsCanCall,
  393. testAsHasRole,
  394. testAsGetAccess,
  395. };