AccessManager.predicate.js 16 KB

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