diff --git a/.gitignore b/.gitignore index 863e596b..7afd997a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ out/ /broadcast/*/31337/ /broadcast/**/dry-run/ +soljson-latest.js + # Dotenv file .env .gas-snapshot @@ -16,7 +18,6 @@ node_modules/ # Mirror of root CHANGELOG.md for Changesets src/CHANGELOG.md - # Docusaurus # Dependencies website/node_modules diff --git a/src/access/AccessControl/Admin/AccessControlAdminFacet.sol b/src/access/AccessControl/Admin/AccessControlAdminFacet.sol index 51f590fc..09ce0378 100644 --- a/src/access/AccessControl/Admin/AccessControlAdminFacet.sol +++ b/src/access/AccessControl/Admin/AccessControlAdminFacet.sol @@ -21,11 +21,34 @@ contract AccessControlAdminFacet { */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + /** * @notice Storage slot identifier. */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice Storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -35,6 +58,22 @@ contract AccessControlAdminFacet { mapping(bytes32 role => bytes32 adminRole) adminRole; } + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice Returns the storage for the AccessControl. * @return s The storage for the AccessControl. @@ -46,6 +85,54 @@ contract AccessControlAdminFacet { } } + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getTemporalStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Sets the admin role for a role. * @param _role The role to set the admin for. @@ -57,12 +144,7 @@ contract AccessControlAdminFacet { AccessControlStorage storage s = getStorage(); bytes32 previousAdminRole = s.adminRole[_role]; - /** - * Check if the caller is the current admin of the role. - */ - if (!s.hasRole[msg.sender][previousAdminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, previousAdminRole); - } + _requireRole(previousAdminRole); s.adminRole[_role] = _adminRole; emit RoleAdminChanged(_role, previousAdminRole, _adminRole); diff --git a/src/access/AccessControl/Admin/AccessControlAdminMod.sol b/src/access/AccessControl/Admin/AccessControlAdminMod.sol index 57c7794d..faa57d8a 100644 --- a/src/access/AccessControl/Admin/AccessControlAdminMod.sol +++ b/src/access/AccessControl/Admin/AccessControlAdminMod.sol @@ -20,11 +20,34 @@ event RoleAdminChanged(bytes32 indexed _role, bytes32 indexed _previousAdminRole */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + /* * @notice Storage slot identifier. */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice Storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -34,6 +57,22 @@ struct AccessControlStorage { mapping(bytes32 role => bytes32 adminRole) adminRole; } +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice Returns the storage for the AccessControl. * @return s The storage for the AccessControl. @@ -45,6 +84,54 @@ function getStorage() pure returns (AccessControlStorage storage s) { } } +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getTemporalStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ +function _requireRole(bytes32 _role) view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} + /** * @notice Sets the admin role for a role. * @param _role The role to set the admin for. @@ -56,12 +143,7 @@ function setRoleAdmin(bytes32 _role, bytes32 _adminRole) { AccessControlStorage storage s = getStorage(); bytes32 previousAdminRole = s.adminRole[_role]; - /** - * Check if the caller is the current admin of the role. - */ - if (!s.hasRole[msg.sender][previousAdminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, previousAdminRole); - } + _requireRole(previousAdminRole); s.adminRole[_role] = _adminRole; emit RoleAdminChanged(_role, previousAdminRole, _adminRole); diff --git a/src/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol b/src/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol index 6183c03c..d6e0ce73 100644 --- a/src/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol +++ b/src/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol @@ -13,6 +13,19 @@ contract AccessControlGrantBatchFacet { */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + /** * @notice Emitted when a role is granted to an account. * @param _role The role that was granted. @@ -26,6 +39,16 @@ contract AccessControlGrantBatchFacet { */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice Storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -35,6 +58,22 @@ contract AccessControlGrantBatchFacet { mapping(bytes32 role => bytes32 adminRole) adminRole; } + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice Returns the storage for the AccessControl. * @return s The storage for the AccessControl. @@ -46,6 +85,54 @@ contract AccessControlGrantBatchFacet { } } + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getTemporalStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Grants a role to multiple accounts in a single transaction. * @param _role The role to grant. @@ -57,12 +144,7 @@ contract AccessControlGrantBatchFacet { AccessControlStorage storage s = getStorage(); bytes32 adminRole = s.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!s.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); uint256 length = _accounts.length; for (uint256 i = 0; i < length; i++) { diff --git a/src/access/AccessControl/Batch/Grant/AccessControlGrantBatchMod.sol b/src/access/AccessControl/Batch/Grant/AccessControlGrantBatchMod.sol index 23d2f14f..afabe94e 100644 --- a/src/access/AccessControl/Batch/Grant/AccessControlGrantBatchMod.sol +++ b/src/access/AccessControl/Batch/Grant/AccessControlGrantBatchMod.sol @@ -12,6 +12,19 @@ pragma solidity >=0.8.30; */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + /** * @notice Emitted when a role is granted to an account. * @param _role The role that was granted. @@ -25,6 +38,16 @@ event RoleGranted(bytes32 indexed _role, address indexed _account, address index */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -34,6 +57,22 @@ struct AccessControlStorage { mapping(bytes32 role => bytes32 adminRole) adminRole; } +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice Returns the storage for the AccessControl. * @return _s The storage for the AccessControl. @@ -45,6 +84,54 @@ function getStorage() pure returns (AccessControlStorage storage _s) { } } +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getTemporalStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ +function _requireRole(bytes32 _role) view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} + /** * @notice function to grant a role to multiple accounts in a single transaction. * @param _role The role to grant. @@ -56,9 +143,7 @@ function grantRoleBatch(bytes32 _role, address[] calldata _accounts) { AccessControlStorage storage s = getStorage(); bytes32 adminRole = s.adminRole[_role]; - if (!s.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); uint256 length = _accounts.length; for (uint256 i = 0; i < length; i++) { diff --git a/src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol b/src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol index 2f0cbfa1..e3a37d9f 100644 --- a/src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol +++ b/src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol @@ -13,6 +13,19 @@ contract AccessControlRevokeBatchFacet { */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + /** * @notice Emitted when a role is revoked from an account. * @param _role The role that was revoked. @@ -26,6 +39,16 @@ contract AccessControlRevokeBatchFacet { */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice Storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -35,6 +58,22 @@ contract AccessControlRevokeBatchFacet { mapping(bytes32 role => bytes32 adminRole) adminRole; } + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice Returns the storage for the AccessControl. * @return s The storage for the AccessControl. @@ -46,6 +85,54 @@ contract AccessControlRevokeBatchFacet { } } + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getTemporalStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Revokes a role from multiple accounts in a single transaction. * @param _role The role to revoke. @@ -57,12 +144,7 @@ contract AccessControlRevokeBatchFacet { AccessControlStorage storage s = getStorage(); bytes32 adminRole = s.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!s.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); uint256 length = _accounts.length; for (uint256 i = 0; i < length; i++) { diff --git a/src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchMod.sol b/src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchMod.sol index 2439ad06..208993f2 100644 --- a/src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchMod.sol +++ b/src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchMod.sol @@ -12,6 +12,19 @@ pragma solidity >=0.8.30; */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + /** * @notice Emitted when a role is revoked from an account. * @param _role The role that was revoked. @@ -25,6 +38,16 @@ event RoleRevoked(bytes32 indexed _role, address indexed _account, address index */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -34,6 +57,22 @@ struct AccessControlStorage { mapping(bytes32 role => bytes32 adminRole) adminRole; } +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice Returns the storage for the AccessControl. * @return _s The storage for the AccessControl. @@ -45,6 +84,54 @@ function getStorage() pure returns (AccessControlStorage storage _s) { } } +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getTemporalStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ +function _requireRole(bytes32 _role) view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} + /** * @notice function to revoke a role from multiple accounts in a single transaction. * @param _role The role to revoke. @@ -56,9 +143,7 @@ function revokeRoleBatch(bytes32 _role, address[] calldata _accounts) { AccessControlStorage storage s = getStorage(); bytes32 adminRole = s.adminRole[_role]; - if (!s.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); uint256 length = _accounts.length; for (uint256 i = 0; i < length; i++) { diff --git a/src/access/AccessControl/Data/AccessControlDataFacet.sol b/src/access/AccessControl/Data/AccessControlDataFacet.sol index 9ecb108e..a021d4ac 100644 --- a/src/access/AccessControl/Data/AccessControlDataFacet.sol +++ b/src/access/AccessControl/Data/AccessControlDataFacet.sol @@ -13,11 +13,34 @@ contract AccessControlDataFacet { */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + /** * @notice Storage slot identifier. */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -27,6 +50,22 @@ contract AccessControlDataFacet { mapping(bytes32 role => bytes32 adminRole) adminRole; } + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice Returns the storage for the AccessControl. * @return s The storage for the AccessControl. @@ -38,6 +77,28 @@ contract AccessControlDataFacet { } } + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getTemporalStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + /** * @notice Returns if an account has a role. * @param _role The role to check. @@ -50,16 +111,29 @@ contract AccessControlDataFacet { } /** - * @notice Checks if an account has a required role. + * @notice Checks if an account has a required role that has not expired and is not paused. * @param _role The role to check. * @param _account The account to check the role for. * @custom:error AccessControlUnauthorizedAccount If the account does not have the role. + * @custom:error AccessControlRoleExpired If the account's role has expired. + * @custom:error AccessControlRolePaused If the role is paused. */ function requireRole(bytes32 _role, address _account) external view { AccessControlStorage storage s = getStorage(); if (!s.hasRole[_account][_role]) { revert AccessControlUnauthorizedAccount(_account, _role); } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[_account][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, _account); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } } /** diff --git a/src/access/AccessControl/Data/AccessControlDataMod.sol b/src/access/AccessControl/Data/AccessControlDataMod.sol index 1c315e9d..262bf06a 100644 --- a/src/access/AccessControl/Data/AccessControlDataMod.sol +++ b/src/access/AccessControl/Data/AccessControlDataMod.sol @@ -12,6 +12,19 @@ pragma solidity >=0.8.30; */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + /* * @notice Storage slot identifier. */ @@ -22,6 +35,16 @@ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); */ bytes32 constant DEFAULT_ADMIN_ROLE = 0x00; +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -31,6 +54,22 @@ struct AccessControlStorage { mapping(bytes32 role => bytes32 adminRole) adminRole; } +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice Returns the storage for the AccessControl. * @return _s The storage for the AccessControl. @@ -43,25 +82,70 @@ function getStorage() pure returns (AccessControlStorage storage _s) { } /** - * @notice function to check if an account has a required role. + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getTemporalStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice function to check if an account has a role. + * @param _role The role to check. + * @param _account The account to check the role for. + * @return True if the account has the role, false otherwise. + */ +function hasRole(bytes32 _role, address _account) view returns (bool) { + AccessControlStorage storage s = getStorage(); + return s.hasRole[_account][_role]; +} + +/** + * @notice function to check if an account has a required role that has not expired and is not paused. * @param _role The role to assert. * @param _account The account to assert the role for. * @custom:error AccessControlUnauthorizedAccount If the account does not have the role. + * @custom:error AccessControlRoleExpired If the account's role has expired. + * @custom:error AccessControlRolePaused If the role is paused. */ function requireRole(bytes32 _role, address _account) view { AccessControlStorage storage s = getStorage(); if (!s.hasRole[_account][_role]) { revert AccessControlUnauthorizedAccount(_account, _role); } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[_account][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, _account); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } } /** - * @notice function to check if an account has a role. - * @param _role The role to check. - * @param _account The account to check the role for. - * @return True if the account has the role, false otherwise. + * @notice function to get the admin role for a role. + * @param _role The role to get the admin for. + * @return The admin role for the given role. */ -function hasRole(bytes32 _role, address _account) view returns (bool) { +function getRoleAdmin(bytes32 _role) view returns (bytes32) { AccessControlStorage storage s = getStorage(); - return s.hasRole[_account][_role]; + return s.adminRole[_role]; } diff --git a/src/access/AccessControl/Grant/AccessControlGrantFacet.sol b/src/access/AccessControl/Grant/AccessControlGrantFacet.sol index 0b0991af..471680da 100644 --- a/src/access/AccessControl/Grant/AccessControlGrantFacet.sol +++ b/src/access/AccessControl/Grant/AccessControlGrantFacet.sol @@ -13,6 +13,19 @@ contract AccessControlGrantFacet { */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + /** * @notice Emitted when a role is granted to an account. * @param _role The role that was granted. @@ -26,6 +39,16 @@ contract AccessControlGrantFacet { */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice Storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -35,6 +58,22 @@ contract AccessControlGrantFacet { mapping(bytes32 role => bytes32 adminRole) adminRole; } + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice Returns the storage for the AccessControl. * @return s The storage for the AccessControl. @@ -46,6 +85,54 @@ contract AccessControlGrantFacet { } } + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getTemporalStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Grants a role to an account. * @param _role The role to grant. @@ -57,12 +144,7 @@ contract AccessControlGrantFacet { AccessControlStorage storage s = getStorage(); bytes32 adminRole = s.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!s.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); bool _hasRole = s.hasRole[_account][_role]; if (!_hasRole) { diff --git a/src/access/AccessControl/Grant/AccessControlGrantMod.sol b/src/access/AccessControl/Grant/AccessControlGrantMod.sol index 3493bc76..d7b2a5d2 100644 --- a/src/access/AccessControl/Grant/AccessControlGrantMod.sol +++ b/src/access/AccessControl/Grant/AccessControlGrantMod.sol @@ -12,6 +12,19 @@ pragma solidity >=0.8.30; */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + /** * @notice Emitted when a role is granted to an account. * @param _role The role that was granted. @@ -25,6 +38,16 @@ event RoleGranted(bytes32 indexed _role, address indexed _account, address index */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -34,6 +57,22 @@ struct AccessControlStorage { mapping(bytes32 role => bytes32 adminRole) adminRole; } +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice Returns the storage for the AccessControl. * @return _s The storage for the AccessControl. @@ -45,6 +84,54 @@ function getStorage() pure returns (AccessControlStorage storage _s) { } } +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getTemporalStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ +function _requireRole(bytes32 _role) view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} + /** * @notice function to grant a role to an account. * @param _role The role to grant. @@ -56,9 +143,7 @@ function grantRole(bytes32 _role, address _account) returns (bool) { AccessControlStorage storage s = getStorage(); bytes32 adminRole = s.adminRole[_role]; - if (!s.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); bool _hasRole = s.hasRole[_account][_role]; if (!_hasRole) { diff --git a/src/access/AccessControl/Pausable/AccessControlPausableFacet.sol b/src/access/AccessControl/Pausable/AccessControlPausableFacet.sol index 956b21f2..9d001f16 100644 --- a/src/access/AccessControl/Pausable/AccessControlPausableFacet.sol +++ b/src/access/AccessControl/Pausable/AccessControlPausableFacet.sol @@ -33,11 +33,28 @@ contract AccessControlPausableFacet { */ error AccessControlRolePaused(bytes32 _role); + /** + * Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * Thrown when attempting to pause the DEFAULT_ADMIN_ROLE. + */ + error AccessControlDefaultAdminRolePaused(); + /** * @notice Storage slot identifier for AccessControl (reused to access roles). */ bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + /** * @notice Storage struct for AccessControl (reused struct definition). * @dev Must match the struct definition in AccessControlDataFacet / AccessControlDataMod. @@ -48,6 +65,14 @@ contract AccessControlPausableFacet { mapping(bytes32 role => bytes32 adminRole) adminRole; } + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + /** * @notice Storage slot identifier for Pausable functionality. */ @@ -72,6 +97,17 @@ contract AccessControlPausableFacet { } } + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getTemporalStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + /** * @notice Returns the storage for AccessControlPausable. * @return s The AccessControlPausable storage struct. @@ -92,6 +128,32 @@ contract AccessControlPausableFacet { return getStorage().pausedRoles[_role]; } + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getAccessControlStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Temporarily disables a role, preventing all accounts from using it. * @param _role The role to pause. @@ -100,20 +162,14 @@ contract AccessControlPausableFacet { * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. */ function pauseRole(bytes32 _role) external { - AccessControlStorage storage acs = getAccessControlStorage(); - AccessControlPausableStorage storage s = getStorage(); + if (_role == 0x00) { + revert AccessControlDefaultAdminRolePaused(); + } - /** - * Get the admin role for this role - */ - bytes32 adminRole = acs.adminRole[_role]; + AccessControlPausableStorage storage s = getStorage(); + bytes32 adminRole = getAccessControlStorage().adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!acs.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); /** * Pause the role @@ -130,20 +186,10 @@ contract AccessControlPausableFacet { * @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. */ function unpauseRole(bytes32 _role) external { - AccessControlStorage storage acs = getAccessControlStorage(); AccessControlPausableStorage storage s = getStorage(); + bytes32 adminRole = getAccessControlStorage().adminRole[_role]; - /** - * Get the admin role for this role - */ - bytes32 adminRole = acs.adminRole[_role]; - - /** - * Check if the caller is the admin of the role. - */ - if (!acs.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); /** * Unpause the role diff --git a/src/access/AccessControl/Pausable/AccessControlPausableMod.sol b/src/access/AccessControl/Pausable/AccessControlPausableMod.sol index 53575e14..c3f51379 100644 --- a/src/access/AccessControl/Pausable/AccessControlPausableMod.sol +++ b/src/access/AccessControl/Pausable/AccessControlPausableMod.sol @@ -32,11 +32,28 @@ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); */ error AccessControlRolePaused(bytes32 _role); +/** + * Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * Thrown when attempting to pause the DEFAULT_ADMIN_ROLE. + */ +error AccessControlDefaultAdminRolePaused(); + /* * @notice Storage slot identifier for AccessControl (reused to access roles). */ bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("compose.accesscontrol"); +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + /** * @notice Storage struct for AccessControl (reused struct definition). * @dev Must match the struct definition in AccessControlDataFacet / AccessControlDataMod. @@ -47,6 +64,14 @@ struct AccessControlStorage { mapping(bytes32 role => bytes32 adminRole) adminRole; } +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + /* * @notice Storage slot identifier for Pausable functionality. */ @@ -71,6 +96,17 @@ function getAccessControlStorage() pure returns (AccessControlStorage storage s) } } +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getTemporalStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + /** * @notice Returns the storage for AccessControlPausable. * @return s The AccessControlPausable storage struct. @@ -97,6 +133,10 @@ function isRolePaused(bytes32 _role) view returns (bool) { * @param _role The role to pause. */ function pauseRole(bytes32 _role) { + if (_role == 0x00) { + revert AccessControlDefaultAdminRolePaused(); + } + AccessControlPausableStorage storage s = getStorage(); s.pausedRoles[_role] = true; emit RolePaused(_role, msg.sender); diff --git a/src/access/AccessControl/Revoke/AccessControlRevokeFacet.sol b/src/access/AccessControl/Revoke/AccessControlRevokeFacet.sol index 7b5bbf43..95236e85 100644 --- a/src/access/AccessControl/Revoke/AccessControlRevokeFacet.sol +++ b/src/access/AccessControl/Revoke/AccessControlRevokeFacet.sol @@ -13,6 +13,19 @@ contract AccessControlRevokeFacet { */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + /** * @notice Emitted when a role is revoked from an account. * @param _role The role that was revoked. @@ -26,6 +39,16 @@ contract AccessControlRevokeFacet { */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice Storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -35,6 +58,22 @@ contract AccessControlRevokeFacet { mapping(bytes32 role => bytes32 adminRole) adminRole; } + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice Returns the storage for the AccessControl. * @return s The storage for the AccessControl. @@ -46,6 +85,54 @@ contract AccessControlRevokeFacet { } } + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getTemporalStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Revokes a role from an account. * @param _role The role to revoke. @@ -57,12 +144,7 @@ contract AccessControlRevokeFacet { AccessControlStorage storage s = getStorage(); bytes32 adminRole = s.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!s.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); bool _hasRole = s.hasRole[_account][_role]; if (_hasRole) { diff --git a/src/access/AccessControl/Revoke/AccessControlRevokeMod.sol b/src/access/AccessControl/Revoke/AccessControlRevokeMod.sol index 842db628..1d672c65 100644 --- a/src/access/AccessControl/Revoke/AccessControlRevokeMod.sol +++ b/src/access/AccessControl/Revoke/AccessControlRevokeMod.sol @@ -12,6 +12,19 @@ pragma solidity >=0.8.30; */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + /** * @notice Emitted when a role is revoked from an account. * @param _role The role that was revoked. @@ -25,6 +38,16 @@ event RoleRevoked(bytes32 indexed _role, address indexed _account, address index */ bytes32 constant STORAGE_POSITION = keccak256("compose.accesscontrol"); +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -34,6 +57,22 @@ struct AccessControlStorage { mapping(bytes32 role => bytes32 adminRole) adminRole; } +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice Returns the storage for the AccessControl. * @return _s The storage for the AccessControl. @@ -45,6 +84,54 @@ function getStorage() pure returns (AccessControlStorage storage _s) { } } +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getTemporalStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ +function _requireRole(bytes32 _role) view { + AccessControlStorage storage s = getStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} + /** * @notice function to revoke a role from an account. * @param _role The role to revoke. @@ -56,12 +143,7 @@ function revokeRole(bytes32 _role, address _account) returns (bool) { AccessControlStorage storage s = getStorage(); bytes32 adminRole = s.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!s.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); bool _hasRole = s.hasRole[_account][_role]; if (_hasRole) { diff --git a/src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol b/src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol index c8615746..215bb43c 100644 --- a/src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol +++ b/src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol @@ -31,6 +31,12 @@ contract AccessControlTemporalGrantFacet { */ error AccessControlRoleExpired(bytes32 _role, address _account); + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + /** * @notice Storage slot identifier for AccessControl (reused to access roles). */ @@ -59,6 +65,19 @@ contract AccessControlTemporalGrantFacet { mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; } + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice Returns the storage for AccessControl. * @return s The AccessControl storage struct. @@ -81,6 +100,43 @@ contract AccessControlTemporalGrantFacet { } } + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getAccessControlStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Grants a role to an account with an expiry timestamp. * @param _role The role to grant. @@ -95,12 +151,7 @@ contract AccessControlTemporalGrantFacet { AccessControlTemporalStorage storage s = getStorage(); bytes32 adminRole = acs.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!acs.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); /** * Require expiry is in the future diff --git a/src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantMod.sol b/src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantMod.sol index 71dca03d..61b717e9 100644 --- a/src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantMod.sol +++ b/src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantMod.sol @@ -30,6 +30,12 @@ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); */ error AccessControlRoleExpired(bytes32 _role, address _account); +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + /* * @notice Storage slot identifier for AccessControl (reused to access roles). */ @@ -58,6 +64,19 @@ struct AccessControlTemporalStorage { mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; } +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice Returns the storage for AccessControl. * @return s The AccessControl storage struct. @@ -80,6 +99,43 @@ function getStorage() pure returns (AccessControlTemporalStorage storage s) { } } +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ +function _requireRole(bytes32 _role) view { + AccessControlStorage storage s = getAccessControlStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} + /** * @notice Grants a role to an account with an expiry timestamp. * @param _role The role to grant. @@ -94,12 +150,7 @@ function grantRoleWithExpiry(bytes32 _role, address _account, uint256 _expiresAt AccessControlTemporalStorage storage s = getStorage(); bytes32 adminRole = acs.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!acs.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); /** * Require expiry is in the future diff --git a/src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol b/src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol index e8395980..6917755a 100644 --- a/src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol +++ b/src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol @@ -21,6 +21,19 @@ contract AccessControlTemporalRevokeFacet { */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + /** * @notice Storage slot identifier for AccessControl (reused to access roles). */ @@ -49,6 +62,19 @@ contract AccessControlTemporalRevokeFacet { mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; } + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice Returns the storage for AccessControl. * @return s The AccessControl storage struct. @@ -71,6 +97,43 @@ contract AccessControlTemporalRevokeFacet { } } + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getAccessControlStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Revokes a temporal role from an account. * @param _role The role to revoke. @@ -84,12 +147,7 @@ contract AccessControlTemporalRevokeFacet { AccessControlTemporalStorage storage s = getStorage(); bytes32 adminRole = acs.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!acs.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); /** * Revoke the role diff --git a/src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeMod.sol b/src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeMod.sol index 0ee20098..41fa9526 100644 --- a/src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeMod.sol +++ b/src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeMod.sol @@ -20,6 +20,19 @@ event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, addre */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + /* * @notice Storage slot identifier for AccessControl (reused to access roles). */ @@ -48,6 +61,19 @@ struct AccessControlTemporalStorage { mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; } +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice Returns the storage for AccessControl. * @return s The AccessControl storage struct. @@ -70,6 +96,44 @@ function getStorage() pure returns (AccessControlTemporalStorage storage s) { } } +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ +function _requireRole(bytes32 _role) view { + AccessControlStorage storage s = getAccessControlStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} + /** * @notice Revokes a temporal role from an account. * @param _role The role to revoke. @@ -83,12 +147,7 @@ function revokeTemporalRole(bytes32 _role, address _account) { AccessControlTemporalStorage storage s = getStorage(); bytes32 adminRole = acs.adminRole[_role]; - /** - * Check if the caller is the admin of the role. - */ - if (!acs.hasRole[msg.sender][adminRole]) { - revert AccessControlUnauthorizedAccount(msg.sender, adminRole); - } + _requireRole(adminRole); /** * Revoke the role diff --git a/src/access/Owner/Renounce/OwnerRenounceFacet.sol b/src/access/Owner/Renounce/OwnerRenounceFacet.sol index 6f9efcf6..8d2ecf96 100644 --- a/src/access/Owner/Renounce/OwnerRenounceFacet.sol +++ b/src/access/Owner/Renounce/OwnerRenounceFacet.sol @@ -46,9 +46,11 @@ contract OwnerRenounceFacet { */ function renounceOwnership() external { OwnerStorage storage s = getStorage(); + if (msg.sender != s.owner) { revert OwnerUnauthorizedAccount(); } + address previousOwner = s.owner; s.owner = address(0); emit OwnershipTransferred(previousOwner, address(0)); diff --git a/src/access/Owner/Renounce/OwnerRenounceMod.sol b/src/access/Owner/Renounce/OwnerRenounceMod.sol index 9efbb822..8741160a 100644 --- a/src/access/Owner/Renounce/OwnerRenounceMod.sol +++ b/src/access/Owner/Renounce/OwnerRenounceMod.sol @@ -47,9 +47,11 @@ function getStorage() pure returns (OwnerStorage storage s) { */ function renounceOwnership() { OwnerStorage storage s = getStorage(); + if (msg.sender != s.owner) { revert OwnerUnauthorizedAccount(); } + address previousOwner = s.owner; s.owner = address(0); emit OwnershipTransferred(previousOwner, address(0)); diff --git a/src/access/Owner/Transfer/OwnerTransferFacet.sol b/src/access/Owner/Transfer/OwnerTransferFacet.sol index 7e835a58..f84029d2 100644 --- a/src/access/Owner/Transfer/OwnerTransferFacet.sol +++ b/src/access/Owner/Transfer/OwnerTransferFacet.sol @@ -47,9 +47,11 @@ contract OwnerTransferFacet { */ function transferOwnership(address _newOwner) external { OwnerStorage storage s = getStorage(); + if (msg.sender != s.owner) { revert OwnerUnauthorizedAccount(); } + address previousOwner = s.owner; s.owner = _newOwner; emit OwnershipTransferred(previousOwner, _newOwner); diff --git a/src/access/Owner/Transfer/OwnerTransferMod.sol b/src/access/Owner/Transfer/OwnerTransferMod.sol index dbaae6da..42b2fd4d 100644 --- a/src/access/Owner/Transfer/OwnerTransferMod.sol +++ b/src/access/Owner/Transfer/OwnerTransferMod.sol @@ -48,9 +48,11 @@ function getStorage() pure returns (OwnerStorage storage s) { */ function transferOwnership(address _newOwner) { OwnerStorage storage s = getStorage(); + if (msg.sender != s.owner) { revert OwnerUnauthorizedAccount(); } + address previousOwner = s.owner; s.owner = _newOwner; emit OwnershipTransferred(previousOwner, _newOwner); diff --git a/src/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceFacet.sol b/src/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceFacet.sol index e9f5fc64..a852c70a 100644 --- a/src/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceFacet.sol +++ b/src/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceFacet.sol @@ -68,9 +68,11 @@ contract OwnerTwoStepRenounceFacet { function renounceOwnership() external { OwnerStorage storage ownerStorage = getOwnerStorage(); PendingOwnerStorage storage pendingStorage = getPendingOwnerStorage(); + if (msg.sender != ownerStorage.owner) { revert OwnerUnauthorizedAccount(); } + address previousOwner = ownerStorage.owner; ownerStorage.owner = address(0); pendingStorage.pendingOwner = address(0); diff --git a/src/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceMod.sol b/src/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceMod.sol index 369302ac..245f883c 100644 --- a/src/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceMod.sol +++ b/src/access/Owner/TwoSteps/Renounce/OwnerTwoStepRenounceMod.sol @@ -70,9 +70,11 @@ function getPendingOwnerStorage() pure returns (PendingOwnerStorage storage s) { function renounceOwnership() { OwnerStorage storage ownerStorage = getOwnerStorage(); PendingOwnerStorage storage pendingStorage = getPendingOwnerStorage(); + if (msg.sender != ownerStorage.owner) { revert OwnerUnauthorizedAccount(); } + address previousOwner = ownerStorage.owner; ownerStorage.owner = address(0); pendingStorage.pendingOwner = address(0); diff --git a/src/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferFacet.sol b/src/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferFacet.sol index b80f5126..6504e65e 100644 --- a/src/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferFacet.sol +++ b/src/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferFacet.sol @@ -72,9 +72,11 @@ contract OwnerTwoStepTransferFacet { */ function transferOwnership(address _newOwner) external { OwnerStorage storage ownerStorage = getOwnerStorage(); + if (msg.sender != ownerStorage.owner) { revert OwnerUnauthorizedAccount(); } + getPendingOwnerStorage().pendingOwner = _newOwner; emit OwnershipTransferStarted(ownerStorage.owner, _newOwner); } @@ -86,9 +88,11 @@ contract OwnerTwoStepTransferFacet { function acceptOwnership() external { OwnerStorage storage ownerStorage = getOwnerStorage(); PendingOwnerStorage storage pendingStorage = getPendingOwnerStorage(); + if (msg.sender != pendingStorage.pendingOwner) { revert OwnerUnauthorizedAccount(); } + address previousOwner = ownerStorage.owner; ownerStorage.owner = pendingStorage.pendingOwner; pendingStorage.pendingOwner = address(0); diff --git a/src/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferMod.sol b/src/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferMod.sol index b4c45998..6e8c351e 100644 --- a/src/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferMod.sol +++ b/src/access/Owner/TwoSteps/Transfer/OwnerTwoStepTransferMod.sol @@ -73,9 +73,11 @@ function getPendingOwnerStorage() pure returns (PendingOwnerStorage storage s) { */ function transferOwnership(address _newOwner) { OwnerStorage storage ownerStorage = getOwnerStorage(); + if (msg.sender != ownerStorage.owner) { revert OwnerUnauthorizedAccount(); } + getPendingOwnerStorage().pendingOwner = _newOwner; emit OwnershipTransferStarted(ownerStorage.owner, _newOwner); } @@ -87,9 +89,11 @@ function transferOwnership(address _newOwner) { function acceptOwnership() { OwnerStorage storage ownerStorage = getOwnerStorage(); PendingOwnerStorage storage pendingStorage = getPendingOwnerStorage(); + if (msg.sender != pendingStorage.pendingOwner) { revert OwnerUnauthorizedAccount(); } + address previousOwner = ownerStorage.owner; ownerStorage.owner = pendingStorage.pendingOwner; pendingStorage.pendingOwner = address(0); diff --git a/src/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol b/src/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol index 634113e1..6e7d3b6c 100644 --- a/src/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol +++ b/src/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol @@ -44,6 +44,19 @@ contract ERC20BridgeableFacet { */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); + /** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ + error AccessControlRoleExpired(bytes32 _role, address _account); + + /** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ + error AccessControlRolePaused(bytes32 _role); + error ERC20InsufficientBalance(address _from, uint256 _accountBalance, uint256 _value); /** @@ -114,6 +127,16 @@ contract ERC20BridgeableFacet { */ bytes32 constant ACCESS_STORAGE_POSITION = keccak256("compose.accesscontrol"); + /** + * @notice Storage slot identifier for Temporal functionality. + */ + bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + + /** + * @notice Storage slot identifier for Pausable functionality. + */ + bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -122,6 +145,22 @@ contract ERC20BridgeableFacet { mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; } + /** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ + struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; + } + + /** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ + struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; + } + /** * @notice helper to return AccessControlStorage at its diamond slot */ @@ -132,6 +171,54 @@ contract ERC20BridgeableFacet { } } + /** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ + function getTemporalStorage() internal pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ + function getPausableStorage() internal pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ + function _requireRole(bytes32 _role) internal view { + AccessControlStorage storage s = getAccessControlStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } + } + /** * @notice Cross-chain mint — callable only by an address having the `trusted-bridge` role. * @param _account The account to mint tokens to. @@ -140,14 +227,7 @@ contract ERC20BridgeableFacet { function crosschainMint(address _account, uint256 _value) external { ERC20Storage storage erc20Storage = getERC20Storage(); - AccessControlStorage storage acs = getAccessControlStorage(); - - /** - * authorize: caller must have the trusted-bridge role - */ - if (!acs.hasRole[msg.sender]["trusted-bridge"]) { - revert AccessControlUnauthorizedAccount(msg.sender, "trusted-bridge"); - } + _requireRole("trusted-bridge"); if (_account == address(0)) { revert ERC20InvalidReceiver(address(0)); @@ -169,14 +249,8 @@ contract ERC20BridgeableFacet { function crosschainBurn(address _from, uint256 _value) external { ERC20Storage storage erc20Storage = getERC20Storage(); - AccessControlStorage storage acs = getAccessControlStorage(); + _requireRole("trusted-bridge"); - /** - * authorize: caller must have the trusted-bridge role - */ - if (!acs.hasRole[msg.sender]["trusted-bridge"]) { - revert AccessControlUnauthorizedAccount(msg.sender, "trusted-bridge"); - } if (_from == address(0)) { revert ERC20InvalidReceiver(address(0)); } @@ -211,6 +285,23 @@ contract ERC20BridgeableFacet { if (!acs.hasRole[_caller]["trusted-bridge"]) { revert ERC20InvalidBridgeAccount(_caller); } + + /** + * Check if the caller role is expired + */ + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 _expiry = ts.roleExpiry[_caller]["trusted-bridge"]; + if (_expiry > 0 && block.timestamp >= _expiry) { + revert AccessControlRoleExpired("trusted-bridge", _caller); + } + + /** + * Check if the caller role is paused + */ + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles["trusted-bridge"]) { + revert AccessControlRolePaused("trusted-bridge"); + } } /** diff --git a/src/token/ERC20/Bridgeable/ERC20BridgeableMod.sol b/src/token/ERC20/Bridgeable/ERC20BridgeableMod.sol index 2ebf36ba..32b56c21 100644 --- a/src/token/ERC20/Bridgeable/ERC20BridgeableMod.sol +++ b/src/token/ERC20/Bridgeable/ERC20BridgeableMod.sol @@ -44,6 +44,19 @@ error ERC20InvalidCallerAddress(address _caller); */ error AccessControlUnauthorizedAccount(address _account, bytes32 _role); +/** + * @notice Thrown when a role has expired. + * @param _role The role that has expired. + * @param _account The account whose role has expired. + */ +error AccessControlRoleExpired(bytes32 _role, address _account); + +/** + * @notice Thrown when a role is paused and an operation requiring that role is attempted. + * @param _role The role that is paused. + */ +error AccessControlRolePaused(bytes32 _role); + error ERC20InsufficientBalance(address _from, uint256 _accountBalance, uint256 _value); /** @@ -114,6 +127,16 @@ function getERC20Storage() pure returns (ERC20Storage storage s) { */ bytes32 constant ACCESS_STORAGE_POSITION = keccak256("compose.accesscontrol"); +/* + * @notice Storage slot identifier for Temporal functionality. + */ +bytes32 constant TEMPORAL_STORAGE_POSITION = keccak256("compose.accesscontrol.temporal"); + +/* + * @notice Storage slot identifier for Pausable functionality. + */ +bytes32 constant PAUSABLE_STORAGE_POSITION = keccak256("compose.accesscontrol.pausable"); + /** * @notice storage struct for the AccessControl. * @custom:storage-location erc8042:compose.accesscontrol @@ -122,6 +145,22 @@ struct AccessControlStorage { mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; } +/** + * @notice Storage struct for AccessControlTemporal. + * @custom:storage-location erc8042:compose.accesscontrol.temporal + */ +struct AccessControlTemporalStorage { + mapping(address account => mapping(bytes32 role => uint256 expiryTimestamp)) roleExpiry; +} + +/** + * @notice Storage struct for AccessControlPausable. + * @custom:storage-location erc8042:compose.accesscontrol.pausable + */ +struct AccessControlPausableStorage { + mapping(bytes32 role => bool paused) pausedRoles; +} + /** * @notice helper to return AccessControlStorage at its diamond slot */ @@ -132,6 +171,54 @@ function getAccessControlStorage() pure returns (AccessControlStorage storage s) } } +/** + * @notice Returns the storage for AccessControlTemporal. + * @return s The AccessControlTemporal storage struct. + */ +function getTemporalStorage() pure returns (AccessControlTemporalStorage storage s) { + bytes32 position = TEMPORAL_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Returns the storage for AccessControlPausable. + * @return s The AccessControlPausable storage struct. + */ +function getPausableStorage() pure returns (AccessControlPausableStorage storage s) { + bytes32 position = PAUSABLE_STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Requires the caller to have a specific role that has not expired and is not paused. + * @param _role The role that the caller must have. + * @dev Reverts with {AccessControlUnauthorizedAccount} if the caller does not have the role. + * @dev Reverts with {AccessControlRoleExpired} if the caller's role has expired. + * @dev Reverts with {AccessControlRolePaused} if the role is paused. + */ +function _requireRole(bytes32 _role) view { + AccessControlStorage storage s = getAccessControlStorage(); + + if (!s.hasRole[msg.sender][_role]) { + revert AccessControlUnauthorizedAccount(msg.sender, _role); + } + + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 expiry = ts.roleExpiry[msg.sender][_role]; + if (expiry > 0 && block.timestamp >= expiry) { + revert AccessControlRoleExpired(_role, msg.sender); + } + + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles[_role]) { + revert AccessControlRolePaused(_role); + } +} + /** * @notice Cross-chain mint — callable only by an address having the `trusted-bridge` role. * @param _account The account to mint tokens to. @@ -140,14 +227,7 @@ function getAccessControlStorage() pure returns (AccessControlStorage storage s) function crosschainMint(address _account, uint256 _value) { ERC20Storage storage erc20Storage = getERC20Storage(); - AccessControlStorage storage acs = getAccessControlStorage(); - - /** - * authorize: caller must have the trusted-bridge role - */ - if (!acs.hasRole[msg.sender]["trusted-bridge"]) { - revert AccessControlUnauthorizedAccount(msg.sender, "trusted-bridge"); - } + _requireRole("trusted-bridge"); if (_account == address(0)) { revert ERC20InvalidReceiver(address(0)); @@ -170,14 +250,7 @@ function crosschainMint(address _account, uint256 _value) { function crosschainBurn(address _from, uint256 _value) { ERC20Storage storage erc20Storage = getERC20Storage(); - AccessControlStorage storage acs = getAccessControlStorage(); - - /** - * authorize: caller must have the trusted-bridge role - */ - if (!acs.hasRole[msg.sender]["trusted-bridge"]) { - revert AccessControlUnauthorizedAccount(msg.sender, "trusted-bridge"); - } + _requireRole("trusted-bridge"); if (_from == address(0)) { revert ERC20InvalidReceiver(address(0)); @@ -212,4 +285,21 @@ function checkTokenBridge(address _caller) view { if (!acs.hasRole[_caller]["trusted-bridge"]) { revert ERC20InvalidBridgeAccount(_caller); } + + /** + * Check if the caller role is expired + */ + AccessControlTemporalStorage storage ts = getTemporalStorage(); + uint256 _expiry = ts.roleExpiry[_caller]["trusted-bridge"]; + if (_expiry > 0 && block.timestamp >= _expiry) { + revert AccessControlRoleExpired("trusted-bridge", _caller); + } + + /** + * Check if the caller role is paused + */ + AccessControlPausableStorage storage ps = getPausableStorage(); + if (ps.pausedRoles["trusted-bridge"]) { + revert AccessControlRolePaused("trusted-bridge"); + } } diff --git a/src/token/ERC20/Permit/ERC20PermitFacet.sol b/src/token/ERC20/Permit/ERC20PermitFacet.sol index cba8b74d..0ce8f449 100644 --- a/src/token/ERC20/Permit/ERC20PermitFacet.sol +++ b/src/token/ERC20/Permit/ERC20PermitFacet.sol @@ -139,6 +139,7 @@ contract ERC20PermitFacet { if (_spender == address(0)) { revert ERC20InvalidSpender(address(0)); } + if (block.timestamp > _deadline) { revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); } diff --git a/src/token/ERC20/Permit/ERC20PermitMod.sol b/src/token/ERC20/Permit/ERC20PermitMod.sol index 8f7a633c..4a720111 100644 --- a/src/token/ERC20/Permit/ERC20PermitMod.sol +++ b/src/token/ERC20/Permit/ERC20PermitMod.sol @@ -62,6 +62,7 @@ bytes32 constant ERC20_STORAGE_POSITION = keccak256("erc20"); */ struct ERC20Storage { mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; mapping(address owner => mapping(address spender => uint256 allowance)) allowance; } @@ -123,6 +124,7 @@ function permit(address _owner, address _spender, uint256 _value, uint256 _deadl if (_spender == address(0)) { revert ERC20InvalidSpender(address(0)); } + if (block.timestamp > _deadline) { revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); } diff --git a/test/unit/access/AccessControl/Admin/facet/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Admin/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..206edc30 --- /dev/null +++ b/test/unit/access/AccessControl/Admin/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {AccessControlAdmin_Base_Test} from "test/unit/access/AccessControl/Admin/AccessControlAdminBase.t.sol"; +import {AccessControlAdminFacet} from "src/access/AccessControl/Admin/AccessControlAdminFacet.sol"; + +contract TemporalExpiry_AccessControlAdminFacet_Test is AccessControlAdmin_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlAdminFacet internal adminFacet; + + function setUp() public override { + super.setUp(); + adminFacet = new AccessControlAdminFacet(); + vm.label(address(adminFacet), "AccessControlAdminFacet"); + seedDefaultAdmin(address(adminFacet)); + } + + function test_ShouldRevert_SetRoleAdmin_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + bytes32 newAdminRole = keccak256("NEW_ADMIN_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(adminFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(adminFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlAdminFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + adminFacet.setRoleAdmin(role, newAdminRole); + } + + function test_ShouldSucceed_SetRoleAdmin_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + bytes32 newAdminRole = keccak256("NEW_ADMIN_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(adminFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(adminFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + adminFacet.setRoleAdmin(role, newAdminRole); + + assertEq(address(adminFacet).adminRole(role), newAdminRole); + } +} diff --git a/test/unit/access/AccessControl/Admin/mod/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Admin/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..9956fdad --- /dev/null +++ b/test/unit/access/AccessControl/Admin/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {AccessControlAdmin_Base_Test} from "test/unit/access/AccessControl/Admin/AccessControlAdminBase.t.sol"; +import { + AccessControlCombinedModHarness +} from "test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol"; + +contract TemporalExpiry_AccessControlAdminMod_Test is AccessControlAdmin_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlCombinedModHarness internal harness; + + function setUp() public override { + super.setUp(); + harness = new AccessControlCombinedModHarness(); + vm.label(address(harness), "AccessControlCombinedModHarness"); + seedDefaultAdmin(address(harness)); + } + + function test_ShouldRevert_SetRoleAdmin_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + bytes32 newAdminRole = keccak256("NEW_ADMIN_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", DEFAULT_ADMIN_ROLE, users.admin) + ); + vm.prank(users.admin); + harness.setRoleAdmin(role, newAdminRole); + } + + function test_ShouldSucceed_SetRoleAdmin_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + bytes32 newAdminRole = keccak256("NEW_ADMIN_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + harness.setRoleAdmin(role, newAdminRole); + + assertEq(address(harness).adminRole(role), newAdminRole); + } +} diff --git a/test/unit/access/AccessControl/Batch/Grant/facet/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Batch/Grant/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..2998d3ff --- /dev/null +++ b/test/unit/access/AccessControl/Batch/Grant/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import { + AccessControlGrantBatch_Base_Test +} from "test/unit/access/AccessControl/Batch/Grant/AccessControlGrantBatchBase.t.sol"; +import {AccessControlGrantBatchFacet} from "src/access/AccessControl/Batch/Grant/AccessControlGrantBatchFacet.sol"; + +contract TemporalExpiry_AccessControlGrantBatchFacet_Test is AccessControlGrantBatch_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlGrantBatchFacet internal grantBatchFacet; + + function setUp() public override { + super.setUp(); + grantBatchFacet = new AccessControlGrantBatchFacet(); + vm.label(address(grantBatchFacet), "AccessControlGrantBatchFacet"); + seedDefaultAdmin(address(grantBatchFacet)); + } + + function test_ShouldRevert_GrantRoleBatch_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + address[] memory accounts = new address[](2); + accounts[0] = users.alice; + accounts[1] = users.bob; + uint256 expiry = block.timestamp + 1 hours; + + address(grantBatchFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(grantBatchFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlGrantBatchFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + grantBatchFacet.grantRoleBatch(role, accounts); + } + + function test_ShouldSucceed_GrantRoleBatch_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + address[] memory accounts = new address[](2); + accounts[0] = users.alice; + accounts[1] = users.bob; + uint256 expiry = block.timestamp + 1 hours; + + address(grantBatchFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(grantBatchFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + grantBatchFacet.grantRoleBatch(role, accounts); + + assertTrue(address(grantBatchFacet).hasRole(users.alice, role)); + assertTrue(address(grantBatchFacet).hasRole(users.bob, role)); + } +} diff --git a/test/unit/access/AccessControl/Batch/Grant/mod/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Batch/Grant/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..529df5c3 --- /dev/null +++ b/test/unit/access/AccessControl/Batch/Grant/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import { + AccessControlGrantBatch_Base_Test +} from "test/unit/access/AccessControl/Batch/Grant/AccessControlGrantBatchBase.t.sol"; +import { + AccessControlCombinedModHarness +} from "test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol"; + +contract TemporalExpiry_AccessControlGrantBatchMod_Test is AccessControlGrantBatch_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlCombinedModHarness internal harness; + + function setUp() public override { + super.setUp(); + harness = new AccessControlCombinedModHarness(); + vm.label(address(harness), "AccessControlCombinedModHarness"); + seedDefaultAdmin(address(harness)); + } + + function test_ShouldRevert_GrantRoleBatch_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + address[] memory accounts = new address[](2); + accounts[0] = users.alice; + accounts[1] = users.bob; + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", DEFAULT_ADMIN_ROLE, users.admin) + ); + vm.prank(users.admin); + harness.grantRoleBatch(role, accounts); + } + + function test_ShouldSucceed_GrantRoleBatch_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + address[] memory accounts = new address[](2); + accounts[0] = users.alice; + accounts[1] = users.bob; + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + harness.grantRoleBatch(role, accounts); + + assertTrue(address(harness).hasRole(users.alice, role)); + assertTrue(address(harness).hasRole(users.bob, role)); + } +} diff --git a/test/unit/access/AccessControl/Batch/Revoke/facet/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Batch/Revoke/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..d273f10b --- /dev/null +++ b/test/unit/access/AccessControl/Batch/Revoke/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import { + AccessControlRevokeBatch_Base_Test +} from "test/unit/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchBase.t.sol"; +import {AccessControlRevokeBatchFacet} from "src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchFacet.sol"; + +contract TemporalExpiry_AccessControlRevokeBatchFacet_Test is AccessControlRevokeBatch_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlRevokeBatchFacet internal revokeBatchFacet; + + function setUp() public override { + super.setUp(); + revokeBatchFacet = new AccessControlRevokeBatchFacet(); + vm.label(address(revokeBatchFacet), "AccessControlRevokeBatchFacet"); + seedDefaultAdmin(address(revokeBatchFacet)); + } + + function test_ShouldRevert_RevokeRoleBatch_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + address[] memory accounts = new address[](2); + accounts[0] = users.alice; + accounts[1] = users.bob; + uint256 expiry = block.timestamp + 1 hours; + + seedRole(address(revokeBatchFacet), role, users.alice); + seedRole(address(revokeBatchFacet), role, users.bob); + + address(revokeBatchFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(revokeBatchFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlRevokeBatchFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + revokeBatchFacet.revokeRoleBatch(role, accounts); + } + + function test_ShouldSucceed_RevokeRoleBatch_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + address[] memory accounts = new address[](2); + accounts[0] = users.alice; + accounts[1] = users.bob; + uint256 expiry = block.timestamp + 1 hours; + + seedRole(address(revokeBatchFacet), role, users.alice); + seedRole(address(revokeBatchFacet), role, users.bob); + + address(revokeBatchFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(revokeBatchFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + revokeBatchFacet.revokeRoleBatch(role, accounts); + + assertFalse(address(revokeBatchFacet).hasRole(users.alice, role)); + assertFalse(address(revokeBatchFacet).hasRole(users.bob, role)); + } +} diff --git a/test/unit/access/AccessControl/Batch/Revoke/mod/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Batch/Revoke/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..72917ed4 --- /dev/null +++ b/test/unit/access/AccessControl/Batch/Revoke/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import { + AccessControlRevokeBatch_Base_Test +} from "test/unit/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchBase.t.sol"; +import { + AccessControlCombinedModHarness +} from "test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol"; + +contract TemporalExpiry_AccessControlRevokeBatchMod_Test is AccessControlRevokeBatch_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlCombinedModHarness internal harness; + + function setUp() public override { + super.setUp(); + harness = new AccessControlCombinedModHarness(); + vm.label(address(harness), "AccessControlCombinedModHarness"); + seedDefaultAdmin(address(harness)); + } + + function test_ShouldRevert_RevokeRoleBatch_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + address[] memory accounts = new address[](2); + accounts[0] = users.alice; + accounts[1] = users.bob; + uint256 expiry = block.timestamp + 1 hours; + + seedRole(address(harness), role, users.alice); + seedRole(address(harness), role, users.bob); + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", DEFAULT_ADMIN_ROLE, users.admin) + ); + vm.prank(users.admin); + harness.revokeRoleBatch(role, accounts); + } + + function test_ShouldSucceed_RevokeRoleBatch_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + address[] memory accounts = new address[](2); + accounts[0] = users.alice; + accounts[1] = users.bob; + uint256 expiry = block.timestamp + 1 hours; + + seedRole(address(harness), role, users.alice); + seedRole(address(harness), role, users.bob); + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + harness.revokeRoleBatch(role, accounts); + + assertFalse(address(harness).hasRole(users.alice, role)); + assertFalse(address(harness).hasRole(users.bob, role)); + } +} diff --git a/test/unit/access/AccessControl/Grant/facet/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Grant/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..ccac6767 --- /dev/null +++ b/test/unit/access/AccessControl/Grant/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {AccessControlGrant_Base_Test} from "test/unit/access/AccessControl/Grant/AccessControlGrantBase.t.sol"; +import {AccessControlGrantFacet} from "src/access/AccessControl/Grant/AccessControlGrantFacet.sol"; + +contract TemporalExpiry_AccessControlGrantFacet_Test is AccessControlGrant_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlGrantFacet internal grantFacet; + + function setUp() public override { + super.setUp(); + grantFacet = new AccessControlGrantFacet(); + vm.label(address(grantFacet), "AccessControlGrantFacet"); + seedDefaultAdmin(address(grantFacet)); + } + + function test_ShouldRevert_GrantRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(grantFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(grantFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlGrantFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + grantFacet.grantRole(role, users.alice); + } + + function test_ShouldSucceed_GrantRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(grantFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(grantFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + grantFacet.grantRole(role, users.alice); + + assertTrue(address(grantFacet).hasRole(users.alice, role)); + } + + function test_ShouldSucceed_GrantRole_WhenCallerHasPermanentAdminRole() external { + bytes32 role = keccak256("TEST_ROLE"); + + vm.prank(users.admin); + grantFacet.grantRole(role, users.alice); + + assertTrue(address(grantFacet).hasRole(users.alice, role)); + } +} diff --git a/test/unit/access/AccessControl/Grant/mod/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Grant/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..fdc7645e --- /dev/null +++ b/test/unit/access/AccessControl/Grant/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {AccessControlGrant_Base_Test} from "test/unit/access/AccessControl/Grant/AccessControlGrantBase.t.sol"; +import { + AccessControlCombinedModHarness +} from "test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol"; + +contract TemporalExpiry_AccessControlGrantMod_Test is AccessControlGrant_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlCombinedModHarness internal harness; + + function setUp() public override { + super.setUp(); + harness = new AccessControlCombinedModHarness(); + vm.label(address(harness), "AccessControlCombinedModHarness"); + seedDefaultAdmin(address(harness)); + } + + function test_ShouldRevert_GrantRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", DEFAULT_ADMIN_ROLE, users.admin) + ); + vm.prank(users.admin); + harness.grantRole(role, users.alice); + } + + function test_ShouldSucceed_GrantRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + harness.grantRole(role, users.alice); + + assertTrue(address(harness).hasRole(users.alice, role)); + } + + function test_ShouldSucceed_GrantRole_WhenCallerHasPermanentAdminRole() external { + bytes32 role = keccak256("TEST_ROLE"); + + vm.prank(users.admin); + harness.grantRole(role, users.alice); + + assertTrue(address(harness).hasRole(users.alice, role)); + } +} diff --git a/test/unit/access/AccessControl/Pausable/facet/fuzz/pausable.t.sol b/test/unit/access/AccessControl/Pausable/facet/fuzz/pausable.t.sol index bdbd75e4..7d164e8b 100644 --- a/test/unit/access/AccessControl/Pausable/facet/fuzz/pausable.t.sol +++ b/test/unit/access/AccessControl/Pausable/facet/fuzz/pausable.t.sol @@ -38,6 +38,8 @@ contract Pausable_AccessControlPausableFacet_Fuzz_Unit_Test is AccessControlPaus } function testFuzz_ShouldPauseRole_WhenCallerHasAdminRole(bytes32 role) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); + vm.expectEmit(address(facet)); emit RolePaused(role, users.admin); @@ -47,7 +49,14 @@ contract Pausable_AccessControlPausableFacet_Fuzz_Unit_Test is AccessControlPaus assertEq(address(facet).isRolePaused(role), true, "isRolePaused"); } + function test_ShouldRevert_PauseRole_WhenRoleIsDefaultAdminRole() external { + vm.expectRevert(AccessControlPausableFacet.AccessControlDefaultAdminRolePaused.selector); + vm.prank(users.admin); + facet.pauseRole(DEFAULT_ADMIN_ROLE); + } + function testFuzz_ShouldRevert_PauseRole_WhenCallerDoesNotHaveAdminRole(bytes32 role, address caller) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); vm.assume(caller != users.admin); vm.expectRevert( @@ -60,6 +69,7 @@ contract Pausable_AccessControlPausableFacet_Fuzz_Unit_Test is AccessControlPaus } function testFuzz_ShouldUnpauseRole_WhenCallerHasAdminRole(bytes32 role) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); seedPausedRole(address(facet), role, true); vm.expectEmit(address(facet)); @@ -110,6 +120,8 @@ contract Pausable_AccessControlPausableFacet_Fuzz_Unit_Test is AccessControlPaus } function testFuzz_ShouldEndUnpaused_WhenMultiplePauseUnpauseCycles(bytes32 role) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); + vm.startPrank(users.admin); facet.pauseRole(role); facet.unpauseRole(role); @@ -121,6 +133,8 @@ contract Pausable_AccessControlPausableFacet_Fuzz_Unit_Test is AccessControlPaus } function testFuzz_ShouldRemainPaused_PauseRole_WhenCalledTwice(bytes32 role) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); + vm.prank(users.admin); facet.pauseRole(role); vm.prank(users.admin); @@ -130,6 +144,7 @@ contract Pausable_AccessControlPausableFacet_Fuzz_Unit_Test is AccessControlPaus } function testFuzz_ShouldRemainUnpaused_UnpauseRole_WhenCalledTwice(bytes32 role) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); seedPausedRole(address(facet), role, true); vm.prank(users.admin); diff --git a/test/unit/access/AccessControl/Pausable/facet/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Pausable/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..468c7578 --- /dev/null +++ b/test/unit/access/AccessControl/Pausable/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {AccessControlPausable_Base_Test} from "test/unit/access/AccessControl/Pausable/AccessControlPausableBase.t.sol"; +import {AccessControlPausableFacet} from "src/access/AccessControl/Pausable/AccessControlPausableFacet.sol"; + +contract TemporalExpiry_AccessControlPausableFacet_Test is AccessControlPausable_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlPausableFacet internal pausableFacet; + + function setUp() public override { + super.setUp(); + pausableFacet = new AccessControlPausableFacet(); + vm.label(address(pausableFacet), "AccessControlPausableFacet"); + seedDefaultAdmin(address(pausableFacet)); + } + + function test_ShouldRevert_PauseRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(pausableFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(pausableFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlPausableFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + pausableFacet.pauseRole(role); + } + + function test_ShouldSucceed_PauseRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(pausableFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(pausableFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + pausableFacet.pauseRole(role); + + assertTrue(pausableFacet.isRolePaused(role)); + } + + function test_ShouldRevert_UnpauseRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + seedPausedRole(address(pausableFacet), role, true); + + address(pausableFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(pausableFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlPausableFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + pausableFacet.unpauseRole(role); + } + + function test_ShouldSucceed_UnpauseRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + seedPausedRole(address(pausableFacet), role, true); + + address(pausableFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(pausableFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + pausableFacet.unpauseRole(role); + + assertFalse(pausableFacet.isRolePaused(role)); + } +} diff --git a/test/unit/access/AccessControl/Pausable/mod/fuzz/pausable.t.sol b/test/unit/access/AccessControl/Pausable/mod/fuzz/pausable.t.sol index f8d484d2..b15f5a99 100644 --- a/test/unit/access/AccessControl/Pausable/mod/fuzz/pausable.t.sol +++ b/test/unit/access/AccessControl/Pausable/mod/fuzz/pausable.t.sol @@ -39,6 +39,8 @@ contract Pausable_AccessControlPausableMod_Fuzz_Unit_Test is AccessControlPausab } function testFuzz_ShouldPauseRole_ForCurrentSurfaceSemantics(bytes32 role, address caller) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); + vm.expectEmit(address(harness)); emit RolePaused(role, caller); @@ -48,6 +50,12 @@ contract Pausable_AccessControlPausableMod_Fuzz_Unit_Test is AccessControlPausab assertEq(address(harness).isRolePaused(role), true, "isRolePaused"); } + function test_ShouldRevert_PauseRole_WhenRoleIsDefaultAdminRole() external { + vm.expectRevert(abi.encodeWithSignature("AccessControlDefaultAdminRolePaused()")); + vm.prank(users.admin); + harness.pauseRole(DEFAULT_ADMIN_ROLE); + } + function testFuzz_ShouldUnpauseRole_ForCurrentSurfaceSemantics(bytes32 role, address caller) external { seedPausedRole(address(harness), role, true); @@ -85,6 +93,8 @@ contract Pausable_AccessControlPausableMod_Fuzz_Unit_Test is AccessControlPausab } function testFuzz_ShouldEndUnpaused_WhenMultiplePauseUnpauseCycles(bytes32 role) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); + vm.startPrank(users.admin); harness.pauseRole(role); harness.unpauseRole(role); @@ -96,6 +106,8 @@ contract Pausable_AccessControlPausableMod_Fuzz_Unit_Test is AccessControlPausab } function testFuzz_ShouldRemainPaused_PauseRole_WhenCalledTwice(bytes32 role) external { + vm.assume(role != DEFAULT_ADMIN_ROLE); + vm.prank(users.admin); harness.pauseRole(role); vm.prank(users.admin); diff --git a/test/unit/access/AccessControl/Pausable/mod/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Pausable/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..56b4a7b3 --- /dev/null +++ b/test/unit/access/AccessControl/Pausable/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {AccessControlPausable_Base_Test} from "test/unit/access/AccessControl/Pausable/AccessControlPausableBase.t.sol"; +import {AccessControlPausableFacet} from "src/access/AccessControl/Pausable/AccessControlPausableFacet.sol"; +import { + AccessControlCombinedModHarness +} from "test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol"; + +contract TemporalExpiry_AccessControlPausableFacet_Test is AccessControlPausable_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlPausableFacet internal pausableFacet; + AccessControlCombinedModHarness internal harness; + + function setUp() public override { + super.setUp(); + pausableFacet = new AccessControlPausableFacet(); + harness = new AccessControlCombinedModHarness(); + vm.label(address(pausableFacet), "AccessControlPausableFacet"); + vm.label(address(harness), "AccessControlCombinedModHarness"); + seedDefaultAdmin(address(harness)); + seedDefaultAdmin(address(pausableFacet)); + } + + function test_ShouldRevert_PauseRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(pausableFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(pausableFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlPausableFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + pausableFacet.pauseRole(role); + } + + function test_ShouldSucceed_PauseRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + pausableFacet.pauseRole(role); + + assertTrue(pausableFacet.isRolePaused(role)); + } + + function test_ShouldRevert_UnpauseRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + seedPausedRole(address(pausableFacet), role, true); + + address(pausableFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(pausableFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlPausableFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + pausableFacet.unpauseRole(role); + } + + function test_ShouldSucceed_UnpauseRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + seedPausedRole(address(pausableFacet), role, true); + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + pausableFacet.unpauseRole(role); + + assertFalse(pausableFacet.isRolePaused(role)); + } +} diff --git a/test/unit/access/AccessControl/Revoke/facet/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Revoke/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..045867cd --- /dev/null +++ b/test/unit/access/AccessControl/Revoke/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {AccessControlRevoke_Base_Test} from "test/unit/access/AccessControl/Revoke/AccessControlRevokeBase.t.sol"; +import {AccessControlRevokeFacet} from "src/access/AccessControl/Revoke/AccessControlRevokeFacet.sol"; + +contract TemporalExpiry_AccessControlRevokeFacet_Test is AccessControlRevoke_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlRevokeFacet internal revokeFacet; + + function setUp() public override { + super.setUp(); + revokeFacet = new AccessControlRevokeFacet(); + vm.label(address(revokeFacet), "AccessControlRevokeFacet"); + seedDefaultAdmin(address(revokeFacet)); + } + + function test_ShouldRevert_RevokeRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(revokeFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(revokeFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlRevokeFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + revokeFacet.revokeRole(role, users.alice); + } + + function test_ShouldSucceed_RevokeRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + address(revokeFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(revokeFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, expiry); + + seedRole(address(revokeFacet), role, users.alice); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + revokeFacet.revokeRole(role, users.alice); + + assertFalse(address(revokeFacet).hasRole(users.alice, role)); + } +} diff --git a/test/unit/access/AccessControl/Revoke/mod/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Revoke/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..a7bf3861 --- /dev/null +++ b/test/unit/access/AccessControl/Revoke/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {AccessControlRevoke_Base_Test} from "test/unit/access/AccessControl/Revoke/AccessControlRevokeBase.t.sol"; +import { + AccessControlCombinedModHarness +} from "test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol"; + +contract TemporalExpiry_AccessControlRevokeMod_Test is AccessControlRevoke_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlCombinedModHarness internal harness; + + function setUp() public override { + super.setUp(); + harness = new AccessControlCombinedModHarness(); + vm.label(address(harness), "AccessControlCombinedModHarness"); + seedDefaultAdmin(address(harness)); + } + + function test_ShouldRevert_RevokeRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", DEFAULT_ADMIN_ROLE, users.admin) + ); + vm.prank(users.admin); + harness.revokeRole(role, users.alice); + } + + function test_ShouldSucceed_RevokeRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 expiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, expiry); + + seedRole(address(harness), role, users.alice); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + harness.revokeRole(role, users.alice); + + assertFalse(address(harness).hasRole(users.alice, role)); + } +} diff --git a/test/unit/access/AccessControl/Temporal/Grant/facet/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Temporal/Grant/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..05cdbf83 --- /dev/null +++ b/test/unit/access/AccessControl/Temporal/Grant/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import { + AccessControlTemporalGrant_Base_Test +} from "test/unit/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantBase.t.sol"; +import { + AccessControlTemporalGrantFacet +} from "src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantFacet.sol"; + +contract TemporalExpiry_AccessControlTemporalGrantFacet_Test is AccessControlTemporalGrant_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlTemporalGrantFacet internal temporalGrantFacet; + + function setUp() public override { + super.setUp(); + temporalGrantFacet = new AccessControlTemporalGrantFacet(); + vm.label(address(temporalGrantFacet), "AccessControlTemporalGrantFacet"); + seedDefaultAdmin(address(temporalGrantFacet)); + } + + function test_ShouldRevert_GrantRoleWithExpiry_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 adminExpiry = block.timestamp + 1 hours; + uint256 roleExpiry = block.timestamp + 2 hours; + + address(temporalGrantFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(temporalGrantFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, adminExpiry); + + vm.warp(adminExpiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlTemporalGrantFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + temporalGrantFacet.grantRoleWithExpiry(role, users.alice, roleExpiry); + } + + function test_ShouldSucceed_GrantRoleWithExpiry_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 adminExpiry = block.timestamp + 2 hours; + uint256 roleExpiry = block.timestamp + 1 hours; + + address(temporalGrantFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(temporalGrantFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, adminExpiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + temporalGrantFacet.grantRoleWithExpiry(role, users.alice, roleExpiry); + + assertTrue(address(temporalGrantFacet).hasRole(users.alice, role)); + assertEq(address(temporalGrantFacet).roleExpiry(users.alice, role), roleExpiry); + } +} diff --git a/test/unit/access/AccessControl/Temporal/Grant/mod/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Temporal/Grant/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..61d5df66 --- /dev/null +++ b/test/unit/access/AccessControl/Temporal/Grant/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import { + AccessControlTemporalGrant_Base_Test +} from "test/unit/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantBase.t.sol"; +import { + AccessControlCombinedModHarness +} from "test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol"; + +contract TemporalExpiry_AccessControlTemporalGrantMod_Test is AccessControlTemporalGrant_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlCombinedModHarness internal harness; + + function setUp() public override { + super.setUp(); + harness = new AccessControlCombinedModHarness(); + vm.label(address(harness), "AccessControlCombinedModHarness"); + seedDefaultAdmin(address(harness)); + } + + function test_ShouldRevert_GrantRoleWithExpiry_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 adminExpiry = block.timestamp + 1 hours; + uint256 roleExpiry = block.timestamp + 2 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, adminExpiry); + + vm.warp(adminExpiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", DEFAULT_ADMIN_ROLE, users.admin) + ); + vm.prank(users.admin); + harness.grantRoleWithExpiry(role, users.alice, roleExpiry); + } + + function test_ShouldSucceed_GrantRoleWithExpiry_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 adminExpiry = block.timestamp + 2 hours; + uint256 roleExpiry = block.timestamp + 1 hours; + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, adminExpiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + harness.grantRoleWithExpiry(role, users.alice, roleExpiry); + + assertTrue(address(harness).hasRole(users.alice, role)); + assertEq(harness.getRoleExpiry(role, users.alice), roleExpiry); + } +} diff --git a/test/unit/access/AccessControl/Temporal/Revoke/facet/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Temporal/Revoke/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..ae420040 --- /dev/null +++ b/test/unit/access/AccessControl/Temporal/Revoke/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import { + AccessControlTemporalRevoke_Base_Test +} from "test/unit/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeBase.t.sol"; +import { + AccessControlTemporalRevokeFacet +} from "src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeFacet.sol"; + +contract TemporalExpiry_AccessControlTemporalRevokeFacet_Test is AccessControlTemporalRevoke_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlTemporalRevokeFacet internal temporalRevokeFacet; + + function setUp() public override { + super.setUp(); + temporalRevokeFacet = new AccessControlTemporalRevokeFacet(); + vm.label(address(temporalRevokeFacet), "AccessControlTemporalRevokeFacet"); + seedDefaultAdmin(address(temporalRevokeFacet)); + } + + function test_ShouldRevert_RevokeTemporalRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 adminExpiry = block.timestamp + 1 hours; + + seedRole(address(temporalRevokeFacet), role, users.alice); + + address(temporalRevokeFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(temporalRevokeFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, adminExpiry); + + vm.warp(adminExpiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + AccessControlTemporalRevokeFacet.AccessControlRoleExpired.selector, DEFAULT_ADMIN_ROLE, users.admin + ) + ); + vm.prank(users.admin); + temporalRevokeFacet.revokeTemporalRole(role, users.alice); + } + + function test_ShouldSucceed_RevokeTemporalRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 adminExpiry = block.timestamp + 1 hours; + + seedRole(address(temporalRevokeFacet), role, users.alice); + seedRoleExpiry(address(temporalRevokeFacet), role, users.alice, block.timestamp + 2 hours); + + address(temporalRevokeFacet).setHasRole(users.admin, DEFAULT_ADMIN_ROLE, true); + address(temporalRevokeFacet).setRoleExpiry(users.admin, DEFAULT_ADMIN_ROLE, adminExpiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + temporalRevokeFacet.revokeTemporalRole(role, users.alice); + + assertFalse(address(temporalRevokeFacet).hasRole(users.alice, role)); + } +} diff --git a/test/unit/access/AccessControl/Temporal/Revoke/mod/fuzz/temporalExpiry.t.sol b/test/unit/access/AccessControl/Temporal/Revoke/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..4a84585a --- /dev/null +++ b/test/unit/access/AccessControl/Temporal/Revoke/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import { + AccessControlTemporalRevoke_Base_Test +} from "test/unit/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeBase.t.sol"; +import { + AccessControlCombinedModHarness +} from "test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol"; + +contract TemporalExpiry_AccessControlTemporalRevokeMod_Test is AccessControlTemporalRevoke_Base_Test { + using AccessControlStorageUtils for address; + + AccessControlCombinedModHarness internal harness; + + function setUp() public override { + super.setUp(); + harness = new AccessControlCombinedModHarness(); + vm.label(address(harness), "AccessControlCombinedModHarness"); + seedDefaultAdmin(address(harness)); + } + + function test_ShouldRevert_RevokeTemporalRole_WhenCallerAdminRoleExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 adminExpiry = block.timestamp + 1 hours; + + seedRole(address(harness), role, users.alice); + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, adminExpiry); + + vm.warp(adminExpiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", DEFAULT_ADMIN_ROLE, users.admin) + ); + vm.prank(users.admin); + harness.revokeTemporalRole(role, users.alice); + } + + function test_ShouldSucceed_RevokeTemporalRole_WhenCallerAdminRoleNotExpired() external { + bytes32 role = keccak256("TEST_ROLE"); + uint256 adminExpiry = block.timestamp + 1 hours; + + seedRole(address(harness), role, users.alice); + seedRoleExpiry(address(harness), role, users.alice, block.timestamp + 2 hours); + + vm.prank(users.admin); + harness.grantRoleWithExpiry(DEFAULT_ADMIN_ROLE, users.admin, adminExpiry); + + vm.warp(block.timestamp + 30 minutes); + + vm.prank(users.admin); + harness.revokeTemporalRole(role, users.alice); + + assertFalse(address(harness).hasRole(users.alice, role)); + } +} diff --git a/test/unit/token/ERC20/Bridgeable/facet/fuzz/temporalExpiry.t.sol b/test/unit/token/ERC20/Bridgeable/facet/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..2079794d --- /dev/null +++ b/test/unit/token/ERC20/Bridgeable/facet/fuzz/temporalExpiry.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; +import {ERC20BridgeableFacet_Base_Test} from "test/unit/token/ERC20/Bridgeable/ERC20BridgeableFacetBase.t.sol"; +import {ERC20BridgeableFacet} from "src/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol"; + +contract TemporalExpiry_ERC20BridgeableFacet_Test is ERC20BridgeableFacet_Base_Test { + using AccessControlStorageUtils for address; + using ERC20StorageUtils for address; + + function test_ShouldRevert_CrosschainMint_WhenBridgeRoleExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(facet).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(facet).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20BridgeableFacet.AccessControlRoleExpired.selector, ERC20_BRIDGE_ROLE, users.alice + ) + ); + facet.crosschainMint(users.bob, 1000); + } + + function test_ShouldSucceed_CrosschainMint_WhenBridgeRoleNotExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(facet).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(facet).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + facet.crosschainMint(users.bob, 1000); + + assertEq(address(facet).balanceOf(users.bob), 1000); + } + + function test_ShouldRevert_CrosschainBurn_WhenBridgeRoleExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(facet).setBalance(users.bob, 1000); + + address(facet).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(facet).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20BridgeableFacet.AccessControlRoleExpired.selector, ERC20_BRIDGE_ROLE, users.alice + ) + ); + facet.crosschainBurn(users.bob, 500); + } + + function test_ShouldSucceed_CrosschainBurn_WhenBridgeRoleNotExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(facet).setBalance(users.bob, 1000); + address(facet).setTotalSupply(1000); + + address(facet).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(facet).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + facet.crosschainBurn(users.bob, 500); + + assertEq(address(facet).balanceOf(users.bob), 500); + } + + function test_ShouldRevert_CheckTokenBridge_WhenBridgeRoleExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(facet).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(facet).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20BridgeableFacet.AccessControlRoleExpired.selector, ERC20_BRIDGE_ROLE, users.alice + ) + ); + facet.checkTokenBridge(users.alice); + } + + function test_ShouldSucceed_CheckTokenBridge_WhenBridgeRoleNotExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(facet).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(facet).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + facet.checkTokenBridge(users.alice); + } +} diff --git a/test/unit/token/ERC20/Bridgeable/mod/fuzz/temporalExpiry.t.sol b/test/unit/token/ERC20/Bridgeable/mod/fuzz/temporalExpiry.t.sol new file mode 100644 index 00000000..7f27610a --- /dev/null +++ b/test/unit/token/ERC20/Bridgeable/mod/fuzz/temporalExpiry.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {AccessControlStorageUtils} from "test/utils/storage/AccessControlStorageUtils.sol"; +import {ERC20StorageUtils} from "test/utils/storage/ERC20StorageUtils.sol"; +import {Base_Test} from "test/Base.t.sol"; +import {ERC20BridgeableModHarness} from "test/utils/harnesses/token/ERC20/ERC20BridgeableModHarness.sol"; + +contract TemporalExpiry_ERC20BridgeableMod_Test is Base_Test { + bytes32 internal constant ERC20_BRIDGE_ROLE = bytes32("trusted-bridge"); + using AccessControlStorageUtils for address; + using ERC20StorageUtils for address; + + ERC20BridgeableModHarness internal bridgeHarness; + + function setUp() public override { + super.setUp(); + bridgeHarness = new ERC20BridgeableModHarness(); + vm.label(address(bridgeHarness), "ERC20BridgeableModHarness"); + } + + function test_ShouldRevert_CrosschainMint_WhenBridgeRoleExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(bridgeHarness).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(bridgeHarness).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", ERC20_BRIDGE_ROLE, users.alice) + ); + bridgeHarness.crosschainMint(users.bob, 1000); + } + + function test_ShouldSucceed_CrosschainMint_WhenBridgeRoleNotExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(bridgeHarness).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(bridgeHarness).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + bridgeHarness.crosschainMint(users.bob, 1000); + + assertEq(address(bridgeHarness).balanceOf(users.bob), 1000); + } + + function test_ShouldRevert_CrosschainBurn_WhenBridgeRoleExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(bridgeHarness).setBalance(users.bob, 1000); + + address(bridgeHarness).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(bridgeHarness).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", ERC20_BRIDGE_ROLE, users.alice) + ); + bridgeHarness.crosschainBurn(users.bob, 500); + } + + function test_ShouldSucceed_CrosschainBurn_WhenBridgeRoleNotExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(bridgeHarness).setBalance(users.bob, 1000); + address(bridgeHarness).setTotalSupply(1000); + + address(bridgeHarness).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(bridgeHarness).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + bridgeHarness.crosschainBurn(users.bob, 500); + + assertEq(address(bridgeHarness).balanceOf(users.bob), 500); + } + + function test_ShouldRevert_CheckTokenBridge_WhenBridgeRoleExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(bridgeHarness).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(bridgeHarness).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(expiry + 1); + + vm.expectRevert( + abi.encodeWithSignature("AccessControlRoleExpired(bytes32,address)", ERC20_BRIDGE_ROLE, users.alice) + ); + bridgeHarness.checkTokenBridge(users.alice); + } + + function test_ShouldSucceed_CheckTokenBridge_WhenBridgeRoleNotExpired() external { + uint256 expiry = block.timestamp + 1 hours; + + address(bridgeHarness).setHasRole(users.alice, ERC20_BRIDGE_ROLE, true); + address(bridgeHarness).setRoleExpiry(users.alice, ERC20_BRIDGE_ROLE, expiry); + + vm.warp(block.timestamp + 30 minutes); + + bridgeHarness.checkTokenBridge(users.alice); + } +} diff --git a/test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol b/test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol new file mode 100644 index 00000000..5c75bced --- /dev/null +++ b/test/utils/harnesses/access/AccessControl/AccessControlCombinedModHarness.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import { + DEFAULT_ADMIN_ROLE, + hasRole as accessControlHasRole, + requireRole as accessControlRequireRole +} from "src/access/AccessControl/Data/AccessControlDataMod.sol"; +import {setRoleAdmin as accessControlSetRoleAdmin} from "src/access/AccessControl/Admin/AccessControlAdminMod.sol"; +import {getRoleAdmin as accessControlGetRoleAdmin} from "src/access/AccessControl/Data/AccessControlDataMod.sol"; +import {grantRole as accessControlGrantRole} from "src/access/AccessControl/Grant/AccessControlGrantMod.sol"; +import {revokeRole as accessControlRevokeRole} from "src/access/AccessControl/Revoke/AccessControlRevokeMod.sol"; +import { + renounceRole as accessControlRenounceRole +} from "src/access/AccessControl/Renounce/AccessControlRenounceMod.sol"; +import { + grantRoleBatch as accessControlGrantRoleBatch +} from "src/access/AccessControl/Batch/Grant/AccessControlGrantBatchMod.sol"; +import { + revokeRoleBatch as accessControlRevokeRoleBatch +} from "src/access/AccessControl/Batch/Revoke/AccessControlRevokeBatchMod.sol"; +import { + getRoleExpiry as accessControlGetRoleExpiry, + isRoleExpired as accessControlIsRoleExpired, + requireValidRole as accessControlRequireValidRole +} from "src/access/AccessControl/Temporal/Data/AccessControlTemporalDataMod.sol"; +import { + grantRoleWithExpiry as accessControlGrantRoleWithExpiry +} from "src/access/AccessControl/Temporal/Grant/AccessControlTemporalGrantMod.sol"; +import { + revokeTemporalRole as accessControlRevokeTemporalRole +} from "src/access/AccessControl/Temporal/Revoke/AccessControlTemporalRevokeMod.sol"; +import { + isRolePaused as accessControlIsRolePaused, + pauseRole as accessControlPauseRole, + unpauseRole as accessControlUnpauseRole +} from "src/access/AccessControl/Pausable/AccessControlPausableMod.sol"; + +contract AccessControlCombinedModHarness { + function DEFAULT_ADMIN_ROLE_VALUE() external pure returns (bytes32) { + return DEFAULT_ADMIN_ROLE; + } + + function hasRole(bytes32 role, address account) external view returns (bool) { + return accessControlHasRole(role, account); + } + + function requireRole(bytes32 role, address account) external view { + accessControlRequireRole(role, account); + } + + function setRoleAdmin(bytes32 role, bytes32 adminRole) external { + accessControlSetRoleAdmin(role, adminRole); + } + + function adminRole(bytes32 role) external view returns (bytes32) { + return accessControlGetRoleAdmin(role); + } + + function grantRole(bytes32 role, address account) external returns (bool) { + return accessControlGrantRole(role, account); + } + + function revokeRole(bytes32 role, address account) external returns (bool) { + return accessControlRevokeRole(role, account); + } + + function renounceRole(bytes32 role, address account) external { + accessControlRenounceRole(role, account); + } + + function grantRoleBatch(bytes32 role, address[] calldata accounts) external { + accessControlGrantRoleBatch(role, accounts); + } + + function revokeRoleBatch(bytes32 role, address[] calldata accounts) external { + accessControlRevokeRoleBatch(role, accounts); + } + + function getRoleExpiry(bytes32 role, address account) external view returns (uint256) { + return accessControlGetRoleExpiry(role, account); + } + + function isRoleExpired(bytes32 role, address account) external view returns (bool) { + return accessControlIsRoleExpired(role, account); + } + + function requireValidRole(bytes32 role, address account) external view { + accessControlRequireValidRole(role, account); + } + + function grantRoleWithExpiry(bytes32 role, address account, uint256 expiresAt) external { + accessControlGrantRoleWithExpiry(role, account, expiresAt); + } + + function revokeTemporalRole(bytes32 role, address account) external { + accessControlRevokeTemporalRole(role, account); + } + + function isRolePaused(bytes32 role) external view returns (bool) { + return accessControlIsRolePaused(role); + } + + function pauseRole(bytes32 role) external { + accessControlPauseRole(role); + } + + function unpauseRole(bytes32 role) external { + accessControlUnpauseRole(role); + } +} diff --git a/test/utils/harnesses/token/ERC20/ERC20BridgeableModHarness.sol b/test/utils/harnesses/token/ERC20/ERC20BridgeableModHarness.sol new file mode 100644 index 00000000..8b18f7ac --- /dev/null +++ b/test/utils/harnesses/token/ERC20/ERC20BridgeableModHarness.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import { + crosschainMint as _crosschainMint, + crosschainBurn as _crosschainBurn, + checkTokenBridge as _checkTokenBridge +} from "src/token/ERC20/Bridgeable/ERC20BridgeableMod.sol"; + +contract ERC20BridgeableModHarness { + function crosschainMint(address account, uint256 value) external { + _crosschainMint(account, value); + } + + function crosschainBurn(address from, uint256 value) external { + _crosschainBurn(from, value); + } + + function checkTokenBridge(address caller) external view { + _checkTokenBridge(caller); + } +}