浏览代码

Improve AccessManager (#4520)

Francisco 2 年之前
父节点
当前提交
b5a3e693e7

+ 2 - 1
contracts/access/manager/AccessManaged.sol

@@ -90,7 +90,8 @@ abstract contract AccessManaged is Context, IAccessManaged {
     }
 
     /**
-     * @dev Transfers control to a new authority. Internal function with no access restriction.
+     * @dev Transfers control to a new authority. Internal function with no access restriction. Allows bypassing the
+     * permissions set by the current authority.
      */
     function _setAuthority(address newAuthority) internal virtual {
         _authority = newAuthority;

+ 107 - 112
contracts/access/manager/AccessManager.sol

@@ -52,7 +52,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
     using Time for *;
 
     struct AccessMode {
-        uint64 familyId;
+        uint64 classId;
         bool closed;
     }
 
@@ -78,7 +78,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
         Time.Delay delay; // delay for granting
     }
 
-    struct Family {
+    struct Class {
         mapping(bytes4 selector => uint64 groupId) allowedGroups;
         Time.Delay adminDelay;
     }
@@ -87,7 +87,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
     uint64 public constant PUBLIC_GROUP = type(uint64).max; // 2**64-1
 
     mapping(address target => AccessMode mode) private _contractMode;
-    mapping(uint64 familyId => Family) private _families;
+    mapping(uint64 classId => Class) private _classes;
     mapping(uint64 groupId => Group) private _groups;
     mapping(bytes32 operationId => uint48 schedule) private _schedules;
     mapping(bytes4 selector => Time.Delay delay) private _adminDelays;
@@ -127,7 +127,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
      * to identify the indirect workflow, and will consider call that require a delay to be forbidden.
      */
     function canCall(address caller, address target, bytes4 selector) public view virtual returns (bool, uint32) {
-        (uint64 familyId, bool closed) = getContractFamily(target);
+        (uint64 classId, bool closed) = getContractClass(target);
         if (closed) {
             return (false, 0);
         } else if (caller == address(this)) {
@@ -135,7 +135,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
             // verify that the call "identifier", which is set during the relay call, is correct.
             return (_relayIdentifier == _hashRelayIdentifier(target, selector), 0);
         } else {
-            uint64 groupId = getFamilyFunctionGroup(familyId, selector);
+            uint64 groupId = getClassFunctionGroup(classId, selector);
             (bool inGroup, uint32 currentDelay) = hasGroup(groupId, caller);
             return inGroup ? (currentDelay == 0, currentDelay) : (false, 0);
         }
@@ -158,21 +158,21 @@ contract AccessManager is Context, Multicall, IAccessManager {
     /**
      * @dev Get the mode under which a contract is operating.
      */
-    function getContractFamily(address target) public view virtual returns (uint64, bool) {
+    function getContractClass(address target) public view virtual returns (uint64, bool) {
         AccessMode storage mode = _contractMode[target];
-        return (mode.familyId, mode.closed);
+        return (mode.classId, mode.closed);
     }
 
     /**
      * @dev Get the permission level (group) required to call a function. This only applies for contract that are
      * operating under the `Custom` mode.
      */
-    function getFamilyFunctionGroup(uint64 familyId, bytes4 selector) public view virtual returns (uint64) {
-        return _families[familyId].allowedGroups[selector];
+    function getClassFunctionGroup(uint64 classId, bytes4 selector) public view virtual returns (uint64) {
+        return _classes[classId].allowedGroups[selector];
     }
 
-    function getFamilyAdminDelay(uint64 familyId) public view virtual returns (uint32) {
-        return _families[familyId].adminDelay.get();
+    function getClassAdminDelay(uint64 classId) public view virtual returns (uint32) {
+        return _classes[classId].adminDelay.get();
     }
 
     /**
@@ -250,11 +250,17 @@ contract AccessManager is Context, Multicall, IAccessManager {
     }
 
     /**
-     * @dev Add `account` to `groupId`. This gives him the authorization to call any function that is restricted to
-     * this group. 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 group. The user will only be able to execute the
-     * operation after the delay expires. During this delay, admin and guardians can cancel the operation (see
-     * {cancel}).
+     * @dev Add `account` to `groupId`, or change its execution delay.
+     *
+     * This gives the account the authorization to call any function that is restricted to this group. 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 group. 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 group, 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
+     * 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:
      *
@@ -267,7 +273,8 @@ contract AccessManager is Context, Multicall, IAccessManager {
     }
 
     /**
-     * @dev Remove an account for a group, with immediate effect.
+     * @dev Remove an account for a group, with immediate effect. If the sender is not in the group, this call has no
+     * effect.
      *
      * Requirements:
      *
@@ -280,7 +287,8 @@ contract AccessManager is Context, Multicall, IAccessManager {
     }
 
     /**
-     * @dev Renounce group permissions for the calling account, with immediate effect.
+     * @dev Renounce group permissions for the calling account, with immediate effect. If the sender is not in
+     * the group, this call has no effect.
      *
      * Requirements:
      *
@@ -295,22 +303,6 @@ contract AccessManager is Context, Multicall, IAccessManager {
         _revokeGroup(groupId, callerConfirmation);
     }
 
-    /**
-     * @dev Set the execution delay for a given account in a given group. This update is not immediate and follows the
-     * delay rules. For example, If a user currently has a delay of 3 hours, and this is called to reduce that delay to
-     * 1 hour, the new delay will take some time to take effect, enforcing that any operation executed in the 3 hours
-     * that follows this update was indeed scheduled before this update.
-     *
-     * Requirements:
-     *
-     * - the caller must be in the group's admins
-     *
-     * Emits a {GroupExecutionDelayUpdated} event
-     */
-    function setExecuteDelay(uint64 groupId, address account, uint32 newDelay) public virtual onlyAuthorized {
-        _setExecuteDelay(groupId, account, newDelay);
-    }
-
     /**
      * @dev Change admin group for a given group.
      *
@@ -351,57 +343,57 @@ contract AccessManager is Context, Multicall, IAccessManager {
     }
 
     /**
-     * @dev Internal version of {grantGroup} without access control.
+     * @dev Internal version of {grantGroup} without access control. Returns true if the group was newly granted.
      *
      * Emits a {GroupGranted} event
      */
-    function _grantGroup(uint64 groupId, address account, uint32 grantDelay, uint32 executionDelay) internal virtual {
+    function _grantGroup(
+        uint64 groupId,
+        address account,
+        uint32 grantDelay,
+        uint32 executionDelay
+    ) internal virtual returns (bool) {
         if (groupId == PUBLIC_GROUP) {
             revert AccessManagerLockedGroup(groupId);
-        } else if (_groups[groupId].members[account].since != 0) {
-            revert AccessManagerAccountAlreadyInGroup(groupId, account);
         }
 
-        uint48 since = Time.timestamp() + grantDelay;
-        _groups[groupId].members[account] = Access({since: since, delay: executionDelay.toDelay()});
+        bool inGroup = _groups[groupId].members[account].since != 0;
+
+        uint48 since;
+
+        if (inGroup) {
+            (_groups[groupId].members[account].delay, since) = _groups[groupId].members[account].delay.withUpdate(
+                executionDelay,
+                minSetback()
+            );
+        } else {
+            since = Time.timestamp() + grantDelay;
+            _groups[groupId].members[account] = Access({since: since, delay: executionDelay.toDelay()});
+        }
 
-        emit GroupGranted(groupId, account, since, executionDelay);
+        emit GroupGranted(groupId, account, executionDelay, since);
+        return !inGroup;
     }
 
     /**
      * @dev Internal version of {revokeGroup} without access control. This logic is also used by {renounceGroup}.
+     * Returns true if the group was previously granted.
      *
      * Emits a {GroupRevoked} event
      */
-    function _revokeGroup(uint64 groupId, address account) internal virtual {
+    function _revokeGroup(uint64 groupId, address account) internal virtual returns (bool) {
         if (groupId == PUBLIC_GROUP) {
             revert AccessManagerLockedGroup(groupId);
-        } else if (_groups[groupId].members[account].since == 0) {
-            revert AccessManagerAccountNotInGroup(groupId, account);
         }
 
-        delete _groups[groupId].members[account];
-
-        emit GroupRevoked(groupId, account);
-    }
-
-    /**
-     * @dev Internal version of {setExecuteDelay} without access control.
-     *
-     * Emits a {GroupExecutionDelayUpdated} event.
-     */
-    function _setExecuteDelay(uint64 groupId, address account, uint32 newDuration) internal virtual {
-        if (groupId == PUBLIC_GROUP || groupId == ADMIN_GROUP) {
-            revert AccessManagerLockedGroup(groupId);
-        } else if (_groups[groupId].members[account].since == 0) {
-            revert AccessManagerAccountNotInGroup(groupId, account);
+        if (_groups[groupId].members[account].since == 0) {
+            return false;
         }
 
-        Time.Delay updated = _groups[groupId].members[account].delay.withUpdate(newDuration, minSetback());
-        _groups[groupId].members[account].delay = updated;
+        delete _groups[groupId].members[account];
 
-        (, , uint48 effect) = updated.unpack();
-        emit GroupExecutionDelayUpdated(groupId, account, newDuration, effect);
+        emit GroupRevoked(groupId, account);
+        return true;
     }
 
     /**
@@ -444,10 +436,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
             revert AccessManagerLockedGroup(groupId);
         }
 
-        Time.Delay updated = _groups[groupId].delay.withUpdate(newDelay, minSetback());
+        (Time.Delay updated, uint48 effect) = _groups[groupId].delay.withUpdate(newDelay, minSetback());
         _groups[groupId].delay = updated;
 
-        (, , uint48 effect) = updated.unpack();
         emit GroupGrantDelayChanged(groupId, newDelay, effect);
     }
 
@@ -462,13 +453,13 @@ contract AccessManager is Context, Multicall, IAccessManager {
      *
      * Emits a {FunctionAllowedGroupUpdated} event per selector
      */
-    function setFamilyFunctionGroup(
-        uint64 familyId,
+    function setClassFunctionGroup(
+        uint64 classId,
         bytes4[] calldata selectors,
         uint64 groupId
     ) public virtual onlyAuthorized {
         for (uint256 i = 0; i < selectors.length; ++i) {
-            _setFamilyFunctionGroup(familyId, selectors[i], groupId);
+            _setClassFunctionGroup(classId, selectors[i], groupId);
         }
     }
 
@@ -477,14 +468,14 @@ contract AccessManager is Context, Multicall, IAccessManager {
      *
      * Emits a {FunctionAllowedGroupUpdated} event
      */
-    function _setFamilyFunctionGroup(uint64 familyId, bytes4 selector, uint64 groupId) internal virtual {
-        _checkValidFamilyId(familyId);
-        _families[familyId].allowedGroups[selector] = groupId;
-        emit FamilyFunctionGroupUpdated(familyId, selector, groupId);
+    function _setClassFunctionGroup(uint64 classId, bytes4 selector, uint64 groupId) internal virtual {
+        _checkValidClassId(classId);
+        _classes[classId].allowedGroups[selector] = groupId;
+        emit ClassFunctionGroupUpdated(classId, selector, groupId);
     }
 
     /**
-     * @dev Set the delay for management operations on a given family of contract.
+     * @dev Set the delay for management operations on a given class of contract.
      *
      * Requirements:
      *
@@ -492,54 +483,57 @@ contract AccessManager is Context, Multicall, IAccessManager {
      *
      * Emits a {FunctionAllowedGroupUpdated} event per selector
      */
-    function setFamilyAdminDelay(uint64 familyId, uint32 newDelay) public virtual onlyAuthorized {
-        _setFamilyAdminDelay(familyId, newDelay);
+    function setClassAdminDelay(uint64 classId, uint32 newDelay) public virtual onlyAuthorized {
+        _setClassAdminDelay(classId, newDelay);
     }
 
     /**
-     * @dev Internal version of {setFamilyAdminDelay} without access control.
+     * @dev Internal version of {setClassAdminDelay} without access control.
      *
-     * Emits a {FamilyAdminDelayUpdated} event
+     * Emits a {ClassAdminDelayUpdated} event
      */
-    function _setFamilyAdminDelay(uint64 familyId, uint32 newDelay) internal virtual {
-        _checkValidFamilyId(familyId);
-        Time.Delay updated = _families[familyId].adminDelay.withUpdate(newDelay, minSetback());
-        _families[familyId].adminDelay = updated;
-        (, , uint48 effect) = updated.unpack();
-        emit FamilyAdminDelayUpdated(familyId, newDelay, effect);
+    function _setClassAdminDelay(uint64 classId, uint32 newDelay) internal virtual {
+        _checkValidClassId(classId);
+        (Time.Delay updated, uint48 effect) = _classes[classId].adminDelay.withUpdate(newDelay, minSetback());
+        _classes[classId].adminDelay = updated;
+        emit ClassAdminDelayUpdated(classId, newDelay, effect);
     }
 
     /**
-     * @dev Reverts if `familyId` is 0.
+     * @dev Reverts if `classId` is 0. This is the default class id given to contracts and it should not have any
+     * configurations.
      */
-    function _checkValidFamilyId(uint64 familyId) private pure {
-        if (familyId == 0) {
-            revert AccessManagerInvalidFamily(familyId);
+    function _checkValidClassId(uint64 classId) private pure {
+        if (classId == 0) {
+            revert AccessManagerInvalidClass(classId);
         }
     }
 
     // =============================================== MODE MANAGEMENT ================================================
     /**
-     * @dev Set the family of a contract.
+     * @dev Set the class of a contract.
      *
      * Requirements:
      *
      * - the caller must be a global admin
      *
-     * Emits a {ContractFamilyUpdated} event.
+     * Emits a {ContractClassUpdated} event.
      */
-    function setContractFamily(address target, uint64 familyId) public virtual onlyAuthorized {
-        _setContractFamily(target, familyId);
+    function setContractClass(address target, uint64 classId) public virtual onlyAuthorized {
+        _setContractClass(target, classId);
     }
 
     /**
-     * @dev Set the family of a contract. This is an internal setter with no access restrictions.
+     * @dev Set the class of a contract. This is an internal setter with no access restrictions.
      *
-     * Emits a {ContractFamilyUpdated} event.
+     * Emits a {ContractClassUpdated} event.
      */
-    function _setContractFamily(address target, uint64 familyId) internal virtual {
-        _contractMode[target].familyId = familyId;
-        emit ContractFamilyUpdated(target, familyId);
+    function _setContractClass(address target, uint64 classId) internal virtual {
+        if (target == address(this)) {
+            revert AccessManagerLockedAccount(target);
+        }
+        _contractMode[target].classId = classId;
+        emit ContractClassUpdated(target, classId);
     }
 
     /**
@@ -561,6 +555,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
      * Emits a {ContractClosed} event.
      */
     function _setContractClosed(address target, bool closed) internal virtual {
+        if (target == address(this)) {
+            revert AccessManagerLockedAccount(target);
+        }
         _contractMode[target].closed = closed;
         emit ContractClosed(target, closed);
     }
@@ -700,9 +697,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
             revert AccessManagerNotScheduled(operationId);
         } else if (caller != msgsender) {
             // calls can only be canceled by the account that scheduled them, a global admin, or by a guardian of the required group.
-            (uint64 familyId, ) = getContractFamily(target);
+            (uint64 classId, ) = getContractClass(target);
             (bool isAdmin, ) = hasGroup(ADMIN_GROUP, msgsender);
-            (bool isGuardian, ) = hasGroup(getGroupGuardian(getFamilyFunctionGroup(familyId, selector)), msgsender);
+            (bool isGuardian, ) = hasGroup(getGroupGuardian(getClassFunctionGroup(classId, selector)), msgsender);
             if (!isAdmin && !isGuardian) {
                 revert AccessManagerCannotCancel(msgsender, caller, target, selector);
             }
@@ -762,32 +759,30 @@ contract AccessManager is Context, Multicall, IAccessManager {
     function _getAdminRestrictions(bytes calldata data) private view returns (bool, uint64, uint32) {
         bytes4 selector = bytes4(data);
 
-        if (selector == this.updateAuthority.selector || selector == this.setContractFamily.selector) {
-            // First argument is a target. Restricted to ADMIN with the family delay corresponding to the target's family
+        if (data.length < 4) {
+            return (false, 0, 0);
+        } else if (selector == this.updateAuthority.selector || selector == this.setContractClass.selector) {
+            // First argument is a target. Restricted to ADMIN with the class delay corresponding to the target's class
             address target = abi.decode(data[0x04:0x24], (address));
-            (uint64 familyId, ) = getContractFamily(target);
-            uint32 delay = getFamilyAdminDelay(familyId);
+            (uint64 classId, ) = getContractClass(target);
+            uint32 delay = getClassAdminDelay(classId);
             return (true, ADMIN_GROUP, delay);
-        } else if (selector == this.setFamilyFunctionGroup.selector) {
-            // First argument is a family. Restricted to ADMIN with the family delay corresponding to the family
-            uint64 familyId = abi.decode(data[0x04:0x24], (uint64));
-            uint32 delay = getFamilyAdminDelay(familyId);
+        } else if (selector == this.setClassFunctionGroup.selector) {
+            // First argument is a class. Restricted to ADMIN with the class delay corresponding to the class
+            uint64 classId = abi.decode(data[0x04:0x24], (uint64));
+            uint32 delay = getClassAdminDelay(classId);
             return (true, ADMIN_GROUP, delay);
         } else if (
             selector == this.labelGroup.selector ||
             selector == this.setGroupAdmin.selector ||
             selector == this.setGroupGuardian.selector ||
             selector == this.setGrantDelay.selector ||
-            selector == this.setFamilyAdminDelay.selector ||
+            selector == this.setClassAdminDelay.selector ||
             selector == this.setContractClosed.selector
         ) {
             // Restricted to ADMIN with no delay beside any execution delay the caller may have
             return (true, ADMIN_GROUP, 0);
-        } else if (
-            selector == this.grantGroup.selector ||
-            selector == this.revokeGroup.selector ||
-            selector == this.setExecuteDelay.selector
-        ) {
+        } else if (selector == this.grantGroup.selector || selector == this.revokeGroup.selector) {
             // First argument is a groupId. Restricted to that group's admin with no delay beside any execution delay the caller may have.
             uint64 groupId = abi.decode(data[0x04:0x24], (uint64));
             uint64 groupAdminId = getGroupAdmin(groupId);

+ 13 - 17
contracts/access/manager/IAccessManager.sol

@@ -22,27 +22,25 @@ interface IAccessManager {
     event OperationCanceled(bytes32 indexed operationId, uint48 schedule);
 
     event GroupLabel(uint64 indexed groupId, string label);
-    event GroupGranted(uint64 indexed groupId, address indexed account, uint48 since, uint32 delay);
+    event GroupGranted(uint64 indexed groupId, address indexed account, uint32 delay, uint48 since);
     event GroupRevoked(uint64 indexed groupId, address indexed account);
-    event GroupExecutionDelayUpdated(uint64 indexed groupId, address indexed account, uint32 delay, uint48 from);
     event GroupAdminChanged(uint64 indexed groupId, uint64 indexed admin);
     event GroupGuardianChanged(uint64 indexed groupId, uint64 indexed guardian);
-    event GroupGrantDelayChanged(uint64 indexed groupId, uint32 delay, uint48 from);
+    event GroupGrantDelayChanged(uint64 indexed groupId, uint32 delay, uint48 since);
 
-    event ContractFamilyUpdated(address indexed target, uint64 indexed familyId);
+    event ContractClassUpdated(address indexed target, uint64 indexed classId);
     event ContractClosed(address indexed target, bool closed);
 
-    event FamilyFunctionGroupUpdated(uint64 indexed familyId, bytes4 selector, uint64 indexed groupId);
-    event FamilyAdminDelayUpdated(uint64 indexed familyId, uint32 delay, uint48 from);
+    event ClassFunctionGroupUpdated(uint64 indexed classId, bytes4 selector, uint64 indexed groupId);
+    event ClassAdminDelayUpdated(uint64 indexed classId, uint32 delay, uint48 since);
 
     error AccessManagerAlreadyScheduled(bytes32 operationId);
     error AccessManagerNotScheduled(bytes32 operationId);
     error AccessManagerNotReady(bytes32 operationId);
     error AccessManagerExpired(bytes32 operationId);
+    error AccessManagerLockedAccount(address account);
     error AccessManagerLockedGroup(uint64 groupId);
-    error AccessManagerInvalidFamily(uint64 familyId);
-    error AccessManagerAccountAlreadyInGroup(uint64 groupId, address account);
-    error AccessManagerAccountNotInGroup(uint64 groupId, address account);
+    error AccessManagerInvalidClass(uint64 classId);
     error AccessManagerBadConfirmation();
     error AccessManagerUnauthorizedAccount(address msgsender, uint64 groupId);
     error AccessManagerUnauthorizedCall(address caller, address target, bytes4 selector);
@@ -56,11 +54,11 @@ interface IAccessManager {
 
     function expiration() external returns (uint32);
 
-    function getContractFamily(address target) external view returns (uint64 familyId, bool closed);
+    function getContractClass(address target) external view returns (uint64 classId, bool closed);
 
-    function getFamilyFunctionGroup(uint64 familyId, bytes4 selector) external view returns (uint64);
+    function getClassFunctionGroup(uint64 classId, bytes4 selector) external view returns (uint64);
 
-    function getFamilyAdminDelay(uint64 familyId) external view returns (uint32);
+    function getClassAdminDelay(uint64 classId) external view returns (uint32);
 
     function getGroupAdmin(uint64 groupId) external view returns (uint64);
 
@@ -80,19 +78,17 @@ interface IAccessManager {
 
     function renounceGroup(uint64 groupId, address callerConfirmation) external;
 
-    function setExecuteDelay(uint64 groupId, address account, uint32 newDelay) external;
-
     function setGroupAdmin(uint64 groupId, uint64 admin) external;
 
     function setGroupGuardian(uint64 groupId, uint64 guardian) external;
 
     function setGrantDelay(uint64 groupId, uint32 newDelay) external;
 
-    function setFamilyFunctionGroup(uint64 familyId, bytes4[] calldata selectors, uint64 groupId) external;
+    function setClassFunctionGroup(uint64 classId, bytes4[] calldata selectors, uint64 groupId) external;
 
-    function setFamilyAdminDelay(uint64 familyId, uint32 newDelay) external;
+    function setClassAdminDelay(uint64 classId, uint32 newDelay) external;
 
-    function setContractFamily(address target, uint64 familyId) external;
+    function setContractClass(address target, uint64 classId) external;
 
     function setContractClosed(address target, bool closed) external;
 

+ 12 - 6
contracts/utils/types/Time.sol

@@ -53,9 +53,13 @@ library Time {
      *
      *
      * The `Delay` type is 128 bits long, and packs the following:
-     * [000:031] uint32 for the current value (duration)
-     * [032:063] uint32 for the pending value (duration)
-     * [064:111] uint48 for the effect date (timepoint)
+     *
+     * ```
+     *   | [uint48]: effect date (timepoint)
+     *   |           | [uint32]: current value (duration)
+     *   ↓           ↓       ↓ [uint32]: pending value (duration)
+     * 0xAAAAAAAAAAAABBBBBBBBCCCCCCCC
+     * ```
      *
      * NOTE: The {get} and {update} function operate using timestamps. Block number based delays should use the
      * {getAt} and {withUpdateAt} variants of these functions.
@@ -110,12 +114,14 @@ library Time {
 
     /**
      * @dev Update a Delay object so that it takes a new duration after at a timepoint that is automatically computed
-     * to enforce the old delay at the moment of the update.
+     * to 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) {
+    function withUpdate(Delay self, uint32 newValue, uint32 minSetback) internal view returns (Delay, uint48) {
         uint32 value = self.get();
         uint32 setback = uint32(Math.max(minSetback, value > newValue ? value - newValue : 0));
-        return self.withUpdateAt(newValue, timestamp() + setback);
+        uint48 effect = timestamp() + setback;
+        return (self.withUpdateAt(newValue, effect), effect);
     }
 
     /**

+ 69 - 107
test/access/manager/AccessManager.test.js

@@ -18,7 +18,7 @@ const GROUPS = {
 };
 Object.assign(GROUPS, Object.fromEntries(Object.entries(GROUPS).map(([key, value]) => [value, key])));
 
-const familyId = web3.utils.toBN(1);
+const classId = web3.utils.toBN(1);
 const executeDelay = web3.utils.toBN(10);
 const grantDelay = web3.utils.toBN(10);
 
@@ -138,24 +138,15 @@ contract('AccessManager', function (accounts) {
 
         it('to a user that is already in the group', async function () {
           expect(await this.manager.hasGroup(GROUPS.SOME, member).then(formatAccess)).to.be.deep.equal([true, '0']);
-
-          await expectRevertCustomError(
-            this.manager.grantGroup(GROUPS.SOME, member, 0, { from: manager }),
-            'AccessManagerAccountAlreadyInGroup',
-            [GROUPS.SOME, member],
-          );
+          await this.manager.grantGroup(GROUPS.SOME, member, 0, { from: manager });
+          expect(await this.manager.hasGroup(GROUPS.SOME, member).then(formatAccess)).to.be.deep.equal([true, '0']);
         });
 
         it('to a user that is scheduled for joining the group', async function () {
           await this.manager.$_grantGroup(GROUPS.SOME, user, 10, 0); // grant delay 10
-
           expect(await this.manager.hasGroup(GROUPS.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']);
-
-          await expectRevertCustomError(
-            this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager }),
-            'AccessManagerAccountAlreadyInGroup',
-            [GROUPS.SOME, user],
-          );
+          await this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager });
+          expect(await this.manager.hasGroup(GROUPS.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']);
         });
 
         it('grant group is restricted', async function () {
@@ -212,6 +203,14 @@ contract('AccessManager', function (accounts) {
           expect(access[3]).to.be.bignumber.equal('0'); // effect
         });
       });
+
+      it('cannot grant public group', async function () {
+        await expectRevertCustomError(
+          this.manager.$_grantGroup(GROUPS.PUBLIC, other, 0, executeDelay, { from: manager }),
+          'AccessManagerLockedGroup',
+          [GROUPS.PUBLIC],
+        );
+      });
     });
 
     describe('revoke group', function () {
@@ -249,12 +248,8 @@ contract('AccessManager', function (accounts) {
 
       it('from a user that is not in the group', async function () {
         expect(await this.manager.hasGroup(GROUPS.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']);
-
-        await expectRevertCustomError(
-          this.manager.revokeGroup(GROUPS.SOME, user, { from: manager }),
-          'AccessManagerAccountNotInGroup',
-          [GROUPS.SOME, user],
-        );
+        await this.manager.revokeGroup(GROUPS.SOME, user, { from: manager });
+        expect(await this.manager.hasGroup(GROUPS.SOME, user).then(formatAccess)).to.be.deep.equal([false, '0']);
       });
 
       it('revoke group is restricted', async function () {
@@ -300,11 +295,7 @@ contract('AccessManager', function (accounts) {
       });
 
       it('for a user that is not in the group', async function () {
-        await expectRevertCustomError(
-          this.manager.renounceGroup(GROUPS.SOME, user, { from: user }),
-          'AccessManagerAccountNotInGroup',
-          [GROUPS.SOME, user],
-        );
+        await this.manager.renounceGroup(GROUPS.SOME, user, { from: user });
       });
 
       it('bad user confirmation', async function () {
@@ -355,27 +346,27 @@ contract('AccessManager', function (accounts) {
     });
 
     describe('change execution delay', function () {
-      it('increassing the delay has immediate effect', async 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.$_setExecuteDelay(GROUPS.SOME, member, oldDelay);
+        await this.manager.$_grantGroup(GROUPS.SOME, member, 0, oldDelay);
 
         const accessBefore = await this.manager.getAccess(GROUPS.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
 
-        const { receipt } = await this.manager.setExecuteDelay(GROUPS.SOME, member, newDelay, {
+        const { receipt } = await this.manager.grantGroup(GROUPS.SOME, member, newDelay, {
           from: manager,
         });
         const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
 
-        expectEvent(receipt, 'GroupExecutionDelayUpdated', {
+        expectEvent(receipt, 'GroupGranted', {
           groupId: GROUPS.SOME,
           account: member,
+          since: timestamp,
           delay: newDelay,
-          from: timestamp,
         });
 
         // immediate effect
@@ -385,27 +376,27 @@ contract('AccessManager', function (accounts) {
         expect(accessAfter[3]).to.be.bignumber.equal('0'); // effect
       });
 
-      it('decreassing the delay takes time', async function () {
+      it('decreasing the delay takes time', async function () {
         const oldDelay = web3.utils.toBN(100);
         const newDelay = web3.utils.toBN(10);
 
-        await this.manager.$_setExecuteDelay(GROUPS.SOME, member, oldDelay);
+        await this.manager.$_grantGroup(GROUPS.SOME, member, 0, oldDelay);
 
         const accessBefore = await this.manager.getAccess(GROUPS.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
 
-        const { receipt } = await this.manager.setExecuteDelay(GROUPS.SOME, member, newDelay, {
+        const { receipt } = await this.manager.grantGroup(GROUPS.SOME, member, newDelay, {
           from: manager,
         });
         const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
 
-        expectEvent(receipt, 'GroupExecutionDelayUpdated', {
+        expectEvent(receipt, 'GroupGranted', {
           groupId: GROUPS.SOME,
           account: member,
+          since: timestamp.add(oldDelay).sub(newDelay),
           delay: newDelay,
-          from: timestamp.add(oldDelay).sub(newDelay),
         });
 
         // delayed effect
@@ -415,49 +406,23 @@ contract('AccessManager', function (accounts) {
         expect(accessAfter[3]).to.be.bignumber.equal(timestamp.add(oldDelay).sub(newDelay)); // effect
       });
 
-      it('cannot set the delay of a non member', async function () {
-        await expectRevertCustomError(
-          this.manager.setExecuteDelay(GROUPS.SOME, other, executeDelay, { from: manager }),
-          'AccessManagerAccountNotInGroup',
-          [GROUPS.SOME, other],
-        );
-      });
-
-      it('cannot set the delay of public and admin groups', async function () {
-        for (const group of [GROUPS.PUBLIC, GROUPS.ADMIN]) {
-          await expectRevertCustomError(
-            this.manager.$_setExecuteDelay(group, other, executeDelay, { from: manager }),
-            'AccessManagerLockedGroup',
-            [group],
-          );
-        }
-      });
-
       it('can set a user execution delay during the grant delay', async function () {
         await this.manager.$_grantGroup(GROUPS.SOME, other, 10, 0);
 
-        const { receipt } = await this.manager.setExecuteDelay(GROUPS.SOME, other, executeDelay, { from: manager });
+        const { receipt } = await this.manager.grantGroup(GROUPS.SOME, other, executeDelay, { from: manager });
         const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
 
-        expectEvent(receipt, 'GroupExecutionDelayUpdated', {
+        expectEvent(receipt, 'GroupGranted', {
           groupId: GROUPS.SOME,
           account: other,
+          since: timestamp,
           delay: executeDelay,
-          from: timestamp,
         });
       });
-
-      it('changing the execution delay is restricted', async function () {
-        await expectRevertCustomError(
-          this.manager.setExecuteDelay(GROUPS.SOME, member, executeDelay, { from: other }),
-          'AccessManagerUnauthorizedAccount',
-          [GROUPS.SOME_ADMIN, other],
-        );
-      });
     });
 
     describe('change grant delay', function () {
-      it('increassing the delay has immediate effect', async 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(GROUPS.SOME, oldDelay);
@@ -467,12 +432,12 @@ contract('AccessManager', function (accounts) {
         const { receipt } = await this.manager.setGrantDelay(GROUPS.SOME, newDelay, { from: admin });
         const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
 
-        expectEvent(receipt, 'GroupGrantDelayChanged', { groupId: GROUPS.SOME, delay: newDelay, from: timestamp });
+        expectEvent(receipt, 'GroupGrantDelayChanged', { groupId: GROUPS.SOME, delay: newDelay, since: timestamp });
 
         expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(newDelay);
       });
 
-      it('increassing the delay has delay effect', async function () {
+      it('increasing the delay has delay effect', async function () {
         const oldDelay = web3.utils.toBN(100);
         const newDelay = web3.utils.toBN(10);
         await this.manager.$_setGrantDelay(GROUPS.SOME, oldDelay);
@@ -485,7 +450,7 @@ contract('AccessManager', function (accounts) {
         expectEvent(receipt, 'GroupGrantDelayChanged', {
           groupId: GROUPS.SOME,
           delay: newDelay,
-          from: timestamp.add(oldDelay).sub(newDelay),
+          since: timestamp.add(oldDelay).sub(newDelay),
         });
 
         expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(oldDelay);
@@ -525,36 +490,33 @@ contract('AccessManager', function (accounts) {
 
       it('admin can set function group', async function () {
         for (const sig of sigs) {
-          expect(await this.manager.getFamilyFunctionGroup(familyId, sig)).to.be.bignumber.equal(GROUPS.ADMIN);
+          expect(await this.manager.getClassFunctionGroup(classId, sig)).to.be.bignumber.equal(GROUPS.ADMIN);
         }
 
-        const { receipt: receipt1 } = await this.manager.setFamilyFunctionGroup(familyId, sigs, GROUPS.SOME, {
+        const { receipt: receipt1 } = await this.manager.setClassFunctionGroup(classId, sigs, GROUPS.SOME, {
           from: admin,
         });
 
         for (const sig of sigs) {
-          expectEvent(receipt1, 'FamilyFunctionGroupUpdated', {
-            familyId,
+          expectEvent(receipt1, 'ClassFunctionGroupUpdated', {
+            classId,
             selector: sig,
             groupId: GROUPS.SOME,
           });
-          expect(await this.manager.getFamilyFunctionGroup(familyId, sig)).to.be.bignumber.equal(GROUPS.SOME);
+          expect(await this.manager.getClassFunctionGroup(classId, sig)).to.be.bignumber.equal(GROUPS.SOME);
         }
 
-        const { receipt: receipt2 } = await this.manager.setFamilyFunctionGroup(
-          familyId,
-          [sigs[1]],
-          GROUPS.SOME_ADMIN,
-          { from: admin },
-        );
-        expectEvent(receipt2, 'FamilyFunctionGroupUpdated', {
-          familyId,
+        const { receipt: receipt2 } = await this.manager.setClassFunctionGroup(classId, [sigs[1]], GROUPS.SOME_ADMIN, {
+          from: admin,
+        });
+        expectEvent(receipt2, 'ClassFunctionGroupUpdated', {
+          classId,
           selector: sigs[1],
           groupId: GROUPS.SOME_ADMIN,
         });
 
         for (const sig of sigs) {
-          expect(await this.manager.getFamilyFunctionGroup(familyId, sig)).to.be.bignumber.equal(
+          expect(await this.manager.getClassFunctionGroup(classId, sig)).to.be.bignumber.equal(
             sig == sigs[1] ? GROUPS.SOME_ADMIN : GROUPS.SOME,
           );
         }
@@ -562,7 +524,7 @@ contract('AccessManager', function (accounts) {
 
       it('non-admin cannot set function group', async function () {
         await expectRevertCustomError(
-          this.manager.setFamilyFunctionGroup(familyId, sigs, GROUPS.SOME, { from: other }),
+          this.manager.setClassFunctionGroup(classId, sigs, GROUPS.SOME, { from: other }),
           'AccessManagerUnauthorizedAccount',
           [other, GROUPS.ADMIN],
         );
@@ -600,25 +562,25 @@ contract('AccessManager', function (accounts) {
             // setup
             await Promise.all([
               this.manager.$_setContractClosed(this.target.address, closed),
-              this.manager.$_setContractFamily(this.target.address, familyId),
-              fnGroup && this.manager.$_setFamilyFunctionGroup(familyId, selector('fnRestricted()'), fnGroup),
-              fnGroup && this.manager.$_setFamilyFunctionGroup(familyId, selector('fnUnrestricted()'), fnGroup),
+              this.manager.$_setContractClass(this.target.address, classId),
+              fnGroup && this.manager.$_setClassFunctionGroup(classId, selector('fnRestricted()'), fnGroup),
+              fnGroup && this.manager.$_setClassFunctionGroup(classId, selector('fnUnrestricted()'), fnGroup),
               ...callerGroups
                 .filter(groupId => groupId != GROUPS.PUBLIC)
                 .map(groupId => this.manager.$_grantGroup(groupId, user, 0, delay ?? 0)),
             ]);
 
             // post setup checks
-            const result = await this.manager.getContractFamily(this.target.address);
-            expect(result[0]).to.be.bignumber.equal(familyId);
+            const result = await this.manager.getContractClass(this.target.address);
+            expect(result[0]).to.be.bignumber.equal(classId);
             expect(result[1]).to.be.equal(closed);
 
             if (fnGroup) {
               expect(
-                await this.manager.getFamilyFunctionGroup(familyId, selector('fnRestricted()')),
+                await this.manager.getClassFunctionGroup(classId, selector('fnRestricted()')),
               ).to.be.bignumber.equal(fnGroup);
               expect(
-                await this.manager.getFamilyFunctionGroup(familyId, selector('fnUnrestricted()')),
+                await this.manager.getClassFunctionGroup(classId, selector('fnUnrestricted()')),
               ).to.be.bignumber.equal(fnGroup);
             }
 
@@ -832,8 +794,8 @@ contract('AccessManager', function (accounts) {
 
     describe('Indirect execution corner-cases', async function () {
       beforeEach(async function () {
-        await this.manager.$_setContractFamily(this.target.address, familyId);
-        await this.manager.$_setFamilyFunctionGroup(familyId, this.callData, GROUPS.SOME);
+        await this.manager.$_setContractClass(this.target.address, classId);
+        await this.manager.$_setClassFunctionGroup(classId, this.callData, GROUPS.SOME);
         await this.manager.$_grantGroup(GROUPS.SOME, user, 0, executeDelay);
       });
 
@@ -996,13 +958,13 @@ contract('AccessManager', function (accounts) {
     });
 
     describe('Contract is managed', function () {
-      beforeEach('add contract to family', async function () {
-        await this.manager.$_setContractFamily(this.ownable.address, familyId);
+      beforeEach('add contract to class', async function () {
+        await this.manager.$_setContractClass(this.ownable.address, classId);
       });
 
       describe('function is open to specific group', function () {
         beforeEach(async function () {
-          await this.manager.$_setFamilyFunctionGroup(familyId, selector('$_checkOwner()'), groupId);
+          await this.manager.$_setClassFunctionGroup(classId, selector('$_checkOwner()'), groupId);
         });
 
         it('directly call: reverts', async function () {
@@ -1026,7 +988,7 @@ contract('AccessManager', function (accounts) {
 
       describe('function is open to public group', function () {
         beforeEach(async function () {
-          await this.manager.$_setFamilyFunctionGroup(familyId, selector('$_checkOwner()'), GROUPS.PUBLIC);
+          await this.manager.$_setClassFunctionGroup(classId, selector('$_checkOwner()'), GROUPS.PUBLIC);
         });
 
         it('directly call: reverts', async function () {
@@ -1087,16 +1049,16 @@ contract('AccessManager', function (accounts) {
   });
 
   // TODO: test all admin functions
-  describe('family delays', function () {
-    const otherFamilyId = '2';
+  describe('class delays', function () {
+    const otherClassId = '2';
     const delay = '1000';
 
-    beforeEach('set contract family', async function () {
+    beforeEach('set contract class', async function () {
       this.target = await AccessManagedTarget.new(this.manager.address);
-      await this.manager.setContractFamily(this.target.address, familyId, { from: admin });
+      await this.manager.setContractClass(this.target.address, classId, { from: admin });
 
-      this.call = () => this.manager.setContractFamily(this.target.address, otherFamilyId, { from: admin });
-      this.data = this.manager.contract.methods.setContractFamily(this.target.address, otherFamilyId).encodeABI();
+      this.call = () => this.manager.setContractClass(this.target.address, otherClassId, { from: admin });
+      this.data = this.manager.contract.methods.setContractClass(this.target.address, otherClassId).encodeABI();
     });
 
     it('without delay: succeeds', async function () {
@@ -1105,20 +1067,20 @@ contract('AccessManager', function (accounts) {
 
     // TODO: here we need to check increase and decrease.
     // - Increasing should have immediate effect
-    // - Decreassing should take time.
+    // - Decreasing should take time.
     describe('with delay', function () {
       beforeEach('set admin delay', async function () {
-        this.tx = await this.manager.setFamilyAdminDelay(familyId, delay, { from: admin });
+        this.tx = await this.manager.setClassAdminDelay(classId, delay, { from: admin });
         this.opId = web3.utils.keccak256(
           web3.eth.abi.encodeParameters(['address', 'address', 'bytes'], [admin, this.manager.address, this.data]),
         );
       });
 
       it('emits event and sets delay', async function () {
-        const from = await clockFromReceipt.timestamp(this.tx.receipt).then(web3.utils.toBN);
-        expectEvent(this.tx.receipt, 'FamilyAdminDelayUpdated', { familyId, delay, from });
+        const since = await clockFromReceipt.timestamp(this.tx.receipt).then(web3.utils.toBN);
+        expectEvent(this.tx.receipt, 'ClassAdminDelayUpdated', { classId, delay, since });
 
-        expect(await this.manager.getFamilyAdminDelay(familyId)).to.be.bignumber.equal(delay);
+        expect(await this.manager.getClassAdminDelay(classId)).to.be.bignumber.equal(delay);
       });
 
       it('without prior scheduling: reverts', async function () {