Improve AccessManager tests (#4613)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Francisco Giordano <fg@frang.io>
This commit is contained in:
@ -13,23 +13,32 @@ 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 "roles".
|
||||
* A smart contract under the control of an AccessManager instance is known as a target, and will inherit from the
|
||||
* {AccessManaged} contract, be connected to this contract as its manager and implement the {AccessManaged-restricted}
|
||||
* modifier on a set of functions selected to be permissioned. Note that any function without this setup won't be
|
||||
* effectively restricted.
|
||||
*
|
||||
* An AccessManager instance will define a set of roles. Accounts can be added into any number of these roles. 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 {setFunctionAllowedRoles}.
|
||||
* The restriction rules for such functions are defined in terms of "roles" identified by an `uint64` and scoped
|
||||
* by target (`address`) and function selectors (`bytes4`). These roles are stored in this contract and can be
|
||||
* configured by admins (`ADMIN_ROLE` members) after a delay (see {getTargetAdminDelay}).
|
||||
*
|
||||
* 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.
|
||||
* For each target contract, admins can configure the following without any delay:
|
||||
*
|
||||
* There is a special role defined by default named "public" which all accounts automatically have.
|
||||
* * The target's {AccessManaged-authority} via {updateAuthority}.
|
||||
* * Close or open a target via {setTargetClosed} keeping the permissions intact.
|
||||
* * The roles that are allowed (or disallowed) to call a given function (identified by its selector) through {setTargetFunctionRole}.
|
||||
*
|
||||
* In addition to the access rules defined by each target's functions being assigned to roles, then entire target can
|
||||
* be "closed". This "closed" mode is set/unset by the admin using {setTargetClosed} and can be used to lock a contract
|
||||
* while permissions are being (re-)configured.
|
||||
* By default every address is member of the `PUBLIC_ROLE` and every target function is restricted to the `ADMIN_ROLE` until configured otherwise.
|
||||
* Additionally, each role has the following configuration options restricted to this manager's admins:
|
||||
*
|
||||
* * A role's admin role via {setRoleAdmin} who can grant or revoke roles.
|
||||
* * A role's guardian role via {setRoleGuardian} who's allowed to cancel operations.
|
||||
* * A delay in which a role takes effect after being granted through {setGrantDelay}.
|
||||
* * A delay of any target's admin action via {setTargetAdminDelay}.
|
||||
* * A role label for discoverability purposes with {labelRole}.
|
||||
*
|
||||
* Any account can be added and removed into any number of these roles by using the {grantRole} and {revokeRole} functions
|
||||
* restricted to each role's admin (see {getRoleAdmin}).
|
||||
*
|
||||
* 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).
|
||||
@ -60,28 +69,30 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
|
||||
// Structure that stores the details for a role/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 role
|
||||
// permission is not available.
|
||||
// Timepoint at which the user gets the permission.
|
||||
// If this is either 0 or in the future, then the role permission is not available.
|
||||
uint48 since;
|
||||
// Delay for execution. Only applies to restricted() / execute() calls.
|
||||
Time.Delay delay;
|
||||
}
|
||||
|
||||
// Structure that stores the details of a role, including:
|
||||
// - the members of the role
|
||||
// - the admin role (that can grant or revoke permissions)
|
||||
// - the guardian role (that can cancel operations targeting functions that need this role)
|
||||
// - the grand delay
|
||||
// Structure that stores the details of a role.
|
||||
struct Role {
|
||||
// Members of the role.
|
||||
mapping(address user => Access access) members;
|
||||
// Admin who can grant or revoke permissions.
|
||||
uint64 admin;
|
||||
// Guardian who can cancel operations targeting functions that need this role.
|
||||
uint64 guardian;
|
||||
// Delay in which the role takes effect after being granted.
|
||||
Time.Delay grantDelay;
|
||||
}
|
||||
|
||||
// Structure that stores the details for a scheduled operation. This structure fits into a single slot.
|
||||
struct Schedule {
|
||||
// Moment at which the operation can be executed.
|
||||
uint48 timepoint;
|
||||
// Operation nonce to allow third-party contracts to identify the operation.
|
||||
uint32 nonce;
|
||||
}
|
||||
|
||||
@ -92,6 +103,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
mapping(uint64 roleId => Role) private _roles;
|
||||
mapping(bytes32 operationId => Schedule) private _schedules;
|
||||
|
||||
// Used to identify operations that are currently being executed via {execute}.
|
||||
// This should be transient storage when supported by the EVM.
|
||||
bytes32 private _executionId;
|
||||
|
||||
@ -120,15 +132,19 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
* & {execute} 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.
|
||||
* Therefore we only return true if the call can be performed without any delay. If the call is subject to a
|
||||
* previously set delay (not zero), 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?
|
||||
* If `immediate` is true, the delay can be disregarded and the operation can be immediately executed, otherwise
|
||||
* the operation can be executed if and only if delay is greater than 0.
|
||||
*
|
||||
* NOTE: The IAuthority interface does not include the `uint32` delay. This is an extension of that interface that
|
||||
* is backward compatible. Some contracts may thus ignore the second return argument. In that case they will fail
|
||||
* to identify the indirect workflow, and will consider calls that require a delay to be forbidden.
|
||||
*
|
||||
* NOTE: This function does not report the permissions of this manager itself. These are defined by the
|
||||
* {_canCallSelf} function instead.
|
||||
*/
|
||||
function canCall(
|
||||
address caller,
|
||||
@ -150,13 +166,16 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
|
||||
/**
|
||||
* @dev Expiration delay for scheduled proposals. Defaults to 1 week.
|
||||
*
|
||||
* IMPORTANT: Avoid overriding the expiration with 0. Otherwise every contract proposal will be expired immediately,
|
||||
* disabling any scheduling usage.
|
||||
*/
|
||||
function expiration() public view virtual returns (uint32) {
|
||||
return 1 weeks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Minimum setback for all delay updates, with the exception of execution delays, which
|
||||
* @dev Minimum setback for all delay updates, with the exception of execution delays. It
|
||||
* can be increased without setback (and in the event of an accidental increase can be reset
|
||||
* via {revokeRole}). Defaults to 5 days.
|
||||
*/
|
||||
@ -165,7 +184,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the mode under which a contract is operating.
|
||||
* @dev Get whether the contract is closed disabling any access. Otherwise role permissions are applied.
|
||||
*/
|
||||
function isTargetClosed(address target) public view virtual returns (bool) {
|
||||
return _targets[target].closed;
|
||||
@ -186,7 +205,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the id of the role that acts as an admin for given role.
|
||||
* @dev Get the id of the role that acts as an admin for the given role.
|
||||
*
|
||||
* The admin permission is required to grant the role, revoke the role and update the execution delay to execute
|
||||
* an operation that is restricted to this role.
|
||||
@ -205,9 +224,10 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the role 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
|
||||
* {RoleGrantDelayChanged} event.
|
||||
* @dev Get the role current grant delay.
|
||||
*
|
||||
* Its value may change at any point without an event emitted following a call to {setGrantDelay}.
|
||||
* Changes to this value, including effect timepoint are notified in advance by the {RoleGrantDelayChanged} event.
|
||||
*/
|
||||
function getRoleGrantDelay(uint64 roleId) public view virtual returns (uint32) {
|
||||
return _roles[roleId].grantDelay.get();
|
||||
@ -237,8 +257,8 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if a given account currently had the permission level corresponding to a given role. Note that this
|
||||
* permission might be associated with a delay. {getAccess} can provide more details.
|
||||
* @dev Check if a given account currently has the permission level corresponding to a given role. Note that this
|
||||
* permission might be associated with an execution delay. {getAccess} can provide more details.
|
||||
*/
|
||||
function hasRole(
|
||||
uint64 roleId,
|
||||
@ -256,6 +276,10 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
/**
|
||||
* @dev Give a label to a role, for improved role discoverabily by UIs.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {RoleLabel} event.
|
||||
*/
|
||||
function labelRole(uint64 roleId, string calldata label) public virtual onlyAuthorized {
|
||||
@ -270,19 +294,20 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* This gives the account the authorization to call any function that is restricted to this role. An optional
|
||||
* execution delay (in seconds) can be set. If that delay is non 0, the user is required to schedule any operation
|
||||
* that is restricted to members this role. The user will only be able to execute the operation after the delay has
|
||||
* that is restricted to members of this role. The user will only be able to execute the operation after the delay has
|
||||
* passed, before it has expired. During this period, admin and guardians can cancel the operation (see {cancel}).
|
||||
*
|
||||
* If the account has already been granted this role, the execution delay will be updated. 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
|
||||
* 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 an admin for the role (see {getRoleAdmin})
|
||||
* - granted role must not be the `PUBLIC_ROLE`
|
||||
*
|
||||
* Emits a {RoleGranted} event
|
||||
* Emits a {RoleGranted} event.
|
||||
*/
|
||||
function grantRole(uint64 roleId, address account, uint32 executionDelay) public virtual onlyAuthorized {
|
||||
_grantRole(roleId, account, getRoleGrantDelay(roleId), executionDelay);
|
||||
@ -295,6 +320,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be an admin for the role (see {getRoleAdmin})
|
||||
* - revoked role must not be the `PUBLIC_ROLE`
|
||||
*
|
||||
* Emits a {RoleRevoked} event if the account had the role.
|
||||
*/
|
||||
@ -303,8 +329,8 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Renounce role permissions for the calling account, with immediate effect. If the sender is not in
|
||||
* the role, this call has no effect.
|
||||
* @dev Renounce role permissions for the calling account with immediate effect. If the sender is not in
|
||||
* the role this call has no effect.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
@ -416,7 +442,10 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
/**
|
||||
* @dev Internal version of {setRoleAdmin} without access control.
|
||||
*
|
||||
* Emits a {RoleAdminChanged} event
|
||||
* Emits a {RoleAdminChanged} event.
|
||||
*
|
||||
* NOTE: Setting the admin role as the `PUBLIC_ROLE` is allowed, but it will effectively allow
|
||||
* anyone to set grant or revoke such role.
|
||||
*/
|
||||
function _setRoleAdmin(uint64 roleId, uint64 admin) internal virtual {
|
||||
if (roleId == ADMIN_ROLE || roleId == PUBLIC_ROLE) {
|
||||
@ -431,7 +460,10 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
/**
|
||||
* @dev Internal version of {setRoleGuardian} without access control.
|
||||
*
|
||||
* Emits a {RoleGuardianChanged} event
|
||||
* Emits a {RoleGuardianChanged} event.
|
||||
*
|
||||
* NOTE: Setting the guardian role as the `PUBLIC_ROLE` is allowed, but it will effectively allow
|
||||
* anyone to cancel any scheduled operation for such role.
|
||||
*/
|
||||
function _setRoleGuardian(uint64 roleId, uint64 guardian) internal virtual {
|
||||
if (roleId == ADMIN_ROLE || roleId == PUBLIC_ROLE) {
|
||||
@ -446,7 +478,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
/**
|
||||
* @dev Internal version of {setGrantDelay} without access control.
|
||||
*
|
||||
* Emits a {RoleGrantDelayChanged} event
|
||||
* Emits a {RoleGrantDelayChanged} event.
|
||||
*/
|
||||
function _setGrantDelay(uint64 roleId, uint32 newDelay) internal virtual {
|
||||
if (roleId == PUBLIC_ROLE) {
|
||||
@ -480,9 +512,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal version of {setFunctionAllowedRole} without access control.
|
||||
* @dev Internal version of {setTargetFunctionRole} without access control.
|
||||
*
|
||||
* Emits a {TargetFunctionRoleUpdated} event
|
||||
* Emits a {TargetFunctionRoleUpdated} event.
|
||||
*/
|
||||
function _setTargetFunctionRole(address target, bytes4 selector, uint64 roleId) internal virtual {
|
||||
_targets[target].allowedRoles[selector] = roleId;
|
||||
@ -496,7 +528,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {TargetAdminDelayUpdated} event per selector
|
||||
* Emits a {TargetAdminDelayUpdated} event.
|
||||
*/
|
||||
function setTargetAdminDelay(address target, uint32 newDelay) public virtual onlyAuthorized {
|
||||
_setTargetAdminDelay(target, newDelay);
|
||||
@ -505,7 +537,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
/**
|
||||
* @dev Internal version of {setTargetAdminDelay} without access control.
|
||||
*
|
||||
* Emits a {TargetAdminDelayUpdated} event
|
||||
* Emits a {TargetAdminDelayUpdated} event.
|
||||
*/
|
||||
function _setTargetAdminDelay(address target, uint32 newDelay) internal virtual {
|
||||
uint48 effect;
|
||||
@ -673,7 +705,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
* This is useful for contract that want to enforce that calls targeting them were scheduled on the manager,
|
||||
* with all the verifications that it implies.
|
||||
*
|
||||
* Emit a {OperationExecuted} event
|
||||
* Emit a {OperationExecuted} event.
|
||||
*/
|
||||
function consumeScheduledOp(address caller, bytes calldata data) public virtual {
|
||||
address target = _msgSender();
|
||||
@ -788,7 +820,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
* Returns:
|
||||
* - bool restricted: does this data match a restricted operation
|
||||
* - uint64: which role is this operation restricted to
|
||||
* - uint32: minimum delay to enforce for that operation (on top of the admin's execution delay)
|
||||
* - uint32: minimum delay to enforce for that operation (max between operation's delay and admin's execution delay)
|
||||
*/
|
||||
function _getAdminRestrictions(
|
||||
bytes calldata data
|
||||
@ -834,14 +866,12 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
|
||||
// =================================================== HELPERS ====================================================
|
||||
/**
|
||||
* @dev An extended version of {canCall} for internal use that considers restrictions for admin functions.
|
||||
* @dev An extended version of {canCall} for internal usage that checks {_canCallSelf}
|
||||
* when the target is this contract.
|
||||
*
|
||||
* Returns:
|
||||
* - bool immediate: whether the operation can be executed immediately (with no delay)
|
||||
* - uint32 delay: the execution delay
|
||||
*
|
||||
* If immediate is true, the delay can be disregarded and the operation can be immediately executed.
|
||||
* If immediate is false, the operation can be executed if and only if delay is greater than 0.
|
||||
*/
|
||||
function _canCallExtended(
|
||||
address caller,
|
||||
|
||||
@ -29,6 +29,13 @@ interface IAccessManager {
|
||||
event OperationCanceled(bytes32 indexed operationId, uint32 indexed nonce);
|
||||
|
||||
event RoleLabel(uint64 indexed roleId, string label);
|
||||
/**
|
||||
* @dev Emitted when `account` is granted `roleId`.
|
||||
*
|
||||
* NOTE: The meaning of the `since` argument depends on the `newMember` argument.
|
||||
* If the role is granted to a new member, the `since` argument indicates when the account becomes a member of the role,
|
||||
* otherwise it indicates the execution delay for this account and roleId is updated.
|
||||
*/
|
||||
event RoleGranted(uint64 indexed roleId, address indexed account, uint32 delay, uint48 since, bool newMember);
|
||||
event RoleRevoked(uint64 indexed roleId, address indexed account);
|
||||
event RoleAdminChanged(uint64 indexed roleId, uint64 indexed admin);
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {AccessManaged} from "../access/manager/AccessManaged.sol";
|
||||
import {StorageSlot} from "../utils/StorageSlot.sol";
|
||||
|
||||
abstract contract AccessManagedTarget is AccessManaged {
|
||||
event CalledRestricted(address caller);
|
||||
@ -17,6 +18,16 @@ abstract contract AccessManagedTarget is AccessManaged {
|
||||
emit CalledUnrestricted(msg.sender);
|
||||
}
|
||||
|
||||
function setIsConsumingScheduledOp(bool isConsuming, bytes32 slot) external {
|
||||
// Memory layout is 0x....<_consumingSchedule (boolean)><authority (address)>
|
||||
bytes32 mask = bytes32(uint256(1 << 160));
|
||||
if (isConsuming) {
|
||||
StorageSlot.getBytes32Slot(slot).value |= mask;
|
||||
} else {
|
||||
StorageSlot.getBytes32Slot(slot).value &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
fallback() external {
|
||||
emit CalledFallback(msg.sender);
|
||||
}
|
||||
|
||||
@ -96,22 +96,26 @@ library Time {
|
||||
* enforce the old delay at the moment of the update. Returns the updated Delay object and the timestamp when the
|
||||
* new delay becomes effective.
|
||||
*/
|
||||
function withUpdate(Delay self, uint32 newValue, uint32 minSetback) internal view returns (Delay, uint48) {
|
||||
function withUpdate(
|
||||
Delay self,
|
||||
uint32 newValue,
|
||||
uint32 minSetback
|
||||
) internal view returns (Delay updatedDelay, uint48 effect) {
|
||||
uint32 value = self.get();
|
||||
uint32 setback = uint32(Math.max(minSetback, value > newValue ? value - newValue : 0));
|
||||
uint48 effect = timestamp() + setback;
|
||||
effect = timestamp() + setback;
|
||||
return (pack(value, newValue, effect), effect);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Split a delay into its components: valueBefore, valueAfter and effect (transition timepoint).
|
||||
*/
|
||||
function unpack(Delay self) internal pure returns (uint32, uint32, uint48) {
|
||||
function unpack(Delay self) internal pure returns (uint32 valueBefore, uint32 valueAfter, uint48 effect) {
|
||||
uint112 raw = Delay.unwrap(self);
|
||||
|
||||
uint32 valueAfter = uint32(raw);
|
||||
uint32 valueBefore = uint32(raw >> 32);
|
||||
uint48 effect = uint48(raw >> 64);
|
||||
valueAfter = uint32(raw);
|
||||
valueBefore = uint32(raw >> 32);
|
||||
effect = uint48(raw >> 64);
|
||||
|
||||
return (valueBefore, valueAfter, effect);
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ VERSION="$(jq -r .version contracts/package.json)"
|
||||
DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")"
|
||||
|
||||
bash "$DIRNAME/patch-apply.sh"
|
||||
sed -i "s/<package-version>/$VERSION/g" contracts/package.json
|
||||
sed -i'' -e "s/<package-version>/$VERSION/g" "contracts/package.json"
|
||||
git add contracts/package.json
|
||||
|
||||
npm run clean
|
||||
|
||||
711
test/access/manager/AccessManager.behavior.js
Normal file
711
test/access/manager/AccessManager.behavior.js
Normal file
@ -0,0 +1,711 @@
|
||||
const { time } = require('@openzeppelin/test-helpers');
|
||||
const {
|
||||
time: { setNextBlockTimestamp },
|
||||
setStorageAt,
|
||||
mine,
|
||||
} = require('@nomicfoundation/hardhat-network-helpers');
|
||||
const { impersonate } = require('../../helpers/account');
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
const { EXPIRATION, EXECUTION_ID_STORAGE_SLOT } = require('../../helpers/access-manager');
|
||||
|
||||
// ============ COMMON PATHS ============
|
||||
|
||||
const COMMON_IS_EXECUTING_PATH = {
|
||||
executing() {
|
||||
it('succeeds', async function () {
|
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller });
|
||||
});
|
||||
},
|
||||
notExecuting() {
|
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerUnauthorizedAccount',
|
||||
[this.caller, this.role.id],
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const COMMON_GET_ACCESS_PATH = {
|
||||
requiredRoleIsGranted: {
|
||||
roleGrantingIsDelayed: {
|
||||
callerHasAnExecutionDelay: {
|
||||
beforeGrantDelay() {
|
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerUnauthorizedAccount',
|
||||
[this.caller, this.role.id],
|
||||
);
|
||||
});
|
||||
},
|
||||
afterGrantDelay: undefined, // Diverges if there's an operation delay or not
|
||||
},
|
||||
callerHasNoExecutionDelay: {
|
||||
beforeGrantDelay() {
|
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerUnauthorizedAccount',
|
||||
[this.caller, this.role.id],
|
||||
);
|
||||
});
|
||||
},
|
||||
afterGrantDelay() {
|
||||
it('succeeds called directly', async function () {
|
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller });
|
||||
});
|
||||
|
||||
it('succeeds via execute', async function () {
|
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller });
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
roleGrantingIsNotDelayed: {
|
||||
callerHasAnExecutionDelay: undefined, // Diverges if there's an operation to schedule or not
|
||||
callerHasNoExecutionDelay() {
|
||||
it('succeeds called directly', async function () {
|
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller });
|
||||
});
|
||||
|
||||
it('succeeds via execute', async function () {
|
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller });
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
requiredRoleIsNotGranted() {
|
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerUnauthorizedAccount',
|
||||
[this.caller, this.role.id],
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const COMMON_SCHEDULABLE_PATH = {
|
||||
scheduled: {
|
||||
before() {
|
||||
it('reverts as AccessManagerNotReady', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerNotReady',
|
||||
[this.operationId],
|
||||
);
|
||||
});
|
||||
},
|
||||
after() {
|
||||
it('succeeds called directly', async function () {
|
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller });
|
||||
});
|
||||
|
||||
it('succeeds via execute', async function () {
|
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller });
|
||||
});
|
||||
},
|
||||
expired() {
|
||||
it('reverts as AccessManagerExpired', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerExpired',
|
||||
[this.operationId],
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
notScheduled() {
|
||||
it('reverts as AccessManagerNotScheduled', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerNotScheduled',
|
||||
[this.operationId],
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const COMMON_SCHEDULABLE_PATH_IF_ZERO_DELAY = {
|
||||
scheduled: {
|
||||
before() {
|
||||
it.skip('is not reachable without a delay');
|
||||
},
|
||||
after() {
|
||||
it.skip('is not reachable without a delay');
|
||||
},
|
||||
expired() {
|
||||
it.skip('is not reachable without a delay');
|
||||
},
|
||||
},
|
||||
notScheduled() {
|
||||
it('succeeds', async function () {
|
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller });
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// ============ MODE HELPERS ============
|
||||
|
||||
/**
|
||||
* @requires this.{manager,target}
|
||||
*/
|
||||
function shouldBehaveLikeClosable({ closed, open }) {
|
||||
describe('when the manager is closed', function () {
|
||||
beforeEach('close', async function () {
|
||||
await this.manager.$_setTargetClosed(this.target.address, true);
|
||||
});
|
||||
|
||||
closed();
|
||||
});
|
||||
|
||||
describe('when the manager is open', function () {
|
||||
beforeEach('open', async function () {
|
||||
await this.manager.$_setTargetClosed(this.target.address, false);
|
||||
});
|
||||
|
||||
open();
|
||||
});
|
||||
}
|
||||
|
||||
// ============ DELAY HELPERS ============
|
||||
|
||||
/**
|
||||
* @requires this.{delay}
|
||||
*/
|
||||
function shouldBehaveLikeDelay(type, { before, after }) {
|
||||
beforeEach('define timestamp when delay takes effect', async function () {
|
||||
const timestamp = await time.latest();
|
||||
this.delayEffect = timestamp.add(this.delay);
|
||||
});
|
||||
|
||||
describe(`when ${type} delay has not taken effect yet`, function () {
|
||||
beforeEach(`set next block timestamp before ${type} takes effect`, async function () {
|
||||
await setNextBlockTimestamp(this.delayEffect.subn(1));
|
||||
});
|
||||
|
||||
before();
|
||||
});
|
||||
|
||||
describe(`when ${type} delay has taken effect`, function () {
|
||||
beforeEach(`set next block timestamp when ${type} takes effect`, async function () {
|
||||
await setNextBlockTimestamp(this.delayEffect);
|
||||
});
|
||||
|
||||
after();
|
||||
});
|
||||
}
|
||||
|
||||
// ============ OPERATION HELPERS ============
|
||||
|
||||
/**
|
||||
* @requires this.{manager,scheduleIn,caller,target,calldata}
|
||||
*/
|
||||
function shouldBehaveLikeSchedulableOperation({ scheduled: { before, after, expired }, notScheduled }) {
|
||||
describe('when operation is scheduled', function () {
|
||||
beforeEach('schedule operation', async function () {
|
||||
await impersonate(this.caller); // May be a contract
|
||||
const { operationId } = await scheduleOperation(this.manager, {
|
||||
caller: this.caller,
|
||||
target: this.target.address,
|
||||
calldata: this.calldata,
|
||||
delay: this.scheduleIn,
|
||||
});
|
||||
this.operationId = operationId;
|
||||
});
|
||||
|
||||
describe('when operation is not ready for execution', function () {
|
||||
beforeEach('set next block time before operation is ready', async function () {
|
||||
this.scheduledAt = await time.latest();
|
||||
const schedule = await this.manager.getSchedule(this.operationId);
|
||||
await setNextBlockTimestamp(schedule.subn(1));
|
||||
});
|
||||
|
||||
before();
|
||||
});
|
||||
|
||||
describe('when operation is ready for execution', function () {
|
||||
beforeEach('set next block time when operation is ready for execution', async function () {
|
||||
this.scheduledAt = await time.latest();
|
||||
const schedule = await this.manager.getSchedule(this.operationId);
|
||||
await setNextBlockTimestamp(schedule);
|
||||
});
|
||||
|
||||
after();
|
||||
});
|
||||
|
||||
describe('when operation has expired', function () {
|
||||
beforeEach('set next block time when operation expired', async function () {
|
||||
this.scheduledAt = await time.latest();
|
||||
const schedule = await this.manager.getSchedule(this.operationId);
|
||||
await setNextBlockTimestamp(schedule.add(EXPIRATION));
|
||||
});
|
||||
|
||||
expired();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when operation is not scheduled', function () {
|
||||
beforeEach('set expected operationId', async function () {
|
||||
this.operationId = await this.manager.hashOperation(this.caller, this.target.address, this.calldata);
|
||||
|
||||
// Assert operation is not scheduled
|
||||
expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal(web3.utils.toBN(0));
|
||||
});
|
||||
|
||||
notScheduled();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires this.{manager,roles,target,calldata}
|
||||
*/
|
||||
function shouldBehaveLikeARestrictedOperation({ callerIsNotTheManager, callerIsTheManager }) {
|
||||
describe('when the call comes from the manager (msg.sender == manager)', function () {
|
||||
beforeEach('define caller as manager', async function () {
|
||||
this.caller = this.manager.address;
|
||||
await impersonate(this.caller);
|
||||
});
|
||||
|
||||
shouldBehaveLikeCanCallExecuting(callerIsTheManager);
|
||||
});
|
||||
|
||||
describe('when the call does not come from the manager (msg.sender != manager)', function () {
|
||||
beforeEach('define non manager caller', function () {
|
||||
this.caller = this.roles.SOME.members[0];
|
||||
});
|
||||
|
||||
callerIsNotTheManager();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires this.{manager,roles,executionDelay,operationDelay,target}
|
||||
*/
|
||||
function shouldBehaveLikeDelayedOperation() {
|
||||
describe('with operation delay', function () {
|
||||
describe('when operation delay is greater than execution delay', function () {
|
||||
beforeEach('set operation delay', async function () {
|
||||
this.operationDelay = this.executionDelay.add(time.duration.hours(1));
|
||||
await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay);
|
||||
this.scheduleIn = this.operationDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
});
|
||||
|
||||
describe('when operation delay is shorter than execution delay', function () {
|
||||
beforeEach('set operation delay', async function () {
|
||||
this.operationDelay = this.executionDelay.sub(time.duration.hours(1));
|
||||
await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay);
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without operation delay', function () {
|
||||
beforeEach('set operation delay', async function () {
|
||||
this.operationDelay = web3.utils.toBN(0);
|
||||
await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay);
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
});
|
||||
}
|
||||
|
||||
// ============ METHOD HELPERS ============
|
||||
|
||||
/**
|
||||
* @requires this.{manager,roles,role,target,calldata}
|
||||
*/
|
||||
function shouldBehaveLikeCanCall({
|
||||
closed,
|
||||
open: {
|
||||
callerIsTheManager,
|
||||
callerIsNotTheManager: { publicRoleIsRequired, specificRoleIsRequired },
|
||||
},
|
||||
}) {
|
||||
shouldBehaveLikeClosable({
|
||||
closed,
|
||||
open() {
|
||||
shouldBehaveLikeARestrictedOperation({
|
||||
callerIsTheManager,
|
||||
callerIsNotTheManager() {
|
||||
shouldBehaveLikeHasRole({
|
||||
publicRoleIsRequired,
|
||||
specificRoleIsRequired,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires this.{target,calldata}
|
||||
*/
|
||||
function shouldBehaveLikeCanCallExecuting({ executing, notExecuting }) {
|
||||
describe('when _executionId is in storage for target and selector', function () {
|
||||
beforeEach('set _executionId flag from calldata and target', async function () {
|
||||
const executionId = await web3.utils.keccak256(
|
||||
web3.eth.abi.encodeParameters(['address', 'bytes4'], [this.target.address, this.calldata.substring(0, 10)]),
|
||||
);
|
||||
await setStorageAt(this.manager.address, EXECUTION_ID_STORAGE_SLOT, executionId);
|
||||
});
|
||||
|
||||
executing();
|
||||
});
|
||||
|
||||
describe('when _executionId does not match target and selector', notExecuting);
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires this.{target,calldata,roles,role}
|
||||
*/
|
||||
function shouldBehaveLikeHasRole({ publicRoleIsRequired, specificRoleIsRequired }) {
|
||||
describe('when the function requires the caller to be granted with the PUBLIC_ROLE', function () {
|
||||
beforeEach('set target function role as PUBLIC_ROLE', async function () {
|
||||
this.role = this.roles.PUBLIC;
|
||||
await this.manager.$_setTargetFunctionRole(this.target.address, this.calldata.substring(0, 10), this.role.id, {
|
||||
from: this.roles.ADMIN.members[0],
|
||||
});
|
||||
});
|
||||
|
||||
publicRoleIsRequired();
|
||||
});
|
||||
|
||||
describe('when the function requires the caller to be granted with a role other than PUBLIC_ROLE', function () {
|
||||
beforeEach('set target function role as PUBLIC_ROLE', async function () {
|
||||
await this.manager.$_setTargetFunctionRole(this.target.address, this.calldata.substring(0, 10), this.role.id, {
|
||||
from: this.roles.ADMIN.members[0],
|
||||
});
|
||||
});
|
||||
|
||||
shouldBehaveLikeGetAccess(specificRoleIsRequired);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires this.{manager,role,caller}
|
||||
*/
|
||||
function shouldBehaveLikeGetAccess({
|
||||
requiredRoleIsGranted: {
|
||||
roleGrantingIsDelayed: {
|
||||
// Because both grant and execution delay are set within the same $_grantRole call
|
||||
// it's not possible to create a set of tests that diverge between grant and execution delay.
|
||||
// Therefore, the shouldBehaveLikeDelay arguments are renamed for clarity:
|
||||
// before => beforeGrantDelay
|
||||
// after => afterGrantDelay
|
||||
callerHasAnExecutionDelay: { beforeGrantDelay: case1, afterGrantDelay: case2 },
|
||||
callerHasNoExecutionDelay: { beforeGrantDelay: case3, afterGrantDelay: case4 },
|
||||
},
|
||||
roleGrantingIsNotDelayed: { callerHasAnExecutionDelay: case5, callerHasNoExecutionDelay: case6 },
|
||||
},
|
||||
requiredRoleIsNotGranted,
|
||||
}) {
|
||||
describe('when the required role is granted to the caller', function () {
|
||||
describe('when role granting is delayed', function () {
|
||||
beforeEach('define delay', function () {
|
||||
this.grantDelay = time.duration.minutes(3);
|
||||
this.delay = this.grantDelay; // For shouldBehaveLikeDelay
|
||||
});
|
||||
|
||||
describe('when caller has an execution delay', function () {
|
||||
beforeEach('set role and delay', async function () {
|
||||
this.executionDelay = time.duration.hours(10);
|
||||
this.delay = this.grantDelay;
|
||||
await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
|
||||
});
|
||||
|
||||
shouldBehaveLikeDelay('grant', { before: case1, after: case2 });
|
||||
});
|
||||
|
||||
describe('when caller has no execution delay', function () {
|
||||
beforeEach('set role and delay', async function () {
|
||||
this.executionDelay = web3.utils.toBN(0);
|
||||
await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
|
||||
});
|
||||
|
||||
shouldBehaveLikeDelay('grant', { before: case3, after: case4 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when role granting is not delayed', function () {
|
||||
beforeEach('define delay', function () {
|
||||
this.grantDelay = web3.utils.toBN(0);
|
||||
});
|
||||
|
||||
describe('when caller has an execution delay', function () {
|
||||
beforeEach('set role and delay', async function () {
|
||||
this.executionDelay = time.duration.hours(10);
|
||||
await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
|
||||
});
|
||||
|
||||
case5();
|
||||
});
|
||||
|
||||
describe('when caller has no execution delay', function () {
|
||||
beforeEach('set role and delay', async function () {
|
||||
this.executionDelay = web3.utils.toBN(0);
|
||||
await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay);
|
||||
});
|
||||
|
||||
case6();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when role is not granted', function () {
|
||||
// Because this helper can be composed with other helpers, it's possible
|
||||
// that role has been set already by another helper.
|
||||
// Although this is highly unlikely, we check for it here to avoid false positives.
|
||||
beforeEach('assert role is unset', async function () {
|
||||
const { since } = await this.manager.getAccess(this.role.id, this.caller);
|
||||
expect(since).to.be.bignumber.equal(web3.utils.toBN(0));
|
||||
});
|
||||
|
||||
requiredRoleIsNotGranted();
|
||||
});
|
||||
}
|
||||
|
||||
// ============ ADMIN OPERATION HELPERS ============
|
||||
|
||||
/**
|
||||
* @requires this.{manager,roles,calldata,role}
|
||||
*/
|
||||
function shouldBehaveLikeDelayedAdminOperation() {
|
||||
const getAccessPath = COMMON_GET_ACCESS_PATH;
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () {
|
||||
beforeEach('consume previously set grant delay', async function () {
|
||||
// Consume previously set delay
|
||||
await mine();
|
||||
});
|
||||
shouldBehaveLikeDelayedOperation();
|
||||
};
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () {
|
||||
beforeEach('set execution delay', async function () {
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeDelayedOperation
|
||||
});
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
};
|
||||
|
||||
beforeEach('set target as manager', function () {
|
||||
this.target = this.manager;
|
||||
});
|
||||
|
||||
shouldBehaveLikeARestrictedOperation({
|
||||
callerIsTheManager: COMMON_IS_EXECUTING_PATH,
|
||||
callerIsNotTheManager() {
|
||||
shouldBehaveLikeHasRole({
|
||||
publicRoleIsRequired() {
|
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerUnauthorizedAccount',
|
||||
[
|
||||
this.caller,
|
||||
this.roles.ADMIN.id, // Although PUBLIC is required, target function role doesn't apply to admin ops
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
specificRoleIsRequired: getAccessPath,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires this.{manager,roles,calldata,role}
|
||||
*/
|
||||
function shouldBehaveLikeNotDelayedAdminOperation() {
|
||||
const getAccessPath = COMMON_GET_ACCESS_PATH;
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () {
|
||||
beforeEach('set execution delay', async function () {
|
||||
await mine();
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
};
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () {
|
||||
beforeEach('set execution delay', async function () {
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
};
|
||||
|
||||
beforeEach('set target as manager', function () {
|
||||
this.target = this.manager;
|
||||
});
|
||||
|
||||
shouldBehaveLikeARestrictedOperation({
|
||||
callerIsTheManager: COMMON_IS_EXECUTING_PATH,
|
||||
callerIsNotTheManager() {
|
||||
shouldBehaveLikeHasRole({
|
||||
publicRoleIsRequired() {
|
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerUnauthorizedAccount',
|
||||
[this.caller, this.roles.ADMIN.id], // Although PUBLIC_ROLE is required, admin ops are not subject to target function roles
|
||||
);
|
||||
});
|
||||
},
|
||||
specificRoleIsRequired: getAccessPath,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires this.{manager,roles,calldata,role}
|
||||
*/
|
||||
function shouldBehaveLikeRoleAdminOperation(roleAdmin) {
|
||||
const getAccessPath = COMMON_GET_ACCESS_PATH;
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () {
|
||||
beforeEach('set operation delay', async function () {
|
||||
await mine();
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
};
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () {
|
||||
beforeEach('set execution delay', async function () {
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
};
|
||||
|
||||
beforeEach('set target as manager', function () {
|
||||
this.target = this.manager;
|
||||
});
|
||||
|
||||
shouldBehaveLikeARestrictedOperation({
|
||||
callerIsTheManager: COMMON_IS_EXECUTING_PATH,
|
||||
callerIsNotTheManager() {
|
||||
shouldBehaveLikeHasRole({
|
||||
publicRoleIsRequired() {
|
||||
it('reverts as AccessManagerUnauthorizedAccount', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagerUnauthorizedAccount',
|
||||
[this.caller, roleAdmin], // Role admin ops require the role's admin
|
||||
);
|
||||
});
|
||||
},
|
||||
specificRoleIsRequired: getAccessPath,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============ RESTRICTED OPERATION HELPERS ============
|
||||
|
||||
/**
|
||||
* @requires this.{manager,roles,calldata,role}
|
||||
*/
|
||||
function shouldBehaveLikeAManagedRestrictedOperation() {
|
||||
function revertUnauthorized() {
|
||||
it('reverts as AccessManagedUnauthorized', async function () {
|
||||
await expectRevertCustomError(
|
||||
web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }),
|
||||
'AccessManagedUnauthorized',
|
||||
[this.caller],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const getAccessPath = COMMON_GET_ACCESS_PATH;
|
||||
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.beforeGrantDelay =
|
||||
revertUnauthorized;
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasNoExecutionDelay.beforeGrantDelay =
|
||||
revertUnauthorized;
|
||||
getAccessPath.requiredRoleIsNotGranted = revertUnauthorized;
|
||||
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () {
|
||||
beforeEach('consume previously set grant delay', async function () {
|
||||
// Consume previously set delay
|
||||
await mine();
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
};
|
||||
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () {
|
||||
beforeEach('consume previously set grant delay', async function () {
|
||||
this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation
|
||||
});
|
||||
shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH);
|
||||
};
|
||||
|
||||
const isExecutingPath = COMMON_IS_EXECUTING_PATH;
|
||||
isExecutingPath.notExecuting = revertUnauthorized;
|
||||
|
||||
shouldBehaveLikeCanCall({
|
||||
closed: revertUnauthorized,
|
||||
open: {
|
||||
callerIsTheManager: isExecutingPath,
|
||||
callerIsNotTheManager: {
|
||||
publicRoleIsRequired() {
|
||||
it('succeeds called directly', async function () {
|
||||
await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller });
|
||||
});
|
||||
|
||||
it('succeeds via execute', async function () {
|
||||
await this.manager.execute(this.target.address, this.calldata, { from: this.caller });
|
||||
});
|
||||
},
|
||||
specificRoleIsRequired: getAccessPath,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============ HELPERS ============
|
||||
|
||||
/**
|
||||
* @requires this.{manager, caller, target, calldata}
|
||||
*/
|
||||
async function scheduleOperation(manager, { caller, target, calldata, delay }) {
|
||||
const timestamp = await time.latest();
|
||||
const scheduledAt = timestamp.addn(1);
|
||||
await setNextBlockTimestamp(scheduledAt); // Fix next block timestamp for predictability
|
||||
const { receipt } = await manager.schedule(target, calldata, scheduledAt.add(delay), {
|
||||
from: caller,
|
||||
});
|
||||
|
||||
return {
|
||||
receipt,
|
||||
scheduledAt,
|
||||
operationId: await manager.hashOperation(caller, target, calldata),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// COMMON PATHS
|
||||
COMMON_SCHEDULABLE_PATH,
|
||||
COMMON_SCHEDULABLE_PATH_IF_ZERO_DELAY,
|
||||
// MODE HELPERS
|
||||
shouldBehaveLikeClosable,
|
||||
// DELAY HELPERS
|
||||
shouldBehaveLikeDelay,
|
||||
// OPERATION HELPERS
|
||||
shouldBehaveLikeSchedulableOperation,
|
||||
// METHOD HELPERS
|
||||
shouldBehaveLikeCanCall,
|
||||
shouldBehaveLikeGetAccess,
|
||||
shouldBehaveLikeHasRole,
|
||||
// ADMIN OPERATION HELPERS
|
||||
shouldBehaveLikeDelayedAdminOperation,
|
||||
shouldBehaveLikeNotDelayedAdminOperation,
|
||||
shouldBehaveLikeRoleAdminOperation,
|
||||
// RESTRICTED OPERATION HELPERS
|
||||
shouldBehaveLikeAManagedRestrictedOperation,
|
||||
// HELPERS
|
||||
scheduleOperation,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
69
test/helpers/access-manager.js
Normal file
69
test/helpers/access-manager.js
Normal file
@ -0,0 +1,69 @@
|
||||
const { time } = require('@openzeppelin/test-helpers');
|
||||
const { MAX_UINT64 } = require('./constants');
|
||||
const { artifacts } = require('hardhat');
|
||||
|
||||
function buildBaseRoles() {
|
||||
const roles = {
|
||||
ADMIN: {
|
||||
id: web3.utils.toBN(0),
|
||||
},
|
||||
SOME_ADMIN: {
|
||||
id: web3.utils.toBN(17),
|
||||
},
|
||||
SOME_GUARDIAN: {
|
||||
id: web3.utils.toBN(35),
|
||||
},
|
||||
SOME: {
|
||||
id: web3.utils.toBN(42),
|
||||
},
|
||||
PUBLIC: {
|
||||
id: MAX_UINT64,
|
||||
},
|
||||
};
|
||||
|
||||
// Names
|
||||
Object.entries(roles).forEach(([name, role]) => (role.name = name));
|
||||
|
||||
// Defaults
|
||||
for (const role of Object.keys(roles)) {
|
||||
roles[role].admin = roles.ADMIN;
|
||||
roles[role].guardian = roles.ADMIN;
|
||||
}
|
||||
|
||||
// Admins
|
||||
roles.SOME.admin = roles.SOME_ADMIN;
|
||||
|
||||
// Guardians
|
||||
roles.SOME.guardian = roles.SOME_GUARDIAN;
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
const formatAccess = access => [access[0], access[1].toString()];
|
||||
|
||||
const MINSETBACK = time.duration.days(5);
|
||||
const EXPIRATION = time.duration.weeks(1);
|
||||
|
||||
let EXECUTION_ID_STORAGE_SLOT = 3n;
|
||||
let CONSUMING_SCHEDULE_STORAGE_SLOT = 0n;
|
||||
try {
|
||||
// Try to get the artifact paths, will throw if it doesn't exist
|
||||
artifacts._getArtifactPathSync('AccessManagerUpgradeable');
|
||||
artifacts._getArtifactPathSync('AccessManagedUpgradeable');
|
||||
|
||||
// ERC-7201 namespace location for AccessManager
|
||||
EXECUTION_ID_STORAGE_SLOT += 0x40c6c8c28789853c7efd823ab20824bbd71718a8a5915e855f6f288c9a26ad00n;
|
||||
// ERC-7201 namespace location for AccessManaged
|
||||
CONSUMING_SCHEDULE_STORAGE_SLOT += 0xf3177357ab46d8af007ab3fdb9af81da189e1068fefdc0073dca88a2cab40a00n;
|
||||
} catch (_) {
|
||||
// eslint-disable-next-line no-empty
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildBaseRoles,
|
||||
formatAccess,
|
||||
MINSETBACK,
|
||||
EXPIRATION,
|
||||
EXECUTION_ID_STORAGE_SLOT,
|
||||
CONSUMING_SCHEDULE_STORAGE_SLOT,
|
||||
};
|
||||
Reference in New Issue
Block a user