diff --git a/src/access/roles/Roles.sol b/src/access/roles/Roles.sol new file mode 100644 index 0000000..0669c2f --- /dev/null +++ b/src/access/roles/Roles.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {IRoles} from "./interface/IRoles.sol"; +import {RolesStorage as Storage} from "./RolesStorage.sol"; + +abstract contract Roles is IRoles { + /*=========== + VIEWS + ===========*/ + + /// @inheritdoc IRoles + function hasRole(bytes32 role, address account) public view virtual returns (bool) { + (bytes32 grantedRoleKey, bytes20 roleSuffix) = Storage._packKey(account, role); + Storage.GrantedRoleData memory grantedRole = Storage.layout()._grantedRoles[grantedRoleKey]; + return grantedRole.exists && grantedRole.roleSuffix == roleSuffix; + } + + /// @inheritdoc IRoles + function getAllGrantedRoles() public view returns (GrantedRole[] memory grantedRoles) { + Storage.Layout storage layout = Storage.layout(); + uint256 len = layout._grantedRoleKeys.length; + grantedRoles = new GrantedRole[](len); + for (uint256 i; i < len; i++) { + bytes32 grantedRoleKey = layout._grantedRoleKeys[i]; + (address account, bytes12 rolePrefix) = Storage._unpackKey(grantedRoleKey); + Storage.GrantedRoleData memory grantedRole = layout._grantedRoles[grantedRoleKey]; + grantedRoles[i] = + GrantedRole(Storage._stitchRole(rolePrefix, grantedRole.roleSuffix), account, uint40(block.timestamp)); + } + return grantedRoles; + } + + /// @inheritdoc IRoles + function checkRole(bytes32 role, address account) public view { + _checkRole(role, account); + } + + /// @dev Function to implement ERC-165 compliance + /// @param interfaceId The interface identifier to check. + /// @return _ Boolean indicating whether the contract supports the specified interface. + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IRoles).interfaceId; + } + + /*============= + SETTERS + =============*/ + + /// @inheritdoc IRoles + function grantRole(bytes32 role, address account) public virtual { + _checkCanUpdateRoles(); + _grantRole(role, account); + } + + /// @inheritdoc IRoles + function revokeRole(bytes32 role, address account) public virtual { + _checkCanUpdateRoles(); + _revokeRole(role, account); + } + + /// @inheritdoc IRoles + function renounceRole(bytes32 role, address account) public virtual { + require(msg.sender == account); + _revokeRole(role, account); + } + + /*=============== + INTERNALS + ===============*/ + + function _grantRole(bytes32 role, address account) internal { + Storage.Layout storage layout = Storage.layout(); + (bytes32 grantedRoleKey, bytes20 roleSuffix) = Storage._packKey(account, role); + if (layout._grantedRoles[grantedRoleKey].exists) { + if (layout._grantedRoles[grantedRoleKey].roleSuffix == roleSuffix) { + bytes12 rolePrefix = bytes12(role); + revert RolePrefixCollision( + role, bytes32(uint256(uint96(rolePrefix)) << 160 | uint256(uint160(roleSuffix))) + ); + } else { + revert RoleAlreadyGranted(role, account); + } + } + // new length will be `len + 1`, so this grantedRole has index `len` + Storage.GrantedRoleData memory grantedRole = + Storage.GrantedRoleData(uint24(layout._grantedRoleKeys.length), uint40(block.timestamp), true, roleSuffix); + + layout._grantedRoles[grantedRoleKey] = grantedRole; + layout._grantedRoleKeys.push(grantedRoleKey); // set new grantedRoleKey at index and increment length + + emit RoleGranted(role, account, msg.sender); + } + + function _revokeRole(bytes32 role, address account) internal { + Storage.Layout storage layout = Storage.layout(); + (bytes32 grantedRoleKey, bytes20 roleSuffix) = Storage._packKey(account, role); + Storage.GrantedRoleData memory oldGrantedRoleData = layout._grantedRoles[grantedRoleKey]; + if (!(oldGrantedRoleData.exists && oldGrantedRoleData.roleSuffix == roleSuffix)) { + revert RoleNotGranted(role, account); + } + + uint256 lastIndex = layout._grantedRoleKeys.length - 1; + // if removing item not at the end of the array, swap item with last in array + if (oldGrantedRoleData.index < lastIndex) { + bytes32 lastRoleKey = layout._grantedRoleKeys[lastIndex]; + Storage.GrantedRoleData memory lastGrantedRoleData = layout._grantedRoles[lastRoleKey]; + lastGrantedRoleData.index = oldGrantedRoleData.index; + layout._grantedRoleKeys[oldGrantedRoleData.index] = lastRoleKey; + layout._grantedRoles[lastRoleKey] = lastGrantedRoleData; + } + delete layout._grantedRoles[grantedRoleKey]; + layout._grantedRoleKeys.pop(); // delete guard in last index and decrement length + + emit RoleRevoked(role, account, msg.sender); + } + + /*=================== + AUTHORIZATION + ===================*/ + + modifier onlyRole(bytes32 role) { + _checkRole(role, msg.sender); + _; + } + + /// @dev Function to ensure `account` has grantedRole to carry out `role` + function _checkRole(bytes32 role, address account) internal view { + if (!hasRole(role, account)) revert RoleNotGranted(role, account); + } + + /// @dev Function to implement access control restricting setter functions + function _checkCanUpdateRoles() internal virtual; +} diff --git a/src/access/roles/RolesStorage.sol b/src/access/roles/RolesStorage.sol new file mode 100644 index 0000000..58f3c1e --- /dev/null +++ b/src/access/roles/RolesStorage.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.8; + +library RolesStorage { + bytes32 internal constant SLOT = keccak256(abi.encode(uint256(keccak256("0xrails.Roles")) - 1)); + + struct Layout { + bytes32[] _grantedRoleKeys; + mapping(bytes32 => GrantedRoleData) _grantedRoles; + } + + struct GrantedRoleData { + uint24 index; // [0..23] + uint40 updatedAt; // [24..63] + bool exists; // [64-71] + bytes20 roleSuffix; // [72..232] + } + + function layout() internal pure returns (Layout storage l) { + bytes32 slot = SLOT; + assembly { + l.slot := slot + } + } + + // key = 0x{address}{rolePrefix} where rolePrefix is the first 12 bytes of bytes32 role + function _packKey(address account, bytes32 role) internal pure returns (bytes32 key, bytes20 roleSuffix) { + key = bytes32(uint256(uint160(account)) << 96 | (uint96(bytes12(role)))); + return (key, bytes20(uint160(uint256(role)))); + } + + function _unpackKey(bytes32 key) internal pure returns (address account, bytes12 rolePrefix) { + account = address(bytes20(key)); + rolePrefix = bytes12(uint96(uint256(key))); + return (account, rolePrefix); + } + + function _stitchRole(bytes12 rolePrefix, bytes20 roleSuffix) internal pure returns (bytes32 role) { + return bytes32(uint256(bytes32(rolePrefix)) | uint256(uint160(roleSuffix))); + } +} diff --git a/src/access/roles/interface/IRoles.sol b/src/access/roles/interface/IRoles.sol new file mode 100644 index 0000000..1881379 --- /dev/null +++ b/src/access/roles/interface/IRoles.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {RolesStorage} from "../RolesStorage.sol"; + +/// @notice Since the Solidity compiler ignores inherited functions, function declarations are made +/// at the top level so their selectors are properly XORed into a nonzero `interfaceId` +interface IRoles { + struct GrantedRole { + bytes32 role; + address account; + uint40 updatedAt; + } + + error RolePrefixCollision(bytes32 role1, bytes32 role2); + error RoleAlreadyGranted(bytes32 role, address account); + error RoleNotGranted(bytes32 role, address account); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {AccessControl-_setupRole}. + */ + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) external view returns (bool); + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {AccessControl-_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) external view returns (bytes32); + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function grantRole(bytes32 role, address account) external; + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function revokeRole(bytes32 role, address account) external; + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `callerConfirmation`. + */ + function renounceRole(bytes32 role, address callerConfirmation) external; + + /// @dev Function to get an array of all existing Role structs. + function getAllGrantedRoles() external view returns (GrantedRole[] memory); + + /// @dev Function to provide reverts when checks for `hasRole()` fails + /// @param role The role to check + /// @param account The account address whose permission to check + function checkRole(bytes32 role, address account) external view; +}