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