Access Control Registry
Abstract
The Access Control Registry (ACR) standard defines a universal interface for managing role-based access control across multiple smart contracts. This standard introduces a centralized registry system allowing access control management for multiple smart contracts. The single access-control registry smart contract manages the user roles across multiple contracts, and can be queryed for contract-specific role information. Additionally, the ACR standard provides functionality to grant and revoke roles for specific accounts, either individually or in bulk, ensuring that only authorized users can perform specific actions within a specific contract.
The core of the standard includes:
-
Registration and Unregistration: Contracts can register with the ACR, specifying an admin who can manage roles within the contract. Contracts can also be unregistered when they are no longer active.
-
Role Management: Admins can grant or revoke roles for accounts, either individually or in batches, ensuring fine-grained control over who can perform what actions within a contract.
-
Role Verification: Any account can verify if another account has a specific role in a registered contract, providing transparency and facilitating easier integration with other systems.
By centralizing access control management, the ACR standard aims to reduce redundancy, minimize errors in access control logic, and provide a clear and standardized approach to role management across smart contracts. This improves security and maintainability, making it easier for developers to implement robust access control mechanisms in their applications.
Motivation
As decentralized applications (dApps) grow in complexity, managing access control across multiple smart contracts becomes increasingly difficult. Current practices involve bespoke implementations, leading to redundancy and potential security flaws. A standardized approach for managing roles and permissions will ensure better interoperability, security, and transparency. By providing a unified interface for registering contracts and managing roles, this standard simplifies development, ensures consistency and enhances security. It facilitates easier integration and auditing, fostering a more robust and interoperable ecosystem.
The advantages of using the provided system might be:
Structured smart contracts management via specialized contracts. Ad-hoc access-control provision of a protocol. Ability to specify custom access control rules to maintain the protocol.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
The AccessControlRegistry
contract provides a standardized interface for managing access control in Ethereum smart contracts. It includes functions to register and unregister contracts, grant and revoke roles for specific contracts, and check if an account has a particular role in a registered contract. Events are emitted for contract registration, unregistration, role grants, and role revocations, ensuring transparency and traceability of access control changes.
Additionally, the AccessControlRegistry MUST reject the registration of zero addresses.
pragma solidity 0.8.23;
interface IAccessControlRegistry {
// Emitted when a contract is registered.
// @param _contract The address of the registered contract.
// @param _admin The address of the admin for the registered contract.
event ContractRegistered(address indexed _contract, address indexed _admin);
// Emitted when a contract is unregistered.
// @param _contract The address of the unregistered contract.
// @param _admin The address who unregistered the contract
event ContractUnregistered(address indexed _contract, address indexed _admin);
// Emitted when a role is granted to an account for a contract.
// @param targetContract The address of the contract.
// @param role The role being granted.
// @param account The address of the account.
event RoleGranted(
address indexed targetContract,
bytes32 indexed role,
address indexed account
);
// Emitted when a role is revoked from an account for a contract.
// @param targetContract The address of the contract.
// @param role The role being revoked.
// @param account The address of the account.
event RoleRevoked(
address indexed targetContract,
bytes32 indexed role,
address indexed account
);
// Registers a contract with the given admin.
// @param _admin The address of the admin for the registered contract.
function registerContract(address _admin) external;
// Unregisters a contract.
// @param _contract The address of the contract to unregister.
function unRegisterContract(address _contract) external;
// Grants roles to multiple accounts for multiple contracts.
// @param targetContracts An array of contract addresses to which roles will be granted.
// @param roles An array of roles to be granted.
// @param accounts An array of accounts to be granted the roles.
function grantRole(
address[] memory targetContracts,
bytes32[] memory roles,
address[] memory accounts
) external;
// Revokes roles from multiple accounts for multiple contracts.
// @param targetContracts An array of contract addresses from which roles will be revoked.
// @param roles An array of roles to be revoked.
// @param accounts An array of accounts from which the roles will be revoked.
function revokeRole(
address[] memory targetContracts,
bytes32[] memory roles,
address[] memory accounts
) external;
//Gets the information of a registered contract.
//@param _contract The address of the contract to get the information.
//@return isActive Whether the contract is active.
//@return admin The address of the admin for the contract.
//MUST revert if the registered contract doesn't exist
function getContractInfo(
address _contract
) external view returns (bool isActive, address admin);
// Gets the information of a registered contract.
// @param _contract The address of the contract to get the information.
// @return isActive Whether the contract is active.
// @return admin The address of the admin for the contract.
// MUST revert if the registered contract doesn't exist`
function getRoleInfo(
address _contract
) external view returns (bool isActive, address admin);
}
Rationale
The IAccessControlRegistry
interface aims to provide a standardized way to manage access control across multiple contracts within the ecosystem. By defining a clear structure and set of events, this interface helps streamline the process of registering, unregistering, and managing roles for contracts. The rationale for each function and event is as follows:
Contract Registration and Unregistration
registerContract(address _admin): This function allows the registration of a new contract along with its admin address. This is crucial for initializing the access control settings for a contract and ensuring that there is an accountable admin who can manage roles and permissions.
unRegisterContract(address _contract): This function enables the removal of a contract from the registry. Unregistering a contract is important when a contract is no longer in use or needs to be decommissioned to prevent unauthorized access.
Role Management
grantRole(address[] memory targetContracts, bytes32[] memory roles, address[] memory accounts): This function allows the assignment of roles to multiple accounts for multiple contracts in a single transaction. This bulk operation is designed to reduce the gas costs and simplify the process of role assignment in large systems with numerous contracts and users.
revokeRole(address[] memory targetContracts, bytes32[] memory roles, address[] memory accounts): Similar to grantRole
, this function facilitates the revocation of roles from multiple accounts across multiple contracts in a single transaction. This ensures efficient management of permissions, especially in scenarios where many users need their roles updated simultaneously.
Role Checking
getRoleInfo(address targetContract, address account, bytes32 role): This view function allows the verification of whether a particular account holds a specific role for a given contract. This is essential for ensuring that operations requiring specific permissions are performed only by authorized users.
Contract Information Retrieval
getContractInfo(address _contract): This function provides the ability to retrieve the status and admin information of a registered contract. It enhances transparency and allows administrators and users to easily query the status and management of any contract within the registry.
Events
ContractRegistered(address indexed _contract, address indexed _admin): Emitted when a new contract is registered, this event ensures that there is a public record of contract registrations, facilitating auditability and transparency.
ContractUnregistered(address indexed _contract, address indexed _admin): Emitted when a contract is unregistered, this event serves to notify the system and its users of the removal of a contract from the registry, which is critical for maintaining an up-to-date and accurate registry.
RoleGranted(address indexed targetContract, bytes32 indexed role, address indexed account): Emitted when a role is granted to an account, this event provides a public log that can be used to track role assignments and changes, ensuring that role grants are transparent and verifiable.
RoleRevoked(address indexed targetContract, bytes32 indexed role, address indexed account): Emitted when a role is revoked from an account, this event similarly ensures that role revocations are publicly logged and traceable, supporting robust access control management.
Reference Implementation
The register
function must be invoked from the registering smart contract.
The grantRole
and revokeRole
functions must be invoked either from the registered contract or the admin of the registered contract.
pragma solidity 0.8.23;
import "./IAccessControlRegistry.sol";
contract AccessControlRegistry is IAccessControlRegistry {
// Contains information about a registered contract.
// @param isActive Indicates whether the contract is active.
// @param admin The address of the admin for the registered contract.
struct ContractInfo {
bool isActive;
address admin;
}
// Mapping to store information of registered contracts
mapping(address => ContractInfo) public contracts;
// Mapping to track roles assigned to accounts for specific contracts
mapping(address => mapping(address => mapping(bytes32 => bool))) public _contractRoles;
// Custom error to handle duplicate registration attempts
error ContractAlreadyRegistered();
// Modifier to check if the caller is an admin or the contract itself
modifier onlyAdminOrContract(address _contract) {
require(
_isAdmin(_contract, msg.sender) ||
(contracts[msg.sender].isActive && msg.sender == _contract),
"Caller is not admin nor contract"
);
_;
}
// Modifier to check if the caller is an admin of the contract
modifier onlyAdmin(address _contract) {
require(
_isAdmin(_contract, msg.sender),
"Caller is not an admin"
);
_;
}
// Modifier to ensure the contract is active
modifier onlyActiveContract(address _contract) {
require(contracts[_contract].isActive, "Contract not registered");
_;
}
// Modifier to validate if the provided address is non-zero
modifier validAddress(address addr) {
require(addr != address(0), "Invalid address");
_;
}
// Registers a contract with the given admin
// _admin: Address of the admin to register
function registerContract(address _admin) external validAddress(_admin) {
address _contract = msg.sender;
// Check if the contract is already registered
ContractInfo storage contractInfo = contracts[_contract];
if (contractInfo.isActive) {
revert ContractAlreadyRegistered();
}
// Register the contract with the provided admin
contractInfo.isActive = true;
contractInfo.admin = _admin;
emit ContractRegistered(_contract, _admin);
}
// Unregisters a contract
// _contract: Address of the contract to unregister
function unRegisterContract(address _contract)
public
onlyAdmin(_contract)
onlyActiveContract(_contract)
{
ContractInfo storage contractInfo = contracts[_contract];
contractInfo.isActive = false;
contractInfo.admin = address(0);
emit ContractUnregistered(_contract, msg.sender);
}
// Grants roles to multiple accounts for multiple contracts
// targetContracts: Array of contract addresses
// roles: Array of roles to grant
// accounts: Array of accounts to assign the roles
function grantRole(
address[] memory targetContracts,
bytes32[] memory roles,
address[] memory accounts
) public {
require(
targetContracts.length == roles.length &&
roles.length == accounts.length,
"Array lengths do not match"
);
uint256 cachedArrayLength = roles.length;
// Grant roles in a batch
for (uint256 i; i < cachedArrayLength; ++i) {
_grantRole(targetContracts[i], roles[i], accounts[i]);
}
}
// Revokes roles from multiple accounts for multiple contracts
// targetContracts: Array of contract addresses
// roles: Array of roles to revoke
// accounts: Array of accounts from which roles are revoked
function revokeRole(
address[] memory targetContracts,
bytes32[] memory roles,
address[] memory accounts
) public {
require(
targetContracts.length == roles.length &&
roles.length == accounts.length,
"Array lengths do not match"
);
uint256 cachedArrayLength = roles.length;
// Revoke roles in a batch
for (uint256 i; i < cachedArrayLength; ++i) {
_revokeRole(targetContracts[i], roles[i], accounts[i]);
}
}
// Retrieves information of a registered contract
// _contract: Address of the contract
// Returns: isActive status and admin address
function getContractInfo(address _contract)
public
view
returns (bool isActive, address admin)
{
ContractInfo storage info = contracts[_contract];
return (info.isActive, info.admin);
}
// Gets role information for an account and contract
// targetContract: Address of the target contract
// account: Address of the account
// role: Role identifier
// Returns: Boolean indicating if the account has the role
function getRoleInfo(
address targetContract,
address account,
bytes32 role
) public view returns (bool) {
return _contractRoles[targetContract][account][role];
}
// Internal function to grant a role to an account for a contract
function _grantRole(
address targetContract,
bytes32 role,
address account
)
internal
onlyAdminOrContract(targetContract)
onlyActiveContract(targetContract)
validAddress(account)
{
_contractRoles[targetContract][account][role] = true;
emit RoleGranted(targetContract, role, account);
}
// Internal function to revoke a role from an account for a contract
function _revokeRole(
address targetContract,
bytes32 role,
address account
)
internal
onlyAdminOrContract(targetContract)
onlyActiveContract(targetContract)
validAddress(account)
{
require(
_contractRoles[targetContract][account][role],
"Role already revoked"
);
_contractRoles[targetContract][account][role] = false;
emit RoleRevoked(targetContract, role, account);
}
// Checks if the caller is an admin for the contract
// _contract: Address of the contract
// _admin: Address of the admin
// Returns: Boolean indicating admin status
function _isAdmin(address _contract, address _admin) internal view returns (bool) {
return _admin == contracts[_contract].admin;
}
}
Design Decisions
There are a few design decisions that have to be explicitly specified to ensure the functionality, security, and efficiency of the IAccessControlRegistry
:
Decentralized Contract Registration
No Central Owner: There is no central owner who can register contracts. This design choice promotes decentralization and ensures that individual contracts are responsible for their own registration and management.
Efficient Storage and Lookup
Mapping Utilization: The use of mappings for storing contract information (mapping(address => ContractInfo) private contracts
) and role assignments (mapping(address => mapping(address => mapping(bytes32 => bool))) private _contractRoles
) ensures efficient storage and lookup. This is crucial for maintaining performance in a large-scale system with numerous contracts and roles.
Role Management Flexibility
Bulk Operations: Functions like grantRole
and revokeRole
allow for the assignment and revocation of roles to multiple accounts for multiple contracts in a single transaction. This bulk operation reduces gas costs and simplifies the process of role management in large systems.
Robust Security Measures
Admin-Only Operations: Functions that modify the state, such as unRegisterContract, _grantRole
, and _revokeRole
, are restricted to contract admins. This ensures that only authorized personnel can manage contracts and roles, reducing the risk of unauthorized changes.
Valid Address Checks: The validAddress
modifier ensures that addresses are non-zero, preventing potential issues with null addresses which could lead to unintended behavior or security vulnerabilities.
Active Contract Checks: The onlyActiveContract
modifier ensures that actions are only performed on active or registered contracts, preventing operations on inactive or unregistered contracts.
Transparent Auditing
Event Logging: Emitting events for each significant action (registration, unregistration, role granting, and revocation) provides a transparent log that can be monitored and audited. This helps in detecting and responding to unauthorized or suspicious activities promptly.
Security Considerations
The AccessControlRegistry
implements several security measures to ensure the integrity and reliability of the access control system:
Admin-Only Restrictions: By limiting state-modifying functions to contract admins, the system prevents unauthorized users from making critical changes.
Active Contract Checks: Operations are restricted to active contracts, reducing the risk of interacting with deprecated or unregistered contracts.
Event Logging: Comprehensive event logging supports transparency and auditability, allowing for effective monitoring and detection of unauthorized actions.
Copyright
Copyright and related rights waived via CC0.