Refactor restriction mechanism in AccessManager to enable enforce executionDelay (#4518)
Co-authored-by: Francisco Giordano <fg@frang.io>
This commit is contained in:
2
.github/workflows/checks.yml
vendored
2
.github/workflows/checks.yml
vendored
@ -42,6 +42,7 @@ jobs:
|
||||
run: npm run test:generation
|
||||
- name: Compare gas costs
|
||||
uses: ./.github/actions/gas-compare
|
||||
if: github.base_ref == 'master'
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
|
||||
@ -63,6 +64,7 @@ jobs:
|
||||
run: npm run test:inheritance
|
||||
- name: Check storage layout
|
||||
uses: ./.github/actions/storage-layout
|
||||
if: github.base_ref == 'master'
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import {IAccessManaged} from "./IAccessManaged.sol";
|
||||
import {Address} from "../../utils/Address.sol";
|
||||
import {Context} from "../../utils/Context.sol";
|
||||
import {Multicall} from "../../utils/Multicall.sol";
|
||||
import {Math} from "../../utils/math/Math.sol";
|
||||
import {Time} from "../../utils/types/Time.sol";
|
||||
|
||||
/**
|
||||
@ -95,20 +96,11 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
bytes32 private _relayIdentifier;
|
||||
|
||||
/**
|
||||
* @dev Check that the caller has a given permission level (`groupId`). Note that this does NOT consider execution
|
||||
* delays that may be associated to that group.
|
||||
* @dev Check that the caller is authorized to perform the operation, following the restrictions encoded in
|
||||
* {_getAdminRestrictions}.
|
||||
*/
|
||||
modifier onlyGroup(uint64 groupId) {
|
||||
_checkGroup(groupId);
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check that the caller is an admin and that the top-level function currently executing has been scheduled
|
||||
* sufficiently ahead of time, if necessary according to admin delays.
|
||||
*/
|
||||
modifier withFamilyDelay(uint64 familyId) {
|
||||
_checkFamilyDelay(familyId);
|
||||
modifier onlyAuthorized() {
|
||||
_checkAuthorized();
|
||||
_;
|
||||
}
|
||||
|
||||
@ -145,7 +137,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
} else {
|
||||
uint64 groupId = getFamilyFunctionGroup(familyId, selector);
|
||||
(bool inGroup, uint32 currentDelay) = hasGroup(groupId, caller);
|
||||
return (inGroup && currentDelay == 0, currentDelay);
|
||||
return inGroup ? (currentDelay == 0, currentDelay) : (false, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,7 +242,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {GroupLabel} event.
|
||||
*/
|
||||
function labelGroup(uint64 groupId, string calldata label) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
function labelGroup(uint64 groupId, string calldata label) public virtual onlyAuthorized {
|
||||
if (groupId == ADMIN_GROUP || groupId == PUBLIC_GROUP) {
|
||||
revert AccessManagerLockedGroup(groupId);
|
||||
}
|
||||
@ -270,11 +262,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {GroupGranted} event
|
||||
*/
|
||||
function grantGroup(
|
||||
uint64 groupId,
|
||||
address account,
|
||||
uint32 executionDelay
|
||||
) public virtual onlyGroup(getGroupAdmin(groupId)) {
|
||||
function grantGroup(uint64 groupId, address account, uint32 executionDelay) public virtual onlyAuthorized {
|
||||
_grantGroup(groupId, account, getGroupGrantDelay(groupId), executionDelay);
|
||||
}
|
||||
|
||||
@ -287,7 +275,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {GroupRevoked} event
|
||||
*/
|
||||
function revokeGroup(uint64 groupId, address account) public virtual onlyGroup(getGroupAdmin(groupId)) {
|
||||
function revokeGroup(uint64 groupId, address account) public virtual onlyAuthorized {
|
||||
_revokeGroup(groupId, account);
|
||||
}
|
||||
|
||||
@ -319,11 +307,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {GroupExecutionDelayUpdated} event
|
||||
*/
|
||||
function setExecuteDelay(
|
||||
uint64 groupId,
|
||||
address account,
|
||||
uint32 newDelay
|
||||
) public virtual onlyGroup(getGroupAdmin(groupId)) {
|
||||
function setExecuteDelay(uint64 groupId, address account, uint32 newDelay) public virtual onlyAuthorized {
|
||||
_setExecuteDelay(groupId, account, newDelay);
|
||||
}
|
||||
|
||||
@ -336,7 +320,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {GroupAdminChanged} event
|
||||
*/
|
||||
function setGroupAdmin(uint64 groupId, uint64 admin) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
function setGroupAdmin(uint64 groupId, uint64 admin) public virtual onlyAuthorized {
|
||||
_setGroupAdmin(groupId, admin);
|
||||
}
|
||||
|
||||
@ -349,7 +333,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {GroupGuardianChanged} event
|
||||
*/
|
||||
function setGroupGuardian(uint64 groupId, uint64 guardian) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
function setGroupGuardian(uint64 groupId, uint64 guardian) public virtual onlyAuthorized {
|
||||
_setGroupGuardian(groupId, guardian);
|
||||
}
|
||||
|
||||
@ -362,7 +346,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {GroupGrantDelayChanged} event
|
||||
*/
|
||||
function setGrantDelay(uint64 groupId, uint32 newDelay) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
function setGrantDelay(uint64 groupId, uint32 newDelay) public virtual onlyAuthorized {
|
||||
_setGrantDelay(groupId, newDelay);
|
||||
}
|
||||
|
||||
@ -482,7 +466,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
uint64 familyId,
|
||||
bytes4[] calldata selectors,
|
||||
uint64 groupId
|
||||
) public virtual onlyGroup(ADMIN_GROUP) withFamilyDelay(familyId) {
|
||||
) public virtual onlyAuthorized {
|
||||
for (uint256 i = 0; i < selectors.length; ++i) {
|
||||
_setFamilyFunctionGroup(familyId, selectors[i], groupId);
|
||||
}
|
||||
@ -508,7 +492,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {FunctionAllowedGroupUpdated} event per selector
|
||||
*/
|
||||
function setFamilyAdminDelay(uint64 familyId, uint32 newDelay) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
function setFamilyAdminDelay(uint64 familyId, uint32 newDelay) public virtual onlyAuthorized {
|
||||
_setFamilyAdminDelay(familyId, newDelay);
|
||||
}
|
||||
|
||||
@ -544,10 +528,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {ContractFamilyUpdated} event.
|
||||
*/
|
||||
function setContractFamily(
|
||||
address target,
|
||||
uint64 familyId
|
||||
) public virtual onlyGroup(ADMIN_GROUP) withFamilyDelay(_getContractFamilyId(target)) {
|
||||
function setContractFamily(address target, uint64 familyId) public virtual onlyAuthorized {
|
||||
_setContractFamily(target, familyId);
|
||||
}
|
||||
|
||||
@ -570,7 +551,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits a {ContractClosed} event.
|
||||
*/
|
||||
function setContractClosed(address target, bool closed) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
function setContractClosed(address target, bool closed) public virtual onlyAuthorized {
|
||||
_setContractClosed(target, closed);
|
||||
}
|
||||
|
||||
@ -636,6 +617,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* Emits an {OperationExecuted} event only if the call was scheduled and delayed.
|
||||
*/
|
||||
// Reentrancy is not an issue because permissions are checked on msg.sender. Additionally,
|
||||
// _consumeScheduledOp guarantees a scheduled operation is only executed once.
|
||||
// slither-disable-next-line reentrancy-no-eth
|
||||
function relay(address target, bytes calldata data) public payable virtual {
|
||||
address caller = _msgSender();
|
||||
|
||||
@ -716,11 +700,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
revert AccessManagerNotScheduled(operationId);
|
||||
} else if (caller != msgsender) {
|
||||
// calls can only be canceled by the account that scheduled them, a global admin, or by a guardian of the required group.
|
||||
(uint64 familyId, ) = getContractFamily(target);
|
||||
(bool isAdmin, ) = hasGroup(ADMIN_GROUP, msgsender);
|
||||
(bool isGuardian, ) = hasGroup(
|
||||
getGroupGuardian(getFamilyFunctionGroup(_getContractFamilyId(target), selector)),
|
||||
msgsender
|
||||
);
|
||||
(bool isGuardian, ) = hasGroup(getGroupGuardian(getFamilyFunctionGroup(familyId, selector)), msgsender);
|
||||
if (!isAdmin && !isGuardian) {
|
||||
revert AccessManagerCannotCancel(msgsender, caller, target, selector);
|
||||
}
|
||||
@ -753,56 +735,96 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*/
|
||||
function updateAuthority(
|
||||
address target,
|
||||
address newAuthority
|
||||
) public virtual onlyGroup(ADMIN_GROUP) withFamilyDelay(_getContractFamilyId(target)) {
|
||||
function updateAuthority(address target, address newAuthority) public virtual onlyAuthorized {
|
||||
IAccessManaged(target).setAuthority(newAuthority);
|
||||
}
|
||||
|
||||
// =================================================== HELPERS ====================================================
|
||||
function _checkGroup(uint64 groupId) internal view virtual {
|
||||
address account = _msgSender();
|
||||
(bool inGroup, ) = hasGroup(groupId, account);
|
||||
if (!inGroup) {
|
||||
revert AccessManagerUnauthorizedAccount(account, groupId);
|
||||
// ================================================= ADMIN LOGIC ==================================================
|
||||
/**
|
||||
* @dev Check if the current call is authorized according to admin logic.
|
||||
*/
|
||||
function _checkAuthorized() private {
|
||||
address caller = _msgSender();
|
||||
(bool allowed, uint32 delay) = _canCallExtended(caller, address(this), _msgData());
|
||||
if (!allowed) {
|
||||
if (delay == 0) {
|
||||
(, uint64 requiredGroup, ) = _getAdminRestrictions(_msgData());
|
||||
revert AccessManagerUnauthorizedAccount(caller, requiredGroup);
|
||||
} else {
|
||||
_consumeScheduledOp(_hashOperation(caller, address(this), _msgData()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _checkFamilyDelay(uint64 familyId) internal virtual {
|
||||
uint32 delay = getFamilyAdminDelay(familyId);
|
||||
if (delay > 0) {
|
||||
_consumeScheduledOp(_hashOperation(_msgSender(), address(this), _msgData()));
|
||||
}
|
||||
}
|
||||
|
||||
function _getContractFamilyId(address target) private view returns (uint64 familyId) {
|
||||
(familyId, ) = getContractFamily(target);
|
||||
}
|
||||
|
||||
function _parseFamilyOperation(bytes calldata data) private view returns (bool, uint64) {
|
||||
/**
|
||||
* @dev Get the admin restrictions of a given function call based on the function and arguments involved.
|
||||
*/
|
||||
function _getAdminRestrictions(bytes calldata data) private view returns (bool, uint64, uint32) {
|
||||
bytes4 selector = bytes4(data);
|
||||
|
||||
if (selector == this.updateAuthority.selector || selector == this.setContractFamily.selector) {
|
||||
return (true, _getContractFamilyId(abi.decode(data[0x04:0x24], (address))));
|
||||
// First argument is a target. Restricted to ADMIN with the family delay corresponding to the target's family
|
||||
address target = abi.decode(data[0x04:0x24], (address));
|
||||
(uint64 familyId, ) = getContractFamily(target);
|
||||
uint32 delay = getFamilyAdminDelay(familyId);
|
||||
return (true, ADMIN_GROUP, delay);
|
||||
} else if (selector == this.setFamilyFunctionGroup.selector) {
|
||||
return (true, abi.decode(data[0x04:0x24], (uint64)));
|
||||
// First argument is a family. Restricted to ADMIN with the family delay corresponding to the family
|
||||
uint64 familyId = abi.decode(data[0x04:0x24], (uint64));
|
||||
uint32 delay = getFamilyAdminDelay(familyId);
|
||||
return (true, ADMIN_GROUP, delay);
|
||||
} else if (
|
||||
selector == this.labelGroup.selector ||
|
||||
selector == this.setGroupAdmin.selector ||
|
||||
selector == this.setGroupGuardian.selector ||
|
||||
selector == this.setGrantDelay.selector ||
|
||||
selector == this.setFamilyAdminDelay.selector ||
|
||||
selector == this.setContractClosed.selector
|
||||
) {
|
||||
// Restricted to ADMIN with no delay beside any execution delay the caller may have
|
||||
return (true, ADMIN_GROUP, 0);
|
||||
} else if (
|
||||
selector == this.grantGroup.selector ||
|
||||
selector == this.revokeGroup.selector ||
|
||||
selector == this.setExecuteDelay.selector
|
||||
) {
|
||||
// First argument is a groupId. Restricted to that group's admin with no delay beside any execution delay the caller may have.
|
||||
uint64 groupId = abi.decode(data[0x04:0x24], (uint64));
|
||||
uint64 groupAdminId = getGroupAdmin(groupId);
|
||||
return (true, groupAdminId, 0);
|
||||
} else {
|
||||
return (false, 0);
|
||||
return (false, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================== HELPERS ====================================================
|
||||
/**
|
||||
* @dev An extended version of {canCall} for internal use that considers restrictions for admin functions.
|
||||
*/
|
||||
function _canCallExtended(address caller, address target, bytes calldata data) private view returns (bool, uint32) {
|
||||
if (target == address(this)) {
|
||||
(bool isFamilyOperation, uint64 familyId) = _parseFamilyOperation(data);
|
||||
uint32 delay = getFamilyAdminDelay(familyId);
|
||||
(bool inGroup, ) = hasGroup(ADMIN_GROUP, caller);
|
||||
return (inGroup && isFamilyOperation && delay == 0, delay);
|
||||
(bool enabled, uint64 groupId, uint32 operationDelay) = _getAdminRestrictions(data);
|
||||
if (!enabled) {
|
||||
return (false, 0);
|
||||
}
|
||||
|
||||
(bool inGroup, uint32 executionDelay) = hasGroup(groupId, caller);
|
||||
if (!inGroup) {
|
||||
return (false, 0);
|
||||
}
|
||||
|
||||
// downcast is safe because both options are uint32
|
||||
uint32 delay = uint32(Math.max(operationDelay, executionDelay));
|
||||
return (delay == 0, delay);
|
||||
} else {
|
||||
bytes4 selector = bytes4(data);
|
||||
return canCall(caller, target, selector);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns true if a schedule timepoint is past its expiration deadline.
|
||||
*/
|
||||
function _isExpired(uint48 timepoint) private view returns (bool) {
|
||||
return timepoint + expiration() <= Time.timestamp();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user