diff --git a/contracts/access/manager/AccessManager.sol b/contracts/access/manager/AccessManager.sol index 234164f219d..9c3f4d06b07 100644 --- a/contracts/access/manager/AccessManager.sol +++ b/contracts/access/manager/AccessManager.sol @@ -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, diff --git a/contracts/access/manager/IAccessManager.sol b/contracts/access/manager/IAccessManager.sol index 0fec166f994..95dd1b65a64 100644 --- a/contracts/access/manager/IAccessManager.sol +++ b/contracts/access/manager/IAccessManager.sol @@ -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); diff --git a/contracts/mocks/AccessManagedTarget.sol b/contracts/mocks/AccessManagedTarget.sol index 0f7c7a193a4..673feedaac6 100644 --- a/contracts/mocks/AccessManagedTarget.sol +++ b/contracts/mocks/AccessManagedTarget.sol @@ -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)> + 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); } diff --git a/contracts/utils/types/Time.sol b/contracts/utils/types/Time.sol index 4ccdc8174f8..12d2659a2a5 100644 --- a/contracts/utils/types/Time.sol +++ b/contracts/utils/types/Time.sol @@ -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); } diff --git a/scripts/upgradeable/transpile.sh b/scripts/upgradeable/transpile.sh index f2126936ca7..ebc8c3219e3 100644 --- a/scripts/upgradeable/transpile.sh +++ b/scripts/upgradeable/transpile.sh @@ -6,7 +6,7 @@ VERSION="$(jq -r .version contracts/package.json)" DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" bash "$DIRNAME/patch-apply.sh" -sed -i "s//$VERSION/g" contracts/package.json +sed -i'' -e "s//$VERSION/g" "contracts/package.json" git add contracts/package.json npm run clean diff --git a/test/access/manager/AccessManager.behavior.js b/test/access/manager/AccessManager.behavior.js new file mode 100644 index 00000000000..d528ffb48b4 --- /dev/null +++ b/test/access/manager/AccessManager.behavior.js @@ -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, +}; diff --git a/test/access/manager/AccessManager.test.js b/test/access/manager/AccessManager.test.js index 5d8ed5de907..705af1a8ae0 100644 --- a/test/access/manager/AccessManager.test.js +++ b/test/access/manager/AccessManager.test.js @@ -1,1030 +1,2592 @@ const { web3 } = require('hardhat'); -const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, time, expectRevert } = require('@openzeppelin/test-helpers'); const { expectRevertCustomError } = require('../../helpers/customError'); const { selector } = require('../../helpers/methods'); const { clockFromReceipt } = require('../../helpers/time'); -const { product } = require('../../helpers/iterate'); -const helpers = require('@nomicfoundation/hardhat-network-helpers'); +const { + buildBaseRoles, + formatAccess, + EXPIRATION, + MINSETBACK, + EXECUTION_ID_STORAGE_SLOT, + CONSUMING_SCHEDULE_STORAGE_SLOT, +} = require('../../helpers/access-manager'); +const { + // 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, +} = require('./AccessManager.behavior'); +const { default: Wallet } = require('ethereumjs-wallet'); +const { + mine, + time: { setNextBlockTimestamp }, + getStorageAt, +} = require('@nomicfoundation/hardhat-network-helpers'); +const { MAX_UINT48 } = require('../../helpers/constants'); +const { impersonate } = require('../../helpers/account'); const AccessManager = artifacts.require('$AccessManager'); const AccessManagedTarget = artifacts.require('$AccessManagedTarget'); const Ownable = artifacts.require('$Ownable'); -const MAX_UINT64 = web3.utils.toBN((2n ** 64n - 1n).toString()); - -const ROLES = { - ADMIN: web3.utils.toBN(0), - SOME_ADMIN: web3.utils.toBN(17), - SOME: web3.utils.toBN(42), - PUBLIC: MAX_UINT64, -}; -Object.assign(ROLES, Object.fromEntries(Object.entries(ROLES).map(([key, value]) => [value, key]))); - -const executeDelay = web3.utils.toBN(10); -const grantDelay = web3.utils.toBN(10); -const MINSETBACK = time.duration.days(5); - -const formatAccess = access => [access[0], access[1].toString()]; +const someAddress = Wallet.generate().getChecksumAddressString(); contract('AccessManager', function (accounts) { - const [admin, manager, member, user, other] = accounts; + const [admin, manager, guardian, member, user, other] = accounts; beforeEach(async function () { + this.roles = buildBaseRoles(); + + // Add members + this.roles.ADMIN.members = [admin]; + this.roles.SOME_ADMIN.members = [manager]; + this.roles.SOME_GUARDIAN.members = [guardian]; + this.roles.SOME.members = [member]; + this.roles.PUBLIC.members = [admin, manager, guardian, member, user, other]; + this.manager = await AccessManager.new(admin); + this.target = await AccessManagedTarget.new(this.manager.address); - // add member to role - await this.manager.$_setRoleAdmin(ROLES.SOME, ROLES.SOME_ADMIN); - await this.manager.$_setRoleGuardian(ROLES.SOME, ROLES.SOME_ADMIN); - await this.manager.$_grantRole(ROLES.SOME_ADMIN, manager, 0, 0); - await this.manager.$_grantRole(ROLES.SOME, member, 0, 0); - }); + for (const { id: roleId, admin, guardian, members } of Object.values(this.roles)) { + if (roleId === this.roles.PUBLIC.id) continue; // Every address belong to public and is locked + if (roleId === this.roles.ADMIN.id) continue; // Admin set during construction and is locked - it('rejects zero address for initialAdmin', async function () { - await expectRevertCustomError(AccessManager.new(constants.ZERO_ADDRESS), 'AccessManagerInvalidInitialAdmin', [ - constants.ZERO_ADDRESS, - ]); - }); + // Set admin role avoiding default + if (admin.id !== this.roles.ADMIN.id) { + await this.manager.$_setRoleAdmin(roleId, admin.id); + } - it('default minsetback is 1 day', async function () { - expect(await this.manager.minSetback()).to.be.bignumber.equal(MINSETBACK); + // Set guardian role avoiding default + if (guardian.id !== this.roles.ADMIN.id) { + await this.manager.$_setRoleGuardian(roleId, guardian.id); + } + + // Grant role to members + for (const member of members) { + await this.manager.$_grantRole(roleId, member, 0, 0); + } + } }); - it('roles are correctly initialized', async function () { - // role admin - expect(await this.manager.getRoleAdmin(ROLES.ADMIN)).to.be.bignumber.equal(ROLES.ADMIN); - expect(await this.manager.getRoleAdmin(ROLES.SOME_ADMIN)).to.be.bignumber.equal(ROLES.ADMIN); - expect(await this.manager.getRoleAdmin(ROLES.SOME)).to.be.bignumber.equal(ROLES.SOME_ADMIN); - expect(await this.manager.getRoleAdmin(ROLES.PUBLIC)).to.be.bignumber.equal(ROLES.ADMIN); - // role guardian - expect(await this.manager.getRoleGuardian(ROLES.ADMIN)).to.be.bignumber.equal(ROLES.ADMIN); - expect(await this.manager.getRoleGuardian(ROLES.SOME_ADMIN)).to.be.bignumber.equal(ROLES.ADMIN); - expect(await this.manager.getRoleGuardian(ROLES.SOME)).to.be.bignumber.equal(ROLES.SOME_ADMIN); - expect(await this.manager.getRoleGuardian(ROLES.PUBLIC)).to.be.bignumber.equal(ROLES.ADMIN); - // role members - expect(await this.manager.hasRole(ROLES.ADMIN, admin).then(formatAccess)).to.be.deep.equal([true, '0']); - expect(await this.manager.hasRole(ROLES.ADMIN, manager).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.ADMIN, member).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.ADMIN, user).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.SOME_ADMIN, admin).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.SOME_ADMIN, manager).then(formatAccess)).to.be.deep.equal([true, '0']); - expect(await this.manager.hasRole(ROLES.SOME_ADMIN, member).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.SOME_ADMIN, user).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.SOME, admin).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.SOME, manager).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.SOME, member).then(formatAccess)).to.be.deep.equal([true, '0']); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); - expect(await this.manager.hasRole(ROLES.PUBLIC, admin).then(formatAccess)).to.be.deep.equal([true, '0']); - expect(await this.manager.hasRole(ROLES.PUBLIC, manager).then(formatAccess)).to.be.deep.equal([true, '0']); - expect(await this.manager.hasRole(ROLES.PUBLIC, member).then(formatAccess)).to.be.deep.equal([true, '0']); - expect(await this.manager.hasRole(ROLES.PUBLIC, user).then(formatAccess)).to.be.deep.equal([true, '0']); + describe('during construction', function () { + it('grants admin role to initialAdmin', async function () { + const manager = await AccessManager.new(other); + expect(await manager.hasRole(this.roles.ADMIN.id, other).then(formatAccess)).to.be.deep.equal([true, '0']); + }); + + it('rejects zero address for initialAdmin', async function () { + await expectRevertCustomError(AccessManager.new(constants.ZERO_ADDRESS), 'AccessManagerInvalidInitialAdmin', [ + constants.ZERO_ADDRESS, + ]); + }); + + it('initializes setup roles correctly', async function () { + for (const { id: roleId, admin, guardian, members } of Object.values(this.roles)) { + expect(await this.manager.getRoleAdmin(roleId)).to.be.bignumber.equal(admin.id); + expect(await this.manager.getRoleGuardian(roleId)).to.be.bignumber.equal(guardian.id); + + for (const user of this.roles.PUBLIC.members) { + expect(await this.manager.hasRole(roleId, user).then(formatAccess)).to.be.deep.equal([ + members.includes(user), + '0', + ]); + } + } + }); }); - describe('Roles management', function () { - describe('label role', function () { - it('admin can emit a label event', async function () { - expectEvent(await this.manager.labelRole(ROLES.SOME, 'Some label', { from: admin }), 'RoleLabel', { - roleId: ROLES.SOME, - label: 'Some label', + describe('getters', function () { + describe('#canCall', function () { + beforeEach('set calldata', function () { + this.calldata = '0x12345678'; + this.role = { id: web3.utils.toBN(379204) }; + }); + + shouldBehaveLikeCanCall({ + closed() { + it('should return false and no delay', async function () { + const { immediate, delay } = await this.manager.canCall( + someAddress, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + open: { + callerIsTheManager: { + executing() { + it('should return true and no delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(true); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + notExecuting() { + it('should return false and no delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + }, + callerIsNotTheManager: { + publicRoleIsRequired() { + it('should return true and no delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(true); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + specificRoleIsRequired: { + requiredRoleIsGranted: { + roleGrantingIsDelayed: { + callerHasAnExecutionDelay: { + beforeGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('should return false and no execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + afterGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + this.scheduleIn = this.executionDelay; // For shouldBehaveLikeSchedulableOperation + }); + + shouldBehaveLikeSchedulableOperation({ + scheduled: { + before() { + beforeEach('consume previously set delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('should return false and execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal(this.executionDelay); + }); + }, + after() { + beforeEach('consume previously set delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('should return false and execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal(this.executionDelay); + }); + }, + expired() { + beforeEach('consume previously set delay', async function () { + // Consume previously set delay + await mine(); + }); + it('should return false and execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal(this.executionDelay); + }); + }, + }, + notScheduled() { + it('should return false and execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal(this.executionDelay); + }); + }, + }); + }, + }, + callerHasNoExecutionDelay: { + beforeGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('should return false and no execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + afterGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('should return true and no execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(true); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + }, + }, + roleGrantingIsNotDelayed: { + callerHasAnExecutionDelay() { + it('should return false and execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal(this.executionDelay); + }); + }, + callerHasNoExecutionDelay() { + it('should return true and no execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(true); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + }, + }, + requiredRoleIsNotGranted() { + it('should return false and no execution delay', async function () { + const { immediate, delay } = await this.manager.canCall( + this.caller, + this.target.address, + this.calldata.substring(0, 10), + ); + expect(immediate).to.be.equal(false); + expect(delay).to.be.bignumber.equal('0'); + }); + }, + }, + }, + }, + }); + }); + + describe('#expiration', function () { + it('has a 7 days default expiration', async function () { + expect(await this.manager.expiration()).to.be.bignumber.equal(EXPIRATION); + }); + }); + + describe('#minSetback', function () { + it('has a 5 days default minimum setback', async function () { + expect(await this.manager.minSetback()).to.be.bignumber.equal(MINSETBACK); + }); + }); + + describe('#isTargetClosed', function () { + shouldBehaveLikeClosable({ + closed() { + it('returns true', async function () { + expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(true); + }); + }, + open() { + it('returns false', async function () { + expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(false); + }); + }, + }); + }); + + describe('#getTargetFunctionRole', function () { + const methodSelector = selector('something(address,bytes)'); + + it('returns the target function role', async function () { + const roleId = web3.utils.toBN(21498); + await this.manager.$_setTargetFunctionRole(this.target.address, methodSelector, roleId); + + expect(await this.manager.getTargetFunctionRole(this.target.address, methodSelector)).to.be.bignumber.equal( + roleId, + ); + }); + + it('returns the ADMIN role if not set', async function () { + expect(await this.manager.getTargetFunctionRole(this.target.address, methodSelector)).to.be.bignumber.equal( + this.roles.ADMIN.id, + ); + }); + }); + + describe('#getTargetAdminDelay', function () { + describe('when the target admin delay is setup', function () { + beforeEach('set target admin delay', async function () { + this.oldDelay = await this.manager.getTargetAdminDelay(this.target.address); + this.newDelay = time.duration.days(10); + + await this.manager.$_setTargetAdminDelay(this.target.address, this.newDelay); + this.delay = MINSETBACK; // For shouldBehaveLikeDelay + }); + + shouldBehaveLikeDelay('effect', { + before() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('returns the old target admin delay', async function () { + expect(await this.manager.getTargetAdminDelay(this.target.address)).to.be.bignumber.equal(this.oldDelay); + }); + }, + after() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('returns the new target admin delay', async function () { + expect(await this.manager.getTargetAdminDelay(this.target.address)).to.be.bignumber.equal(this.newDelay); + }); + }, }); }); - it('admin can re-emit a label event', async function () { - await this.manager.labelRole(ROLES.SOME, 'Some label', { from: admin }); + it('returns the 0 if not set', async function () { + expect(await this.manager.getTargetAdminDelay(this.target.address)).to.be.bignumber.equal('0'); + }); + }); + + describe('#getRoleAdmin', function () { + const roleId = web3.utils.toBN(5234907); + + it('returns the role admin', async function () { + const adminId = web3.utils.toBN(789433); + + await this.manager.$_setRoleAdmin(roleId, adminId); + + expect(await this.manager.getRoleAdmin(roleId)).to.be.bignumber.equal(adminId); + }); + + it('returns the ADMIN role if not set', async function () { + expect(await this.manager.getRoleAdmin(roleId)).to.be.bignumber.equal(this.roles.ADMIN.id); + }); + }); + + describe('#getRoleGuardian', function () { + const roleId = web3.utils.toBN(5234907); - expectEvent(await this.manager.labelRole(ROLES.SOME, 'Updated label', { from: admin }), 'RoleLabel', { - roleId: ROLES.SOME, - label: 'Updated label', + it('returns the role guardian', async function () { + const guardianId = web3.utils.toBN(789433); + + await this.manager.$_setRoleGuardian(roleId, guardianId); + + expect(await this.manager.getRoleGuardian(roleId)).to.be.bignumber.equal(guardianId); + }); + + it('returns the ADMIN role if not set', async function () { + expect(await this.manager.getRoleGuardian(roleId)).to.be.bignumber.equal(this.roles.ADMIN.id); + }); + }); + + describe('#getRoleGrantDelay', function () { + const roleId = web3.utils.toBN(9248439); + + describe('when the grant admin delay is setup', function () { + beforeEach('set grant admin delay', async function () { + this.oldDelay = await this.manager.getRoleGrantDelay(roleId); + this.newDelay = time.duration.days(11); + + await this.manager.$_setGrantDelay(roleId, this.newDelay); + this.delay = MINSETBACK; // For shouldBehaveLikeDelay + }); + + shouldBehaveLikeDelay('grant', { + before() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('returns the old role grant delay', async function () { + expect(await this.manager.getRoleGrantDelay(roleId)).to.be.bignumber.equal(this.oldDelay); + }); + }, + after() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('returns the new role grant delay', async function () { + expect(await this.manager.getRoleGrantDelay(roleId)).to.be.bignumber.equal(this.newDelay); + }); + }, }); }); - it('emitting a label is restricted', async function () { - await expectRevertCustomError( - this.manager.labelRole(ROLES.SOME, 'Invalid label', { from: other }), - 'AccessManagerUnauthorizedAccount', - [other, ROLES.ADMIN], + it('returns 0 if delay is not set', async function () { + expect(await this.manager.getTargetAdminDelay(this.target.address)).to.be.bignumber.equal('0'); + }); + }); + + describe('#getAccess', function () { + beforeEach('set role', function () { + this.role = { id: web3.utils.toBN(9452) }; + this.caller = user; + }); + + shouldBehaveLikeGetAccess({ + requiredRoleIsGranted: { + roleGrantingIsDelayed: { + callerHasAnExecutionDelay: { + beforeGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('role is not in effect and execution delay is set', async function () { + const access = await this.manager.getAccess(this.role.id, this.caller); + expect(access[0]).to.be.bignumber.equal(this.delayEffect); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Not in effect yet + expect(await time.latest()).to.be.bignumber.lt(access[0]); + }); + }, + afterGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('access has role in effect and execution delay is set', async function () { + const access = await this.manager.getAccess(this.role.id, this.caller); + + expect(access[0]).to.be.bignumber.equal(this.delayEffect); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + expect(await time.latest()).to.be.bignumber.equal(access[0]); + }); + }, + }, + callerHasNoExecutionDelay: { + beforeGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('access has role not in effect without execution delay', async function () { + const access = await this.manager.getAccess(this.role.id, this.caller); + expect(access[0]).to.be.bignumber.equal(this.delayEffect); // inEffectSince + expect(access[1]).to.be.bignumber.equal('0'); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Not in effect yet + expect(await time.latest()).to.be.bignumber.lt(access[0]); + }); + }, + afterGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('role is in effect without execution delay', async function () { + const access = await this.manager.getAccess(this.role.id, this.caller); + expect(access[0]).to.be.bignumber.equal(this.delayEffect); // inEffectSince + expect(access[1]).to.be.bignumber.equal('0'); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + expect(await time.latest()).to.be.bignumber.equal(access[0]); + }); + }, + }, + }, + roleGrantingIsNotDelayed: { + callerHasAnExecutionDelay() { + it('access has role in effect and execution delay is set', async function () { + const access = await this.manager.getAccess(this.role.id, this.caller); + expect(access[0]).to.be.bignumber.equal(await time.latest()); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + expect(await time.latest()).to.be.bignumber.equal(access[0]); + }); + }, + callerHasNoExecutionDelay() { + it('access has role in effect without execution delay', async function () { + const access = await this.manager.getAccess(this.role.id, this.caller); + expect(access[0]).to.be.bignumber.equal(await time.latest()); // inEffectSince + expect(access[1]).to.be.bignumber.equal('0'); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + expect(await time.latest()).to.be.bignumber.equal(access[0]); + }); + }, + }, + }, + requiredRoleIsNotGranted() { + it('has empty access', async function () { + const access = await this.manager.getAccess(this.role.id, this.caller); + expect(access[0]).to.be.bignumber.equal('0'); // inEffectSince + expect(access[1]).to.be.bignumber.equal('0'); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + }); + }, + }); + }); + + describe('#hasRole', function () { + beforeEach('setup shouldBehaveLikeHasRole', function () { + this.role = { id: web3.utils.toBN(49832) }; + this.calldata = '0x1234'; + this.caller = user; + }); + + shouldBehaveLikeHasRole({ + publicRoleIsRequired() { + it('has PUBLIC role', async function () { + const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); + expect(isMember).to.be.true; + expect(executionDelay).to.be.bignumber.eq('0'); + }); + }, + specificRoleIsRequired: { + requiredRoleIsGranted: { + roleGrantingIsDelayed: { + callerHasAnExecutionDelay: { + beforeGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('does not have role but execution delay', async function () { + const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); + expect(isMember).to.be.false; + expect(executionDelay).to.be.bignumber.eq(this.executionDelay); + }); + }, + afterGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('has role and execution delay', async function () { + const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); + expect(isMember).to.be.true; + expect(executionDelay).to.be.bignumber.eq(this.executionDelay); + }); + }, + }, + callerHasNoExecutionDelay: { + beforeGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('does not have role nor execution delay', async function () { + const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); + expect(isMember).to.be.false; + expect(executionDelay).to.be.bignumber.eq('0'); + }); + }, + afterGrantDelay() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('has role and no execution delay', async function () { + const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); + expect(isMember).to.be.true; + expect(executionDelay).to.be.bignumber.eq('0'); + }); + }, + }, + }, + roleGrantingIsNotDelayed: { + callerHasAnExecutionDelay() { + it('has role and execution delay', async function () { + const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); + expect(isMember).to.be.true; + expect(executionDelay).to.be.bignumber.eq(this.executionDelay); + }); + }, + callerHasNoExecutionDelay() { + it('has role and no execution delay', async function () { + const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); + expect(isMember).to.be.true; + expect(executionDelay).to.be.bignumber.eq('0'); + }); + }, + }, + }, + requiredRoleIsNotGranted() { + it('has no role and no execution delay', async function () { + const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); + expect(isMember).to.be.false; + expect(executionDelay).to.be.bignumber.eq('0'); + }); + }, + }, + }); + }); + + describe('#getSchedule', function () { + beforeEach('set role and calldata', async function () { + const method = 'fnRestricted()'; + this.caller = user; + this.role = { id: web3.utils.toBN(493590) }; + await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // nonzero execution delay + + this.calldata = await this.target.contract.methods[method]().encodeABI(); + this.scheduleIn = time.duration.days(10); // For shouldBehaveLikeSchedulableOperation + }); + + shouldBehaveLikeSchedulableOperation({ + scheduled: { + before() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('returns schedule in the future', async function () { + const schedule = await this.manager.getSchedule(this.operationId); + expect(schedule).to.be.bignumber.equal(this.scheduledAt.add(this.scheduleIn)); + expect(schedule).to.be.bignumber.gt(await time.latest()); + }); + }, + after() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('returns schedule', async function () { + const schedule = await this.manager.getSchedule(this.operationId); + expect(schedule).to.be.bignumber.equal(this.scheduledAt.add(this.scheduleIn)); + expect(schedule).to.be.bignumber.eq(await time.latest()); + }); + }, + expired() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('returns 0', async function () { + expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal('0'); + }); + }, + }, + notScheduled() { + it('defaults to 0', async function () { + expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal('0'); + }); + }, + }); + }); + + describe('#getNonce', function () { + describe('when operation is scheduled', function () { + beforeEach('schedule operation', async function () { + const method = 'fnRestricted()'; + this.caller = user; + this.role = { id: web3.utils.toBN(4209043) }; + await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // nonzero execution delay + + this.calldata = await this.target.contract.methods[method]().encodeABI(); + this.delay = time.duration.days(10); + + const { operationId } = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }); + this.operationId = operationId; + }); + + it('returns nonce', async function () { + expect(await this.manager.getNonce(this.operationId)).to.be.bignumber.equal('1'); + }); + }); + + describe('when is not scheduled', function () { + it('returns default 0', async function () { + expect(await this.manager.getNonce(web3.utils.keccak256('operation'))).to.be.bignumber.equal('0'); + }); + }); + }); + + describe('#hashOperation', function () { + it('returns an operationId', async function () { + const calldata = '0x123543'; + const address = someAddress; + + const args = [user, address, calldata]; + + expect(await this.manager.hashOperation(...args)).to.be.bignumber.eq( + await web3.utils.keccak256(web3.eth.abi.encodeParameters(['address', 'address', 'bytes'], args)), ); }); }); + }); - describe('grant role', function () { - describe('without a grant delay', function () { - it('without an execute delay', async function () { - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); - - const { receipt } = await this.manager.grantRole(ROLES.SOME, user, 0, { from: manager }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'RoleGranted', { - roleId: ROLES.SOME, - account: user, - since: timestamp, - delay: '0', - newMember: true, + describe('admin operations', function () { + beforeEach('set required role', function () { + this.role = this.roles.ADMIN; + }); + + describe('subject to a delay', function () { + describe('#labelRole', function () { + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'labelRole(uint64,string)'; + const args = [123443, 'TEST']; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([true, '0']); + shouldBehaveLikeDelayedAdminOperation(); + }); - const access = await this.manager.getAccess(ROLES.SOME, user); - expect(access[0]).to.be.bignumber.equal(timestamp); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect + it('emits an event with the label', async function () { + expectEvent(await this.manager.labelRole(this.roles.SOME.id, 'Some label', { from: admin }), 'RoleLabel', { + roleId: this.roles.SOME.id, + label: 'Some label', + }); }); - it('with an execute delay', async function () { - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); - - const { receipt } = await this.manager.grantRole(ROLES.SOME, user, executeDelay, { from: manager }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'RoleGranted', { - roleId: ROLES.SOME, - account: user, - since: timestamp, - delay: executeDelay, - newMember: true, + it('updates label on a second call', async function () { + await this.manager.labelRole(this.roles.SOME.id, 'Some label', { from: admin }); + + expectEvent(await this.manager.labelRole(this.roles.SOME.id, 'Updated label', { from: admin }), 'RoleLabel', { + roleId: this.roles.SOME.id, + label: 'Updated label', }); + }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([ - true, - executeDelay.toString(), - ]); + it('reverts labeling PUBLIC_ROLE', async function () { + await expectRevertCustomError( + this.manager.labelRole(this.roles.PUBLIC.id, 'Some label', { from: admin }), + 'AccessManagerLockedRole', + [this.roles.PUBLIC.id], + ); + }); + + it('reverts labeling ADMIN_ROLE', async function () { + await expectRevertCustomError( + this.manager.labelRole(this.roles.ADMIN.id, 'Some label', { from: admin }), + 'AccessManagerLockedRole', + [this.roles.ADMIN.id], + ); + }); + }); + + describe('#setRoleAdmin', function () { + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'setRoleAdmin(uint64,uint64)'; + const args = [93445, 84532]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + }); - const access = await this.manager.getAccess(ROLES.SOME, user); - expect(access[0]).to.be.bignumber.equal(timestamp); // inRoleSince - expect(access[1]).to.be.bignumber.equal(executeDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect + shouldBehaveLikeDelayedAdminOperation(); }); - it('to a user that is already in the role', async function () { - expect(await this.manager.hasRole(ROLES.SOME, member).then(formatAccess)).to.be.deep.equal([true, '0']); - await this.manager.grantRole(ROLES.SOME, member, 0, { from: manager }); - expect(await this.manager.hasRole(ROLES.SOME, member).then(formatAccess)).to.be.deep.equal([true, '0']); + it("sets any role's admin if called by an admin", async function () { + expect(await this.manager.getRoleAdmin(this.roles.SOME.id)).to.be.bignumber.equal(this.roles.SOME_ADMIN.id); + + const { receipt } = await this.manager.setRoleAdmin(this.roles.SOME.id, this.roles.ADMIN.id, { from: admin }); + expectEvent(receipt, 'RoleAdminChanged', { roleId: this.roles.SOME.id, admin: this.roles.ADMIN.id }); + + expect(await this.manager.getRoleAdmin(this.roles.SOME.id)).to.be.bignumber.equal(this.roles.ADMIN.id); }); - it('to a user that is scheduled for joining the role', async function () { - await this.manager.$_grantRole(ROLES.SOME, user, 10, 0); // grant delay 10 - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); - await this.manager.grantRole(ROLES.SOME, user, 0, { from: manager }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); + it('reverts setting PUBLIC_ROLE admin', async function () { + await expectRevertCustomError( + this.manager.setRoleAdmin(this.roles.PUBLIC.id, this.roles.ADMIN.id, { from: admin }), + 'AccessManagerLockedRole', + [this.roles.PUBLIC.id], + ); }); - it('grant role is restricted', async function () { + it('reverts setting ADMIN_ROLE admin', async function () { await expectRevertCustomError( - this.manager.grantRole(ROLES.SOME, user, 0, { from: other }), - 'AccessManagerUnauthorizedAccount', - [other, ROLES.SOME_ADMIN], + this.manager.setRoleAdmin(this.roles.ADMIN.id, this.roles.ADMIN.id, { from: admin }), + 'AccessManagerLockedRole', + [this.roles.ADMIN.id], ); }); }); - describe('with a grant delay', function () { - beforeEach(async function () { - await this.manager.$_setGrantDelay(ROLES.SOME, grantDelay); - await time.increase(MINSETBACK); + describe('#setRoleGuardian', function () { + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'setRoleGuardian(uint64,uint64)'; + const args = [93445, 84532]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + }); + + shouldBehaveLikeDelayedAdminOperation(); + }); + + it("sets any role's guardian if called by an admin", async function () { + expect(await this.manager.getRoleGuardian(this.roles.SOME.id)).to.be.bignumber.equal( + this.roles.SOME_GUARDIAN.id, + ); + + const { receipt } = await this.manager.setRoleGuardian(this.roles.SOME.id, this.roles.ADMIN.id, { + from: admin, + }); + expectEvent(receipt, 'RoleGuardianChanged', { roleId: this.roles.SOME.id, guardian: this.roles.ADMIN.id }); + + expect(await this.manager.getRoleGuardian(this.roles.SOME.id)).to.be.bignumber.equal(this.roles.ADMIN.id); + }); + + it('reverts setting PUBLIC_ROLE admin', async function () { + await expectRevertCustomError( + this.manager.setRoleGuardian(this.roles.PUBLIC.id, this.roles.ADMIN.id, { from: admin }), + 'AccessManagerLockedRole', + [this.roles.PUBLIC.id], + ); + }); + + it('reverts setting ADMIN_ROLE admin', async function () { + await expectRevertCustomError( + this.manager.setRoleGuardian(this.roles.ADMIN.id, this.roles.ADMIN.id, { from: admin }), + 'AccessManagerLockedRole', + [this.roles.ADMIN.id], + ); }); + }); - it('granted role is not active immediately', async function () { - const { receipt } = await this.manager.grantRole(ROLES.SOME, user, 0, { from: manager }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'RoleGranted', { - roleId: ROLES.SOME, - account: user, - since: timestamp.add(grantDelay), - delay: '0', - newMember: true, + describe('#setGrantDelay', function () { + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'setGrantDelay(uint64,uint32)'; + const args = [984910, time.duration.days(2)]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); + shouldBehaveLikeDelayedAdminOperation(); + }); - const access = await this.manager.getAccess(ROLES.SOME, user); - expect(access[0]).to.be.bignumber.equal(timestamp.add(grantDelay)); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect + it('reverts setting grant delay for the PUBLIC_ROLE', async function () { + await expectRevertCustomError( + this.manager.setGrantDelay(this.roles.PUBLIC.id, web3.utils.toBN(69), { from: admin }), + 'AccessManagerLockedRole', + [this.roles.PUBLIC.id], + ); }); - it('granted role is active after the delay', async function () { - const { receipt } = await this.manager.grantRole(ROLES.SOME, user, 0, { from: manager }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'RoleGranted', { - roleId: ROLES.SOME, - account: user, - since: timestamp.add(grantDelay), - delay: '0', - newMember: true, + describe('when increasing the delay', function () { + const oldDelay = web3.utils.toBN(10); + const newDelay = web3.utils.toBN(100); + + beforeEach('sets old delay', async function () { + this.role = this.roles.SOME; + await this.manager.$_setGrantDelay(this.role.id, oldDelay); + await time.increase(MINSETBACK); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); + }); + + it('increases the delay after minsetback', async function () { + const { receipt } = await this.manager.setGrantDelay(this.role.id, newDelay, { from: admin }); + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + expectEvent(receipt, 'RoleGrantDelayChanged', { + roleId: this.role.id, + delay: newDelay, + since: timestamp.add(MINSETBACK), + }); + + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); + await time.increase(MINSETBACK); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(newDelay); + }); + }); + + describe('when reducing the delay', function () { + const oldDelay = time.duration.days(10); + + beforeEach('sets old delay', async function () { + this.role = this.roles.SOME; + await this.manager.$_setGrantDelay(this.role.id, oldDelay); + await time.increase(MINSETBACK); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); + }); + + describe('when the delay difference is shorter than minimum setback', function () { + const newDelay = oldDelay.subn(1); + + it('increases the delay after minsetback', async function () { + const { receipt } = await this.manager.setGrantDelay(this.role.id, newDelay, { from: admin }); + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + expectEvent(receipt, 'RoleGrantDelayChanged', { + roleId: this.role.id, + delay: newDelay, + since: timestamp.add(MINSETBACK), + }); + + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); + await time.increase(MINSETBACK); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(newDelay); + }); }); - await time.increase(grantDelay); + describe('when the delay difference is longer than minimum setback', function () { + const newDelay = web3.utils.toBN(1); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([true, '0']); + beforeEach('assert delay difference is higher than minsetback', function () { + expect(oldDelay.sub(newDelay)).to.be.bignumber.gt(MINSETBACK); + }); - const access = await this.manager.getAccess(ROLES.SOME, user); - expect(access[0]).to.be.bignumber.equal(timestamp.add(grantDelay)); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect + it('increases the delay after delay difference', async function () { + const setback = oldDelay.sub(newDelay); + const { receipt } = await this.manager.setGrantDelay(this.role.id, newDelay, { from: admin }); + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + expectEvent(receipt, 'RoleGrantDelayChanged', { + roleId: this.role.id, + delay: newDelay, + since: timestamp.add(setback), + }); + + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); + await time.increase(setback); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(newDelay); + }); + }); }); }); - it('cannot grant public role', async function () { - await expectRevertCustomError( - this.manager.$_grantRole(ROLES.PUBLIC, other, 0, executeDelay, { from: manager }), - 'AccessManagerLockedRole', - [ROLES.PUBLIC], - ); + describe('#setTargetAdminDelay', function () { + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'setTargetAdminDelay(address,uint32)'; + const args = [someAddress, time.duration.days(3)]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + }); + + shouldBehaveLikeDelayedAdminOperation(); + }); + + describe('when increasing the delay', function () { + const oldDelay = time.duration.days(10); + const newDelay = time.duration.days(11); + const target = someAddress; + + beforeEach('sets old delay', async function () { + await this.manager.$_setTargetAdminDelay(target, oldDelay); + await time.increase(MINSETBACK); + expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); + }); + + it('increases the delay after minsetback', async function () { + const { receipt } = await this.manager.setTargetAdminDelay(target, newDelay, { from: admin }); + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + expectEvent(receipt, 'TargetAdminDelayUpdated', { + target, + delay: newDelay, + since: timestamp.add(MINSETBACK), + }); + + expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); + await time.increase(MINSETBACK); + expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(newDelay); + }); + }); + + describe('when reducing the delay', function () { + const oldDelay = time.duration.days(10); + const target = someAddress; + + beforeEach('sets old delay', async function () { + await this.manager.$_setTargetAdminDelay(target, oldDelay); + await time.increase(MINSETBACK); + expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); + }); + + describe('when the delay difference is shorter than minimum setback', function () { + const newDelay = oldDelay.subn(1); + + it('increases the delay after minsetback', async function () { + const { receipt } = await this.manager.setTargetAdminDelay(target, newDelay, { from: admin }); + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + expectEvent(receipt, 'TargetAdminDelayUpdated', { + target, + delay: newDelay, + since: timestamp.add(MINSETBACK), + }); + + expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); + await time.increase(MINSETBACK); + expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(newDelay); + }); + }); + + describe('when the delay difference is longer than minimum setback', function () { + const newDelay = web3.utils.toBN(1); + + beforeEach('assert delay difference is higher than minsetback', function () { + expect(oldDelay.sub(newDelay)).to.be.bignumber.gt(MINSETBACK); + }); + + it('increases the delay after delay difference', async function () { + const setback = oldDelay.sub(newDelay); + const { receipt } = await this.manager.setTargetAdminDelay(target, newDelay, { from: admin }); + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + expectEvent(receipt, 'TargetAdminDelayUpdated', { + target, + delay: newDelay, + since: timestamp.add(setback), + }); + + expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); + await time.increase(setback); + expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(newDelay); + }); + }); + }); }); }); - describe('revoke role', function () { - it('from a user that is already in the role', async function () { - expect(await this.manager.hasRole(ROLES.SOME, member).then(formatAccess)).to.be.deep.equal([true, '0']); + describe('not subject to a delay', function () { + describe('#updateAuthority', function () { + beforeEach('create a target and a new authority', async function () { + this.newAuthority = await AccessManager.new(admin); + this.newManagedTarget = await AccessManagedTarget.new(this.manager.address); + }); + + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'updateAuthority(address,address)'; + const args = [this.newManagedTarget.address, this.newAuthority.address]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + }); - const { receipt } = await this.manager.revokeRole(ROLES.SOME, member, { from: manager }); - expectEvent(receipt, 'RoleRevoked', { roleId: ROLES.SOME, account: member }); + shouldBehaveLikeNotDelayedAdminOperation(); + }); - expect(await this.manager.hasRole(ROLES.SOME, member).then(formatAccess)).to.be.deep.equal([false, '0']); + it('changes the authority', async function () { + expect(await this.newManagedTarget.authority()).to.be.equal(this.manager.address); - const access = await this.manager.getAccess(ROLES.SOME, user); - expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect + const { tx } = await this.manager.updateAuthority(this.newManagedTarget.address, this.newAuthority.address, { + from: admin, + }); + + // Managed contract is responsible of notifying the change through an event + await expectEvent.inTransaction(tx, this.newManagedTarget, 'AuthorityUpdated', { + authority: this.newAuthority.address, + }); + + expect(await this.newManagedTarget.authority()).to.be.equal(this.newAuthority.address); + }); }); - it('from a user that is scheduled for joining the role', async function () { - await this.manager.$_grantRole(ROLES.SOME, user, 10, 0); // grant delay 10 + describe('#setTargetClosed', function () { + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'setTargetClosed(address,bool)'; + const args = [someAddress, true]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); + shouldBehaveLikeNotDelayedAdminOperation(); + }); - const { receipt } = await this.manager.revokeRole(ROLES.SOME, user, { from: manager }); - expectEvent(receipt, 'RoleRevoked', { roleId: ROLES.SOME, account: user }); + it('closes and opens a target', async function () { + const close = await this.manager.setTargetClosed(this.target.address, true, { from: admin }); + expectEvent(close.receipt, 'TargetClosed', { target: this.target.address, closed: true }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); + expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(true); - const access = await this.manager.getAccess(ROLES.SOME, user); - expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect - }); + const open = await this.manager.setTargetClosed(this.target.address, false, { from: admin }); + expectEvent(open.receipt, 'TargetClosed', { target: this.target.address, closed: false }); + expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(false); + }); - it('from a user that is not in the role', async function () { - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); - await this.manager.revokeRole(ROLES.SOME, user, { from: manager }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); + it('reverts if closing the manager', async function () { + await expectRevertCustomError( + this.manager.setTargetClosed(this.manager.address, true, { from: admin }), + 'AccessManagerLockedAccount', + [this.manager.address], + ); + }); }); - it('revoke role is restricted', async function () { - await expectRevertCustomError( - this.manager.revokeRole(ROLES.SOME, member, { from: other }), - 'AccessManagerUnauthorizedAccount', - [other, ROLES.SOME_ADMIN], - ); + describe('#setTargetFunctionRole', function () { + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'setTargetFunctionRole(address,bytes4[],uint64)'; + const args = [someAddress, ['0x12345678'], 443342]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + }); + + shouldBehaveLikeNotDelayedAdminOperation(); + }); + + const sigs = ['someFunction()', 'someOtherFunction(uint256)', 'oneMoreFunction(address,uint8)'].map(selector); + + it('sets function roles', async function () { + for (const sig of sigs) { + expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal( + this.roles.ADMIN.id, + ); + } + + const { receipt: receipt1 } = await this.manager.setTargetFunctionRole( + this.target.address, + sigs, + this.roles.SOME.id, + { + from: admin, + }, + ); + + for (const sig of sigs) { + expectEvent(receipt1, 'TargetFunctionRoleUpdated', { + target: this.target.address, + selector: sig, + roleId: this.roles.SOME.id, + }); + expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal( + this.roles.SOME.id, + ); + } + + const { receipt: receipt2 } = await this.manager.setTargetFunctionRole( + this.target.address, + [sigs[1]], + this.roles.SOME_ADMIN.id, + { + from: admin, + }, + ); + expectEvent(receipt2, 'TargetFunctionRoleUpdated', { + target: this.target.address, + selector: sigs[1], + roleId: this.roles.SOME_ADMIN.id, + }); + + for (const sig of sigs) { + expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal( + sig == sigs[1] ? this.roles.SOME_ADMIN.id : this.roles.SOME.id, + ); + } + }); }); - }); - describe('renounce role', function () { - it('for a user that is already in the role', async function () { - expect(await this.manager.hasRole(ROLES.SOME, member).then(formatAccess)).to.be.deep.equal([true, '0']); + describe('role admin operations', function () { + const ANOTHER_ADMIN = web3.utils.toBN(0xdeadc0de1); + const ANOTHER_ROLE = web3.utils.toBN(0xdeadc0de2); + + beforeEach('set required role', async function () { + // Make admin a member of ANOTHER_ADMIN + await this.manager.$_grantRole(ANOTHER_ADMIN, admin, 0, 0); + await this.manager.$_setRoleAdmin(ANOTHER_ROLE, ANOTHER_ADMIN); + + this.role = { id: ANOTHER_ADMIN }; + this.user = user; + await this.manager.$_grantRole(this.role.id, this.user, 0, 0); + }); + + describe('#grantRole', function () { + describe('restrictions', function () { + beforeEach('set method and args', function () { + const method = 'grantRole(uint64,address,uint32)'; + const args = [ANOTHER_ROLE, someAddress, 0]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + }); + + shouldBehaveLikeRoleAdminOperation(ANOTHER_ADMIN); + }); + + it('reverts when granting PUBLIC_ROLE', async function () { + await expectRevertCustomError( + this.manager.grantRole(this.roles.PUBLIC.id, user, 0, { + from: admin, + }), + 'AccessManagerLockedRole', + [this.roles.PUBLIC.id], + ); + }); + + describe('when the user is not a role member', function () { + describe('with grant delay', function () { + beforeEach('set grant delay and grant role', async function () { + // Delay granting + this.grantDelay = time.duration.weeks(2); + await this.manager.$_setGrantDelay(ANOTHER_ROLE, this.grantDelay); + await time.increase(MINSETBACK); + + // Grant role + this.executionDelay = time.duration.days(3); + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ + false, + '0', + ]); + const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.executionDelay, { + from: admin, + }); + + this.receipt = receipt; + this.delay = this.grantDelay; // For shouldBehaveLikeDelay + }); + + shouldBehaveLikeDelay('grant', { + before() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('does not grant role to the user yet', async function () { + const timestamp = await clockFromReceipt.timestamp(this.receipt).then(web3.utils.toBN); + expectEvent(this.receipt, 'RoleGranted', { + roleId: ANOTHER_ROLE, + account: this.user, + since: timestamp.add(this.grantDelay), + delay: this.executionDelay, + newMember: true, + }); + + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal(timestamp.add(this.grantDelay)); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Not in effect yet + const currentTimestamp = await time.latest(); + expect(currentTimestamp).to.be.a.bignumber.lt(access[0]); + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + false, + this.executionDelay.toString(), + ]); + }); + }, + after() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('grants role to the user', async function () { + const timestamp = await clockFromReceipt.timestamp(this.receipt).then(web3.utils.toBN); + expectEvent(this.receipt, 'RoleGranted', { + roleId: ANOTHER_ROLE, + account: this.user, + since: timestamp.add(this.grantDelay), + delay: this.executionDelay, + newMember: true, + }); + + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal(timestamp.add(this.grantDelay)); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + const currentTimestamp = await time.latest(); + expect(currentTimestamp).to.be.a.bignumber.equal(access[0]); + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + this.executionDelay.toString(), + ]); + }); + }, + }); + }); + + describe('without grant delay', function () { + beforeEach('set granting delay', async function () { + // Delay granting + this.grantDelay = 0; + await this.manager.$_setGrantDelay(ANOTHER_ROLE, this.grantDelay); + await time.increase(MINSETBACK); + }); + + it('immediately grants the role to the user', async function () { + this.executionDelay = time.duration.days(6); + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ + false, + '0', + ]); + const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.executionDelay, { + from: admin, + }); + + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + expectEvent(receipt, 'RoleGranted', { + roleId: ANOTHER_ROLE, + account: this.user, + since: timestamp, + delay: this.executionDelay, + newMember: true, + }); + + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal(timestamp); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + const currentTimestamp = await time.latest(); + expect(currentTimestamp).to.be.a.bignumber.equal(access[0]); + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + this.executionDelay.toString(), + ]); + }); + }); + }); + + describe('when the user is already a role member', function () { + beforeEach('make user role member', async function () { + this.previousExecutionDelay = time.duration.days(6); + await this.manager.$_grantRole(ANOTHER_ROLE, this.user, 0, this.previousExecutionDelay); + this.oldAccess = await this.manager.getAccess(ANOTHER_ROLE, user); + }); + + describe('with grant delay', function () { + beforeEach('set granting delay', async function () { + // Delay granting + const grantDelay = time.duration.weeks(2); + await this.manager.$_setGrantDelay(ANOTHER_ROLE, grantDelay); + await time.increase(MINSETBACK); + }); - const { receipt } = await this.manager.renounceRole(ROLES.SOME, member, { from: member }); - expectEvent(receipt, 'RoleRevoked', { roleId: ROLES.SOME, account: member }); + describe('when increasing the execution delay', function () { + beforeEach('set increased new execution delay', async function () { + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ + true, + this.previousExecutionDelay.toString(), + ]); - expect(await this.manager.hasRole(ROLES.SOME, member).then(formatAccess)).to.be.deep.equal([false, '0']); + this.newExecutionDelay = this.previousExecutionDelay.add(time.duration.days(4)); + }); - const access = await this.manager.getAccess(ROLES.SOME, member); - expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect - }); + it('emits event and immediately changes the execution delay', async function () { + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ + true, + this.previousExecutionDelay.toString(), + ]); + const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay, { + from: admin, + }); + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + + expectEvent(receipt, 'RoleGranted', { + roleId: ANOTHER_ROLE, + account: this.user, + since: timestamp, + delay: this.newExecutionDelay, + newMember: false, + }); + + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.newExecutionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + this.newExecutionDelay.toString(), + ]); + }); + }); - it('for a user that is schedule for joining the role', async function () { - await this.manager.$_grantRole(ROLES.SOME, user, 10, 0); // grant delay 10 + describe('when decreasing the execution delay', function () { + beforeEach('decrease execution delay', async function () { + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ + true, + this.previousExecutionDelay.toString(), + ]); + + this.newExecutionDelay = this.previousExecutionDelay.sub(time.duration.days(4)); + const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay, { + from: admin, + }); + this.grantTimestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + + this.receipt = receipt; + this.delay = this.previousExecutionDelay.sub(this.newExecutionDelay); // For shouldBehaveLikeDelay + }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); + it('emits event', function () { + expectEvent(this.receipt, 'RoleGranted', { + roleId: ANOTHER_ROLE, + account: this.user, + since: this.grantTimestamp.add(this.delay), + delay: this.newExecutionDelay, + newMember: false, + }); + }); - const { receipt } = await this.manager.renounceRole(ROLES.SOME, user, { from: user }); - expectEvent(receipt, 'RoleRevoked', { roleId: ROLES.SOME, account: user }); + shouldBehaveLikeDelay('execution delay effect', { + before() { + beforeEach('consume effect delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('does not change the execution delay yet', async function () { + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.previousExecutionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal(this.newExecutionDelay); // pendingDelay + expect(access[3]).to.be.bignumber.equal(this.grantTimestamp.add(this.delay)); // pendingDelayEffect + + // Not in effect yet + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + this.previousExecutionDelay.toString(), + ]); + }); + }, + after() { + beforeEach('consume effect delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('changes the execution delay', async function () { + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + + expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.newExecutionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + this.newExecutionDelay.toString(), + ]); + }); + }, + }); + }); + }); - expect(await this.manager.hasRole(ROLES.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']); + describe('without grant delay', function () { + beforeEach('set granting delay', async function () { + // Delay granting + const grantDelay = 0; + await this.manager.$_setGrantDelay(ANOTHER_ROLE, grantDelay); + await time.increase(MINSETBACK); + }); - const access = await this.manager.getAccess(ROLES.SOME, user); - expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect - }); + describe('when increasing the execution delay', function () { + beforeEach('set increased new execution delay', async function () { + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ + true, + this.previousExecutionDelay.toString(), + ]); - it('for a user that is not in the role', async function () { - await this.manager.renounceRole(ROLES.SOME, user, { from: user }); - }); + this.newExecutionDelay = this.previousExecutionDelay.add(time.duration.days(4)); + }); - it('bad user confirmation', async function () { - await expectRevertCustomError( - this.manager.renounceRole(ROLES.SOME, member, { from: user }), - 'AccessManagerBadConfirmation', - [], - ); - }); - }); + it('emits event and immediately changes the execution delay', async function () { + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ + true, + this.previousExecutionDelay.toString(), + ]); + const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay, { + from: admin, + }); + const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + + expectEvent(receipt, 'RoleGranted', { + roleId: ANOTHER_ROLE, + account: this.user, + since: timestamp, + delay: this.newExecutionDelay, + newMember: false, + }); + + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.newExecutionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + this.newExecutionDelay.toString(), + ]); + }); + }); - describe('change role admin', function () { - it("admin can set any role's admin", async function () { - expect(await this.manager.getRoleAdmin(ROLES.SOME)).to.be.bignumber.equal(ROLES.SOME_ADMIN); + describe('when decreasing the execution delay', function () { + beforeEach('decrease execution delay', async function () { + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ + true, + this.previousExecutionDelay.toString(), + ]); + + this.newExecutionDelay = this.previousExecutionDelay.sub(time.duration.days(4)); + const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay, { + from: admin, + }); + this.grantTimestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + + this.receipt = receipt; + this.delay = this.previousExecutionDelay.sub(this.newExecutionDelay); // For shouldBehaveLikeDelay + }); - const { receipt } = await this.manager.setRoleAdmin(ROLES.SOME, ROLES.ADMIN, { from: admin }); - expectEvent(receipt, 'RoleAdminChanged', { roleId: ROLES.SOME, admin: ROLES.ADMIN }); + it('emits event', function () { + expectEvent(this.receipt, 'RoleGranted', { + roleId: ANOTHER_ROLE, + account: this.user, + since: this.grantTimestamp.add(this.delay), + delay: this.newExecutionDelay, + newMember: false, + }); + }); - expect(await this.manager.getRoleAdmin(ROLES.SOME)).to.be.bignumber.equal(ROLES.ADMIN); - }); + shouldBehaveLikeDelay('execution delay effect', { + before() { + beforeEach('consume effect delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('does not change the execution delay yet', async function () { + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.previousExecutionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal(this.newExecutionDelay); // pendingDelay + expect(access[3]).to.be.bignumber.equal(this.grantTimestamp.add(this.delay)); // pendingDelayEffect + + // Not in effect yet + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + this.previousExecutionDelay.toString(), + ]); + }); + }, + after() { + beforeEach('consume effect delay', async function () { + // Consume previously set delay + await mine(); + }); + + it('changes the execution delay', async function () { + // Access is correctly stored + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + + expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.be.bignumber.equal(this.newExecutionDelay); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + + // Already in effect + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + this.newExecutionDelay.toString(), + ]); + }); + }, + }); + }); + }); + }); + }); - it("setting a role's admin is restricted", async function () { - await expectRevertCustomError( - this.manager.setRoleAdmin(ROLES.SOME, ROLES.SOME, { from: manager }), - 'AccessManagerUnauthorizedAccount', - [manager, ROLES.ADMIN], - ); - }); - }); + describe('#revokeRole', function () { + describe('restrictions', function () { + beforeEach('set method and args', async function () { + const method = 'revokeRole(uint64,address)'; + const args = [ANOTHER_ROLE, someAddress]; + this.calldata = this.manager.contract.methods[method](...args).encodeABI(); - describe('change role guardian', function () { - it("admin can set any role's admin", async function () { - expect(await this.manager.getRoleGuardian(ROLES.SOME)).to.be.bignumber.equal(ROLES.SOME_ADMIN); + // Need to be set before revoking + await this.manager.$_grantRole(...args, 0, 0); + }); - const { receipt } = await this.manager.setRoleGuardian(ROLES.SOME, ROLES.ADMIN, { from: admin }); - expectEvent(receipt, 'RoleGuardianChanged', { roleId: ROLES.SOME, guardian: ROLES.ADMIN }); + shouldBehaveLikeRoleAdminOperation(ANOTHER_ADMIN); + }); - expect(await this.manager.getRoleGuardian(ROLES.SOME)).to.be.bignumber.equal(ROLES.ADMIN); - }); + describe('when role has been granted', function () { + beforeEach('grant role with grant delay', async function () { + this.grantDelay = time.duration.weeks(1); + await this.manager.$_grantRole(ANOTHER_ROLE, user, this.grantDelay, 0); - it("setting a role's admin is restricted", async function () { - await expectRevertCustomError( - this.manager.setRoleGuardian(ROLES.SOME, ROLES.SOME, { from: other }), - 'AccessManagerUnauthorizedAccount', - [other, ROLES.ADMIN], - ); - }); - }); + this.delay = this.grantDelay; // For shouldBehaveLikeDelay + }); - describe('change execution delay', function () { - it('increasing the delay has immediate effect', async function () { - const oldDelay = web3.utils.toBN(10); - const newDelay = web3.utils.toBN(100); + shouldBehaveLikeDelay('grant', { + before() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); - // role is already granted (with no delay) in the initial setup. this update takes time. - await this.manager.$_grantRole(ROLES.SOME, member, 0, oldDelay); + it('revokes a granted role that will take effect in the future', async function () { + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + false, + '0', + ]); + + const { receipt } = await this.manager.revokeRole(ANOTHER_ROLE, user, { from: admin }); + expectEvent(receipt, 'RoleRevoked', { roleId: ANOTHER_ROLE, account: user }); + + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + false, + '0', + ]); + + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince + expect(access[1]).to.be.bignumber.equal('0'); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // effect + }); + }, + after() { + beforeEach('consume previously set grant delay', async function () { + // Consume previously set delay + await mine(); + }); - const accessBefore = await this.manager.getAccess(ROLES.SOME, member); - expect(accessBefore[1]).to.be.bignumber.equal(oldDelay); // currentDelay - expect(accessBefore[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(accessBefore[3]).to.be.bignumber.equal('0'); // effect + it('revokes a granted role that already took effect', async function () { + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + true, + '0', + ]); + + const { receipt } = await this.manager.revokeRole(ANOTHER_ROLE, user, { from: admin }); + expectEvent(receipt, 'RoleRevoked', { roleId: ANOTHER_ROLE, account: user }); + + expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + false, + '0', + ]); + + const access = await this.manager.getAccess(ANOTHER_ROLE, user); + expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince + expect(access[1]).to.be.bignumber.equal('0'); // currentDelay + expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay + expect(access[3]).to.be.bignumber.equal('0'); // effect + }); + }, + }); + }); - const { receipt } = await this.manager.grantRole(ROLES.SOME, member, newDelay, { - from: manager, - }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - - expectEvent(receipt, 'RoleGranted', { - roleId: ROLES.SOME, - account: member, - since: timestamp, - delay: newDelay, - newMember: false, - }); + describe('when role has not been granted', function () { + it('has no effect', async function () { + expect(await this.manager.hasRole(this.roles.SOME.id, user).then(formatAccess)).to.be.deep.equal([ + false, + '0', + ]); + const { receipt } = await this.manager.revokeRole(this.roles.SOME.id, user, { from: manager }); + expectEvent.notEmitted(receipt, 'RoleRevoked', { roleId: ANOTHER_ROLE, account: user }); + expect(await this.manager.hasRole(this.roles.SOME.id, user).then(formatAccess)).to.be.deep.equal([ + false, + '0', + ]); + }); + }); - // immediate effect - const accessAfter = await this.manager.getAccess(ROLES.SOME, member); - expect(accessAfter[1]).to.be.bignumber.equal(newDelay); // currentDelay - expect(accessAfter[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(accessAfter[3]).to.be.bignumber.equal('0'); // effect + it('reverts revoking PUBLIC_ROLE', async function () { + await expectRevertCustomError( + this.manager.revokeRole(this.roles.PUBLIC.id, user, { from: admin }), + 'AccessManagerLockedRole', + [this.roles.PUBLIC.id], + ); + }); + }); }); - it('decreasing the delay takes time', async function () { - const oldDelay = web3.utils.toBN(100); - const newDelay = web3.utils.toBN(10); - - // role is already granted (with no delay) in the initial setup. this update takes time. - await this.manager.$_grantRole(ROLES.SOME, member, 0, oldDelay); + describe('self role operations', function () { + describe('#renounceRole', function () { + beforeEach('grant role', async function () { + this.role = { id: web3.utils.toBN(783164) }; + this.caller = user; + await this.manager.$_grantRole(this.role.id, this.caller, 0, 0); + }); - const accessBefore = await this.manager.getAccess(ROLES.SOME, member); - expect(accessBefore[1]).to.be.bignumber.equal(oldDelay); // currentDelay - expect(accessBefore[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(accessBefore[3]).to.be.bignumber.equal('0'); // effect + it('renounces a role', async function () { + expect(await this.manager.hasRole(this.role.id, this.caller).then(formatAccess)).to.be.deep.equal([ + true, + '0', + ]); + const { receipt } = await this.manager.renounceRole(this.role.id, this.caller, { + from: this.caller, + }); + expectEvent(receipt, 'RoleRevoked', { + roleId: this.role.id, + account: this.caller, + }); + expect(await this.manager.hasRole(this.role.id, this.caller).then(formatAccess)).to.be.deep.equal([ + false, + '0', + ]); + }); - const { receipt } = await this.manager.grantRole(ROLES.SOME, member, newDelay, { - from: manager, - }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - const setback = oldDelay.sub(newDelay); - - expectEvent(receipt, 'RoleGranted', { - roleId: ROLES.SOME, - account: member, - since: timestamp.add(setback), - delay: newDelay, - newMember: false, - }); + it('reverts if renouncing the PUBLIC_ROLE', async function () { + await expectRevertCustomError( + this.manager.renounceRole(this.roles.PUBLIC.id, this.caller, { + from: this.caller, + }), + 'AccessManagerLockedRole', + [this.roles.PUBLIC.id], + ); + }); - // no immediate effect - const accessAfter = await this.manager.getAccess(ROLES.SOME, member); - expect(accessAfter[1]).to.be.bignumber.equal(oldDelay); // currentDelay - expect(accessAfter[2]).to.be.bignumber.equal(newDelay); // pendingDelay - expect(accessAfter[3]).to.be.bignumber.equal(timestamp.add(setback)); // effect - - // delayed effect - await time.increase(setback); - const accessAfterSetback = await this.manager.getAccess(ROLES.SOME, member); - expect(accessAfterSetback[1]).to.be.bignumber.equal(newDelay); // currentDelay - expect(accessAfterSetback[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(accessAfterSetback[3]).to.be.bignumber.equal('0'); // effect - }); - - it('can set a user execution delay during the grant delay', async function () { - await this.manager.$_grantRole(ROLES.SOME, other, 10, 0); - // here: "other" is pending to get the role, but doesn't yet have it. - - const { receipt } = await this.manager.grantRole(ROLES.SOME, other, executeDelay, { from: manager }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - - // increasing the execution delay from 0 to executeDelay is immediate - expectEvent(receipt, 'RoleGranted', { - roleId: ROLES.SOME, - account: other, - since: timestamp, - delay: executeDelay, - newMember: false, + it('reverts if renouncing with bad caller confirmation', async function () { + await expectRevertCustomError( + this.manager.renounceRole(this.role.id, someAddress, { + from: this.caller, + }), + 'AccessManagerBadConfirmation', + [], + ); + }); }); }); }); + }); - describe('change grant delay', function () { - it('increasing the delay has immediate effect', async function () { - const oldDelay = web3.utils.toBN(10); - const newDelay = web3.utils.toBN(100); - - await this.manager.$_setGrantDelay(ROLES.SOME, oldDelay); - await time.increase(MINSETBACK); - - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(oldDelay); + describe('access managed target operations', function () { + describe('when calling a restricted target function', function () { + const method = 'fnRestricted()'; - const { receipt } = await this.manager.setGrantDelay(ROLES.SOME, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - const setback = web3.utils.BN.max(MINSETBACK, oldDelay.sub(newDelay)); + beforeEach('set required role', function () { + this.role = { id: web3.utils.toBN(3597243) }; + this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + }); - expect(setback).to.be.bignumber.equal(MINSETBACK); - expectEvent(receipt, 'RoleGrantDelayChanged', { - roleId: ROLES.SOME, - delay: newDelay, - since: timestamp.add(setback), + describe('restrictions', function () { + beforeEach('set method and args', function () { + this.calldata = this.target.contract.methods[method]().encodeABI(); + this.caller = user; }); - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(oldDelay); - await time.increase(setback); - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(newDelay); + shouldBehaveLikeAManagedRestrictedOperation(); }); - it('increasing the delay has delay effect #1', async function () { - const oldDelay = web3.utils.toBN(100); - const newDelay = web3.utils.toBN(10); + it('succeeds called by a role member', async function () { + await this.manager.$_grantRole(this.role.id, user, 0, 0); - await this.manager.$_setGrantDelay(ROLES.SOME, oldDelay); - await time.increase(MINSETBACK); - - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(oldDelay); - - const { receipt } = await this.manager.setGrantDelay(ROLES.SOME, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - const setback = web3.utils.BN.max(MINSETBACK, oldDelay.sub(newDelay)); - - expect(setback).to.be.bignumber.equal(MINSETBACK); - expectEvent(receipt, 'RoleGrantDelayChanged', { - roleId: ROLES.SOME, - delay: newDelay, - since: timestamp.add(setback), + const { receipt } = await this.target.methods[method]({ + data: this.calldata, + from: user, + }); + expectEvent(receipt, 'CalledRestricted', { + caller: user, }); - - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(oldDelay); - await time.increase(setback); - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(newDelay); }); + }); - it('increasing the delay has delay effect #2', async function () { - const oldDelay = time.duration.days(30); // more than the minsetback - const newDelay = web3.utils.toBN(10); - - await this.manager.$_setGrantDelay(ROLES.SOME, oldDelay); - await time.increase(MINSETBACK); - - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(oldDelay); - - const { receipt } = await this.manager.setGrantDelay(ROLES.SOME, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - const setback = web3.utils.BN.max(MINSETBACK, oldDelay.sub(newDelay)); - - expect(setback).to.be.bignumber.gt(MINSETBACK); - expectEvent(receipt, 'RoleGrantDelayChanged', { - roleId: ROLES.SOME, - delay: newDelay, - since: timestamp.add(setback), - }); + describe('when calling a non-restricted target function', function () { + const method = 'fnUnrestricted()'; - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(oldDelay); - await time.increase(setback); - expect(await this.manager.getRoleGrantDelay(ROLES.SOME)).to.be.bignumber.equal(newDelay); + beforeEach('set required role', async function () { + this.role = { id: web3.utils.toBN(879435) }; + await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); }); - it('changing the grant delay is restricted', async function () { - await expectRevertCustomError( - this.manager.setGrantDelay(ROLES.SOME, grantDelay, { from: other }), - 'AccessManagerUnauthorizedAccount', - [ROLES.ADMIN, other], - ); + it('succeeds called by anyone', async function () { + const { receipt } = await this.target.methods[method]({ + data: this.calldata, + from: user, + }); + expectEvent(receipt, 'CalledUnrestricted', { + caller: user, + }); }); }); }); - describe('with AccessManaged target contract', function () { - beforeEach('deploy target contract', async function () { - this.target = await AccessManagedTarget.new(this.manager.address); - // helpers for indirect calls - this.callData = selector('fnRestricted()'); - this.call = [this.target.address, this.callData]; - this.opId = web3.utils.keccak256( - web3.eth.abi.encodeParameters(['address', 'address', 'bytes'], [user, ...this.call]), - ); - this.direct = (opts = {}) => this.target.fnRestricted({ from: user, ...opts }); - this.schedule = (opts = {}) => this.manager.schedule(...this.call, 0, { from: user, ...opts }); - this.execute = (opts = {}) => this.manager.execute(...this.call, { from: user, ...opts }); - this.cancel = (opts = {}) => this.manager.cancel(user, ...this.call, { from: user, ...opts }); - }); + describe('#schedule', function () { + const method = 'fnRestricted()'; - describe('Change function permissions', function () { - const sigs = ['someFunction()', 'someOtherFunction(uint256)', 'oneMoreFunction(address,uint8)'].map(selector); + beforeEach('set target function role', async function () { + this.role = { id: web3.utils.toBN(498305) }; + this.caller = user; - it('admin can set function role', async function () { - for (const sig of sigs) { - expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal(ROLES.ADMIN); - } + await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // nonzero execution delay - const { receipt: receipt1 } = await this.manager.setTargetFunctionRole(this.target.address, sigs, ROLES.SOME, { - from: admin, - }); + this.calldata = this.target.contract.methods[method]().encodeABI(); + this.delay = time.duration.weeks(2); + }); - for (const sig of sigs) { - expectEvent(receipt1, 'TargetFunctionRoleUpdated', { - target: this.target.address, - selector: sig, - roleId: ROLES.SOME, + describe('restrictions', function () { + shouldBehaveLikeCanCall({ + closed() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + await expectRevertCustomError( + scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); }); - expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal(ROLES.SOME); - } - - const { receipt: receipt2 } = await this.manager.setTargetFunctionRole( - this.target.address, - [sigs[1]], - ROLES.SOME_ADMIN, - { - from: admin, + }, + open: { + callerIsTheManager: { + executing() { + it.skip('is not reachable because schedule is not restrictable'); + }, + notExecuting() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + await expectRevertCustomError( + scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, }, - ); - expectEvent(receipt2, 'TargetFunctionRoleUpdated', { - target: this.target.address, - selector: sigs[1], - roleId: ROLES.SOME_ADMIN, - }); + callerIsNotTheManager: { + publicRoleIsRequired() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + // scheduleOperation is not used here because it alters the next block timestamp + await expectRevertCustomError( + this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { + from: this.caller, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + specificRoleIsRequired: { + requiredRoleIsGranted: { + roleGrantingIsDelayed: { + callerHasAnExecutionDelay: { + beforeGrantDelay() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + // scheduleOperation is not used here because it alters the next block timestamp + await expectRevertCustomError( + this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { + from: this.caller, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + afterGrantDelay() { + it('succeeds', async function () { + // scheduleOperation is not used here because it alters the next block timestamp + await this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { + from: this.caller, + }); + }); + }, + }, + callerHasNoExecutionDelay: { + beforeGrantDelay() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + // scheduleOperation is not used here because it alters the next block timestamp + await expectRevertCustomError( + this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { + from: this.caller, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + afterGrantDelay() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + // scheduleOperation is not used here because it alters the next block timestamp + await expectRevertCustomError( + this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { + from: this.caller, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + }, + }, + roleGrantingIsNotDelayed: { + callerHasAnExecutionDelay() { + it('succeeds', async function () { + await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }); + }); + }, + callerHasNoExecutionDelay() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + // scheduleOperation is not used here because it alters the next block timestamp + await expectRevertCustomError( + this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { + from: this.caller, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + }, + }, + requiredRoleIsNotGranted() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + await expectRevertCustomError( + scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + }, + }, + }, + }); + }); - for (const sig of sigs) { - expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal( - sig == sigs[1] ? ROLES.SOME_ADMIN : ROLES.SOME, - ); - } + it('schedules an operation at the specified execution date if it is larger than caller execution delay', async function () { + const { operationId, scheduledAt, receipt } = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, }); - it('non-admin cannot set function role', async function () { - await expectRevertCustomError( - this.manager.setTargetFunctionRole(this.target.address, sigs, ROLES.SOME, { from: other }), - 'AccessManagerUnauthorizedAccount', - [other, ROLES.ADMIN], - ); + expect(await this.manager.getSchedule(operationId)).to.be.bignumber.equal(scheduledAt.add(this.delay)); + expectEvent(receipt, 'OperationScheduled', { + operationId, + nonce: '1', + schedule: scheduledAt.add(this.delay), + target: this.target.address, + data: this.calldata, }); }); - // WIP - describe('Calling restricted & unrestricted functions', function () { - for (const [callerRoles, fnRole, closed, delay] of product( - [[], [ROLES.SOME]], - [undefined, ROLES.ADMIN, ROLES.SOME, ROLES.PUBLIC], - [false, true], - [null, executeDelay], - )) { - // can we call with a delay ? - const indirectSuccess = (fnRole == ROLES.PUBLIC || callerRoles.includes(fnRole)) && !closed; - - // can we call without a delay ? - const directSuccess = (fnRole == ROLES.PUBLIC || (callerRoles.includes(fnRole) && !delay)) && !closed; - - const description = [ - 'Caller in roles', - '[' + (callerRoles ?? []).map(roleId => ROLES[roleId]).join(', ') + ']', - delay ? 'with a delay' : 'without a delay', - '+', - 'functions open to roles', - '[' + (ROLES[fnRole] ?? '') + ']', - closed ? `(closed)` : '', - ].join(' '); - - describe(description, function () { - beforeEach(async function () { - if (!delay || fnRole === ROLES.PUBLIC) this.skip(); // TODO: Fixed in #4613 - - // setup - await Promise.all([ - this.manager.$_setTargetClosed(this.target.address, closed), - fnRole && this.manager.$_setTargetFunctionRole(this.target.address, selector('fnRestricted()'), fnRole), - fnRole && this.manager.$_setTargetFunctionRole(this.target.address, selector('fnUnrestricted()'), fnRole), - ...callerRoles - .filter(roleId => roleId != ROLES.PUBLIC) - .map(roleId => this.manager.$_grantRole(roleId, user, 0, delay ?? 0)), - ]); - - // post setup checks - expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(closed); - - if (fnRole) { - expect( - await this.manager.getTargetFunctionRole(this.target.address, selector('fnRestricted()')), - ).to.be.bignumber.equal(fnRole); - expect( - await this.manager.getTargetFunctionRole(this.target.address, selector('fnUnrestricted()')), - ).to.be.bignumber.equal(fnRole); - } - - for (const roleId of callerRoles) { - const access = await this.manager.getAccess(roleId, user); - if (roleId == ROLES.PUBLIC) { - expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect - } else { - expect(access[0]).to.be.bignumber.gt('0'); // inRoleSince - expect(access[1]).to.be.bignumber.eq(String(delay ?? 0)); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect - } - } - }); - - 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 ? 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.direct(); - - if (directSuccess) { - expectEvent(await promise, 'CalledRestricted', { caller: user }); - } else if (indirectSuccess) { - await expectRevertCustomError(promise, 'AccessManagerNotScheduled', [this.opId]); - } else { - await expectRevertCustomError(promise, 'AccessManagedUnauthorized', [user]); - } - }); + it('schedules an operation at the minimum execution date if no specified execution date (when == 0)', async function () { + const executionDelay = await time.duration.hours(72); + await this.manager.$_grantRole(this.role.id, this.caller, 0, executionDelay); - it('Calling indirectly: only execute', async function () { - // execute without schedule - if (directSuccess) { - const nonceBefore = await this.manager.getNonce(this.opId); - const { receipt, tx } = await this.execute(); - - expectEvent.notEmitted(receipt, 'OperationExecuted', { operationId: this.opId }); - await expectEvent.inTransaction(tx, this.target, 'CalledRestricted', { caller: this.manager.address }); - - // nonce is not modified - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore); - } else if (indirectSuccess) { - await expectRevertCustomError(this.execute(), 'AccessManagerNotScheduled', [this.opId]); - } else { - await expectRevertCustomError(this.execute(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } - }); + const timestamp = await time.latest(); + const scheduledAt = timestamp.addn(1); + await setNextBlockTimestamp(scheduledAt); + const { receipt } = await this.manager.schedule(this.target.address, this.calldata, 0, { + from: this.caller, + }); - it('Calling indirectly: schedule and execute', async function () { - if (directSuccess || indirectSuccess) { - const nonceBefore = await this.manager.getNonce(this.opId); - const { receipt } = await this.schedule(); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + const operationId = await this.manager.hashOperation(this.caller, this.target.address, this.calldata); - expectEvent(receipt, 'OperationScheduled', { - operationId: this.opId, - caller: user, - target: this.call[0], - data: this.call[1], - }); + expect(await this.manager.getSchedule(operationId)).to.be.bignumber.equal(scheduledAt.add(executionDelay)); + expectEvent(receipt, 'OperationScheduled', { + operationId, + nonce: '1', + schedule: scheduledAt.add(executionDelay), + target: this.target.address, + data: this.calldata, + }); + }); - // if can call directly, delay should be 0. Otherwise, the delay should be applied - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal( - timestamp.add(directSuccess ? web3.utils.toBN(0) : delay), - ); + it('increases the nonce of an operation scheduled more than once', async function () { + // Setup and check initial nonce + const expectedOperationId = await web3.utils.keccak256( + web3.eth.abi.encodeParameters( + ['address', 'address', 'bytes'], + [this.caller, this.target.address, this.calldata], + ), + ); + expect(await this.manager.getNonce(expectedOperationId)).to.be.bignumber.eq('0'); + + // Schedule + const op1 = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }); + expectEvent(op1.receipt, 'OperationScheduled', { + operationId: op1.operationId, + nonce: '1', + schedule: op1.scheduledAt.add(this.delay), + target: this.target.address, + data: this.calldata, + }); + expect(expectedOperationId).to.eq(op1.operationId); - // nonce is incremented - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); - - // execute without wait - if (directSuccess) { - const { receipt, tx } = await this.execute(); - - await expectEvent.inTransaction(tx, this.target, 'CalledRestricted', { caller: this.manager.address }); - if (delay && fnRole !== ROLES.PUBLIC) { - expectEvent(receipt, 'OperationExecuted', { operationId: this.opId }); - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); - } - - // nonce is not modified by execute - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); - } else if (indirectSuccess) { - await expectRevertCustomError(this.execute(), 'AccessManagerNotReady', [this.opId]); - } else { - await expectRevertCustomError(this.execute(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } - } else { - await expectRevertCustomError(this.schedule(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } - }); + // Consume + await time.increase(this.delay); + await this.manager.$_consumeScheduledOp(expectedOperationId); - it('Calling indirectly: schedule wait and execute', async function () { - if (directSuccess || indirectSuccess) { - const nonceBefore = await this.manager.getNonce(this.opId); - const { receipt } = await this.schedule(); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + // Check nonce + expect(await this.manager.getNonce(expectedOperationId)).to.be.bignumber.eq('1'); - expectEvent(receipt, 'OperationScheduled', { - operationId: this.opId, - caller: user, - target: this.call[0], - data: this.call[1], - }); + // Schedule again + const op2 = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }); + expectEvent(op2.receipt, 'OperationScheduled', { + operationId: op2.operationId, + nonce: '2', + schedule: op2.scheduledAt.add(this.delay), + target: this.target.address, + data: this.calldata, + }); + expect(expectedOperationId).to.eq(op2.operationId); - // if can call directly, delay should be 0. Otherwise, the delay should be applied - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal( - timestamp.add(directSuccess ? web3.utils.toBN(0) : delay), - ); + // Check final nonce + expect(await this.manager.getNonce(expectedOperationId)).to.be.bignumber.eq('2'); + }); - // nonce is incremented - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); - - // wait - await time.increase(delay ?? 0); - - // execute without wait - if (directSuccess || indirectSuccess) { - const { receipt, tx } = await this.execute(); - - await expectEvent.inTransaction(tx, this.target, 'CalledRestricted', { caller: this.manager.address }); - if (delay && fnRole !== ROLES.PUBLIC) { - expectEvent(receipt, 'OperationExecuted', { operationId: this.opId }); - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); - } - - // nonce is not modified by execute - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); - } else { - await expectRevertCustomError(this.execute(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } - } else { - await expectRevertCustomError(this.schedule(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } - }); + it('reverts if the specified execution date is before the current timestamp + caller execution delay', async function () { + const executionDelay = time.duration.weeks(1).add(this.delay); + await this.manager.$_grantRole(this.role.id, this.caller, 0, executionDelay); - it('Calling directly: schedule and call', async function () { - if (directSuccess || indirectSuccess) { - const nonceBefore = await this.manager.getNonce(this.opId); - const { receipt } = await this.schedule(); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + await expectRevertCustomError( + scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); - expectEvent(receipt, 'OperationScheduled', { - operationId: this.opId, - caller: user, - target: this.call[0], - data: this.call[1], - }); + it('reverts if an operation is already schedule', async function () { + const { operationId } = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }); - // if can call directly, delay should be 0. Otherwise, the delay should be applied - const schedule = timestamp.add(directSuccess ? web3.utils.toBN(0) : delay); - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal(schedule); - - // nonce is incremented - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); - - // execute without wait - const promise = this.direct(); - if (directSuccess) { - expectEvent(await promise, 'CalledRestricted', { caller: user }); - - // schedule is not reset - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal(schedule); - - // nonce is not modified by execute - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); - } else if (indirectSuccess) { - await expectRevertCustomError(promise, 'AccessManagerNotReady', [this.opId]); - } else { - await expectRevertCustomError(promise, 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } - } else { - await expectRevertCustomError(this.schedule(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } - }); + await expectRevertCustomError( + scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.delay, + }), + 'AccessManagerAlreadyScheduled', + [operationId], + ); + }); - it('Calling directly: schedule wait and call', async function () { - if (directSuccess || indirectSuccess) { - const nonceBefore = await this.manager.getNonce(this.opId); - const { receipt } = await this.schedule(); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + it('panics scheduling calldata with less than 4 bytes', async function () { + const calldata = '0x1234'; // 2 bytes - expectEvent(receipt, 'OperationScheduled', { - operationId: this.opId, - caller: user, - target: this.call[0], - data: this.call[1], - }); + // Managed contract + await expectRevert.unspecified( + scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: calldata, + delay: this.delay, + }), + ); - // if can call directly, delay should be 0. Otherwise, the delay should be applied - const schedule = timestamp.add(directSuccess ? web3.utils.toBN(0) : delay); - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal(schedule); + // Manager contract + await expectRevert.unspecified( + scheduleOperation(this.manager, { + caller: this.caller, + target: this.manager.address, + calldata: calldata, + delay: this.delay, + }), + ); + }); - // nonce is incremented - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); + it('reverts scheduling an unknown operation to the manager', async function () { + const calldata = '0x12345678'; - // wait - await time.increase(delay ?? 0); + await expectRevertCustomError( + scheduleOperation(this.manager, { + caller: this.caller, + target: this.manager.address, + calldata, + delay: this.delay, + }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.manager.address, calldata], + ); + }); + }); - // execute without wait - const promise = await this.direct(); - if (directSuccess) { - expectEvent(await promise, 'CalledRestricted', { caller: user }); + describe('#execute', function () { + const method = 'fnRestricted()'; - // schedule is not reset - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal(schedule); + beforeEach('set target function role', async function () { + this.role = { id: web3.utils.toBN(9825430) }; + this.caller = user; - // nonce is not modified by execute - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); - } else if (indirectSuccess) { - const receipt = await promise; + await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + await this.manager.$_grantRole(this.role.id, this.caller, 0, 0); - expectEvent(receipt, 'CalledRestricted', { caller: user }); - await expectEvent.inTransaction(receipt.tx, this.manager, 'OperationExecuted', { - operationId: this.opId, - }); + this.calldata = this.target.contract.methods[method]().encodeABI(); + }); - // schedule is reset - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); - - // nonce is not modified by execute - expect(await this.manager.getNonce(this.opId)).to.be.bignumber.equal(nonceBefore.addn(1)); - } else { - await expectRevertCustomError(this.direct(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } - } else { - await expectRevertCustomError(this.schedule(), 'AccessManagerUnauthorizedCall', [user, ...this.call]); - } + describe('restrictions', function () { + shouldBehaveLikeCanCall({ + closed() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + await expectRevertCustomError( + this.manager.execute(this.target.address, this.calldata, { from: this.caller }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); }); - - it('Scheduling for later than needed'); // TODO - }); - } + }, + open: { + callerIsTheManager: { + executing() { + it('succeeds', async function () { + await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + }); + }, + notExecuting() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + await expectRevertCustomError( + this.manager.execute(this.target.address, this.calldata, { from: this.caller }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + }, + callerIsNotTheManager: { + publicRoleIsRequired() { + shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH_IF_ZERO_DELAY); + }, + specificRoleIsRequired: { + requiredRoleIsGranted: { + roleGrantingIsDelayed: { + callerHasAnExecutionDelay: { + beforeGrantDelay() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + await expectRevertCustomError( + this.manager.execute(this.target.address, this.calldata, { from: this.caller }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + afterGrantDelay() { + beforeEach('define schedule delay', async function () { + // Consume previously set delay + await mine(); + this.scheduleIn = time.duration.days(21); + }); + + shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); + }, + }, + callerHasNoExecutionDelay: { + beforeGrantDelay() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + await expectRevertCustomError( + this.manager.execute(this.target.address, this.calldata, { from: this.caller }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + afterGrantDelay() { + beforeEach('define schedule delay', async function () { + // Consume previously set delay + await mine(); + }); + + shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH_IF_ZERO_DELAY); + }, + }, + }, + roleGrantingIsNotDelayed: { + callerHasAnExecutionDelay() { + beforeEach('define schedule delay', async function () { + this.scheduleIn = time.duration.days(15); + }); + + shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH); + }, + callerHasNoExecutionDelay() { + shouldBehaveLikeSchedulableOperation(COMMON_SCHEDULABLE_PATH_IF_ZERO_DELAY); + }, + }, + }, + requiredRoleIsNotGranted() { + it('reverts as AccessManagerUnauthorizedCall', async function () { + await expectRevertCustomError( + this.manager.execute(this.target.address, this.calldata, { from: this.caller }), + 'AccessManagerUnauthorizedCall', + [this.caller, this.target.address, this.calldata.substring(0, 10)], + ); + }); + }, + }, + }, + }, + }); }); - describe('Indirect execution corner-cases', async function () { - beforeEach(async function () { - await this.manager.$_setTargetFunctionRole(this.target.address, this.callData, ROLES.SOME); - await this.manager.$_grantRole(ROLES.SOME, user, 0, executeDelay); - }); + it('executes with a delay consuming the scheduled operation', async function () { + const delay = time.duration.hours(4); + await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // Execution delay is needed so the operation is consumed - it('Checking canCall when caller is the manager depend on the _executionId', async function () { - 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'); + const { operationId } = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay, }); + await time.increase(delay); + const { receipt } = await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + expectEvent(receipt, 'OperationExecuted', { + operationId, + nonce: '1', + }); + expect(await this.manager.getSchedule(operationId)).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)); - - // too early - await helpers.time.setNextBlockTimestamp(timestamp.add(executeDelay).subn(1)); - await expectRevertCustomError(this.execute(), '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)); + it('executes with no delay consuming a scheduled operation', async function () { + const delay = time.duration.hours(4); - // ok - await helpers.time.setNextBlockTimestamp(timestamp.add(executeDelay)); - await this.execute(); + // give caller an execution delay + await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); - // the success happened when the delay was reached (earliest possible) - expect(await time.latest()).to.be.bignumber.equal(timestamp.add(executeDelay)); + const { operationId } = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay, }); - it('Cannot schedule an already scheduled operation', async function () { - const { receipt } = await this.schedule(); - expectEvent(receipt, 'OperationScheduled', { - operationId: this.opId, - caller: user, - target: this.call[0], - data: this.call[1], - }); + // remove the execution delay + await this.manager.$_grantRole(this.role.id, this.caller, 0, 0); - await expectRevertCustomError(this.schedule(), 'AccessManagerAlreadyScheduled', [this.opId]); + await time.increase(delay); + const { receipt } = await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + expectEvent(receipt, 'OperationExecuted', { + operationId, + nonce: '1', }); + expect(await this.manager.getSchedule(operationId)).to.be.bignumber.equal('0'); + }); - it('Cannot cancel an operation that is not scheduled', async function () { - await expectRevertCustomError(this.cancel(), 'AccessManagerNotScheduled', [this.opId]); - }); + it('keeps the original _executionId after finishing the call', async function () { + const executionIdBefore = await getStorageAt(this.manager.address, EXECUTION_ID_STORAGE_SLOT); + await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + const executionIdAfter = await getStorageAt(this.manager.address, EXECUTION_ID_STORAGE_SLOT); + expect(executionIdBefore).to.be.bignumber.equal(executionIdAfter); + }); - it('Cannot cancel an operation that is already executed', async function () { - await this.schedule(); - await time.increase(executeDelay); - await this.execute(); + it('reverts executing twice', async function () { + const delay = time.duration.hours(2); + await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // Execution delay is needed so the operation is consumed - await expectRevertCustomError(this.cancel(), 'AccessManagerNotScheduled', [this.opId]); + const { operationId } = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay, }); + await time.increase(delay); + await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await expectRevertCustomError( + this.manager.execute(this.target.address, this.calldata, { from: this.caller }), + 'AccessManagerNotScheduled', + [operationId], + ); + }); + }); - it('Scheduler can cancel', async function () { - await this.schedule(); + describe('#consumeScheduledOp', function () { + beforeEach('define scheduling parameters', async function () { + const method = 'fnRestricted()'; + this.caller = this.target.address; + this.calldata = this.target.contract.methods[method]().encodeABI(); + this.role = { id: web3.utils.toBN(9834983) }; - expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0'); + await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // nonzero execution delay - expectEvent(await this.cancel({ from: manager }), 'OperationCanceled', { operationId: this.opId }); + this.scheduleIn = time.duration.hours(10); // For shouldBehaveLikeSchedulableOperation + }); - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); + describe('when caller is not consuming scheduled operation', function () { + beforeEach('set consuming false', async function () { + await this.target.setIsConsumingScheduledOp(false, `0x${CONSUMING_SCHEDULE_STORAGE_SLOT.toString(16)}`); }); - 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 }), 'OperationCanceled', { operationId: this.opId }); + it('reverts as AccessManagerUnauthorizedConsume', async function () { + await impersonate(this.caller); + await expectRevertCustomError( + this.manager.consumeScheduledOp(this.caller, this.calldata, { from: this.caller }), + 'AccessManagerUnauthorizedConsume', + [this.caller], + ); + }); + }); - expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0'); + describe('when caller is consuming scheduled operation', function () { + beforeEach('set consuming true', async function () { + await this.target.setIsConsumingScheduledOp(true, `0x${CONSUMING_SCHEDULE_STORAGE_SLOT.toString(16)}`); }); - it('Cancel is restricted', async function () { - await this.schedule(); + shouldBehaveLikeSchedulableOperation({ + scheduled: { + before() { + it('reverts as AccessManagerNotReady', async function () { + await impersonate(this.caller); + await expectRevertCustomError( + this.manager.consumeScheduledOp(this.caller, this.calldata, { from: this.caller }), + 'AccessManagerNotReady', + [this.operationId], + ); + }); + }, + after() { + it('consumes the scheduled operation and resets timepoint', async function () { + expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal( + this.scheduledAt.add(this.scheduleIn), + ); + await impersonate(this.caller); + const { receipt } = await this.manager.consumeScheduledOp(this.caller, this.calldata, { + from: this.caller, + }); + expectEvent(receipt, 'OperationExecuted', { + operationId: this.operationId, + nonce: '1', + }); + expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal('0'); + }); + }, + expired() { + it('reverts as AccessManagerExpired', async function () { + await impersonate(this.caller); + await expectRevertCustomError( + this.manager.consumeScheduledOp(this.caller, this.calldata, { from: this.caller }), + 'AccessManagerExpired', + [this.operationId], + ); + }); + }, + }, + notScheduled() { + it('reverts as AccessManagerNotScheduled', async function () { + await impersonate(this.caller); + await expectRevertCustomError( + this.manager.consumeScheduledOp(this.caller, this.calldata, { from: this.caller }), + 'AccessManagerNotScheduled', + [this.operationId], + ); + }); + }, + }); + }); + }); - expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0'); + describe('#cancelScheduledOp', function () { + const method = 'fnRestricted()'; - await expectRevertCustomError(this.cancel({ from: other }), 'AccessManagerUnauthorizedCancel', [ - other, - user, - ...this.call, - ]); + beforeEach('setup scheduling', async function () { + this.caller = this.roles.SOME.members[0]; + await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.roles.SOME.id); + await this.manager.$_grantRole(this.roles.SOME.id, this.caller, 0, 1); // nonzero execution delay - expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0'); - }); + this.calldata = await this.target.contract.methods[method]().encodeABI(); + this.scheduleIn = time.duration.days(10); // For shouldBehaveLikeSchedulableOperation + }); - it('Can re-schedule after execution', async function () { - await this.schedule(); - await time.increase(executeDelay); - await this.execute(); + shouldBehaveLikeSchedulableOperation({ + scheduled: { + before() { + describe('when caller is the scheduler', function () { + it('succeeds', async function () { + await this.manager.cancel(this.caller, this.target.address, this.calldata, { from: this.caller }); + }); + }); - // reschedule - const { receipt } = await this.schedule(); - expectEvent(receipt, 'OperationScheduled', { - operationId: this.opId, - caller: user, - target: this.call[0], - data: this.call[1], - }); - }); + describe('when caller is an admin', function () { + it('succeeds', async function () { + await this.manager.cancel(this.caller, this.target.address, this.calldata, { + from: this.roles.ADMIN.members[0], + }); + }); + }); - it('Can re-schedule after cancel', async function () { - await this.schedule(); - await this.cancel(); + describe('when caller is the role guardian', function () { + it('succeeds', async function () { + await this.manager.cancel(this.caller, this.target.address, this.calldata, { + from: this.roles.SOME_GUARDIAN.members[0], + }); + }); + }); - // reschedule - const { receipt } = await this.schedule(); - expectEvent(receipt, 'OperationScheduled', { - operationId: this.opId, - caller: user, - target: this.call[0], - data: this.call[1], + describe('when caller is any other account', function () { + it('reverts as AccessManagerUnauthorizedCancel', async function () { + await expectRevertCustomError( + this.manager.cancel(this.caller, this.target.address, this.calldata, { from: other }), + 'AccessManagerUnauthorizedCancel', + [other, this.caller, this.target.address, selector(method)], + ); + }); + }); + }, + after() { + it('succeeds', async function () { + await this.manager.cancel(this.caller, this.target.address, this.calldata, { from: this.caller }); + }); + }, + expired() { + it('succeeds', async function () { + await this.manager.cancel(this.caller, this.target.address, this.calldata, { from: this.caller }); + }); + }, + }, + notScheduled() { + it('reverts as AccessManagerNotScheduled', async function () { + await expectRevertCustomError( + this.manager.cancel(this.caller, this.target.address, this.calldata), + 'AccessManagerNotScheduled', + [this.operationId], + ); }); + }, + }); + + it('cancels an operation and resets schedule', async function () { + const { operationId } = await scheduleOperation(this.manager, { + caller: this.caller, + target: this.target.address, + calldata: this.calldata, + delay: this.scheduleIn, }); + const { receipt } = await this.manager.cancel(this.caller, this.target.address, this.calldata, { + from: this.caller, + }); + expectEvent(receipt, 'OperationCanceled', { + operationId, + nonce: '1', + }); + expect(await this.manager.getSchedule(operationId)).to.be.bignumber.eq('0'); }); }); @@ -1095,7 +2657,11 @@ contract('AccessManager', function (accounts) { describe('function is open to public role', function () { beforeEach(async function () { - await this.manager.$_setTargetFunctionRole(this.ownable.address, selector('$_checkOwner()'), ROLES.PUBLIC); + await this.manager.$_setTargetFunctionRole( + this.ownable.address, + selector('$_checkOwner()'), + this.roles.PUBLIC.id, + ); }); it('directly call: reverts', async function () { @@ -1114,50 +2680,4 @@ contract('AccessManager', function (accounts) { }); }); }); - - describe('authority update', function () { - beforeEach(async function () { - this.newManager = await AccessManager.new(admin); - this.target = await AccessManagedTarget.new(this.manager.address); - }); - - 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 }); - await 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 }), - 'AccessManagerUnauthorizedAccount', - [other, ROLES.ADMIN], - ); - }); - - it('setAuthority is restricted on AccessManaged', async function () { - await expectRevertCustomError( - this.target.setAuthority(this.newManager.address, { from: admin }), - 'AccessManagedUnauthorized', - [admin], - ); - }); - }); - - // TODO: - // - check opening/closing a contract - // - check updating the contract delay - // - check the delay applies to admin function - describe.skip('contract modes', function () {}); }); diff --git a/test/helpers/access-manager.js b/test/helpers/access-manager.js new file mode 100644 index 00000000000..7dfc4c33d13 --- /dev/null +++ b/test/helpers/access-manager.js @@ -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, +};