Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend onlyAuthorized to support extra functions in AccessManager #5014

Merged
merged 8 commits into from May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/light-news-listen.md
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`AccessManager`: Allow the `onlyAuthorized` modifier to restrict functions added to the manager.
16 changes: 10 additions & 6 deletions contracts/access/manager/AccessManager.sol
Expand Up @@ -586,7 +586,9 @@ contract AccessManager is Context, Multicall, IAccessManager {

// ================================================= ADMIN LOGIC ==================================================
/**
* @dev Check if the current call is authorized according to admin logic.
* @dev Check if the current call is authorized according to admin and roles logic.
*
* WARNING: Carefully review the considerations of {AccessManaged-restricted} since they apply to this modifier.
*/
function _checkAuthorized() private {
address caller = _msgSender();
Expand All @@ -611,7 +613,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
*/
function _getAdminRestrictions(
bytes calldata data
) private view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) {
) private view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) {
if (data.length < 4) {
return (false, 0, 0);
Amxx marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down Expand Up @@ -648,7 +650,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
return (true, getRoleAdmin(roleId), 0);
}

return (false, 0, 0);
return (false, getTargetFunctionRole(address(this), selector), 0);
}

// =================================================== HELPERS ====================================================
Expand All @@ -673,7 +675,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
}

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

(bool enabled, uint64 roleId, uint32 operationDelay) = _getAdminRestrictions(data);
if (!enabled) {
(bool adminRestricted, uint64 roleId, uint32 operationDelay) = _getAdminRestrictions(data);

// isTragetClosed apply to non-admin-restricted function
if (!adminRestricted && isTargetClosed(address(this))) {
return (false, 0);
}

Expand Down
21 changes: 21 additions & 0 deletions contracts/mocks/AccessManagerMock.sol
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {AccessManager} from "../access/manager/AccessManager.sol";
import {StorageSlot} from "../utils/StorageSlot.sol";

contract AccessManagerMock is AccessManager {
event CalledRestricted(address caller);
event CalledUnrestricted(address caller);

constructor(address initialAdmin) AccessManager(initialAdmin) {}

function fnRestricted() public onlyAuthorized {
emit CalledRestricted(msg.sender);
}

function fnUnrestricted() public {
emit CalledUnrestricted(msg.sender);
}
}
53 changes: 53 additions & 0 deletions test/access/manager/AccessManager.behavior.js
Expand Up @@ -193,9 +193,62 @@ function shouldBehaveLikeAManagedRestrictedOperation() {
});
}

/**
* @requires this.{manager,roles,calldata,role}
*/
function shouldBehaveLikeASelfRestrictedOperation() {
const getAccessPath = LIKE_COMMON_GET_ACCESS;

function testScheduleOperation(mineDelay) {
return function self() {
self.mineDelay = mineDelay;
beforeEach('sets execution delay', async function () {
this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
});
testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
};
}

getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay =
testScheduleOperation(true);
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = testScheduleOperation(false);

beforeEach('set target as manager', function () {
this.target = this.manager;
});

const isExecutingPath = LIKE_COMMON_IS_EXECUTING;
isExecutingPath.notExecuting = function () {
it('reverts as AccessManagerUnauthorizedAccount', async function () {
await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
.to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount')
.withArgs(this.caller, 0); // There's no way to know the required role of a function executed by the manager since the selector will be that of "execute"
});
};

testAsRestrictedOperation({
callerIsTheManager: isExecutingPath,
callerIsNotTheManager() {
testAsHasRole({
publicRoleIsRequired() {
it('succeeds called directly', async function () {
await this.caller.sendTransaction({ to: this.target, data: this.calldata });
});

it('succeeds via execute', async function () {
await this.manager.connect(this.caller).execute(this.target, this.calldata);
});
},
specificRoleIsRequired: getAccessPath,
});
},
});
}

module.exports = {
shouldBehaveLikeDelayedAdminOperation,
shouldBehaveLikeNotDelayedAdminOperation,
shouldBehaveLikeRoleAdminOperation,
shouldBehaveLikeAManagedRestrictedOperation,
shouldBehaveLikeASelfRestrictedOperation,
};
57 changes: 56 additions & 1 deletion test/access/manager/AccessManager.test.js
Expand Up @@ -23,6 +23,7 @@ const {
shouldBehaveLikeNotDelayedAdminOperation,
shouldBehaveLikeRoleAdminOperation,
shouldBehaveLikeAManagedRestrictedOperation,
shouldBehaveLikeASelfRestrictedOperation,
} = require('./AccessManager.behavior');

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

const manager = await ethers.deployContract('$AccessManager', [admin]);
const manager = await ethers.deployContract('$AccessManagerMock', [admin]);
const target = await ethers.deployContract('$AccessManagedTarget', [manager]);

for (const { id: roleId, admin, guardian, members } of Object.values(roles)) {
Expand Down Expand Up @@ -1670,6 +1671,60 @@ describe('AccessManager', function () {
});
});

describe('access managed self operations', function () {
describe('when calling a restricted target function', function () {
beforeEach('set required role', function () {
this.method = this.target.fnRestricted.getFragment();
this.role = { id: 785913n };
this.manager.$_setTargetFunctionRole(this.target, this.method.selector, this.role.id);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we set the function role for this.manager, this.method.selector ? It feels like we are testing restricted operation on the target when we should test the restricted operation on the manager.

});

describe('restrictions', function () {
beforeEach('set method and args', function () {
this.calldata = this.target.interface.encodeFunctionData(this.method, []);
this.caller = this.user;
});

shouldBehaveLikeASelfRestrictedOperation();
});

it('succeeds called by a role member', async function () {
await this.manager.$_grantRole(this.role.id, this.user, 0, 0);

await expect(
this.target.connect(this.user)[this.method.selector]({
data: this.calldata,
}),
)
.to.emit(this.target, 'CalledRestricted')
.withArgs(this.user);
});
});

describe('when calling a non-restricted target function', function () {
const method = 'fnUnrestricted()';

beforeEach('set required role', async function () {
this.role = { id: 879435n };
await this.manager.$_setTargetFunctionRole(
this.target,
this.target[method].getFragment().selector,
this.role.id,
);
});

it('succeeds called by anyone', async function () {
await expect(
this.target.connect(this.user)[method]({
data: this.calldata,
}),
)
.to.emit(this.target, 'CalledUnrestricted')
.withArgs(this.user);
});
});
});

describe('access managed target operations', function () {
describe('when calling a restricted target function', function () {
beforeEach('set required role', function () {
Expand Down