Skip to content

Commit

Permalink
feat: OzAccessControl
Browse files Browse the repository at this point in the history
  • Loading branch information
alrxy committed Nov 27, 2024
1 parent 70f9ef7 commit b24cbc0
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 14 deletions.
92 changes: 83 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,30 @@ This repository provides a framework for developing middleware in a modular and

- **Extensions**: Modular contracts that provide additional functionalities. Key extensions include:

- **Operators**: Manages operator registration and vault relationships.
- **Operators**: Manages operator registration and operator's vault.

- **KeyStorage**: Manages operator keys. Variants include `KeyStorage256`, `KeyStorageBytes`, and `NoKeyStorage`.

- **AccessManager**: Controls access to restricted functions. Implementations include `OwnableAccessManager`, `OzAccessManaged`, and `NoAccessManager`.
- **AccessManager**: Controls access to restricted functions. Implementations include `OwnableAccessManager`, `OzAccessControl`, `OzAccessManaged`, and `NoAccessManager`.

- **CaptureTimestamp**: Captures the active state at specific timestamps. Options are `EpochCapture` and `TimestampCapture`.

- **Signature Verification**: Verifies operator signatures. Implementations include `ECDSASig` and `EdDSASig`.

- **StakePower**: Calculates operator power based on stake. Implementations include `EqualStakePower` for 1:1 stake-to-power ratio, and can be extended for custom power calculations.

- **SharedVaults**: Manages vaults shared between all operators.

- **Subnetworks**: Manages subnetworks.


## Middleware Examples

Below are examples of middleware implementations using different combinations of the extensions.

#### SimplePosMiddleware
```solidity
contract SimplePosMiddleware is SharedVaults, Operators, KeyStorage256, OwnableAccessManager, EpochCapture {
contract SimplePosMiddleware is SharedVaults, Operators, KeyStorage256, OwnableAccessManager, EpochCapture, EqualStakePower {
// Implementation details...
}
```
Expand All @@ -40,7 +45,7 @@ Features:
#### SqrtTaskMiddleware

```solidity
contract SqrtTaskMiddleware is SharedVaults, Operators, NoKeyStorage, EIP712, OwnableAccessManager, TimestampCapture {
contract SqrtTaskMiddleware is SharedVaults, Operators, NoKeyStorage, EIP712, OwnableAccessManager, TimestampCapture, EqualStakePower {
// Implementation details...
}
```
Expand All @@ -54,7 +59,7 @@ Features:
#### SelfRegisterMiddleware

```solidity
contract SelfRegisterMiddleware is SharedVaults, SelfRegisterOperators, KeyStorage256, ECDSASig, NoAccessManager, TimestampCapture {
contract SelfRegisterMiddleware is SharedVaults, SelfRegisterOperators, KeyStorage256, ECDSASig, NoAccessManager, TimestampCapture, EqualStakePower {
// Implementation details...
}
```
Expand All @@ -75,7 +80,7 @@ contract SelfRegisterEd25519Middleware is SharedVaults, SelfRegisterOperators, K

Features:

- Similar to `SelfRegisterMiddleware` but uses Ed25519 keys and signatures.
- Similar to `SelfRegisterMiddleware` but uses Ed25519 keys and EdDSA signatures.

## Getting Started

Expand All @@ -85,7 +90,41 @@ To develop your middleware:

2. **Choose Extensions**: Based on your requirements, include extensions for operator management, key storage, access control, and timestamp capturing.

3. **Initialize Properly**: Ensure all inherited contracts are properly initialized. For upgradeable contracts, use the `initializer` modifier and call `_disableInitializers` in the constructor to prevent double initialization.
3. **Initialize Properly**: Ensure all inherited contracts are properly initialized:

- Use the `initializer` modifier on your initialization function
- Call `_disableInitializers()` in the constructor for upgradeable contracts
- Initialize all inherited contracts in the correct order
- Pass required parameters to each contract's initialization function
- Follow initialization order from most base to most derived contract
- Note: If your contract is not upgradeable, initialization can be done directly in the constructor:
```solidity
constructor(
address network,
uint48 slashingWindow,
address vaultRegistry,
address operatorRegistry,
address operatorNetOptIn,
address admin
) {
initialize(network, slashingWindow, vaultRegistry, operatorRegistry, operatorNetOptIn, admin);
}
```
- Example initialization pattern:
```solidity
function initialize(
address network,
uint48 slashingWindow,
address vaultRegistry,
address operatorRegistry,
address operatorNetOptIn,
address admin
) public initializer {
__BaseMiddleware_init(network, slashingWindow, vaultRegistry, operatorRegistry, operatorNetOptIn);
__OzAccessManaged_init(admin);
__AdditionalExtension_init();
}
```
4. **Implement Required Functions**: Override functions as needed to implement your middleware's logic.
Expand All @@ -111,14 +150,49 @@ contract MyCustomMiddleware is BaseMiddleware, Operators, KeyStorage256, Ownable
}
```

5. **Configure OzAccessControl Roles**: When using OzAccessControl, set up roles and permissions:

```solidity
// Define role identifiers as constants
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
bytes32 public constant VAULT_ROLE = keccak256("VAULT_ROLE");
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
function initialize(...) public initializer {
// Initialize base contracts
__BaseMiddleware_init(...);
__OzAccessControl_init(admin);
// Set up role hierarchy
_setRoleAdmin(OPERATOR_ROLE, MANAGER_ROLE); // Manager role can grant/revoke operator role
_setRoleAdmin(VAULT_ROLE, MANAGER_ROLE); // Manager role can grant/revoke vault role
_setRoleAdmin(MANAGER_ROLE, DEFAULT_ADMIN_ROLE); // Default admin can grant/revoke manager role
// Assign roles to function selectors
_setSelectorRole(this.registerOperator.selector, OPERATOR_ROLE);
_setSelectorRole(this.registerVault.selector, VAULT_ROLE);
_setSelectorRole(this.updateParameters.selector, MANAGER_ROLE);
// Grant initial roles
_grantRole(MANAGER_ROLE, admin);
}
```

## Notes

- **Storage Slots**: When creating extensions, ensure you follow the ERC-7201 standard for storage slot allocation to prevent conflicts.

- **Versioning**: Include a public constant variable for versioning in your contracts (e.g., `uint64 public constant MyExtension_VERSION = 1;`).

- **Access Control**: Choose an appropriate `AccessManager` based on your needs. For unrestricted access, use `NoAccessManager`. For owner-based access, use `OwnableAccessManager`.

- **Access Control**: Choose an appropriate `AccessManager` based on your needs:
- `NoAccessManager`: Allows unrestricted access to all functions
- `OwnableAccessManager`: Restricts access to a single owner address
- `OzAccessControl`: Implements OpenZeppelin-style role-based access control where different roles can be assigned to specific function selectors. Roles can be granted and revoked by role admins, with a default admin role that can manage all other roles. Roles can be set up by:
1. Granting roles to addresses using `grantRole(bytes32 role, address account)`
2. Setting role admins with `_setRoleAdmin(bytes32 role, bytes32 adminRole)`
3. Assigning roles to function selectors via `_setSelectorRole(bytes4 selector, bytes32 role)`
- `OzAccessManaged`: Wraps OpenZeppelin's AccessManaged contract to integrate with external access control systems

- **Key Storage**: Select a `KeyStorage` implementation that fits your key requirements. Use `KeyStorage256` for 256-bit keys, `KeyStorageBytes` for arbitrary-length keys, or `NoKeyStorage` if keys are not needed.

This framework provides flexibility in building middleware by allowing you to mix and match various extensions based on your requirements. By following the modular approach and best practices outlined, you can develop robust middleware solutions that integrate seamlessly with the network.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ abstract contract NoAccessManager is BaseMiddleware {
* @notice Checks access and always allows access
* @dev This function is called internally to enforce access control and will always allow access
*/
function _checkAccess() internal pure override {
function _checkAccess() internal pure virtual override {
// Allow all access by default
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ abstract contract OwnableAccessManager is BaseMiddleware {
* @notice Checks if the caller has access (is the owner)
* @dev Reverts if the caller is not the owner
*/
function _checkAccess() internal view override {
function _checkAccess() internal view virtual override {
if (msg.sender != _owner()) {
revert OnlyOwnerCanCall(msg.sender);
}
Expand Down
192 changes: 192 additions & 0 deletions src/middleware/extensions/access-managers/OzAccessControl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {BaseMiddleware} from "../../BaseMiddleware.sol";

/**
* @title OzAccessControl
* @notice A middleware extension that implements role-based access control
* @dev Implements BaseMiddleware with role-based access control functionality
*/
abstract contract OzAccessControl is BaseMiddleware {
uint64 public constant OzAccessControl_VERSION = 1;

struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}

bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

/// @custom:storage-location erc7201:symbiotic.storage.OzAccessControl
struct OzAccessControlStorage {
mapping(bytes32 role => RoleData) _roles;
mapping(bytes4 selector => bytes32 role) _selectorRoles;
}

// keccak256(abi.encode(uint256(keccak256("symbiotic.storage.OzAccessControl")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant OzAccessControlStorageLocation =
0xbe09a78a256419d2b885312b60a13e8082d8ab3c36c463fff4fbb086f1e96f00;

function _getOzAccessControlStorage() private pure returns (OzAccessControlStorage storage $) {
assembly {
$.slot := OzAccessControlStorageLocation
}
}

error AccessControlUnauthorizedAccount(address account, bytes32 role);
error AccessControlBadConfirmation();

event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
event SelectorRoleSet(bytes4 indexed selector, bytes32 indexed role);

/**
* @notice Initializes the contract with a default admin
* @param defaultAdmin The address to set as the default admin
*/
function __OzAccessControl_init(
address defaultAdmin
) internal onlyInitializing {
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
}

/**
* @notice Returns true if account has been granted role
* @param role The role to check
* @param account The account to check
* @return bool True if account has role
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
OzAccessControlStorage storage $ = _getOzAccessControlStorage();
return $._roles[role].hasRole[account];
}

/**
* @notice Returns the admin role that controls the specified role
* @param role The role to get the admin for
* @return bytes32 The admin role
*/
function getRoleAdmin(
bytes32 role
) public view virtual returns (bytes32) {
OzAccessControlStorage storage $ = _getOzAccessControlStorage();
return $._roles[role].adminRole;
}

/**
* @notice Returns the role required for a function selector
* @param selector The function selector
* @return bytes32 The required role
*/
function getRole(
bytes4 selector
) public view virtual returns (bytes32) {
OzAccessControlStorage storage $ = _getOzAccessControlStorage();
return $._selectorRoles[selector];
}

/**
* @notice Grants role to account if caller has admin role
* @param role The role to grant
* @param account The account to grant the role to
*/
function grantRole(bytes32 role, address account) public virtual {
bytes32 adminRole = getRoleAdmin(role);
if (!hasRole(adminRole, msg.sender)) {
revert AccessControlUnauthorizedAccount(msg.sender, adminRole);
}
_grantRole(role, account);
}

/**
* @notice Revokes role from account if caller has admin role
* @param role The role to revoke
* @param account The account to revoke the role from
*/
function revokeRole(bytes32 role, address account) public virtual {
bytes32 adminRole = getRoleAdmin(role);
if (!hasRole(adminRole, msg.sender)) {
revert AccessControlUnauthorizedAccount(msg.sender, adminRole);
}
_revokeRole(role, account);
}

/**
* @notice Allows an account to renounce a role they have
* @param role The role to renounce
* @param callerConfirmation Address of the caller for confirmation
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != msg.sender) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}

/**
* @notice Sets the role required for a function selector
* @param selector The function selector
* @param role The required role
*/
function _setSelectorRole(bytes4 selector, bytes32 role) internal virtual {
OzAccessControlStorage storage $ = _getOzAccessControlStorage();
$._selectorRoles[selector] = role;
emit SelectorRoleSet(selector, role);
}

/**
* @notice Sets the admin role for a role
* @param role The role to set admin for
* @param adminRole The new admin role
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
OzAccessControlStorage storage $ = _getOzAccessControlStorage();
bytes32 previousAdminRole = getRoleAdmin(role);
$._roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}

/**
* @notice Internal function to grant a role
* @param role The role to grant
* @param account The account to grant the role to
* @return bool True if role was granted
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
if (!hasRole(role, account)) {
OzAccessControlStorage storage $ = _getOzAccessControlStorage();
$._roles[role].hasRole[account] = true;
emit RoleGranted(role, account, msg.sender);
return true;
}
return false;
}

/**
* @notice Internal function to revoke a role
* @param role The role to revoke
* @param account The account to revoke the role from
* @return bool True if role was revoked
*/
function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
if (hasRole(role, account)) {
OzAccessControlStorage storage $ = _getOzAccessControlStorage();
$._roles[role].hasRole[account] = false;
emit RoleRevoked(role, account, msg.sender);
return true;
}
return false;
}

/**
* @notice Checks access based on role required for the function selector
* @dev Implements BaseMiddleware's _checkAccess function
*/
function _checkAccess() internal view virtual override {
if (!hasRole(getRole(msg.sig), msg.sender)) {
revert AccessControlUnauthorizedAccount(msg.sender, getRole(msg.sig));
}
}
}
6 changes: 3 additions & 3 deletions src/middleware/extensions/access-managers/OzAccessManaged.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {BaseMiddleware} from "../../BaseMiddleware.sol";

/**
* @title OzAccessManaged
* @notice A middleware extension that integrates OpenZeppelin's AccessManager for access control
* @notice A middleware extension that integrates OpenZeppelin's AccessManaged for access control
* @dev Implements BaseMiddleware with OpenZeppelin's AccessManagedUpgradeable functionality
*/
abstract contract OzAccessManaged is BaseMiddleware, AccessManagedUpgradeable {
Expand All @@ -25,10 +25,10 @@ abstract contract OzAccessManaged is BaseMiddleware, AccessManagedUpgradeable {
}

/**
* @notice Checks if the caller has access through the OpenZeppelin AccessManager
* @notice Checks if the caller has access through the OpenZeppelin AccessManaged
* @dev Delegates access check to OpenZeppelin's _checkCanCall function
*/
function _checkAccess() internal override {
function _checkAccess() internal virtual override {
_checkCanCall(msg.sender, msg.data);
}
}

0 comments on commit b24cbc0

Please sign in to comment.