Skip to content

Commit fb30ada

Browse files
Amxxernestognw
andauthored
Fix bypassing target admin delay using execute (#6388)
Co-authored-by: ernestognw <ernestognw@gmail.com>
1 parent ca6f5fa commit fb30ada

File tree

6 files changed

+74
-16
lines changed

6 files changed

+74
-16
lines changed

.changeset/legal-cameras-sin.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`: treat `setAuthority` differently in `canCall` to prevent bypassing the `updateAuthority` security using an `execute`.

contracts/access/manager/AccessManager.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ contract AccessManager is Context, Multicall, IAccessManager {
148148
// Caller is AccessManager, this means the call was sent through {execute} and it already checked
149149
// permissions. We verify that the call "identifier", which is set during {execute}, is correct.
150150
return (_isExecuting(target, selector), 0);
151+
} else if (selector == IAccessManaged.setAuthority.selector) {
152+
(bool isAdmin, uint32 executionDelay) = hasRole(ADMIN_ROLE, caller);
153+
uint32 adminDelay = getTargetAdminDelay(target);
154+
uint32 setAuthorityDelay = uint32(Math.max(executionDelay, adminDelay));
155+
return isAdmin ? (setAuthorityDelay == 0, setAuthorityDelay) : (false, 0);
151156
} else {
152157
uint64 roleId = getTargetFunctionRole(target, selector);
153158
(bool isMember, uint32 currentDelay) = hasRole(roleId, caller);
@@ -388,6 +393,11 @@ contract AccessManager is Context, Multicall, IAccessManager {
388393
* Emits a {TargetFunctionRoleUpdated} event.
389394
*/
390395
function _setTargetFunctionRole(address target, bytes4 selector, uint64 roleId) internal virtual {
396+
if (selector == IAccessManaged.setAuthority.selector) {
397+
// Prevent updating authority using an execute call, instead only allow it through updateAuthority to
398+
// ensure the proper delay and admin restrictions are applied.
399+
revert AccessManagerLockedFunction(selector);
400+
}
391401
_targets[target].allowedRoles[selector] = roleId;
392402
emit TargetFunctionRoleUpdated(target, selector, roleId);
393403
}

contracts/access/manager/IAccessManager.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ interface IAccessManager {
8080
error AccessManagerNotReady(bytes32 operationId);
8181
error AccessManagerExpired(bytes32 operationId);
8282
error AccessManagerLockedRole(uint64 roleId);
83+
error AccessManagerLockedFunction(bytes4 selector);
8384
error AccessManagerBadConfirmation();
8485
error AccessManagerUnauthorizedAccount(address msgsender, uint64 roleId);
8586
error AccessManagerUnauthorizedCall(address caller, address target, bytes4 selector);

test/access/manager/AccessManager.behavior.js

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,15 @@ function shouldBehaveLikeDelayedAdminOperation() {
5757
*/
5858
function shouldBehaveLikeNotDelayedAdminOperation() {
5959
const getAccessPath = LIKE_COMMON_GET_ACCESS;
60-
61-
function testScheduleOperation(mineDelay) {
62-
return function self() {
63-
self.mineDelay = mineDelay;
64-
beforeEach('set execution delay', async function () {
65-
this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
66-
});
67-
testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
68-
};
69-
}
70-
60+
testAsDelayedOperation.mineDelay = true;
7161
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay =
72-
testScheduleOperation(true);
73-
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = testScheduleOperation(false);
62+
testAsDelayedOperation;
63+
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () {
64+
beforeEach('set execution delay', async function () {
65+
this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
66+
});
67+
testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
68+
};
7469

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

test/access/manager/AccessManager.predicate.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ function testAsDelayedOperation() {
275275
describe('when operation delay is greater than execution delay', function () {
276276
beforeEach('set operation delay', async function () {
277277
this.operationDelay = this.executionDelay + time.duration.hours(1);
278-
await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
278+
await this.manager.$_setTargetAdminDelay(this.operationDelayTarget ?? this.target, this.operationDelay);
279279
this.scheduleIn = this.operationDelay; // For testAsSchedulableOperation
280280
});
281281

@@ -285,7 +285,7 @@ function testAsDelayedOperation() {
285285
describe('when operation delay is shorter than execution delay', function () {
286286
beforeEach('set operation delay', async function () {
287287
this.operationDelay = this.executionDelay - time.duration.hours(1);
288-
await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
288+
await this.manager.$_setTargetAdminDelay(this.operationDelayTarget ?? this.target, this.operationDelay);
289289
this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
290290
});
291291

@@ -296,7 +296,7 @@ function testAsDelayedOperation() {
296296
describe('without operation delay', function () {
297297
beforeEach('set operation delay', async function () {
298298
this.operationDelay = 0n;
299-
await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay);
299+
await this.manager.$_setTargetAdminDelay(this.operationDelayTarget ?? this.target, this.operationDelay);
300300
this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
301301
});
302302

test/access/manager/AccessManager.test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,7 @@ describe('AccessManager', function () {
10631063

10641064
describe('restrictions', function () {
10651065
beforeEach('set method and args', function () {
1066+
this.operationDelayTarget = this.newManagedTarget;
10661067
this.calldata = this.manager.interface.encodeFunctionData('updateAuthority(address,address)', [
10671068
this.newManagedTarget.target,
10681069
this.newAuthority.target,
@@ -1086,6 +1087,7 @@ describe('AccessManager', function () {
10861087
describe('#setTargetClosed', function () {
10871088
describe('restrictions', function () {
10881089
beforeEach('set method and args', function () {
1090+
this.operationDelayTarget = this.other;
10891091
const args = [this.other.address, true];
10901092
const method = this.manager.interface.getFunction('setTargetClosed(address,bool)');
10911093
this.calldata = this.manager.interface.encodeFunctionData(method, args);
@@ -1124,6 +1126,7 @@ describe('AccessManager', function () {
11241126
describe('#setTargetFunctionRole', function () {
11251127
describe('restrictions', function () {
11261128
beforeEach('set method and args', function () {
1129+
this.operationDelayTarget = this.other;
11271130
const args = [this.other.address, ['0x12345678'], 443342];
11281131
const method = this.manager.interface.getFunction('setTargetFunctionRole(address,bytes4[],uint64)');
11291132
this.calldata = this.manager.interface.encodeFunctionData(method, args);
@@ -1162,6 +1165,15 @@ describe('AccessManager', function () {
11621165
);
11631166
}
11641167
});
1168+
1169+
it('reverts setting a function role for the setAuthority selector', async function () {
1170+
const { selector } = this.target.interface.getFunction('setAuthority');
1171+
await expect(
1172+
this.manager.connect(this.admin).$_setTargetFunctionRole(this.target, selector, this.roles.ADMIN.id),
1173+
)
1174+
.to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedFunction')
1175+
.withArgs(selector);
1176+
});
11651177
});
11661178

11671179
describe('role admin operations', function () {
@@ -2240,6 +2252,41 @@ describe('AccessManager', function () {
22402252
.to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled')
22412253
.withArgs(operationId);
22422254
});
2255+
2256+
it('can execute a setAuthority call when no target admin delay is set', async function () {
2257+
const newAuthority = await ethers.deployContract('$AccessManager', [this.admin]);
2258+
2259+
await expect(this.manager.getTargetAdminDelay(this.target)).to.eventually.equal(0n);
2260+
2261+
await expect(
2262+
this.manager
2263+
.connect(this.admin)
2264+
.execute(this.target, this.target.interface.encodeFunctionData('setAuthority', [newAuthority.target])),
2265+
)
2266+
.to.emit(this.target, 'AuthorityUpdated')
2267+
.withArgs(newAuthority);
2268+
});
2269+
2270+
it('cannot execute a setAuthority call when a target admin delay is set', async function () {
2271+
const newAuthority = await ethers.deployContract('$AccessManager', [this.admin]);
2272+
2273+
await this.manager.$_setTargetAdminDelay(this.target, 10n);
2274+
await time.increaseBy.timestamp(5n * 86400n, true); // minSetBack is 5 days
2275+
2276+
await expect(this.manager.getTargetAdminDelay(this.target)).to.eventually.equal(10n);
2277+
2278+
// cannot update directly - there is a delay
2279+
await expect(
2280+
this.manager.connect(this.admin).updateAuthority(this.target, newAuthority),
2281+
).to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled');
2282+
2283+
// cannot bypass via execute either - targetAdminDelay is enforced for setAuthority
2284+
await expect(
2285+
this.manager
2286+
.connect(this.admin)
2287+
.execute(this.target, this.target.interface.encodeFunctionData('setAuthority', [newAuthority.target])),
2288+
).to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled');
2289+
});
22432290
});
22442291

22452292
describe('#consumeScheduledOp', function () {

0 commit comments

Comments
 (0)