Skip to content

Add Roles spike #52

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions src/access/roles/Roles.sol
Original file line number Diff line number Diff line change
@@ -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;
}
42 changes: 42 additions & 0 deletions src/access/roles/RolesStorage.sol
Original file line number Diff line number Diff line change
@@ -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)));
}
}
94 changes: 94 additions & 0 deletions src/access/roles/interface/IRoles.sol
Original file line number Diff line number Diff line change
@@ -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;
}