Access Manager (#4416)

audit/2023-08-07
Francisco Giordano 2 years ago
parent 8a92fb82ea
commit bf5786aae0
  1. 99
      contracts/access/manager/AccessManaged.sol
  2. 616
      contracts/access/manager/AccessManager.sol
  3. 120
      contracts/access/manager/IAccessManager.sol
  4. 37
      contracts/access/manager/IAuthority.sol
  5. 15
      contracts/access/manager/IManaged.sol
  6. 42
      contracts/access/manager/utils/AccessManagedAdapter.sol
  7. 211
      contracts/governance/extensions/GovernorTimelock.sol
  8. 18
      contracts/mocks/AccessManagedTarget.sol
  9. 131
      contracts/utils/types/Time.sol
  10. 14
      package-lock.json
  11. 927
      test/access/manager/AccessManager.test.js
  12. 158
      test/access/manager/utils/AccessManagedAdapter.test.js
  13. 1
      test/helpers/enums.js
  14. 5
      test/helpers/methods.js

@ -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));
}
}

14
package-lock.json generated

@ -45,7 +45,7 @@
"solhint": "^3.3.6",
"solhint-plugin-openzeppelin": "file:scripts/solhint-custom",
"solidity-ast": "^0.4.25",
"solidity-coverage": "^0.8.0",
"solidity-coverage": "^0.8.4",
"solidity-docgen": "^0.6.0-beta.29",
"undici": "^5.22.1",
"web3": "^1.3.0",
@ -12796,9 +12796,9 @@
}
},
"node_modules/solidity-coverage/node_modules/@solidity-parser/parser": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz",
"integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==",
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.1.tgz",
"integrity": "sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw==",
"dev": true,
"dependencies": {
"antlr4ts": "^0.5.0-alpha.4"
@ -25272,9 +25272,9 @@
},
"dependencies": {
"@solidity-parser/parser": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz",
"integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==",
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.1.tgz",
"integrity": "sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw==",
"dev": true,
"requires": {
"antlr4ts": "^0.5.0-alpha.4"

@ -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],
);
});
});

@ -8,4 +8,5 @@ module.exports = {
VoteType: Enum('Against', 'For', 'Abstain'),
Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'),
OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'),
AccessMode: Enum('Custom', 'Closed', 'Open'),
};

@ -0,0 +1,5 @@
const { soliditySha3 } = require('web3-utils');
module.exports = {
selector: signature => soliditySha3(signature).substring(0, 10),
};
Loading…
Cancel
Save