Access Manager (#4416)
parent
8a92fb82ea
commit
bf5786aae0
@ -0,0 +1,99 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {IAuthority, safeCanCall} from "./IAuthority.sol"; |
||||
import {IManaged} from "./IManaged.sol"; |
||||
import {Context} from "../../utils/Context.sol"; |
||||
|
||||
/** |
||||
* @dev This contract module makes available a {restricted} modifier. Functions decorated with this modifier will be |
||||
* permissioned according to an "authority": a contract like {AccessManager} that follows the {IAuthority} interface, |
||||
* implementing a policy that allows certain callers to access certain functions. |
||||
* |
||||
* IMPORTANT: The `restricted` modifier should never be used on `internal` functions, judiciously used in `public` |
||||
* functions, and ideally only used in `external` functions. See {restricted}. |
||||
*/ |
||||
abstract contract AccessManaged is Context, IManaged { |
||||
address private _authority; |
||||
|
||||
/** |
||||
* @dev Initializes the contract connected to an initial authority. |
||||
*/ |
||||
constructor(address initialAuthority) { |
||||
_setAuthority(initialAuthority); |
||||
} |
||||
|
||||
/** |
||||
* @dev Restricts access to a function as defined by the connected Authority for this contract and the |
||||
* caller and selector of the function that entered the contract. |
||||
* |
||||
* [IMPORTANT] |
||||
* ==== |
||||
* In general, this modifier should only be used on `external` functions. It is okay to use it on `public` functions |
||||
* that are used as external entry points and are not called internally. Unless you know what you're doing, it |
||||
* should never be used on `internal` functions. Failure to follow these rules can have critical security |
||||
* implications! This is because the permissions are determined by the function that entered the contract, i.e. the |
||||
* function at the bottom of the call stack, and not the function where the modifier is visible in the source code. |
||||
* ==== |
||||
* |
||||
* [NOTE] |
||||
* ==== |
||||
* Selector collisions are mitigated by scoping permissions per contract, but some edge cases must be considered: |
||||
* |
||||
* * If the https://docs.soliditylang.org/en/latest/contracts.html#receive-ether-function[`receive()`] function is restricted, |
||||
* any other function with a `0x00000000` selector will share permissions with `receive()`. |
||||
* * Similarly, if there's no `receive()` function but a `fallback()` instead, the fallback might be called with empty `calldata`, |
||||
* sharing the `0x00000000` selector permissions as well. |
||||
* * For any other selector, if the restricted function is set on an upgradeable contract, an upgrade may remove the restricted |
||||
* function and replace it with a new method whose selector replaces the last one, keeping the previous permissions. |
||||
* ==== |
||||
*/ |
||||
modifier restricted() { |
||||
_checkCanCall(_msgSender(), address(this), msg.sig); |
||||
_; |
||||
} |
||||
|
||||
/** |
||||
* @dev Returns the current authority. |
||||
*/ |
||||
function authority() public view virtual returns (address) { |
||||
return _authority; |
||||
} |
||||
|
||||
/** |
||||
* @dev Transfers control to a new authority. The caller must be the current authority. |
||||
*/ |
||||
function setAuthority(address newAuthority) public virtual { |
||||
address caller = _msgSender(); |
||||
if (caller != authority()) { |
||||
revert AccessManagedUnauthorized(caller); |
||||
} |
||||
if (newAuthority.code.length == 0) { |
||||
revert AccessManagedInvalidAuthority(newAuthority); |
||||
} |
||||
_setAuthority(newAuthority); |
||||
} |
||||
|
||||
/** |
||||
* @dev Transfers control to a new authority. Internal function with no access restriction. |
||||
*/ |
||||
function _setAuthority(address newAuthority) internal virtual { |
||||
_authority = newAuthority; |
||||
emit AuthorityUpdated(newAuthority); |
||||
} |
||||
|
||||
/** |
||||
* @dev Reverts if the caller is not allowed to call the function identified by a selector. |
||||
*/ |
||||
function _checkCanCall(address caller, address target, bytes4 selector) internal view virtual { |
||||
(bool allowed, uint32 delay) = safeCanCall(authority(), caller, target, selector); |
||||
if (!allowed) { |
||||
if (delay > 0) { |
||||
revert AccessManagedRequiredDelay(caller, delay); |
||||
} else { |
||||
revert AccessManagedUnauthorized(caller); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,616 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {IAccessManager} from "./IAccessManager.sol"; |
||||
import {IManaged} from "./IManaged.sol"; |
||||
import {IAuthority} from "./IAuthority.sol"; |
||||
import {AccessManagedAdapter} from "./utils/AccessManagedAdapter.sol"; |
||||
import {Address} from "../../utils/Address.sol"; |
||||
import {Context} from "../../utils/Context.sol"; |
||||
import {Multicall} from "../../utils/Multicall.sol"; |
||||
import {Time} from "../../utils/types/Time.sol"; |
||||
|
||||
/** |
||||
* @dev AccessManager is a central contract to store the permissions of a system. |
||||
* |
||||
* The smart contracts under the control of an AccessManager instance will have a set of "restricted" functions, and the |
||||
* exact details of how access is restricted for each of those functions is configurable by the admins of the instance. |
||||
* These restrictions are expressed in terms of "groups". |
||||
* |
||||
* An AccessManager instance will define a set of groups. Accounts can be added into any number of these groups. Each of |
||||
* them defines a role, and may confer access to some of the restricted functions in the system, as configured by admins |
||||
* through the use of {setFunctionAllowedGroup}. |
||||
* |
||||
* Note that a function in a target contract may become permissioned in this way only when: 1) said contract is |
||||
* {AccessManaged} and is connected to this contract as its manager, and 2) said function is decorated with the |
||||
* `restricted` modifier. |
||||
* |
||||
* There is a special group defined by default named "public" which all accounts automatically have. |
||||
* |
||||
* Contracts where functions are mapped to groups are said to be in a "custom" mode, but contracts can also be |
||||
* configured in two special modes: 1) the "open" mode, where all functions are allowed to the "public" group, and 2) |
||||
* the "closed" mode, where no function is allowed to any group. |
||||
* |
||||
* Since all the permissions of the managed system can be modified by the admins of this instance, it is expected that |
||||
* they will be highly secured (e.g., a multisig or a well-configured DAO). |
||||
* |
||||
* NOTE: This contract implements a form of the {IAuthority} interface, but {canCall} has additional return data so it |
||||
* doesn't inherit `IAuthority`. It is however compatible with the `IAuthority` interface since the first 32 bytes of |
||||
* the return data are a boolean as expected by that interface. |
||||
*/ |
||||
contract AccessManager is Context, Multicall, IAccessManager { |
||||
using Time for *; |
||||
|
||||
uint256 public constant ADMIN_GROUP = type(uint256).min; // 0 |
||||
uint256 public constant PUBLIC_GROUP = type(uint256).max; // 2**256-1 |
||||
|
||||
mapping(address target => AccessMode mode) private _contractMode; |
||||
mapping(address target => mapping(bytes4 selector => uint256 groupId)) private _allowedGroups; |
||||
mapping(uint256 groupId => Group) private _groups; |
||||
mapping(bytes32 operationId => uint48 schedule) private _schedules; |
||||
|
||||
// This should be transcient storage when supported by the EVM. |
||||
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. |
||||
*/ |
||||
modifier onlyGroup(uint256 groupId) { |
||||
address msgsender = _msgSender(); |
||||
if (!hasGroup(groupId, msgsender)) { |
||||
revert AccessControlUnauthorizedAccount(msgsender, groupId); |
||||
} |
||||
_; |
||||
} |
||||
|
||||
constructor(address initialAdmin) { |
||||
// admin is active immediately and without any execution delay. |
||||
_grantGroup(ADMIN_GROUP, initialAdmin, 0, 0); |
||||
} |
||||
|
||||
// =================================================== GETTERS ==================================================== |
||||
/** |
||||
* @dev Check if an address (`caller`) is authorised to call a given function on a given contract directly (with |
||||
* no restriction). Additionally, it returns the delay needed to perform the call indirectly through the {schedule} |
||||
* & {relay} workflow. |
||||
* |
||||
* This function is usually called by the targeted contract to control immediate execution of restricted functions. |
||||
* Therefore we only return true is the call can be performed without any delay. If the call is subject to a delay, |
||||
* then the function should return false, and the caller should schedule the operation for future execution. |
||||
* |
||||
* We may be able to hash the operation, and check if the call was scheduled, but we would not be able to cleanup |
||||
* the schedule, leaving the possibility of multiple executions. Maybe this function should not be view? |
||||
* |
||||
* NOTE: The IAuthority interface does not include the `uint32` delay. This is an extension of that interface that |
||||
* is backward compatible. Some contract may thus ignore the second return argument. In that case they will fail |
||||
* to identify the indirect workflow, and will consider call that require a delay to be forbidden. |
||||
*/ |
||||
function canCall(address caller, address target, bytes4 selector) public view virtual returns (bool, uint32) { |
||||
AccessMode mode = getContractMode(target); |
||||
if (mode == AccessMode.Open) { |
||||
return (true, 0); |
||||
} else if (mode == AccessMode.Closed) { |
||||
return (false, 0); |
||||
} else if (caller == address(this)) { |
||||
// Caller is AccessManager => call was relayed. In that case the relay already checked permissions. We |
||||
// verify that the call "identifier", which is set during the relay call, is correct. |
||||
return (_relayIdentifier == keccak256(abi.encodePacked(target, selector)), 0); |
||||
} else { |
||||
uint256 groupId = getFunctionAllowedGroup(target, selector); |
||||
bool inGroup = hasGroup(groupId, caller); |
||||
uint32 executeDelay = inGroup ? getAccess(groupId, caller).delay.get() : 0; |
||||
return (inGroup && executeDelay == 0, executeDelay); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the mode under which a contract is operating. |
||||
*/ |
||||
function getContractMode(address target) public view virtual returns (AccessMode) { |
||||
return _contractMode[target]; |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the permission level (group) required to call a function. This only applies for contract that are |
||||
* operating under the `Custom` mode. |
||||
*/ |
||||
function getFunctionAllowedGroup(address target, bytes4 selector) public view virtual returns (uint256) { |
||||
return _allowedGroups[target][selector]; |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the id of the group that acts as an admin for given group. |
||||
* |
||||
* The admin permission is required to grant the group, revoke the group and update the execution delay to execute |
||||
* an operation that is restricted to this group. |
||||
*/ |
||||
function getGroupAdmin(uint256 groupId) public view virtual returns (uint256) { |
||||
return _groups[groupId].admin; |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the group that acts as a guardian for a given group. |
||||
* |
||||
* The guardian permission allows canceling operations that have been scheduled under the group. |
||||
*/ |
||||
function getGroupGuardian(uint256 groupId) public view virtual returns (uint256) { |
||||
return _groups[groupId].guardian; |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the group current grant delay, that value may change at any point, without an event emitted, following |
||||
* a call to {setGrantDelay}. Changes to this value, including effect timepoint are notified by the |
||||
* {GroupGrantDelayChanged} event. |
||||
*/ |
||||
function getGroupGrantDelay(uint256 groupId) public view virtual returns (uint32) { |
||||
return _groups[groupId].delay.get(); |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the access details for a given account in a given group. These details include the timepoint at which |
||||
* membership becomes active, and the delay applied to all operation by this user that require this permission |
||||
* level. |
||||
*/ |
||||
function getAccess(uint256 groupId, address account) public view virtual returns (Access memory) { |
||||
return _groups[groupId].members[account]; |
||||
} |
||||
|
||||
/** |
||||
* @dev Check if a given account currently had the permission level corresponding to a given group. Note that this |
||||
* permission might be associated with a delay. {getAccess} can provide more details. |
||||
*/ |
||||
function hasGroup(uint256 groupId, address account) public view virtual returns (bool) { |
||||
return groupId == PUBLIC_GROUP || getAccess(groupId, account).since.isSetAndPast(Time.timestamp()); |
||||
} |
||||
|
||||
// =============================================== GROUP MANAGEMENT =============================================== |
||||
/** |
||||
* @dev Give a label to a group, for improved group discoverabily by UIs. |
||||
* |
||||
* Emits a {GroupLabel} event. |
||||
*/ |
||||
function labelGroup(uint256 groupId, string calldata label) public virtual onlyGroup(ADMIN_GROUP) { |
||||
emit GroupLabel(groupId, label); |
||||
} |
||||
|
||||
/** |
||||
* @dev Give permission to an account to execute function restricted to a group. Optionally, a delay can be |
||||
* enforced for any function call, byt this user, that require this level of permission. This call is only |
||||
* effective after a grant delay that is specific to the group being granted. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be in the group's admins |
||||
* |
||||
* Emits a {GroupGranted} event |
||||
*/ |
||||
function grantGroup( |
||||
uint256 groupId, |
||||
address account, |
||||
uint32 executionDelay |
||||
) public virtual onlyGroup(getGroupAdmin(groupId)) { |
||||
_grantGroup(groupId, account, getGroupGrantDelay(groupId), executionDelay); |
||||
} |
||||
|
||||
/** |
||||
* @dev Remove an account for a group, with immediate effect. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be in the group's admins |
||||
* |
||||
* Emits a {GroupRevoked} event |
||||
*/ |
||||
function revokeGroup(uint256 groupId, address account) public virtual onlyGroup(getGroupAdmin(groupId)) { |
||||
_revokeGroup(groupId, account); |
||||
} |
||||
|
||||
/** |
||||
* @dev Renounce group permissions for the calling account, with immediate effect. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be `callerConfirmation`. |
||||
* |
||||
* Emits a {GroupRevoked} event |
||||
*/ |
||||
function renounceGroup(uint256 groupId, address callerConfirmation) public virtual { |
||||
if (callerConfirmation != _msgSender()) { |
||||
revert AccessManagerBadConfirmation(); |
||||
} |
||||
_revokeGroup(groupId, callerConfirmation); |
||||
} |
||||
|
||||
/** |
||||
* @dev Set the execution delay for a given account in a given group. This update is not immediate and follows the |
||||
* delay rules. For example, If a user currently has a delay of 3 hours, and this is called to reduce that delay to |
||||
* 1 hour, the new delay will take some time to take effect, enforcing that any operation executed in the 3 hours |
||||
* that follows this update was indeed scheduled before this update. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be in the group's admins |
||||
* |
||||
* Emits a {GroupExecutionDelayUpdate} event |
||||
*/ |
||||
function setExecuteDelay( |
||||
uint256 groupId, |
||||
address account, |
||||
uint32 newDelay |
||||
) public virtual onlyGroup(getGroupAdmin(groupId)) { |
||||
_setExecuteDelay(groupId, account, newDelay); |
||||
} |
||||
|
||||
/** |
||||
* @dev Change admin group for a given group. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be a global admin |
||||
* |
||||
* Emits a {GroupAdminChanged} event |
||||
*/ |
||||
function setGroupAdmin(uint256 groupId, uint256 admin) public virtual onlyGroup(ADMIN_GROUP) { |
||||
_setGroupAdmin(groupId, admin); |
||||
} |
||||
|
||||
/** |
||||
* @dev Change guardian group for a given group. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be a global admin |
||||
* |
||||
* Emits a {GroupGuardianChanged} event |
||||
*/ |
||||
function setGroupGuardian(uint256 groupId, uint256 guardian) public virtual onlyGroup(ADMIN_GROUP) { |
||||
_setGroupGuardian(groupId, guardian); |
||||
} |
||||
|
||||
/** |
||||
* @dev Update the . |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be a global admin |
||||
* |
||||
* Emits a {GroupGrantDelayChanged} event |
||||
*/ |
||||
function setGrantDelay(uint256 groupId, uint32 newDelay) public virtual onlyGroup(ADMIN_GROUP) { |
||||
_setGrantDelay(groupId, newDelay); |
||||
} |
||||
|
||||
/** |
||||
* @dev Internal version of {grantGroup} without access control. |
||||
* |
||||
* Emits a {GroupGranted} event |
||||
*/ |
||||
function _grantGroup(uint256 groupId, address account, uint32 grantDelay, uint32 executionDelay) internal virtual { |
||||
if (groupId == PUBLIC_GROUP) { |
||||
revert AccessManagerLockedGroup(groupId); |
||||
} else if (_groups[groupId].members[account].since != 0) { |
||||
revert AccessManagerAcountAlreadyInGroup(groupId, account); |
||||
} |
||||
|
||||
uint48 since = Time.timestamp() + grantDelay; |
||||
_groups[groupId].members[account] = Access({since: since, delay: executionDelay.toDelay()}); |
||||
|
||||
emit GroupGranted(groupId, account, since, executionDelay); |
||||
} |
||||
|
||||
/** |
||||
* @dev Internal version of {revokeGroup} without access control. This logic is also used by {renounceGroup}. |
||||
* |
||||
* Emits a {GroupRevoked} event |
||||
*/ |
||||
function _revokeGroup(uint256 groupId, address account) internal virtual { |
||||
if (groupId == PUBLIC_GROUP) { |
||||
revert AccessManagerLockedGroup(groupId); |
||||
} else if (_groups[groupId].members[account].since == 0) { |
||||
revert AccessManagerAcountNotInGroup(groupId, account); |
||||
} |
||||
|
||||
delete _groups[groupId].members[account]; |
||||
|
||||
emit GroupRevoked(groupId, account); |
||||
} |
||||
|
||||
/** |
||||
* @dev Internal version of {setExecuteDelay} without access control. |
||||
* |
||||
* Emits a {GroupExecutionDelayUpdate} event |
||||
*/ |
||||
function _setExecuteDelay(uint256 groupId, address account, uint32 newDuration) internal virtual { |
||||
if (groupId == PUBLIC_GROUP) { |
||||
revert AccessManagerLockedGroup(groupId); |
||||
} else if (_groups[groupId].members[account].since == 0) { |
||||
revert AccessManagerAcountNotInGroup(groupId, account); |
||||
} |
||||
|
||||
Time.Delay newDelay = _groups[groupId].members[account].delay.update(newDuration, 0); // TODO: minsetback ? |
||||
_groups[groupId].members[account].delay = newDelay; |
||||
|
||||
(, , uint48 effectPoint) = newDelay.split(); |
||||
emit GroupExecutionDelayUpdate(groupId, account, newDuration, effectPoint); |
||||
} |
||||
|
||||
/** |
||||
* @dev Internal version of {setGroupAdmin} without access control. |
||||
* |
||||
* Emits a {GroupAdminChanged} event |
||||
*/ |
||||
function _setGroupAdmin(uint256 groupId, uint256 admin) internal virtual { |
||||
if (groupId == ADMIN_GROUP || groupId == PUBLIC_GROUP) { |
||||
revert AccessManagerLockedGroup(groupId); |
||||
} |
||||
|
||||
_groups[groupId].admin = admin; |
||||
|
||||
emit GroupAdminChanged(groupId, admin); |
||||
} |
||||
|
||||
/** |
||||
* @dev Internal version of {setGroupGuardian} without access control. |
||||
* |
||||
* Emits a {GroupGuardianChanged} event |
||||
*/ |
||||
function _setGroupGuardian(uint256 groupId, uint256 guardian) internal virtual { |
||||
if (groupId == ADMIN_GROUP || groupId == PUBLIC_GROUP) { |
||||
revert AccessManagerLockedGroup(groupId); |
||||
} |
||||
|
||||
_groups[groupId].guardian = guardian; |
||||
|
||||
emit GroupGuardianChanged(groupId, guardian); |
||||
} |
||||
|
||||
/** |
||||
* @dev Internal version of {setGrantDelay} without access control. |
||||
* |
||||
* Emits a {GroupGrantDelayChanged} event |
||||
*/ |
||||
function _setGrantDelay(uint256 groupId, uint32 newDelay) internal virtual { |
||||
if (groupId == PUBLIC_GROUP) { |
||||
revert AccessManagerLockedGroup(groupId); |
||||
} |
||||
|
||||
Time.Delay updated = _groups[groupId].delay.update(newDelay, 0); // TODO: minsetback ? |
||||
_groups[groupId].delay = updated; |
||||
|
||||
(, , uint48 effect) = updated.split(); |
||||
emit GroupGrantDelayChanged(groupId, newDelay, effect); |
||||
} |
||||
|
||||
// ============================================= FUNCTION MANAGEMENT ============================================== |
||||
/** |
||||
* @dev Set the level of permission (`group`) required to call functions identified by the `selectors` in the |
||||
* `target` contract. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be a global admin |
||||
* |
||||
* Emits a {FunctionAllowedGroupUpdated} event per selector |
||||
*/ |
||||
function setFunctionAllowedGroup( |
||||
address target, |
||||
bytes4[] calldata selectors, |
||||
uint256 groupId |
||||
) public virtual onlyGroup(ADMIN_GROUP) { |
||||
// todo set delay or document risks |
||||
for (uint256 i = 0; i < selectors.length; ++i) { |
||||
_setFunctionAllowedGroup(target, selectors[i], groupId); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @dev Internal version of {setFunctionAllowedGroup} without access control. |
||||
* |
||||
* Emits a {FunctionAllowedGroupUpdated} event |
||||
*/ |
||||
function _setFunctionAllowedGroup(address target, bytes4 selector, uint256 groupId) internal virtual { |
||||
_allowedGroups[target][selector] = groupId; |
||||
emit FunctionAllowedGroupUpdated(target, selector, groupId); |
||||
} |
||||
|
||||
// =============================================== MODE MANAGEMENT ================================================ |
||||
/** |
||||
* @dev Set the operating mode of a contract to Custom. This enables the group mechanism for per-function access |
||||
* restriction and delay enforcement. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be a global admin |
||||
* |
||||
* Emits a {AccessModeUpdated} event. |
||||
*/ |
||||
function setContractModeCustom(address target) public virtual onlyGroup(ADMIN_GROUP) { |
||||
// todo set delay or document risks |
||||
_setContractMode(target, AccessMode.Custom); |
||||
} |
||||
|
||||
/** |
||||
* @dev Set the operating mode of a contract to Open. This allows anyone to call any `restricted()` function with |
||||
* no delay. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be a global admin |
||||
* |
||||
* Emits a {AccessModeUpdated} event. |
||||
*/ |
||||
function setContractModeOpen(address target) public virtual onlyGroup(ADMIN_GROUP) { |
||||
// todo set delay or document risks |
||||
_setContractMode(target, AccessMode.Open); |
||||
} |
||||
|
||||
/** |
||||
* @dev Set the operating mode of a contract to Close. This prevents anyone from calling any `restricted()` |
||||
* function. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be a global admin |
||||
* |
||||
* Emits a {AccessModeUpdated} event. |
||||
*/ |
||||
function setContractModeClosed(address target) public virtual onlyGroup(ADMIN_GROUP) { |
||||
// todo set delay or document risks |
||||
_setContractMode(target, AccessMode.Closed); |
||||
} |
||||
|
||||
/** |
||||
* @dev Set the operating mode of a contract. This is an internal setter with no access restrictions. |
||||
* |
||||
* Emits a {AccessModeUpdated} event. |
||||
*/ |
||||
function _setContractMode(address target, AccessMode mode) internal virtual { |
||||
_contractMode[target] = mode; |
||||
emit AccessModeUpdated(target, mode); |
||||
} |
||||
|
||||
// ============================================== DELAYED OPERATIONS ============================================== |
||||
/** |
||||
* @dev Return the timepoint at which a scheduled operation will be ready for execution. This returns 0 if the |
||||
* operation is not yet scheduled, was executed or was canceled. |
||||
*/ |
||||
function getSchedule(bytes32 id) public view virtual returns (uint48) { |
||||
return _schedules[id]; |
||||
} |
||||
|
||||
/** |
||||
* @dev Schedule a delayed operation, and return the operation identifier. |
||||
* |
||||
* Emits a {Scheduled} event. |
||||
*/ |
||||
function schedule(address target, bytes calldata data) public virtual returns (bytes32) { |
||||
address caller = _msgSender(); |
||||
bytes4 selector = bytes4(data[0:4]); |
||||
|
||||
// Fetch restriction to that apply to the caller on the targeted function |
||||
(bool allowed, uint32 setback) = canCall(caller, target, selector); |
||||
|
||||
// If caller is not authorised, revert |
||||
if (!allowed && setback == 0) { |
||||
revert AccessManagerUnauthorizedCall(caller, target, selector); |
||||
} |
||||
|
||||
// If caller is authorised, schedule operation |
||||
bytes32 operationId = _hashOperation(caller, target, data); |
||||
if (_schedules[operationId] != 0) { |
||||
revert AccessManagerAlreadyScheduled(operationId); |
||||
} |
||||
_schedules[operationId] = Time.timestamp() + setback; |
||||
|
||||
emit Scheduled(operationId, caller, target, data); |
||||
return operationId; |
||||
} |
||||
|
||||
/** |
||||
* @dev Execute a function that is delay restricted, provided it was properly scheduled beforehand, or the |
||||
* execution delay is 0. |
||||
* |
||||
* Emits a {Executed} event if the call was scheduled. Unscheduled call (with no delay) do not emit that event. |
||||
*/ |
||||
function relay(address target, bytes calldata data) public payable virtual { |
||||
relayViaAdapter(target, data, address(0)); |
||||
} |
||||
|
||||
/** |
||||
* @dev Execute a function that is delay restricted in the same way as {relay} but through an |
||||
* {AccessManagedAdapter}. |
||||
*/ |
||||
function relayViaAdapter(address target, bytes calldata data, address adapter) public payable virtual { |
||||
address caller = _msgSender(); |
||||
bytes4 selector = bytes4(data[0:4]); |
||||
|
||||
// Fetch restriction to that apply to the caller on the targeted function |
||||
(bool allowed, uint32 setback) = canCall(caller, target, selector); |
||||
|
||||
// If caller is not authorised, revert |
||||
if (!allowed && setback == 0) { |
||||
revert AccessManagerUnauthorizedCall(caller, target, selector); |
||||
} |
||||
|
||||
// If caller is authorised, check operation was scheduled early enough |
||||
bytes32 operationId = _hashOperation(caller, target, data); |
||||
uint48 timepoint = _schedules[operationId]; |
||||
if (setback != 0) { |
||||
if (timepoint == 0) { |
||||
revert AccessManagerNotScheduled(operationId); |
||||
} else if (timepoint > Time.timestamp()) { |
||||
revert AccessManagerNotReady(operationId); |
||||
} |
||||
} |
||||
if (timepoint != 0) { |
||||
delete _schedules[operationId]; |
||||
emit Executed(operationId); |
||||
} |
||||
|
||||
// Mark the target and selector as authorised |
||||
bytes32 relayIdentifierBefore = _relayIdentifier; |
||||
_relayIdentifier = keccak256(abi.encodePacked(target, selector)); |
||||
|
||||
if (adapter != address(0)) { |
||||
// Perform call through adapter |
||||
AccessManagedAdapter(adapter).relay{value: msg.value}(target, data); |
||||
} else { |
||||
// Perform call directly |
||||
Address.functionCallWithValue(target, data, msg.value); |
||||
} |
||||
|
||||
// Reset relay identifier |
||||
_relayIdentifier = relayIdentifierBefore; |
||||
} |
||||
|
||||
/** |
||||
* @dev Cancel a scheduled (delayed) operation. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be the proposer, or a guardian of the targeted function |
||||
* |
||||
* Emits a {Canceled} event. |
||||
*/ |
||||
function cancel(address caller, address target, bytes calldata data) public virtual { |
||||
address msgsender = _msgSender(); |
||||
bytes4 selector = bytes4(data[0:4]); |
||||
|
||||
bytes32 operationId = _hashOperation(caller, target, data); |
||||
if (_schedules[operationId] == 0) { |
||||
revert AccessManagerNotScheduled(operationId); |
||||
} else if ( |
||||
caller != msgsender && |
||||
!hasGroup(ADMIN_GROUP, msgsender) && |
||||
!hasGroup(getGroupGuardian(getFunctionAllowedGroup(target, selector)), msgsender) |
||||
) { |
||||
// calls can only be canceled by the account that scheduled them, a global admin, or by a guardian of the required group. |
||||
revert AccessManagerCannotCancel(msgsender, caller, target, selector); |
||||
} |
||||
|
||||
delete _schedules[operationId]; |
||||
emit Canceled(operationId); |
||||
} |
||||
|
||||
/** |
||||
* @dev Hashing function for delayed operations |
||||
*/ |
||||
function _hashOperation(address caller, address target, bytes calldata data) private pure returns (bytes32) { |
||||
return keccak256(abi.encode(caller, target, data)); |
||||
} |
||||
|
||||
// ==================================================== OTHERS ==================================================== |
||||
/** |
||||
* @dev Change the AccessManager instance used by a contract that correctly uses this instance. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - the caller must be a global admin |
||||
*/ |
||||
function updateAuthority(IManaged target, address newAuthority) public virtual onlyGroup(ADMIN_GROUP) { |
||||
// todo set delay or document risks |
||||
target.setAuthority(newAuthority); |
||||
} |
||||
} |
@ -0,0 +1,120 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {IManaged} from "./IManaged.sol"; |
||||
import {Time} from "../../utils/types/Time.sol"; |
||||
|
||||
interface IAccessManager { |
||||
enum AccessMode { |
||||
Custom, |
||||
Closed, |
||||
Open |
||||
} |
||||
|
||||
// Structure that stores the details for a group/account pair. This structures fit into a single slot. |
||||
struct Access { |
||||
// Timepoint at which the user gets the permission. If this is either 0, or in the future, the group permission |
||||
// are not available. Should be checked using {Time-isSetAndPast} |
||||
uint48 since; |
||||
// delay for execution. Only applies to restricted() / relay() calls. This does not restrict access to |
||||
// functions that use the `onlyGroup` modifier. |
||||
Time.Delay delay; |
||||
} |
||||
|
||||
// Structure that stores the details of a group, including: |
||||
// - the members of the group |
||||
// - the admin group (that can grant or revoke permissions) |
||||
// - the guardian group (that can cancel operations targeting functions that need this group |
||||
// - the grand delay |
||||
struct Group { |
||||
mapping(address user => Access access) members; |
||||
uint256 admin; |
||||
uint256 guardian; |
||||
Time.Delay delay; // delay for granting |
||||
} |
||||
|
||||
/** |
||||
* @dev A delay operation was schedule. |
||||
*/ |
||||
event Scheduled(bytes32 operationId, address caller, address target, bytes data); |
||||
|
||||
/** |
||||
* @dev A scheduled operation was executed. |
||||
*/ |
||||
event Executed(bytes32 operationId); |
||||
|
||||
/** |
||||
* @dev A scheduled operation was canceled. |
||||
*/ |
||||
event Canceled(bytes32 operationId); |
||||
|
||||
event GroupLabel(uint256 indexed groupId, string label); |
||||
event GroupGranted(uint256 indexed groupId, address indexed account, uint48 since, uint32 delay); |
||||
event GroupRevoked(uint256 indexed groupId, address indexed account); |
||||
event GroupExecutionDelayUpdate(uint256 indexed groupId, address indexed account, uint32 delay, uint48 from); |
||||
event GroupAdminChanged(uint256 indexed groupId, uint256 indexed admin); |
||||
event GroupGuardianChanged(uint256 indexed groupId, uint256 indexed guardian); |
||||
event GroupGrantDelayChanged(uint256 indexed groupId, uint32 delay, uint48 from); |
||||
event AccessModeUpdated(address indexed target, AccessMode mode); |
||||
event FunctionAllowedGroupUpdated(address indexed target, bytes4 selector, uint256 indexed groupId); |
||||
|
||||
error AccessManagerAlreadyScheduled(bytes32 operationId); |
||||
error AccessManagerNotScheduled(bytes32 operationId); |
||||
error AccessManagerNotReady(bytes32 operationId); |
||||
error AccessManagerLockedGroup(uint256 groupId); |
||||
error AccessManagerAcountAlreadyInGroup(uint256 groupId, address account); |
||||
error AccessManagerAcountNotInGroup(uint256 groupId, address account); |
||||
error AccessManagerBadConfirmation(); |
||||
error AccessControlUnauthorizedAccount(address msgsender, uint256 groupId); |
||||
error AccessManagerUnauthorizedCall(address caller, address target, bytes4 selector); |
||||
error AccessManagerCannotCancel(address msgsender, address caller, address target, bytes4 selector); |
||||
|
||||
function canCall( |
||||
address caller, |
||||
address target, |
||||
bytes4 selector |
||||
) external view returns (bool allowed, uint32 delay); |
||||
|
||||
function getContractMode(address target) external view returns (AccessMode); |
||||
|
||||
function getFunctionAllowedGroup(address target, bytes4 selector) external view returns (uint256); |
||||
|
||||
function getGroupAdmin(uint256 group) external view returns (uint256); |
||||
|
||||
function getGroupGuardian(uint256 group) external view returns (uint256); |
||||
|
||||
function getGroupGrantDelay(uint256 groupId) external view returns (uint32); |
||||
|
||||
function getAccess(uint256 group, address account) external view returns (Access memory); |
||||
|
||||
function hasGroup(uint256 group, address account) external view returns (bool); |
||||
|
||||
function grantGroup(uint256 group, address account, uint32 executionDelay) external; |
||||
|
||||
function revokeGroup(uint256 group, address account) external; |
||||
|
||||
function renounceGroup(uint256 group, address callerConfirmation) external; |
||||
|
||||
function setExecuteDelay(uint256 group, address account, uint32 newDelay) external; |
||||
|
||||
function setGroupAdmin(uint256 group, uint256 admin) external; |
||||
|
||||
function setGroupGuardian(uint256 group, uint256 guardian) external; |
||||
|
||||
function setGrantDelay(uint256 group, uint32 newDelay) external; |
||||
|
||||
function setContractModeCustom(address target) external; |
||||
|
||||
function setContractModeOpen(address target) external; |
||||
|
||||
function setContractModeClosed(address target) external; |
||||
|
||||
function schedule(address target, bytes calldata data) external returns (bytes32); |
||||
|
||||
function cancel(address caller, address target, bytes calldata data) external; |
||||
|
||||
function relay(address target, bytes calldata data) external payable; |
||||
|
||||
function updateAuthority(IManaged target, address newAuthority) external; |
||||
} |
@ -0,0 +1,37 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
/** |
||||
* @dev Standard interface for permissioning originally defined in Dappsys. |
||||
*/ |
||||
interface IAuthority { |
||||
/** |
||||
* @dev Returns true if the caller can invoke on a target the function identified by a function selector. |
||||
*/ |
||||
function canCall(address caller, address target, bytes4 selector) external view returns (bool allowed); |
||||
} |
||||
|
||||
/** |
||||
* @dev Since `AccessManager` implements an extended IAuthority interface, invoking `canCall` with backwards compatibility |
||||
* for the preexisting `IAuthority` interface requires special care to avoid reverting on insufficient return data. |
||||
* This helper function takes care of invoking `canCall` in a backwards compatible way without reverting. |
||||
*/ |
||||
function safeCanCall( |
||||
address authority, |
||||
address caller, |
||||
address target, |
||||
bytes4 selector |
||||
) view returns (bool allowed, uint32 delay) { |
||||
(bool success, bytes memory data) = authority.staticcall( |
||||
abi.encodeCall(IAuthority.canCall, (caller, target, selector)) |
||||
); |
||||
if (success) { |
||||
if (data.length >= 0x40) { |
||||
(allowed, delay) = abi.decode(data, (bool, uint32)); |
||||
} else if (data.length >= 0x20) { |
||||
allowed = abi.decode(data, (bool)); |
||||
} |
||||
} |
||||
return (allowed, delay); |
||||
} |
@ -0,0 +1,15 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
interface IManaged { |
||||
event AuthorityUpdated(address authority); |
||||
|
||||
error AccessManagedUnauthorized(address caller); |
||||
error AccessManagedRequiredDelay(address caller, uint32 delay); |
||||
error AccessManagedInvalidAuthority(address authority); |
||||
|
||||
function authority() external view returns (address); |
||||
|
||||
function setAuthority(address) external; |
||||
} |
@ -0,0 +1,42 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {AccessManaged} from "../AccessManaged.sol"; |
||||
import {Address} from "../../../utils/Address.sol"; |
||||
|
||||
/** |
||||
* @dev This contract can be used to migrate existing {Ownable} or {AccessControl} contracts into an {AccessManager} |
||||
* system. |
||||
* |
||||
* Ownable contracts can have their ownership transferred to an instance of this adapter. AccessControl contracts can |
||||
* grant all roles to the adapter, while ideally revoking them from all other accounts. Subsequently, the permissions |
||||
* for those contracts can be managed centrally and with function granularity in the {AccessManager} instance the |
||||
* adapter is connected to. |
||||
* |
||||
* Permissioned interactions with thus migrated contracts must go through the adapter's {relay} function and will |
||||
* proceed if the function is allowed for the caller in the AccessManager instance. |
||||
*/ |
||||
contract AccessManagedAdapter is AccessManaged { |
||||
error AccessManagedAdapterUnauthorizedSelfRelay(); |
||||
|
||||
/** |
||||
* @dev Initializes an adapter connected to an AccessManager instance. |
||||
*/ |
||||
constructor(address initialAuthority) AccessManaged(initialAuthority) {} |
||||
|
||||
/** |
||||
* @dev Relays a function call to the target contract. The call will be relayed if the AccessManager allows the |
||||
* caller access to this function in the target contract, i.e. if the caller is a member of the group that is |
||||
* allowed for the function. |
||||
*/ |
||||
function relay(address target, bytes calldata data) external payable { |
||||
if (target == address(this)) { |
||||
revert AccessManagedAdapterUnauthorizedSelfRelay(); |
||||
} |
||||
|
||||
_checkCanCall(_msgSender(), target, bytes4(data[0:4])); |
||||
|
||||
Address.functionCallWithValue(target, data, msg.value); |
||||
} |
||||
} |
@ -0,0 +1,211 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {IGovernorTimelock} from "./IGovernorTimelock.sol"; |
||||
import {IGovernor, Governor} from "../Governor.sol"; |
||||
import {IManaged} from "../../access/manager/IManaged.sol"; |
||||
import {IAuthority, safeCanCall} from "../../access/manager/IAuthority.sol"; |
||||
import {IAccessManager} from "../../access/manager/IAccessManager.sol"; |
||||
import {Address} from "../../utils/Address.sol"; |
||||
import {Math} from "../../utils/math/Math.sol"; |
||||
|
||||
/** |
||||
* @dev TODO |
||||
* |
||||
* _Available since v5.0._ |
||||
*/ |
||||
abstract contract GovernorTimelock is IGovernorTimelock, Governor { |
||||
struct ExecutionDetail { |
||||
address authority; |
||||
uint32 delay; |
||||
} |
||||
|
||||
mapping(uint256 => ExecutionDetail[]) private _executionDetails; |
||||
mapping(uint256 => uint256) private _proposalEta; |
||||
|
||||
/** |
||||
* @dev Overridden version of the {Governor-state} function with added support for the `Queued` and `Expired` state. |
||||
*/ |
||||
function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) { |
||||
ProposalState currentState = super.state(proposalId); |
||||
|
||||
if (currentState == ProposalState.Succeeded && proposalEta(proposalId) != 0) { |
||||
return ProposalState.Queued; |
||||
} else { |
||||
return currentState; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @dev Public accessor to check the eta of a queued proposal. |
||||
*/ |
||||
function proposalEta(uint256 proposalId) public view virtual override returns (uint256) { |
||||
return _proposalEta[proposalId]; |
||||
} |
||||
|
||||
/** |
||||
* @dev Public accessor to check the execution details. |
||||
*/ |
||||
function proposalExecutionDetails(uint256 proposalId) public view returns (ExecutionDetail[] memory) { |
||||
return _executionDetails[proposalId]; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {IGovernor-propose} |
||||
*/ |
||||
function propose( |
||||
address[] memory targets, |
||||
uint256[] memory values, |
||||
bytes[] memory calldatas, |
||||
string memory description |
||||
) public virtual override(IGovernor, Governor) returns (uint256) { |
||||
uint256 proposalId = super.propose(targets, values, calldatas, description); |
||||
|
||||
ExecutionDetail[] storage details = _executionDetails[proposalId]; |
||||
for (uint256 i = 0; i < targets.length; ++i) { |
||||
details.push(_detectExecutionDetails(targets[i], bytes4(calldatas[i]))); |
||||
} |
||||
|
||||
return proposalId; |
||||
} |
||||
|
||||
/** |
||||
* @dev Function to queue a proposal to the timelock. |
||||
* |
||||
* NOTE: execution delay is estimated based on the delay information retrieved in {proposal}. This value may be |
||||
* off if the delay were updated during the vote. |
||||
*/ |
||||
function queue( |
||||
address[] memory targets, |
||||
uint256[] memory values, |
||||
bytes[] memory calldatas, |
||||
bytes32 descriptionHash |
||||
) public virtual override returns (uint256) { |
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); |
||||
|
||||
ProposalState currentState = state(proposalId); |
||||
if (currentState != ProposalState.Succeeded) { |
||||
revert GovernorUnexpectedProposalState( |
||||
proposalId, |
||||
currentState, |
||||
_encodeStateBitmap(ProposalState.Succeeded) |
||||
); |
||||
} |
||||
|
||||
ExecutionDetail[] storage details = _executionDetails[proposalId]; |
||||
ExecutionDetail memory detail; |
||||
uint32 setback = 0; |
||||
|
||||
for (uint256 i = 0; i < targets.length; ++i) { |
||||
detail = details[i]; |
||||
if (detail.authority != address(0)) { |
||||
IAccessManager(detail.authority).schedule(targets[i], calldatas[i]); |
||||
} |
||||
setback = uint32(Math.max(setback, detail.delay)); // cast is safe, both parameters are uint32 |
||||
} |
||||
|
||||
uint256 eta = block.timestamp + setback; |
||||
_proposalEta[proposalId] = eta; |
||||
|
||||
return eta; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {IGovernor-_execute} |
||||
*/ |
||||
function _execute( |
||||
uint256 proposalId, |
||||
address[] memory targets, |
||||
uint256[] memory values, |
||||
bytes[] memory calldatas, |
||||
bytes32 /*descriptionHash*/ |
||||
) internal virtual override { |
||||
ExecutionDetail[] storage details = _executionDetails[proposalId]; |
||||
ExecutionDetail memory detail; |
||||
|
||||
// TODO: enforce ETA (might include some _defaultDelaySeconds that are not enforced by any authority) |
||||
|
||||
for (uint256 i = 0; i < targets.length; ++i) { |
||||
detail = details[i]; |
||||
if (detail.authority != address(0)) { |
||||
IAccessManager(detail.authority).relay{value: values[i]}(targets[i], calldatas[i]); |
||||
} else { |
||||
(bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]); |
||||
Address.verifyCallResult(success, returndata); |
||||
} |
||||
} |
||||
|
||||
delete _executionDetails[proposalId]; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {IGovernor-_cancel} |
||||
*/ |
||||
function _cancel( |
||||
address[] memory targets, |
||||
uint256[] memory values, |
||||
bytes[] memory calldatas, |
||||
bytes32 descriptionHash |
||||
) internal virtual override returns (uint256) { |
||||
uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash); |
||||
|
||||
ExecutionDetail[] storage details = _executionDetails[proposalId]; |
||||
ExecutionDetail memory detail; |
||||
|
||||
for (uint256 i = 0; i < targets.length; ++i) { |
||||
detail = details[i]; |
||||
if (detail.authority != address(0)) { |
||||
IAccessManager(detail.authority).cancel(address(this), targets[i], calldatas[i]); |
||||
} |
||||
} |
||||
|
||||
delete _executionDetails[proposalId]; |
||||
|
||||
return proposalId; |
||||
} |
||||
|
||||
/** |
||||
* @dev Default delay to apply to function calls that are not (scheduled and) executed through an AccessManager. |
||||
* |
||||
* NOTE: execution delays are processed by the AccessManager contracts. We expect these to always be in seconds. |
||||
* Therefore, the default delay that is enforced for calls that don't go through an access manager is also in |
||||
* seconds, regardless of the Governor's clock mode. |
||||
*/ |
||||
function _defaultDelaySeconds() internal view virtual returns (uint32) { |
||||
return 0; |
||||
} |
||||
|
||||
/** |
||||
* @dev Check if the execution of a call needs to be performed through an AccessManager and what delay should be |
||||
* applied to this call. |
||||
* |
||||
* Returns { manager: address(0), delay: _defaultDelaySeconds() } if: |
||||
* - target does not have code |
||||
* - target does not implement IManaged |
||||
* - calling canCall on the target's manager returns a 0 delay |
||||
* - calling canCall on the target's manager reverts |
||||
* Otherwise (calling canCall on the target's manager returns a non 0 delay), return the address of the |
||||
* AccessManager to use, and the delay for this call. |
||||
*/ |
||||
function _detectExecutionDetails(address target, bytes4 selector) private view returns (ExecutionDetail memory) { |
||||
bool success; |
||||
bytes memory returndata; |
||||
|
||||
// Get authority |
||||
(success, returndata) = target.staticcall(abi.encodeCall(IManaged.authority, ())); |
||||
if (success && returndata.length >= 0x20) { |
||||
address authority = abi.decode(returndata, (address)); |
||||
|
||||
// Check if governor can call, and try to detect a delay |
||||
(bool authorized, uint32 delay) = safeCanCall(authority, address(this), target, selector); |
||||
|
||||
// If direct call is not authorized, and delayed call is possible |
||||
if (!authorized && delay > 0) { |
||||
return ExecutionDetail({authority: authority, delay: delay}); |
||||
} |
||||
} |
||||
|
||||
return ExecutionDetail({authority: address(0), delay: _defaultDelaySeconds()}); |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {AccessManaged} from "../access/manager/AccessManaged.sol"; |
||||
|
||||
abstract contract AccessManagedTarget is AccessManaged { |
||||
event CalledRestricted(address caller); |
||||
event CalledUnrestricted(address caller); |
||||
|
||||
function fnRestricted() public restricted { |
||||
emit CalledRestricted(msg.sender); |
||||
} |
||||
|
||||
function fnUnrestricted() public { |
||||
emit CalledUnrestricted(msg.sender); |
||||
} |
||||
} |
@ -0,0 +1,131 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {Math} from "../math/Math.sol"; |
||||
import {SafeCast} from "../math/SafeCast.sol"; |
||||
|
||||
/** |
||||
* @dev This library provides helpers for manipulating time-related objects. |
||||
* |
||||
* It uses the following types: |
||||
* - `uint48` for timepoints |
||||
* - `uint32` for durations |
||||
* |
||||
* While the library doesn't provide specific types for timepoints and duration, it does provide: |
||||
* - a `Delay` type to represent duration that can be programmed to change value automatically at a given point |
||||
* - additional helper functions |
||||
*/ |
||||
library Time { |
||||
using Time for *; |
||||
|
||||
/** |
||||
* @dev Get the block timestamp as a Timepoint |
||||
*/ |
||||
function timestamp() internal view returns (uint48) { |
||||
return SafeCast.toUint48(block.timestamp); |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the block number as a Timepoint |
||||
*/ |
||||
function blockNumber() internal view returns (uint48) { |
||||
return SafeCast.toUint48(block.number); |
||||
} |
||||
|
||||
/** |
||||
* @dev Check if a timepoint is set, and in the past |
||||
*/ |
||||
function isSetAndPast(uint48 timepoint, uint48 ref) internal pure returns (bool) { |
||||
return timepoint != 0 && timepoint <= ref; |
||||
} |
||||
|
||||
// ==================================================== Delay ===================================================== |
||||
/** |
||||
* @dev A `Delay` is a uint32 duration that can be programmed to change value automatically at a given point in the |
||||
* future. The "effect" timepoint describes when the transitions happens from the "old" value to the "new" value. |
||||
* This allows updating the delay applied to some operation while keeping so guarantees. |
||||
* |
||||
* In particular, the {update} function guarantees that is the delay is reduced, the old delay still applies for |
||||
* some time. For example if the delay is currently 7 days to do an upgrade, the admin should not be able to set |
||||
* the delay to 0 and upgrade immediately. If the admin wants to reduce the delay, the old delay (7 days) should |
||||
* still apply for some time. |
||||
* |
||||
* |
||||
* The `Delay` type is 128 bits long, and packs the following: |
||||
* [000:031] uint32 for the current value (duration) |
||||
* [032:063] uint32 for the pending value (duration) |
||||
* [064:111] uint48 for the effect date (timepoint) |
||||
* |
||||
* NOTE: The {get} and {update} function operate using timestamps. Block number based delays should use the |
||||
* {getAt} and {updateAt} variants of these functions. |
||||
*/ |
||||
type Delay is uint112; |
||||
|
||||
/** |
||||
* @dev Wrap a Duration into a Delay to add the one-step "update in the future" feature |
||||
*/ |
||||
function toDelay(uint32 duration) internal pure returns (Delay) { |
||||
return Delay.wrap(duration); |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the value the Delay will be at a given timepoint |
||||
*/ |
||||
function getAt(Delay self, uint48 timepoint) internal pure returns (uint32) { |
||||
(uint32 oldValue, uint32 newValue, uint48 effect) = self.split(); |
||||
return (effect == 0 || effect > timepoint) ? oldValue : newValue; |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the current value. |
||||
*/ |
||||
function get(Delay self) internal view returns (uint32) { |
||||
return self.getAt(timestamp()); |
||||
} |
||||
|
||||
/** |
||||
* @dev Get the pending value, and effect timepoint. If the effect timepoint is 0, then the pending value should |
||||
* not be considered. |
||||
*/ |
||||
function getPending(Delay self) internal pure returns (uint32, uint48) { |
||||
(, uint32 newValue, uint48 effect) = self.split(); |
||||
return (newValue, effect); |
||||
} |
||||
|
||||
/** |
||||
* @dev Update a Delay object so that a new duration takes effect at a given timepoint. |
||||
*/ |
||||
function updateAt(Delay self, uint32 newValue, uint48 effect) internal view returns (Delay) { |
||||
return pack(self.get(), newValue, effect); |
||||
} |
||||
|
||||
/** |
||||
* @dev Update a Delay object so that it takes a new duration after at a timepoint that is automatically computed |
||||
* to enforce the old delay at the moment of the update. |
||||
*/ |
||||
function update(Delay self, uint32 newValue, uint32 minSetback) internal view returns (Delay) { |
||||
uint32 value = self.get(); |
||||
uint32 setback = uint32(Math.max(minSetback, value > newValue ? value - newValue : 0)); |
||||
return self.updateAt(newValue, timestamp() + setback); |
||||
} |
||||
|
||||
/** |
||||
* @dev Split a delay into its components: oldValue, newValue and effect (transition timepoint). |
||||
*/ |
||||
function split(Delay self) internal pure returns (uint32, uint32, uint48) { |
||||
uint112 raw = Delay.unwrap(self); |
||||
return ( |
||||
uint32(raw), // oldValue |
||||
uint32(raw >> 32), // newValue |
||||
uint48(raw >> 64) // effect |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* @dev pack the components into a Delay object. |
||||
*/ |
||||
function pack(uint32 oldValue, uint32 newValue, uint48 effect) internal pure returns (Delay) { |
||||
return Delay.wrap(uint112(oldValue) | (uint112(newValue) << 32) | (uint112(effect) << 64)); |
||||
} |
||||
} |
@ -0,0 +1,927 @@ |
||||
const { expectEvent, constants, time } = require('@openzeppelin/test-helpers'); |
||||
const { expectRevertCustomError } = require('../../helpers/customError'); |
||||
const { AccessMode } = require('../../helpers/enums'); |
||||
const { selector } = require('../../helpers/methods'); |
||||
const { clockFromReceipt } = require('../../helpers/time'); |
||||
|
||||
const AccessManager = artifacts.require('$AccessManager'); |
||||
const AccessManagedTarget = artifacts.require('$AccessManagedTarget'); |
||||
|
||||
const GROUPS = { |
||||
ADMIN: web3.utils.toBN(0), |
||||
SOME_ADMIN: web3.utils.toBN(17), |
||||
SOME: web3.utils.toBN(42), |
||||
PUBLIC: constants.MAX_UINT256, |
||||
}; |
||||
Object.assign(GROUPS, Object.fromEntries(Object.entries(GROUPS).map(([key, value]) => [value, key]))); |
||||
|
||||
const executeDelay = web3.utils.toBN(10); |
||||
const grantDelay = web3.utils.toBN(10); |
||||
|
||||
const MAX_UINT = n => web3.utils.toBN(1).shln(n).subn(1); |
||||
|
||||
const split = delay => ({ |
||||
oldValue: web3.utils.toBN(delay).shrn(0).and(MAX_UINT(32)).toString(), |
||||
newValue: web3.utils.toBN(delay).shrn(32).and(MAX_UINT(32)).toString(), |
||||
effect: web3.utils.toBN(delay).shrn(64).and(MAX_UINT(48)).toString(), |
||||
}); |
||||
|
||||
contract('AccessManager', function (accounts) { |
||||
const [admin, manager, member, user, other] = accounts; |
||||
|
||||
beforeEach(async function () { |
||||
this.manager = await AccessManager.new(admin); |
||||
this.target = await AccessManagedTarget.new(this.manager.address); |
||||
|
||||
// add member to group
|
||||
await this.manager.$_setGroupAdmin(GROUPS.SOME, GROUPS.SOME_ADMIN); |
||||
await this.manager.$_setGroupGuardian(GROUPS.SOME, GROUPS.SOME_ADMIN); |
||||
await this.manager.$_grantGroup(GROUPS.SOME_ADMIN, manager, 0, 0); |
||||
await this.manager.$_grantGroup(GROUPS.SOME, member, 0, 0); |
||||
|
||||
// helpers for indirect calls
|
||||
this.call = [this.target.address, selector('fnRestricted()')]; |
||||
this.opId = web3.utils.keccak256( |
||||
web3.eth.abi.encodeParameters(['address', 'address', 'bytes'], [user, ...this.call]), |
||||
); |
||||
this.schedule = (opts = {}) => this.manager.schedule(...this.call, { from: user, ...opts }); |
||||
this.relay = (opts = {}) => this.manager.relay(...this.call, { from: user, ...opts }); |
||||
this.cancel = (opts = {}) => this.manager.cancel(user, ...this.call, { from: user, ...opts }); |
||||
}); |
||||
|
||||
it('groups are correctly initialized', async function () { |
||||
// group admin
|
||||
expect(await this.manager.getGroupAdmin(GROUPS.ADMIN)).to.be.bignumber.equal(GROUPS.ADMIN); |
||||
expect(await this.manager.getGroupAdmin(GROUPS.SOME_ADMIN)).to.be.bignumber.equal(GROUPS.ADMIN); |
||||
expect(await this.manager.getGroupAdmin(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.SOME_ADMIN); |
||||
expect(await this.manager.getGroupAdmin(GROUPS.PUBLIC)).to.be.bignumber.equal(GROUPS.ADMIN); |
||||
// group guardian
|
||||
expect(await this.manager.getGroupGuardian(GROUPS.ADMIN)).to.be.bignumber.equal(GROUPS.ADMIN); |
||||
expect(await this.manager.getGroupGuardian(GROUPS.SOME_ADMIN)).to.be.bignumber.equal(GROUPS.ADMIN); |
||||
expect(await this.manager.getGroupGuardian(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.SOME_ADMIN); |
||||
expect(await this.manager.getGroupGuardian(GROUPS.PUBLIC)).to.be.bignumber.equal(GROUPS.ADMIN); |
||||
// group members
|
||||
expect(await this.manager.hasGroup(GROUPS.ADMIN, admin)).to.be.equal(true); |
||||
expect(await this.manager.hasGroup(GROUPS.ADMIN, manager)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.ADMIN, member)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.ADMIN, user)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.SOME_ADMIN, admin)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.SOME_ADMIN, manager)).to.be.equal(true); |
||||
expect(await this.manager.hasGroup(GROUPS.SOME_ADMIN, member)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.SOME_ADMIN, user)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, admin)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, manager)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.equal(true); |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.equal(false); |
||||
expect(await this.manager.hasGroup(GROUPS.PUBLIC, admin)).to.be.equal(true); |
||||
expect(await this.manager.hasGroup(GROUPS.PUBLIC, manager)).to.be.equal(true); |
||||
expect(await this.manager.hasGroup(GROUPS.PUBLIC, member)).to.be.equal(true); |
||||
expect(await this.manager.hasGroup(GROUPS.PUBLIC, user)).to.be.equal(true); |
||||
}); |
||||
|
||||
describe('Groups management', function () { |
||||
describe('label group', function () { |
||||
it('admin can emit a label event', async function () { |
||||
expectEvent(await this.manager.labelGroup(GROUPS.SOME, 'Some label', { from: admin }), 'GroupLabel', { |
||||
groupId: GROUPS.SOME, |
||||
label: 'Some label', |
||||
}); |
||||
}); |
||||
|
||||
it('admin can re-emit a label event', async function () { |
||||
await this.manager.labelGroup(GROUPS.SOME, 'Some label', { from: admin }); |
||||
|
||||
expectEvent(await this.manager.labelGroup(GROUPS.SOME, 'Updated label', { from: admin }), 'GroupLabel', { |
||||
groupId: GROUPS.SOME, |
||||
label: 'Updated label', |
||||
}); |
||||
}); |
||||
|
||||
it('emitting a label is restricted', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.labelGroup(GROUPS.SOME, 'Invalid label', { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[other, GROUPS.ADMIN], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('grand group', function () { |
||||
describe('without a grant delay', function () { |
||||
it('without an execute delay', async function () { |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
const { receipt } = await this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager }); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
expectEvent(receipt, 'GroupGranted', { groupId: GROUPS.SOME, account: user, since: timestamp, delay: '0' }); |
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.true; |
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
expect(since).to.be.bignumber.equal(timestamp); |
||||
}); |
||||
|
||||
it('with an execute delay', async function () { |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
const { receipt } = await this.manager.grantGroup(GROUPS.SOME, user, executeDelay, { from: manager }); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
expectEvent(receipt, 'GroupGranted', { |
||||
groupId: GROUPS.SOME, |
||||
account: user, |
||||
since: timestamp, |
||||
delay: executeDelay, |
||||
}); |
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.true; |
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user); |
||||
expect(delay).to.be.bignumber.equal(executeDelay); |
||||
expect(since).to.be.bignumber.equal(timestamp); |
||||
}); |
||||
|
||||
it('to a user that is already in the group', async function () { |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.true; |
||||
|
||||
await expectRevertCustomError( |
||||
this.manager.grantGroup(GROUPS.SOME, member, 0, { from: manager }), |
||||
'AccessManagerAcountAlreadyInGroup', |
||||
[GROUPS.SOME, member], |
||||
); |
||||
}); |
||||
|
||||
it('to a user that is scheduled for joining the group', async function () { |
||||
await this.manager.$_grantGroup(GROUPS.SOME, user, 10, 0); // grant delay 10
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
await expectRevertCustomError( |
||||
this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager }), |
||||
'AccessManagerAcountAlreadyInGroup', |
||||
[GROUPS.SOME, user], |
||||
); |
||||
}); |
||||
|
||||
it('grant group is restricted', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.grantGroup(GROUPS.SOME, user, 0, { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[other, GROUPS.SOME_ADMIN], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('with a grant delay', function () { |
||||
beforeEach(async function () { |
||||
await this.manager.$_setGrantDelay(GROUPS.SOME, grantDelay); |
||||
}); |
||||
|
||||
it('granted group is not active immediatly', async function () { |
||||
const { receipt } = await this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager }); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
expectEvent(receipt, 'GroupGranted', { |
||||
groupId: GROUPS.SOME, |
||||
account: user, |
||||
since: timestamp.add(grantDelay), |
||||
delay: '0', |
||||
}); |
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
expect(since).to.be.bignumber.equal(timestamp.add(grantDelay)); |
||||
}); |
||||
|
||||
it('granted group is active after the delay', async function () { |
||||
const { receipt } = await this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager }); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
expectEvent(receipt, 'GroupGranted', { |
||||
groupId: GROUPS.SOME, |
||||
account: user, |
||||
since: timestamp.add(grantDelay), |
||||
delay: '0', |
||||
}); |
||||
|
||||
await time.increase(grantDelay); |
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.true; |
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
expect(since).to.be.bignumber.equal(timestamp.add(grantDelay)); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('revoke group', function () { |
||||
it('from a user that is already in the group', async function () { |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.true; |
||||
|
||||
const { receipt } = await this.manager.revokeGroup(GROUPS.SOME, member, { from: manager }); |
||||
expectEvent(receipt, 'GroupRevoked', { groupId: GROUPS.SOME, account: member }); |
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.false; |
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
expect(since).to.be.bignumber.equal('0'); |
||||
}); |
||||
|
||||
it('from a user that is scheduled for joining the group', async function () { |
||||
await this.manager.$_grantGroup(GROUPS.SOME, user, 10, 0); // grant delay 10
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
const { receipt } = await this.manager.revokeGroup(GROUPS.SOME, user, { from: manager }); |
||||
expectEvent(receipt, 'GroupRevoked', { groupId: GROUPS.SOME, account: user }); |
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
expect(since).to.be.bignumber.equal('0'); |
||||
}); |
||||
|
||||
it('from a user that is not in the group', async function () { |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
await expectRevertCustomError( |
||||
this.manager.revokeGroup(GROUPS.SOME, user, { from: manager }), |
||||
'AccessManagerAcountNotInGroup', |
||||
[GROUPS.SOME, user], |
||||
); |
||||
}); |
||||
|
||||
it('revoke group is restricted', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.revokeGroup(GROUPS.SOME, member, { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[other, GROUPS.SOME_ADMIN], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('renounce group', function () { |
||||
it('for a user that is already in the group', async function () { |
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.true; |
||||
|
||||
const { receipt } = await this.manager.renounceGroup(GROUPS.SOME, member, { from: member }); |
||||
expectEvent(receipt, 'GroupRevoked', { groupId: GROUPS.SOME, account: member }); |
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.false; |
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, member); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
expect(since).to.be.bignumber.equal('0'); |
||||
}); |
||||
|
||||
it('for a user that is schedule for joining the group', async function () { |
||||
await this.manager.$_grantGroup(GROUPS.SOME, user, 10, 0); // grant delay 10
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
const { receipt } = await this.manager.renounceGroup(GROUPS.SOME, user, { from: user }); |
||||
expectEvent(receipt, 'GroupRevoked', { groupId: GROUPS.SOME, account: user }); |
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false; |
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user); |
||||
expect(delay).to.be.bignumber.equal('0'); |
||||
expect(since).to.be.bignumber.equal('0'); |
||||
}); |
||||
|
||||
it('for a user that is not in the group', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.renounceGroup(GROUPS.SOME, user, { from: user }), |
||||
'AccessManagerAcountNotInGroup', |
||||
[GROUPS.SOME, user], |
||||
); |
||||
}); |
||||
|
||||
it('bad user confirmation', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.renounceGroup(GROUPS.SOME, member, { from: user }), |
||||
'AccessManagerBadConfirmation', |
||||
[], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('change group admin', function () { |
||||
it("admin can set any group's admin", async function () { |
||||
expect(await this.manager.getGroupAdmin(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.SOME_ADMIN); |
||||
|
||||
const { receipt } = await this.manager.setGroupAdmin(GROUPS.SOME, GROUPS.ADMIN, { from: admin }); |
||||
expectEvent(receipt, 'GroupAdminChanged', { groupId: GROUPS.SOME, admin: GROUPS.ADMIN }); |
||||
|
||||
expect(await this.manager.getGroupAdmin(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.ADMIN); |
||||
}); |
||||
|
||||
it("seeting a group's admin is restricted", async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.setGroupAdmin(GROUPS.SOME, GROUPS.SOME, { from: manager }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[manager, GROUPS.ADMIN], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('change group guardian', function () { |
||||
it("admin can set any group's admin", async function () { |
||||
expect(await this.manager.getGroupGuardian(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.SOME_ADMIN); |
||||
|
||||
const { receipt } = await this.manager.setGroupGuardian(GROUPS.SOME, GROUPS.ADMIN, { from: admin }); |
||||
expectEvent(receipt, 'GroupGuardianChanged', { groupId: GROUPS.SOME, guardian: GROUPS.ADMIN }); |
||||
|
||||
expect(await this.manager.getGroupGuardian(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.ADMIN); |
||||
}); |
||||
|
||||
it("setting a group's admin is restricted", async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.setGroupGuardian(GROUPS.SOME, GROUPS.SOME, { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[other, GROUPS.ADMIN], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('change execution delay', function () { |
||||
it('increassing the delay has immediate effect', async function () { |
||||
const oldDelay = web3.utils.toBN(10); |
||||
const newDelay = web3.utils.toBN(100); |
||||
|
||||
const { receipt: receipt1 } = await this.manager.$_setExecuteDelay(GROUPS.SOME, member, oldDelay); |
||||
const timestamp1 = await clockFromReceipt.timestamp(receipt1).then(web3.utils.toBN); |
||||
|
||||
const delayBefore = await this.manager.getAccess(GROUPS.SOME, member).then(([, delay]) => split(delay)); |
||||
expect(delayBefore.oldValue).to.be.bignumber.equal('0'); |
||||
expect(delayBefore.newValue).to.be.bignumber.equal(oldDelay); |
||||
expect(delayBefore.effect).to.be.bignumber.equal(timestamp1); |
||||
|
||||
const { receipt: receipt2 } = await this.manager.setExecuteDelay(GROUPS.SOME, member, newDelay, { |
||||
from: manager, |
||||
}); |
||||
const timestamp2 = await clockFromReceipt.timestamp(receipt2).then(web3.utils.toBN); |
||||
|
||||
expectEvent(receipt2, 'GroupExecutionDelayUpdate', { |
||||
groupId: GROUPS.SOME, |
||||
account: member, |
||||
delay: newDelay, |
||||
from: timestamp2, |
||||
}); |
||||
|
||||
// immediate effect
|
||||
const delayAfter = await this.manager.getAccess(GROUPS.SOME, member).then(([, delay]) => split(delay)); |
||||
expect(delayAfter.oldValue).to.be.bignumber.equal(oldDelay); |
||||
expect(delayAfter.newValue).to.be.bignumber.equal(newDelay); |
||||
expect(delayAfter.effect).to.be.bignumber.equal(timestamp2); |
||||
}); |
||||
|
||||
it('decreassing the delay takes time', async function () { |
||||
const oldDelay = web3.utils.toBN(100); |
||||
const newDelay = web3.utils.toBN(10); |
||||
|
||||
const { receipt: receipt1 } = await this.manager.$_setExecuteDelay(GROUPS.SOME, member, oldDelay); |
||||
const timestamp1 = await clockFromReceipt.timestamp(receipt1).then(web3.utils.toBN); |
||||
|
||||
const delayBefore = await this.manager.getAccess(GROUPS.SOME, member).then(([, delay]) => split(delay)); |
||||
expect(delayBefore.oldValue).to.be.bignumber.equal('0'); |
||||
expect(delayBefore.newValue).to.be.bignumber.equal(oldDelay); |
||||
expect(delayBefore.effect).to.be.bignumber.equal(timestamp1); |
||||
|
||||
const { receipt: receipt2 } = await this.manager.setExecuteDelay(GROUPS.SOME, member, newDelay, { |
||||
from: manager, |
||||
}); |
||||
const timestamp2 = await clockFromReceipt.timestamp(receipt2).then(web3.utils.toBN); |
||||
|
||||
expectEvent(receipt2, 'GroupExecutionDelayUpdate', { |
||||
groupId: GROUPS.SOME, |
||||
account: member, |
||||
delay: newDelay, |
||||
from: timestamp2.add(oldDelay).sub(newDelay), |
||||
}); |
||||
|
||||
// delayed effect
|
||||
const delayAfter = await this.manager.getAccess(GROUPS.SOME, member).then(([, delay]) => split(delay)); |
||||
|
||||
expect(delayAfter.oldValue).to.be.bignumber.equal(oldDelay); |
||||
expect(delayAfter.newValue).to.be.bignumber.equal(newDelay); |
||||
expect(delayAfter.effect).to.be.bignumber.equal(timestamp2.add(oldDelay).sub(newDelay)); |
||||
}); |
||||
|
||||
it('cannot set the delay of a non member', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.setExecuteDelay(GROUPS.SOME, other, executeDelay, { from: manager }), |
||||
'AccessManagerAcountNotInGroup', |
||||
[GROUPS.SOME, other], |
||||
); |
||||
}); |
||||
|
||||
it('can set a user execution delay during the grant delay', async function () { |
||||
await this.manager.$_grantGroup(GROUPS.SOME, other, 10, 0); |
||||
|
||||
const { receipt } = await this.manager.setExecuteDelay(GROUPS.SOME, other, executeDelay, { from: manager }); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
|
||||
expectEvent(receipt, 'GroupExecutionDelayUpdate', { |
||||
groupId: GROUPS.SOME, |
||||
account: other, |
||||
delay: executeDelay, |
||||
from: timestamp, |
||||
}); |
||||
}); |
||||
|
||||
it('changing the execution delay is restricted', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.setExecuteDelay(GROUPS.SOME, member, executeDelay, { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[GROUPS.SOME_ADMIN, other], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('change grant delay', function () { |
||||
it('increassing the delay has immediate effect', async function () { |
||||
const oldDelay = web3.utils.toBN(10); |
||||
const newDelay = web3.utils.toBN(100); |
||||
await this.manager.$_setGrantDelay(GROUPS.SOME, oldDelay); |
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(oldDelay); |
||||
|
||||
const { receipt } = await this.manager.setGrantDelay(GROUPS.SOME, newDelay, { from: admin }); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
|
||||
expectEvent(receipt, 'GroupGrantDelayChanged', { groupId: GROUPS.SOME, delay: newDelay, from: timestamp }); |
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(newDelay); |
||||
}); |
||||
|
||||
it('increassing the delay has delay effect', async function () { |
||||
const oldDelay = web3.utils.toBN(100); |
||||
const newDelay = web3.utils.toBN(10); |
||||
await this.manager.$_setGrantDelay(GROUPS.SOME, oldDelay); |
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(oldDelay); |
||||
|
||||
const { receipt } = await this.manager.setGrantDelay(GROUPS.SOME, newDelay, { from: admin }); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
|
||||
expectEvent(receipt, 'GroupGrantDelayChanged', { |
||||
groupId: GROUPS.SOME, |
||||
delay: newDelay, |
||||
from: timestamp.add(oldDelay).sub(newDelay), |
||||
}); |
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(oldDelay); |
||||
|
||||
await time.increase(oldDelay.sub(newDelay)); |
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(newDelay); |
||||
}); |
||||
|
||||
it('changing the grant delay is restricted', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.setGrantDelay(GROUPS.SOME, grantDelay, { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[GROUPS.ADMIN, other], |
||||
); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('Mode management', function () { |
||||
for (const [modeName, mode] of Object.entries(AccessMode)) { |
||||
describe(`setContractMode${modeName}`, function () { |
||||
it('set the mode and emits an event', async function () { |
||||
// set the target to another mode, so we can check the effects
|
||||
await this.manager.$_setContractMode( |
||||
this.target.address, |
||||
Object.values(AccessMode).find(m => m != mode), |
||||
); |
||||
|
||||
expect(await this.manager.getContractMode(this.target.address)).to.not.be.bignumber.equal(mode); |
||||
|
||||
expectEvent( |
||||
await this.manager[`setContractMode${modeName}`](this.target.address, { from: admin }), |
||||
'AccessModeUpdated', |
||||
{ target: this.target.address, mode }, |
||||
); |
||||
|
||||
expect(await this.manager.getContractMode(this.target.address)).to.be.bignumber.equal(mode); |
||||
}); |
||||
|
||||
it('is restricted', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager[`setContractMode${modeName}`](this.target.address, { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[other, GROUPS.ADMIN], |
||||
); |
||||
}); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
describe('Change function permissions', function () { |
||||
const sigs = ['someFunction()', 'someOtherFunction(uint256)', 'oneMoreFunction(address,uint8)'].map(selector); |
||||
|
||||
it('admin can set function allowed group', async function () { |
||||
for (const sig of sigs) { |
||||
expect(await this.manager.getFunctionAllowedGroup(this.target.address, sig)).to.be.bignumber.equal( |
||||
GROUPS.ADMIN, |
||||
); |
||||
} |
||||
|
||||
const { receipt: receipt1 } = await this.manager.setFunctionAllowedGroup(this.target.address, sigs, GROUPS.SOME, { |
||||
from: admin, |
||||
}); |
||||
|
||||
for (const sig of sigs) { |
||||
expectEvent(receipt1, 'FunctionAllowedGroupUpdated', { |
||||
target: this.target.address, |
||||
selector: sig, |
||||
groupId: GROUPS.SOME, |
||||
}); |
||||
expect(await this.manager.getFunctionAllowedGroup(this.target.address, sig)).to.be.bignumber.equal(GROUPS.SOME); |
||||
} |
||||
|
||||
const { receipt: receipt2 } = await this.manager.setFunctionAllowedGroup( |
||||
this.target.address, |
||||
[sigs[1]], |
||||
GROUPS.SOME_ADMIN, |
||||
{ from: admin }, |
||||
); |
||||
expectEvent(receipt2, 'FunctionAllowedGroupUpdated', { |
||||
target: this.target.address, |
||||
selector: sigs[1], |
||||
groupId: GROUPS.SOME_ADMIN, |
||||
}); |
||||
|
||||
for (const sig of sigs) { |
||||
expect(await this.manager.getFunctionAllowedGroup(this.target.address, sig)).to.be.bignumber.equal( |
||||
sig == sigs[1] ? GROUPS.SOME_ADMIN : GROUPS.SOME, |
||||
); |
||||
} |
||||
}); |
||||
|
||||
it('changing function permissions is restricted', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.setFunctionAllowedGroup(this.target.address, sigs, GROUPS.SOME, { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[other, GROUPS.ADMIN], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('Calling restricted & unrestricted functions', function () { |
||||
const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [ai, bi].flat()))); |
||||
|
||||
for (const [callerOpt, targetOpt] of product( |
||||
[ |
||||
{ groups: [] }, |
||||
{ groups: [GROUPS.SOME] }, |
||||
{ groups: [GROUPS.SOME], delay: executeDelay }, |
||||
{ groups: [GROUPS.SOME, GROUPS.PUBLIC], delay: executeDelay }, |
||||
], |
||||
[ |
||||
{ mode: AccessMode.Open }, |
||||
{ mode: AccessMode.Closed }, |
||||
{ mode: AccessMode.Custom, group: GROUPS.ADMIN }, |
||||
{ mode: AccessMode.Custom, group: GROUPS.SOME }, |
||||
{ mode: AccessMode.Custom, group: GROUPS.PUBLIC }, |
||||
], |
||||
)) { |
||||
const public = |
||||
targetOpt.mode == AccessMode.Open || (targetOpt.mode == AccessMode.Custom && targetOpt.group == GROUPS.PUBLIC); |
||||
|
||||
// can we call with a delay ?
|
||||
const indirectSuccess = |
||||
public || (targetOpt.mode == AccessMode.Custom && callerOpt.groups?.includes(targetOpt.group)); |
||||
|
||||
// can we call without a delay ?
|
||||
const directSuccess = |
||||
public || |
||||
(targetOpt.mode == AccessMode.Custom && callerOpt.groups?.includes(targetOpt.group) && !callerOpt.delay); |
||||
|
||||
const description = [ |
||||
'Caller in groups', |
||||
'[' + (callerOpt.groups ?? []).map(groupId => GROUPS[groupId]).join(', ') + ']', |
||||
callerOpt.delay ? 'with a delay' : 'without a delay', |
||||
'+', |
||||
'contract in mode', |
||||
Object.keys(AccessMode)[targetOpt.mode.toNumber()], |
||||
targetOpt.mode == AccessMode.Custom ? `(${GROUPS[targetOpt.group]})` : '', |
||||
].join(' '); |
||||
|
||||
describe(description, function () { |
||||
beforeEach(async function () { |
||||
// setup
|
||||
await Promise.all([ |
||||
this.manager.$_setContractMode(this.target.address, targetOpt.mode), |
||||
targetOpt.group && |
||||
this.manager.$_setFunctionAllowedGroup(this.target.address, selector('fnRestricted()'), targetOpt.group), |
||||
targetOpt.group && |
||||
this.manager.$_setFunctionAllowedGroup( |
||||
this.target.address, |
||||
selector('fnUnrestricted()'), |
||||
targetOpt.group, |
||||
), |
||||
...(callerOpt.groups ?? []) |
||||
.filter(groupId => groupId != GROUPS.PUBLIC) |
||||
.map(groupId => this.manager.$_grantGroup(groupId, user, 0, callerOpt.delay ?? 0)), |
||||
]); |
||||
|
||||
// post setup checks
|
||||
expect(await this.manager.getContractMode(this.target.address)).to.be.bignumber.equal(targetOpt.mode); |
||||
if (targetOpt.group) { |
||||
expect( |
||||
await this.manager.getFunctionAllowedGroup(this.target.address, selector('fnRestricted()')), |
||||
).to.be.bignumber.equal(targetOpt.group); |
||||
expect( |
||||
await this.manager.getFunctionAllowedGroup(this.target.address, selector('fnUnrestricted()')), |
||||
).to.be.bignumber.equal(targetOpt.group); |
||||
} |
||||
for (const groupId of callerOpt.groups ?? []) { |
||||
const access = await this.manager.getAccess(groupId, user); |
||||
if (groupId == GROUPS.PUBLIC) { |
||||
expect(access.since).to.be.bignumber.eq('0'); |
||||
expect(access.delay).to.be.bignumber.eq('0'); |
||||
} else { |
||||
expect(access.since).to.be.bignumber.gt('0'); |
||||
expect(access.delay).to.be.bignumber.eq(String(callerOpt.delay ?? 0)); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
it('canCall', async function () { |
||||
const result = await this.manager.canCall(user, this.target.address, selector('fnRestricted()')); |
||||
expect(result[0]).to.be.equal(directSuccess); |
||||
expect(result[1]).to.be.bignumber.equal(!directSuccess && indirectSuccess ? callerOpt.delay ?? '0' : '0'); |
||||
}); |
||||
|
||||
it('Calling a non restricted function never revert', async function () { |
||||
expectEvent(await this.target.fnUnrestricted({ from: user }), 'CalledUnrestricted', { |
||||
caller: user, |
||||
}); |
||||
}); |
||||
|
||||
it(`Calling a restricted function directly should ${directSuccess ? 'succeed' : 'revert'}`, async function () { |
||||
const promise = this.target.fnRestricted({ from: user }); |
||||
|
||||
if (directSuccess) { |
||||
expectEvent(await promise, 'CalledRestricted', { caller: user }); |
||||
} else { |
||||
await expectRevertCustomError(promise, 'AccessManagedUnauthorized', [user]); |
||||
} |
||||
}); |
||||
|
||||
it('Calling indirectly: only relay', async function () { |
||||
// relay without schedule
|
||||
if (directSuccess) { |
||||
const { receipt, tx } = await this.relay(); |
||||
expectEvent.notEmitted(receipt, 'Executed', { operationId: this.opId }); |
||||
expectEvent.inTransaction(tx, this.target, 'Calledrestricted', { caller: this.manager.address }); |
||||
} else if (indirectSuccess) { |
||||
await expectRevertCustomError(this.relay(), 'AccessManagerNotScheduled', [this.opId]); |
||||
} else { |
||||
await expectRevertCustomError(this.relay(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); |
||||
} |
||||
}); |
||||
|
||||
it('Calling indirectly: schedule and relay', async function () { |
||||
if (directSuccess || indirectSuccess) { |
||||
const { receipt } = await this.schedule(); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
|
||||
expectEvent(receipt, 'Scheduled', { |
||||
operationId: this.opId, |
||||
caller: user, |
||||
target: this.call[0], |
||||
data: this.call[1], |
||||
}); |
||||
|
||||
// if can call directly, delay should be 0. Otherwize, the delay should be applied
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal( |
||||
timestamp.add(directSuccess ? web3.utils.toBN(0) : callerOpt.delay), |
||||
); |
||||
|
||||
// execute without wait
|
||||
if (directSuccess) { |
||||
const { receipt, tx } = await this.relay(); |
||||
expectEvent(receipt, 'Executed', { operationId: this.opId }); |
||||
expectEvent.inTransaction(tx, this.target, 'Calledrestricted', { caller: this.manager.address }); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); |
||||
} else if (indirectSuccess) { |
||||
await expectRevertCustomError(this.relay(), 'AccessManagerNotReady', [this.opId]); |
||||
} else { |
||||
await expectRevertCustomError(this.relay(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); |
||||
} |
||||
} else { |
||||
await expectRevertCustomError(this.schedule(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); |
||||
} |
||||
}); |
||||
|
||||
it('Calling indirectly: schedule wait and relay', async function () { |
||||
if (directSuccess || indirectSuccess) { |
||||
const { receipt } = await this.schedule(); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
|
||||
expectEvent(receipt, 'Scheduled', { |
||||
operationId: this.opId, |
||||
caller: user, |
||||
target: this.call[0], |
||||
data: this.call[1], |
||||
}); |
||||
|
||||
// if can call directly, delay should be 0. Otherwize, the delay should be applied
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal( |
||||
timestamp.add(directSuccess ? web3.utils.toBN(0) : callerOpt.delay), |
||||
); |
||||
|
||||
// wait
|
||||
await time.increase(callerOpt.delay ?? 0); |
||||
|
||||
// execute without wait
|
||||
if (directSuccess || indirectSuccess) { |
||||
const { receipt, tx } = await this.relay(); |
||||
expectEvent(receipt, 'Executed', { operationId: this.opId }); |
||||
expectEvent.inTransaction(tx, this.target, 'Calledrestricted', { caller: this.manager.address }); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); |
||||
} else { |
||||
await expectRevertCustomError(this.relay(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); |
||||
} |
||||
} else { |
||||
await expectRevertCustomError(this.schedule(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
describe('Indirect execution corner-cases', async function () { |
||||
beforeEach(async function () { |
||||
await this.manager.$_setFunctionAllowedGroup(...this.call, GROUPS.SOME); |
||||
await this.manager.$_grantGroup(GROUPS.SOME, user, 0, executeDelay); |
||||
}); |
||||
|
||||
it('Checking canCall when caller is the manager depend on the _relayIdentifier', async function () { |
||||
expect(await this.manager.getContractMode(this.target.address)).to.be.bignumber.equal(AccessMode.Custom); |
||||
|
||||
const result = await this.manager.canCall(this.manager.address, this.target.address, '0x00000000'); |
||||
expect(result[0]).to.be.false; |
||||
expect(result[1]).to.be.bignumber.equal('0'); |
||||
}); |
||||
|
||||
it('Cannot execute earlier', async function () { |
||||
const { receipt } = await this.schedule(); |
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal(timestamp.add(executeDelay)); |
||||
|
||||
// we need to set the clock 2 seconds before the value, because the increaseTo "consumes" the timestamp
|
||||
// and the next transaction will be one after that (see check bellow)
|
||||
await time.increaseTo(timestamp.add(executeDelay).subn(2)); |
||||
|
||||
// too early
|
||||
await expectRevertCustomError(this.relay(), 'AccessManagerNotReady', [this.opId]); |
||||
|
||||
// the revert happened one second before the execution delay expired
|
||||
expect(await time.latest()).to.be.bignumber.equal(timestamp.add(executeDelay).subn(1)); |
||||
|
||||
// ok
|
||||
await this.relay(); |
||||
|
||||
// the success happened when the delay was reached (earliest possible)
|
||||
expect(await time.latest()).to.be.bignumber.equal(timestamp.add(executeDelay)); |
||||
}); |
||||
|
||||
it('Cannot schedule an already scheduled operation', async function () { |
||||
const { receipt } = await this.schedule(); |
||||
expectEvent(receipt, 'Scheduled', { |
||||
operationId: this.opId, |
||||
caller: user, |
||||
target: this.call[0], |
||||
data: this.call[1], |
||||
}); |
||||
|
||||
await expectRevertCustomError(this.schedule(), 'AccessManagerAlreadyScheduled', [this.opId]); |
||||
}); |
||||
|
||||
it('Cannot cancel an operation that is not scheduled', async function () { |
||||
await expectRevertCustomError(this.cancel(), 'AccessManagerNotScheduled', [this.opId]); |
||||
}); |
||||
|
||||
it('Cannot cancel an operation that is not already relayed', async function () { |
||||
await this.schedule(); |
||||
await time.increase(executeDelay); |
||||
await this.relay(); |
||||
|
||||
await expectRevertCustomError(this.cancel(), 'AccessManagerNotScheduled', [this.opId]); |
||||
}); |
||||
|
||||
it('Scheduler can cancel', async function () { |
||||
await this.schedule(); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0'); |
||||
|
||||
expectEvent(await this.cancel({ from: manager }), 'Canceled', { operationId: this.opId }); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); |
||||
}); |
||||
|
||||
it('Guardian can cancel', async function () { |
||||
await this.schedule(); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0'); |
||||
|
||||
expectEvent(await this.cancel({ from: manager }), 'Canceled', { operationId: this.opId }); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); |
||||
}); |
||||
|
||||
it('Cancel is restricted', async function () { |
||||
await this.schedule(); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0'); |
||||
|
||||
await expectRevertCustomError(this.cancel({ from: other }), 'AccessManagerCannotCancel', [ |
||||
other, |
||||
user, |
||||
...this.call, |
||||
]); |
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0'); |
||||
}); |
||||
|
||||
it('Can re-schedule after execution', async function () { |
||||
await this.schedule(); |
||||
await time.increase(executeDelay); |
||||
await this.relay(); |
||||
|
||||
// reschedule
|
||||
const { receipt } = await this.schedule(); |
||||
expectEvent(receipt, 'Scheduled', { |
||||
operationId: this.opId, |
||||
caller: user, |
||||
target: this.call[0], |
||||
data: this.call[1], |
||||
}); |
||||
}); |
||||
|
||||
it('Can re-schedule after cancel', async function () { |
||||
await this.schedule(); |
||||
await this.cancel(); |
||||
|
||||
// reschedule
|
||||
const { receipt } = await this.schedule(); |
||||
expectEvent(receipt, 'Scheduled', { |
||||
operationId: this.opId, |
||||
caller: user, |
||||
target: this.call[0], |
||||
data: this.call[1], |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('authority update', function () { |
||||
beforeEach(async function () { |
||||
this.newManager = await AccessManager.new(admin); |
||||
}); |
||||
|
||||
it('admin can change authority', async function () { |
||||
expect(await this.target.authority()).to.be.equal(this.manager.address); |
||||
|
||||
const { tx } = await this.manager.updateAuthority(this.target.address, this.newManager.address, { from: admin }); |
||||
expectEvent.inTransaction(tx, this.target, 'AuthorityUpdated', { authority: this.newManager.address }); |
||||
|
||||
expect(await this.target.authority()).to.be.equal(this.newManager.address); |
||||
}); |
||||
|
||||
it('cannot set an address without code as the authority', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.updateAuthority(this.target.address, user, { from: admin }), |
||||
'AccessManagedInvalidAuthority', |
||||
[user], |
||||
); |
||||
}); |
||||
|
||||
it('updateAuthority is restricted on manager', async function () { |
||||
await expectRevertCustomError( |
||||
this.manager.updateAuthority(this.target.address, this.newManager.address, { from: other }), |
||||
'AccessControlUnauthorizedAccount', |
||||
[other, GROUPS.ADMIN], |
||||
); |
||||
}); |
||||
|
||||
it('setAuthority is restricted on AccessManaged', async function () { |
||||
await expectRevertCustomError( |
||||
this.target.setAuthority(this.newManager.address, { from: admin }), |
||||
'AccessManagedUnauthorized', |
||||
[admin], |
||||
); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,158 @@ |
||||
const { constants, time } = require('@openzeppelin/test-helpers'); |
||||
const { expectRevertCustomError } = require('../../../helpers/customError'); |
||||
const { AccessMode } = require('../../../helpers/enums'); |
||||
const { selector } = require('../../../helpers/methods'); |
||||
|
||||
const AccessManager = artifacts.require('$AccessManager'); |
||||
const AccessManagedAdapter = artifacts.require('AccessManagedAdapter'); |
||||
const Ownable = artifacts.require('$Ownable'); |
||||
|
||||
const groupId = web3.utils.toBN(1); |
||||
|
||||
contract('AccessManagedAdapter', function (accounts) { |
||||
const [admin, user, other] = accounts; |
||||
|
||||
beforeEach(async function () { |
||||
this.manager = await AccessManager.new(admin); |
||||
this.adapter = await AccessManagedAdapter.new(this.manager.address); |
||||
this.ownable = await Ownable.new(this.adapter.address); |
||||
|
||||
// add user to group
|
||||
await this.manager.$_grantGroup(groupId, user, 0, 0); |
||||
}); |
||||
|
||||
it('initial state', async function () { |
||||
expect(await this.adapter.authority()).to.be.equal(this.manager.address); |
||||
expect(await this.ownable.owner()).to.be.equal(this.adapter.address); |
||||
}); |
||||
|
||||
describe('Contract is Closed', function () { |
||||
beforeEach(async function () { |
||||
await this.manager.$_setContractMode(this.ownable.address, AccessMode.Closed); |
||||
}); |
||||
|
||||
it('directly call: reverts', async function () { |
||||
await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]); |
||||
}); |
||||
|
||||
it('relayed call (with group): reverts', async function () { |
||||
await expectRevertCustomError( |
||||
this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user }), |
||||
'AccessManagedUnauthorized', |
||||
[user], |
||||
); |
||||
}); |
||||
|
||||
it('relayed call (without group): reverts', async function () { |
||||
await expectRevertCustomError( |
||||
this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: other }), |
||||
'AccessManagedUnauthorized', |
||||
[other], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('Contract is Open', function () { |
||||
beforeEach(async function () { |
||||
await this.manager.$_setContractMode(this.ownable.address, AccessMode.Open); |
||||
}); |
||||
|
||||
it('directly call: reverts', async function () { |
||||
await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]); |
||||
}); |
||||
|
||||
it('relayed call (with group): success', async function () { |
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user }); |
||||
}); |
||||
|
||||
it('relayed call (without group): success', async function () { |
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: other }); |
||||
}); |
||||
}); |
||||
|
||||
describe('Contract is in Custom mode', function () { |
||||
beforeEach(async function () { |
||||
await this.manager.$_setContractMode(this.ownable.address, AccessMode.Custom); |
||||
}); |
||||
|
||||
describe('function is open to specific group', function () { |
||||
beforeEach(async function () { |
||||
await this.manager.$_setFunctionAllowedGroup(this.ownable.address, selector('$_checkOwner()'), groupId); |
||||
}); |
||||
|
||||
it('directly call: reverts', async function () { |
||||
await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]); |
||||
}); |
||||
|
||||
it('relayed call (with group): success', async function () { |
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user }); |
||||
}); |
||||
|
||||
it('relayed call (without group): reverts', async function () { |
||||
await expectRevertCustomError( |
||||
this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: other }), |
||||
'AccessManagedUnauthorized', |
||||
[other], |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('function is open to public group', function () { |
||||
beforeEach(async function () { |
||||
await this.manager.$_setFunctionAllowedGroup( |
||||
this.ownable.address, |
||||
selector('$_checkOwner()'), |
||||
constants.MAX_UINT256, |
||||
); |
||||
}); |
||||
|
||||
it('directly call: reverts', async function () { |
||||
await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]); |
||||
}); |
||||
|
||||
it('relayed call (with group): success', async function () { |
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user }); |
||||
}); |
||||
|
||||
it('relayed call (without group): success', async function () { |
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: other }); |
||||
}); |
||||
}); |
||||
|
||||
describe('function is available with execution delay', function () { |
||||
const delay = 10; |
||||
|
||||
beforeEach(async function () { |
||||
await this.manager.$_setExecuteDelay(groupId, user, delay); |
||||
await this.manager.$_setFunctionAllowedGroup(this.ownable.address, selector('$_checkOwner()'), groupId); |
||||
}); |
||||
|
||||
it('unscheduled call reverts', async function () { |
||||
await expectRevertCustomError( |
||||
this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user }), |
||||
'AccessManagedRequiredDelay', |
||||
[user, delay], |
||||
); |
||||
}); |
||||
|
||||
it('scheduled call succeeds', async function () { |
||||
await this.manager.schedule(this.ownable.address, selector('$_checkOwner()'), { from: user }); |
||||
await time.increase(delay); |
||||
await this.manager.relayViaAdapter(this.ownable.address, selector('$_checkOwner()'), this.adapter.address, { |
||||
from: user, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
it('bubble revert reasons', async function () { |
||||
const { address } = await Ownable.new(admin); |
||||
await this.manager.$_setContractMode(address, AccessMode.Open); |
||||
|
||||
await expectRevertCustomError( |
||||
this.adapter.relay(address, selector('$_checkOwner()')), |
||||
'OwnableUnauthorizedAccount', |
||||
[this.adapter.address], |
||||
); |
||||
}); |
||||
}); |
@ -0,0 +1,5 @@ |
||||
const { soliditySha3 } = require('web3-utils'); |
||||
|
||||
module.exports = { |
||||
selector: signature => soliditySha3(signature).substring(0, 10), |
||||
}; |
Loading…
Reference in new issue