AccessManager.predicate.js 16 KB

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