Skip to content

Commit 52e0e3e

Browse files
ernestognwAmxx
andauthored
Extend onlyAuthorized to support extra functions in AccessManager (#5014)
Co-authored-by: Hadrien Croubois <[email protected]>
1 parent b64c902 commit 52e0e3e

File tree

7 files changed

+175
-32
lines changed

7 files changed

+175
-32
lines changed

.changeset/light-news-listen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`AccessManager`: Allow the `onlyAuthorized` modifier to restrict functions added to the manager.

certora/diff/access_manager_AccessManager.sol.patch

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@
6464
*/
6565
function _getAdminRestrictions(
6666
bytes calldata data
67-
- ) private view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) {
68-
+ ) internal view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) { // private → internal for FV
67+
- ) private view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) {
68+
+ ) internal view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) { // private → internal for FV
6969
if (data.length < 4) {
7070
return (false, 0, 0);
7171
}

contracts/access/manager/AccessManager.sol

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,6 @@ contract AccessManager is Context, Multicall, IAccessManager {
412412
* Emits a {TargetClosed} event.
413413
*/
414414
function _setTargetClosed(address target, bool closed) internal virtual {
415-
if (target == address(this)) {
416-
revert AccessManagerLockedAccount(target);
417-
}
418415
_targets[target].closed = closed;
419416
emit TargetClosed(target, closed);
420417
}
@@ -586,7 +583,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
586583

587584
// ================================================= ADMIN LOGIC ==================================================
588585
/**
589-
* @dev Check if the current call is authorized according to admin logic.
586+
* @dev Check if the current call is authorized according to admin and roles logic.
587+
*
588+
* WARNING: Carefully review the considerations of {AccessManaged-restricted} since they apply to this modifier.
590589
*/
591590
function _checkAuthorized() private {
592591
address caller = _msgSender();
@@ -611,7 +610,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
611610
*/
612611
function _getAdminRestrictions(
613612
bytes calldata data
614-
) private view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) {
613+
) private view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) {
615614
if (data.length < 4) {
616615
return (false, 0, 0);
617616
}
@@ -648,7 +647,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
648647
return (true, getRoleAdmin(roleId), 0);
649648
}
650649

651-
return (false, 0, 0);
650+
return (false, getTargetFunctionRole(address(this), selector), 0);
652651
}
653652

654653
// =================================================== HELPERS ====================================================
@@ -673,7 +672,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
673672
}
674673

675674
/**
676-
* @dev A version of {canCall} that checks for admin restrictions in this contract.
675+
* @dev A version of {canCall} that checks for restrictions in this contract.
677676
*/
678677
function _canCallSelf(address caller, bytes calldata data) private view returns (bool immediate, uint32 delay) {
679678
if (data.length < 4) {
@@ -686,8 +685,10 @@ contract AccessManager is Context, Multicall, IAccessManager {
686685
return (_isExecuting(address(this), _checkSelector(data)), 0);
687686
}
688687

689-
(bool enabled, uint64 roleId, uint32 operationDelay) = _getAdminRestrictions(data);
690-
if (!enabled) {
688+
(bool adminRestricted, uint64 roleId, uint32 operationDelay) = _getAdminRestrictions(data);
689+
690+
// isTragetClosed apply to non-admin-restricted function
691+
if (!adminRestricted && isTargetClosed(address(this))) {
691692
return (false, 0);
692693
}
693694

contracts/access/manager/IAccessManager.sol

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ interface IAccessManager {
8282
error AccessManagerNotScheduled(bytes32 operationId);
8383
error AccessManagerNotReady(bytes32 operationId);
8484
error AccessManagerExpired(bytes32 operationId);
85-
error AccessManagerLockedAccount(address account);
8685
error AccessManagerLockedRole(uint64 roleId);
8786
error AccessManagerBadConfirmation();
8887
error AccessManagerUnauthorizedAccount(address msgsender, uint64 roleId);
@@ -108,7 +107,7 @@ interface IAccessManager {
108107
* is backward compatible. Some contracts may thus ignore the second return argument. In that case they will fail
109108
* to identify the indirect workflow, and will consider calls that require a delay to be forbidden.
110109
*
111-
* NOTE: This function does not report the permissions of this manager itself. These are defined by the
110+
* NOTE: This function does not report the permissions of the admin functions in the manager itself. These are defined by the
112111
* {AccessManager} documentation.
113112
*/
114113
function canCall(
@@ -134,6 +133,8 @@ interface IAccessManager {
134133

135134
/**
136135
* @dev Get whether the contract is closed disabling any access. Otherwise role permissions are applied.
136+
*
137+
* NOTE: When the manager itself is closed, admin functions are still accessible to avoid locking the contract.
137138
*/
138139
function isTargetClosed(address target) external view returns (bool);
139140

@@ -308,6 +309,8 @@ interface IAccessManager {
308309
/**
309310
* @dev Set the closed flag for a contract.
310311
*
312+
* Closing the manager itself won't disable access to admin methods to avoid locking the contract.
313+
*
311314
* Requirements:
312315
*
313316
* - the caller must be a global admin

contracts/mocks/AccessManagerMock.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {AccessManager} from "../access/manager/AccessManager.sol";
6+
import {StorageSlot} from "../utils/StorageSlot.sol";
7+
8+
contract AccessManagerMock is AccessManager {
9+
event CalledRestricted(address caller);
10+
event CalledUnrestricted(address caller);
11+
12+
constructor(address initialAdmin) AccessManager(initialAdmin) {}
13+
14+
function fnRestricted() public onlyAuthorized {
15+
emit CalledRestricted(msg.sender);
16+
}
17+
18+
function fnUnrestricted() public {
19+
emit CalledUnrestricted(msg.sender);
20+
}
21+
}

test/access/manager/AccessManager.behavior.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,65 @@ function shouldBehaveLikeAManagedRestrictedOperation() {
193193
});
194194
}
195195

196+
/**
197+
* @requires this.{target,manager,roles,calldata,role}
198+
*/
199+
function shouldBehaveLikeASelfRestrictedOperation() {
200+
function revertUnauthorized() {
201+
it('reverts as AccessManagerUnauthorizedAccount', async function () {
202+
await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
203+
.to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount')
204+
.withArgs(this.caller, this.role?.id ?? 0n);
205+
});
206+
}
207+
208+
const getAccessPath = LIKE_COMMON_GET_ACCESS;
209+
210+
function testScheduleOperation(mineDelay) {
211+
return function self() {
212+
self.mineDelay = mineDelay;
213+
beforeEach('sets execution delay', async function () {
214+
this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
215+
});
216+
testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
217+
};
218+
}
219+
220+
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay =
221+
testScheduleOperation(true);
222+
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = testScheduleOperation(false);
223+
224+
beforeEach('set target as manager', function () {
225+
this.target = this.manager;
226+
});
227+
228+
const isExecutingPath = LIKE_COMMON_IS_EXECUTING;
229+
isExecutingPath.notExecuting = revertUnauthorized;
230+
231+
testAsCanCall({
232+
closed: revertUnauthorized,
233+
open: {
234+
callerIsTheManager: isExecutingPath,
235+
callerIsNotTheManager: {
236+
publicRoleIsRequired() {
237+
it('succeeds called directly', async function () {
238+
await this.caller.sendTransaction({ to: this.target, data: this.calldata });
239+
});
240+
241+
it('succeeds via execute', async function () {
242+
await this.manager.connect(this.caller).execute(this.target, this.calldata);
243+
});
244+
},
245+
specificRoleIsRequired: getAccessPath,
246+
},
247+
},
248+
});
249+
}
250+
196251
module.exports = {
197252
shouldBehaveLikeDelayedAdminOperation,
198253
shouldBehaveLikeNotDelayedAdminOperation,
199254
shouldBehaveLikeRoleAdminOperation,
200255
shouldBehaveLikeAManagedRestrictedOperation,
256+
shouldBehaveLikeASelfRestrictedOperation,
201257
};

test/access/manager/AccessManager.test.js

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const {
2323
shouldBehaveLikeNotDelayedAdminOperation,
2424
shouldBehaveLikeRoleAdminOperation,
2525
shouldBehaveLikeAManagedRestrictedOperation,
26+
shouldBehaveLikeASelfRestrictedOperation,
2627
} = require('./AccessManager.behavior');
2728

2829
const {
@@ -48,7 +49,7 @@ async function fixture() {
4849
roles.SOME.members = [member];
4950
roles.PUBLIC.members = [admin, roleAdmin, roleGuardian, member, user, other];
5051

51-
const manager = await ethers.deployContract('$AccessManager', [admin]);
52+
const manager = await ethers.deployContract('$AccessManagerMock', [admin]);
5253
const target = await ethers.deployContract('$AccessManagedTarget', [manager]);
5354

5455
for (const { id: roleId, admin, guardian, members } of Object.values(roles)) {
@@ -1105,10 +1106,18 @@ describe('AccessManager', function () {
11051106
expect(await this.manager.isTargetClosed(this.target)).to.be.false;
11061107
});
11071108

1108-
it('reverts if closing the manager', async function () {
1109-
await expect(this.manager.connect(this.admin).setTargetClosed(this.manager, true))
1110-
.to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedAccount')
1111-
.withArgs(this.manager);
1109+
describe('when the target is the manager', async function () {
1110+
it('closes and opens the manager', async function () {
1111+
await expect(this.manager.connect(this.admin).setTargetClosed(this.manager, true))
1112+
.to.emit(this.manager, 'TargetClosed')
1113+
.withArgs(this.manager, true);
1114+
expect(await this.manager.isTargetClosed(this.manager)).to.be.true;
1115+
1116+
await expect(this.manager.connect(this.admin).setTargetClosed(this.manager, false))
1117+
.to.emit(this.manager, 'TargetClosed')
1118+
.withArgs(this.manager, false);
1119+
expect(await this.manager.isTargetClosed(this.manager)).to.be.false;
1120+
});
11121121
});
11131122
});
11141123

@@ -1670,18 +1679,74 @@ describe('AccessManager', function () {
16701679
});
16711680
});
16721681

1682+
describe('access managed self operations', function () {
1683+
describe('when calling a restricted target function', function () {
1684+
const method = 'fnRestricted()';
1685+
1686+
beforeEach('set required role', async function () {
1687+
this.role = { id: 785913n };
1688+
await this.manager.$_setTargetFunctionRole(
1689+
this.manager,
1690+
this.manager[method].getFragment().selector,
1691+
this.role.id,
1692+
);
1693+
});
1694+
1695+
describe('restrictions', function () {
1696+
beforeEach('set method and args', function () {
1697+
this.caller = this.user;
1698+
this.calldata = this.manager.interface.encodeFunctionData(method, []);
1699+
});
1700+
1701+
shouldBehaveLikeASelfRestrictedOperation();
1702+
});
1703+
1704+
it('succeeds called by a role member', async function () {
1705+
await this.manager.$_grantRole(this.role.id, this.user, 0, 0);
1706+
1707+
await expect(this.manager.connect(this.user)[method]())
1708+
.to.emit(this.manager, 'CalledRestricted')
1709+
.withArgs(this.user);
1710+
});
1711+
});
1712+
1713+
describe('when calling a non-restricted target function', function () {
1714+
const method = 'fnUnrestricted()';
1715+
1716+
beforeEach('set required role', async function () {
1717+
this.role = { id: 879435n };
1718+
await this.manager.$_setTargetFunctionRole(
1719+
this.manager,
1720+
this.manager[method].getFragment().selector,
1721+
this.role.id,
1722+
);
1723+
});
1724+
1725+
it('succeeds called by anyone', async function () {
1726+
await expect(this.manager.connect(this.user)[method]())
1727+
.to.emit(this.manager, 'CalledUnrestricted')
1728+
.withArgs(this.user);
1729+
});
1730+
});
1731+
});
1732+
16731733
describe('access managed target operations', function () {
16741734
describe('when calling a restricted target function', function () {
1675-
beforeEach('set required role', function () {
1676-
this.method = this.target.fnRestricted.getFragment();
1735+
const method = 'fnRestricted()';
1736+
1737+
beforeEach('set required role', async function () {
16771738
this.role = { id: 3597243n };
1678-
this.manager.$_setTargetFunctionRole(this.target, this.method.selector, this.role.id);
1739+
await this.manager.$_setTargetFunctionRole(
1740+
this.target,
1741+
this.target[method].getFragment().selector,
1742+
this.role.id,
1743+
);
16791744
});
16801745

16811746
describe('restrictions', function () {
16821747
beforeEach('set method and args', function () {
1683-
this.calldata = this.target.interface.encodeFunctionData(this.method, []);
16841748
this.caller = this.user;
1749+
this.calldata = this.target.interface.encodeFunctionData(method, []);
16851750
});
16861751

16871752
shouldBehaveLikeAManagedRestrictedOperation();
@@ -1690,11 +1755,7 @@ describe('AccessManager', function () {
16901755
it('succeeds called by a role member', async function () {
16911756
await this.manager.$_grantRole(this.role.id, this.user, 0, 0);
16921757

1693-
await expect(
1694-
this.target.connect(this.user)[this.method.selector]({
1695-
data: this.calldata,
1696-
}),
1697-
)
1758+
await expect(this.target.connect(this.user)[method]())
16981759
.to.emit(this.target, 'CalledRestricted')
16991760
.withArgs(this.user);
17001761
});
@@ -1713,11 +1774,7 @@ describe('AccessManager', function () {
17131774
});
17141775

17151776
it('succeeds called by anyone', async function () {
1716-
await expect(
1717-
this.target.connect(this.user)[method]({
1718-
data: this.calldata,
1719-
}),
1720-
)
1777+
await expect(this.target.connect(this.user)[method]())
17211778
.to.emit(this.target, 'CalledUnrestricted')
17221779
.withArgs(this.user);
17231780
});

0 commit comments

Comments
 (0)