-
Notifications
You must be signed in to change notification settings - Fork 61
Feat/accesscontrol temporal facets #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mudgen
merged 5 commits into
Perfect-Abstractions:main
from
isihin-3:feat/accesscontrol-temporal-facets
Nov 14, 2025
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
4602e65
feat(accesscontrol): Add AccessControlTemporalFacet
isihin-3 b62d853
feat(accesscontrol): Add AccessControlTemporalFacet
isihin-3 9323621
Merge branch 'Perfect-Abstractions:main' into feat/accesscontrol-temp…
isihin-3 45b212e
Merge branch 'Perfect-Abstractions:main' into feat/accesscontrol-temp…
isihin-3 67cb321
bug fixed
isihin-3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
178 changes: 178 additions & 0 deletions
178
src/access/AccessControlTemporal/AccessControlTemporalFacet.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity >=0.8.30; | ||
|
|
||
| contract AccessControlTemporalFacet { | ||
| /// @notice Event emitted when a role is granted with an expiry timestamp. | ||
| /// @param _role The role that was granted. | ||
| /// @param _account The account that was granted the role. | ||
| /// @param _expiresAt The timestamp when the role expires. | ||
| /// @param _sender The account that granted the role. | ||
| event RoleGrantedWithExpiry( | ||
| bytes32 indexed _role, address indexed _account, uint256 _expiresAt, address indexed _sender | ||
| ); | ||
|
|
||
| /// @notice Event emitted when a temporal role is revoked. | ||
| /// @param _role The role that was revoked. | ||
| /// @param _account The account from which the role was revoked. | ||
| /// @param _sender The account that revoked the role. | ||
| event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); | ||
|
|
||
| /// @notice Thrown when the account does not have a specific role. | ||
| /// @param _role The role that the account does not have. | ||
| /// @param _account The account that does not have the role. | ||
| 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 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 AccessControlFacet. | ||
| /// @custom:storage-location erc8042:compose.accesscontrol | ||
| struct AccessControlStorage { | ||
| mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; | ||
| 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 Returns the storage for AccessControl. | ||
| /// @return s The AccessControl storage struct. | ||
| function getAccessControlStorage() internal pure returns (AccessControlStorage storage s) { | ||
| bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; | ||
| assembly { | ||
| s.slot := position | ||
| } | ||
| } | ||
|
|
||
| /// @notice Returns the storage for AccessControlTemporal. | ||
| /// @return s The AccessControlTemporal storage struct. | ||
| function getStorage() internal pure returns (AccessControlTemporalStorage storage s) { | ||
| bytes32 position = TEMPORAL_STORAGE_POSITION; | ||
| assembly { | ||
| s.slot := position | ||
| } | ||
| } | ||
|
|
||
| /// @notice Returns the expiry timestamp for a role assignment. | ||
| /// @param _role The role to check. | ||
| /// @param _account The account to check. | ||
| /// @return The expiry timestamp, or 0 if no expiry is set. | ||
| function getRoleExpiry(bytes32 _role, address _account) external view returns (uint256) { | ||
| return getStorage().roleExpiry[_account][_role]; | ||
| } | ||
|
|
||
| /// @notice Checks if a role assignment has expired. | ||
| /// @param _role The role to check. | ||
| /// @param _account The account to check. | ||
| /// @return True if the role has expired or doesn't exist, false if still valid. | ||
| function isRoleExpired(bytes32 _role, address _account) external view returns (bool) { | ||
| AccessControlStorage storage acs = getAccessControlStorage(); | ||
| AccessControlTemporalStorage storage s = getStorage(); | ||
| uint256 expiry = s.roleExpiry[_account][_role]; | ||
|
|
||
| // If no expiry set (0), role is valid if account has it | ||
| if (expiry == 0) { | ||
| return !acs.hasRole[_account][_role]; | ||
| } | ||
|
|
||
| // Role is expired if current time is past expiry | ||
| return block.timestamp >= expiry; | ||
| } | ||
|
|
||
| /// @notice Grants a role to an account with an expiry timestamp. | ||
| /// @param _role The role to grant. | ||
| /// @param _account The account to grant the role to. | ||
| /// @param _expiresAt The timestamp when the role should expire (must be in the future). | ||
| /// @dev Only the admin of the role can grant it with expiry. | ||
| /// Emits a {RoleGrantedWithExpiry} event. | ||
| /// @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. | ||
| function grantRoleWithExpiry(bytes32 _role, address _account, uint256 _expiresAt) external { | ||
| AccessControlStorage storage acs = getAccessControlStorage(); | ||
| 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); | ||
| } | ||
|
|
||
| // Require expiry is in the future | ||
| if (_expiresAt <= block.timestamp) { | ||
| revert AccessControlRoleExpired(_role, _account); | ||
| } | ||
|
|
||
| // Grant the role | ||
| bool _hasRole = acs.hasRole[_account][_role]; | ||
| if (!_hasRole) { | ||
| acs.hasRole[_account][_role] = true; | ||
| } | ||
|
|
||
| // Set expiry timestamp | ||
| s.roleExpiry[_account][_role] = _expiresAt; | ||
| emit RoleGrantedWithExpiry(_role, _account, _expiresAt, msg.sender); | ||
| } | ||
|
|
||
| /// @notice Revokes a temporal role from an account. | ||
| /// @param _role The role to revoke. | ||
| /// @param _account The account to revoke the role from. | ||
| /// @dev Only the admin of the role can revoke it. | ||
| /// Emits a {TemporalRoleRevoked} event. | ||
| /// @custom:error AccessControlUnauthorizedAccount If the caller is not the admin of the role. | ||
| function revokeTemporalRole(bytes32 _role, address _account) external { | ||
| AccessControlStorage storage acs = getAccessControlStorage(); | ||
| 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); | ||
| } | ||
|
|
||
| // Revoke the role | ||
| bool _hasRole = acs.hasRole[_account][_role]; | ||
|
|
||
| // Only revoke if the role is currently granted | ||
| if (_hasRole) { | ||
| // Revoke the role from AccessControl storage | ||
| acs.hasRole[_account][_role] = false; | ||
|
|
||
| // Clear expiry timestamp | ||
| s.roleExpiry[_account][_role] = 0; | ||
|
|
||
| emit TemporalRoleRevoked(_role, _account, msg.sender); | ||
| } | ||
| } | ||
|
|
||
| /// @notice Checks if an account has a valid (non-expired) role. | ||
| /// @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 role has expired. | ||
| function requireValidRole(bytes32 _role, address _account) external view { | ||
| AccessControlStorage storage acs = getAccessControlStorage(); | ||
| AccessControlTemporalStorage storage s = getStorage(); | ||
|
|
||
| // Check if account has the role | ||
| if (!acs.hasRole[_account][_role]) { | ||
| revert AccessControlUnauthorizedAccount(_account, _role); | ||
| } | ||
|
|
||
| // Check if role has expired | ||
| uint256 expiry = s.roleExpiry[_account][_role]; | ||
| if (expiry > 0 && block.timestamp >= expiry) { | ||
| revert AccessControlRoleExpired(_role, _account); | ||
| } | ||
| } | ||
| } | ||
160 changes: 160 additions & 0 deletions
160
src/access/AccessControlTemporal/LibAccessControlTemporal.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity >=0.8.30; | ||
|
|
||
| library LibAccessControlTemporal { | ||
| /// @notice Event emitted when a role is granted with an expiry timestamp. | ||
| /// @param _role The role that was granted. | ||
| /// @param _account The account that was granted the role. | ||
| /// @param _expiresAt The timestamp when the role expires. | ||
| /// @param _sender The account that granted the role. | ||
| event RoleGrantedWithExpiry( | ||
| bytes32 indexed _role, address indexed _account, uint256 _expiresAt, address indexed _sender | ||
| ); | ||
|
|
||
| /// @notice Event emitted when a temporal role is revoked. | ||
| /// @param _role The role that was revoked. | ||
| /// @param _account The account from which the role was revoked. | ||
| /// @param _sender The account that revoked the role. | ||
| event TemporalRoleRevoked(bytes32 indexed _role, address indexed _account, address indexed _sender); | ||
|
|
||
| /// @notice Thrown when the account does not have a specific role. | ||
| /// @param _role The role that the account does not have. | ||
| /// @param _account The account that does not have the role. | ||
| 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 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 AccessControlFacet. | ||
| /// @custom:storage-location erc8042:compose.accesscontrol | ||
| struct AccessControlStorage { | ||
| mapping(address account => mapping(bytes32 role => bool hasRole)) hasRole; | ||
| 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 Returns the storage for AccessControl. | ||
| /// @return s The AccessControl storage struct. | ||
| function getAccessControlStorage() internal pure returns (AccessControlStorage storage s) { | ||
| bytes32 position = ACCESS_CONTROL_STORAGE_POSITION; | ||
| assembly { | ||
| s.slot := position | ||
| } | ||
| } | ||
|
|
||
| /// @notice Returns the storage for AccessControlTemporal. | ||
| /// @return s The AccessControlTemporal storage struct. | ||
| function getStorage() internal pure returns (AccessControlTemporalStorage storage s) { | ||
| bytes32 position = TEMPORAL_STORAGE_POSITION; | ||
| assembly { | ||
| s.slot := position | ||
| } | ||
| } | ||
|
|
||
| /// @notice function to get the expiry timestamp for a role assignment. | ||
| /// @param _role The role to check. | ||
| /// @param _account The account to check. | ||
| /// @return The expiry timestamp, or 0 if no expiry is set. | ||
| function getRoleExpiry(bytes32 _role, address _account) internal view returns (uint256) { | ||
| AccessControlTemporalStorage storage s = getStorage(); | ||
| return s.roleExpiry[_account][_role]; | ||
| } | ||
|
|
||
| /// @notice function to check if a role assignment has expired. | ||
| /// @param _role The role to check. | ||
| /// @param _account The account to check. | ||
| /// @return True if the role has expired or doesn't exist, false if still valid. | ||
| function isRoleExpired(bytes32 _role, address _account) internal view returns (bool) { | ||
| AccessControlStorage storage acs = getAccessControlStorage(); | ||
| AccessControlTemporalStorage storage s = getStorage(); | ||
| uint256 expiry = s.roleExpiry[_account][_role]; | ||
|
|
||
| // If no expiry set (0), role is valid if account has it | ||
| if (expiry == 0) { | ||
| return !acs.hasRole[_account][_role]; | ||
| } | ||
|
|
||
| // Role is expired if current time is past expiry | ||
| return block.timestamp >= expiry; | ||
| } | ||
|
|
||
| /// @notice function to grant a role with an expiry timestamp. | ||
| /// @param _role The role to grant. | ||
| /// @param _account The account to grant the role to. | ||
| /// @param _expiresAt The timestamp when the role should expire. | ||
| /// @return True if the role was granted, false otherwise. | ||
| function grantRoleWithExpiry(bytes32 _role, address _account, uint256 _expiresAt) internal returns (bool) { | ||
| AccessControlStorage storage acs = getAccessControlStorage(); | ||
| AccessControlTemporalStorage storage s = getStorage(); | ||
|
|
||
| // Grant the role | ||
| bool _hasRole = acs.hasRole[_account][_role]; | ||
| if (!_hasRole) { | ||
| acs.hasRole[_account][_role] = true; | ||
| } | ||
|
|
||
| // Set expiry timestamp | ||
| s.roleExpiry[_account][_role] = _expiresAt; | ||
| emit RoleGrantedWithExpiry(_role, _account, _expiresAt, msg.sender); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /// @notice function to revoke a temporal role. | ||
| /// @param _role The role to revoke. | ||
| /// @param _account The account to revoke the role from. | ||
| /// @return True if the role was revoked, false otherwise. | ||
| function revokeTemporalRole(bytes32 _role, address _account) internal returns (bool) { | ||
| AccessControlStorage storage acs = getAccessControlStorage(); | ||
| AccessControlTemporalStorage storage s = getStorage(); | ||
|
|
||
| bool _hasRole = acs.hasRole[_account][_role]; | ||
| if (!_hasRole) { | ||
| return false; | ||
| } | ||
|
|
||
| acs.hasRole[_account][_role] = false; | ||
|
|
||
| // Clear expiry timestamp only when the role existed | ||
| s.roleExpiry[_account][_role] = 0; | ||
|
Comment on lines
+125
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the approach different with the non lib ones where this is return early. but the issue are fixed on both cases. thanks |
||
|
|
||
| emit TemporalRoleRevoked(_role, _account, msg.sender); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /// @notice function to check if an account has a valid (non-expired) role. | ||
| /// @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 role has expired. | ||
| function requireValidRole(bytes32 _role, address _account) internal view { | ||
| AccessControlStorage storage acs = getAccessControlStorage(); | ||
| AccessControlTemporalStorage storage s = getStorage(); | ||
|
|
||
| // Check if account has the role | ||
| if (!acs.hasRole[_account][_role]) { | ||
| revert AccessControlUnauthorizedAccount(_account, _role); | ||
| } | ||
|
|
||
| // Check if role has expired | ||
| uint256 expiry = s.roleExpiry[_account][_role]; | ||
| if (expiry > 0 && block.timestamp >= expiry) { | ||
| revert AccessControlRoleExpired(_role, _account); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good. thanks for the fix