Abstract
This standard defines rules for mapping contract role naming to security semantics, enabling authorization sources and role purposes to be derived directly from role names. Building on the role.{category}.{action} naming syntax of the Contract Role Naming Standard, this standard specifies two semantic mechanisms — the Governor authorization and revocation pattern and the Category-Action registry — exposed through NatSpec annotations for off-chain consumption and the IERCRoleSemantics interface for on-chain queries.
Motivation
Smart contract role naming currently lacks a unified standard — protocols name roles freely, and role identifiers generally do not encode security information such as hierarchy, dependencies, purpose, and authorization relationships. This naming limitation prevents critical security semantics from being read directly from role names:
- Opaque authorization relationships: Authorization relationships between roles (who can grant or revoke whose roles) are configured in a scattered manner within contract code, with no explicit association between role names and authorization relationships. The admin relationship of admin roles cannot be derived from naming to determine authorization sources.
- Indistinguishable hierarchy: In existing RBAC implementations, admin roles (typically using
bytes32(0)as identifier) and single-operation roles carry no hierarchy information in their naming, requiring per-contract source code review during cross-protocol audits. For example,role.token.mintis an atomic permission for executing operations, whilerole.token.governis a governance permission for managing authorization — the latter can grant the former, making its permissions far greater.
The common thread across these two problems is that critical security information is encoded in contract implementations and cannot be directly derived from role identifiers. Building on the Contract Role Naming Standard’s role.{category}.{action} naming syntax, this standard defines derivation rules so that role names themselves carry hierarchy, authorization, and dependency information, enabling security semantics to be read directly from naming.
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.
Contracts implementing this standard MUST first conform to the naming convention requirements of the Contract Role Naming Standard: role.{category}.{action}.
Action Lexical Classification
role.{category}.{action} — action identifiers typically carry multiple semantic meanings, with danger levels and authorization implications varying across business scenarios. This standard classifies them into three types, serving as the foundation for subsequent derivation rules:
| Category | Semantics | Examples | Classification Necessity |
|---|---|---|---|
| atomic | Execute a single business operation | role.token.mint (mint), role.bridge.relay |
Can only execute operations, cannot grant roles |
| scope | Jurisdiction over multiple operations within a domain | role.treasury.operator (manage daily treasury operations) |
Broader jurisdiction than a single atomic |
| govern | Manage granting and revoking of roles within the same domain | role.token.govern (manage token domain role assignments) |
Governance authority, can grant atomic and scope roles under the same category |
Constraints for each action type:
- atomic: Newly defined roles SHOULD prefer this type.
- scope: MUST be used with implied action context, currently reserved for future extension.
- govern: MUST only be used for role governance roles, naturally covering both granting and revocation operations. SHOULD NOT be granted to the same address as same-domain atomic roles (risk of overlapping governor and operator identities). Runtime isolation policies are defined by the security baseline framework.
Governor Authorization and Revocation Pattern
role.{category}.govern is the authorization management role for each category — holders can grant and revoke atomic roles within that category. Granting and revocation follow a symmetric principle: the roles a governor can grant, it can also revoke. This implements a separation between role assignment authority and role usage authority: the governor determines “who can use” and “who cannot use”, while atomic roles only handle “what to do”. Authorization sources can be derived without lookup tables.
role.admin.root ── role.token.govern ── role.token.mint
│ ├── role.token.burn
│ ├── role.token.freeze
│ └── role.token.unfreeze
├── role.system.govern ── role.system.pause
│ └── role.system.upgrade
└── role.bridge.govern ── role.bridge.relay
The govern role under the same category can grant all atomic and scope roles within that category. This mapping is simultaneously defined by each row of the Category-Action registry for two things: (1) valid action combinations, and (2) the corresponding governor role.
Two Implementation Paths:
- Explicit Governor Role (recommended): Define
role.{category}.governconstants and configure role admin relationships so that governors manage atomic roles. - Implicit Governor (compatible with existing contracts): Continue using
role.admin.rootas the admin for all roles, but declare the logical governor relationship through NatSpec@custom:role-governorannotations. No contract code modification needed — semantic derivation remains valid, and static analysis tools read authorization semantics from annotations.
NatSpec Semantic Annotations
Role naming does not encode hierarchy and authorization sources. Contracts RECOMMENDED use standardized NatSpec annotations at role constant declarations for static analysis tool off-chain verification:
/// @custom:role-type atomic
/// @custom:role-governor role.token.govern
bytes32 public constant ROLE_TOKEN_MINT = keccak256("role.token.mint");
/// @custom:role-type governor
/// @custom:role-grants role.token.mint,role.token.burn,role.token.freeze,role.token.unfreeze
/// @custom:role-governor role.admin.root
bytes32 public constant ROLE_TOKEN_GOVERN = keccak256("role.token.govern");
/// @custom:role-type root
bytes32 public constant ROLE_ADMIN_ROOT = keccak256("role.admin.root");
/// @custom:role-type atomic
/// @custom:role-governor role.system.govern
bytes32 public constant ROLE_SYSTEM_UPGRADE = keccak256("role.system.upgrade");
Annotation Field Specification:
| Field | Type | Semantics |
|---|---|---|
@custom:role-type |
root/governor/atomic/scope | Role type, corresponds to Action Lexical Classification (atomic/scope/govern) and the root exception |
@custom:role-governor |
Role string | Governor, i.e., who granted this role (root roles have no this field) |
@custom:role-grants |
Comma-separated role strings | Governor type only: list of roles that can be granted and revoked, forming bidirectional verification with role-governor |
Extension Mechanism: Implementers MAY define additional @custom:role-* annotation fields. New fields MUST follow the role-{dimension-name} naming prefix and declare their semantics in project documentation. Future standard versions may incorporate widely-adopted community extension fields into the core set.
Rationale
Basis for Governor Pattern
Existing RBAC implementations typically support setting an admin role for each role, but there is no explicit association between role naming and admin relationships. This standard makes authorization relationships derivable from naming through the role.{category}.govern naming pattern: the govern role under the same category is naturally the admin of that category’s atomic roles.
NatSpec Annotation Design
NatSpec annotations use the @custom: prefix following Solidity ecosystem convention. Field selection follows the principle of minimal sufficiency:
role-type: Encodes role type information, the most important security dimension that naming cannot express.role-governor: Encodes authorization source, addressing the audit need of “who granted this role.”role-grants: Only needed for governor types, encoding “the list of roles that can be granted and revoked,” forming bidirectional verification withrole-governor.
Annotations are designed as off-chain conventions (RECOMMENDED), while providing structured semantic information for static analysis tools.
Category-Action Registry
The registry defines which category-action combinations belong to known safe patterns. Combinations not in the registry are not necessarily unsafe, but when undefined category-actions appear, they SHOULD be flagged for manual review.
| Category | Atomic | Govern | Scope | Description |
|---|---|---|---|---|
| admin | — | — | root (exception) | Highest privilege role, not managed by any governor |
| token | mint, burn, freeze, unfreeze | govern | — | Token lifecycle management: mint, burn, freeze, unfreeze |
| system | pause, upgrade | govern | — | System operations: global pause, contract upgrade |
| treasury | withdraw | govern | — | Treasury: withdraw on-chain funds |
| bridge | relay | govern | — | Cross-chain relay: verify and forward cross-chain messages |
Registry Constraints:
- category-action pairs not in the registry should be flagged for manual review.
- The registry MAY be extended through community governance processes.
On-Chain Semantic Interface
Role combinatorial safety can be queried on-chain through a standardized interface:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERCRoleSemantics {
/// @notice Whether the role belongs to a known safe combination in the registry
function isKnownRole(bytes32 roleHash) external view returns (bool);
}
isKnownRole() returns false for unregistered hashes, but this does not indicate unsafety — it only indicates the need for manual review.
Implementations SHOULD declare the IERCRoleSemantics interface through EIP-165’s supportsInterface().
Action Type Naming: Design Rationale for govern
This standard selects govern as the authorization management action type for three reasons: (1) govern directly encodes role governance semantics — governors grant and revoke roles within their category; (2) govern has precise semantics — naturally covering both granting and revocation operations without ambiguity; (3) govern aligns with the role.{category}.govern naming pattern, enabling the action type to be directly derived from the action identifier.
Backwards Compatibility
This standard is designed to be compatible with EIP-5982.
Contract Role Naming Standard
This standard is an extension of the Contract Role Naming Standard and does not modify any of its specification requirements. Contracts implementing only the role naming standard can operate independently without awareness of this standard.
EIP-5982
The semantic rules defined by this standard are orthogonal to EIP-5982’s RBAC interface mechanism. isKnownRole() provides role safety combination verification, complementing EIP-5982’s hasRole() check. However, EIP-5982’s current status is Stagnant; adopters should note that its status may affect interoperability guarantees.
Existing RBAC System Compatibility
Contracts implementing this standard that also use other RBAC systems SHOULD align role admin relationships with Governor semantics:
// Example of aligning role management relationships to Governor semantics
_setRoleAdmin(ROLE_TOKEN_MINT, ROLE_TOKEN_GOVERN); // role.token.govern manages role.token.mint
_setRoleAdmin(ROLE_TOKEN_BURN, ROLE_TOKEN_GOVERN); // role.token.govern manages role.token.burn
_setRoleAdmin(ROLE_TOKEN_GOVERN, ROLE_ADMIN_ROOT); // role.admin.root manages role.token.govern
Test Cases
| Role Name | isKnownRole | Expected NatSpec Annotation | Description |
|---|---|---|---|
role.admin.root |
true | type=root | Unique exception |
role.token.govern |
true | type=governor | action=“govern” → governor type |
role.system.govern |
true | type=governor | action=“govern” → governor type |
role.token.mint |
true | type=atomic | Registered atomic role |
role.system.pause |
true | type=atomic | Registered atomic role |
role.treasury.withdraw |
true | type=atomic | Registered atomic role |
role.bridge.relay |
true | type=atomic | Registered atomic role |
| Unknown hash | false | — | Not in registry |
Reference Implementation
The following implementation is for demonstration purposes only, under the MIT license. Production deployments should undergo a complete security audit.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC165 {
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
interface IERCRoleSemantics {
/// @notice Whether the role belongs to a known safe combination in the registry
function isKnownRole(bytes32 roleHash) external view returns (bool);
}
/**
* @title RoleSemanticsStandard
* @dev Contract Role Semantics Standard reference implementation
* Demonstrates semantic verification and the IERCRoleSemantics interface
*/
contract RoleSemanticsStandard is IERC165, IERCRoleSemantics {
// --- Role Constants (inherited from Naming Standard) ---
/// @custom:role-type root
bytes32 public constant ROLE_ADMIN_ROOT = keccak256("role.admin.root");
/// @custom:role-type governor
/// @custom:role-grants role.token.mint,role.token.burn,role.token.freeze,role.token.unfreeze
/// @custom:role-governor role.admin.root
bytes32 public constant ROLE_TOKEN_GOVERN = keccak256("role.token.govern");
/// @custom:role-type atomic
/// @custom:role-governor role.token.govern
bytes32 public constant ROLE_TOKEN_MINT = keccak256("role.token.mint");
/// @custom:role-type atomic
/// @custom:role-governor role.token.govern
bytes32 public constant ROLE_TOKEN_BURN = keccak256("role.token.burn");
/// @custom:role-type atomic
/// @custom:role-governor role.token.govern
bytes32 public constant ROLE_TOKEN_FREEZE = keccak256("role.token.freeze");
/// @custom:role-type atomic
/// @custom:role-governor role.token.govern
bytes32 public constant ROLE_TOKEN_UNFREEZE = keccak256("role.token.unfreeze");
/// @custom:role-type governor
/// @custom:role-grants role.system.pause,role.system.upgrade
/// @custom:role-governor role.admin.root
bytes32 public constant ROLE_SYSTEM_GOVERN = keccak256("role.system.govern");
/// @custom:role-type atomic
/// @custom:role-governor role.system.govern
bytes32 public constant ROLE_SYSTEM_PAUSE = keccak256("role.system.pause");
/// @custom:role-type atomic
/// @custom:role-governor role.system.govern
bytes32 public constant ROLE_SYSTEM_UPGRADE = keccak256("role.system.upgrade");
/// @custom:role-type governor
/// @custom:role-grants role.treasury.withdraw
/// @custom:role-governor role.admin.root
bytes32 public constant ROLE_TREASURY_GOVERN = keccak256("role.treasury.govern");
/// @custom:role-type atomic
/// @custom:role-governor role.treasury.govern
bytes32 public constant ROLE_TREASURY_WITHDRAW = keccak256("role.treasury.withdraw");
/// @custom:role-type governor
/// @custom:role-grants role.bridge.relay
/// @custom:role-governor role.admin.root
bytes32 public constant ROLE_BRIDGE_GOVERN = keccak256("role.bridge.govern");
/// @custom:role-type atomic
/// @custom:role-governor role.bridge.govern
bytes32 public constant ROLE_BRIDGE_RELAY = keccak256("role.bridge.relay");
// --- EIP-165 Interface Declaration ---
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return interfaceId == type(IERC165).interfaceId
|| interfaceId == type(IERCRoleSemantics).interfaceId;
}
// --- Semantic Registry ---
mapping(bytes32 => bool) private _knownRoleRegistry;
constructor() {
_registerKnownRole(ROLE_ADMIN_ROOT);
_registerKnownRole(ROLE_TOKEN_GOVERN);
_registerKnownRole(ROLE_TOKEN_MINT);
_registerKnownRole(ROLE_TOKEN_BURN);
_registerKnownRole(ROLE_TOKEN_FREEZE);
_registerKnownRole(ROLE_TOKEN_UNFREEZE);
_registerKnownRole(ROLE_SYSTEM_GOVERN);
_registerKnownRole(ROLE_SYSTEM_PAUSE);
_registerKnownRole(ROLE_SYSTEM_UPGRADE);
_registerKnownRole(ROLE_TREASURY_GOVERN);
_registerKnownRole(ROLE_TREASURY_WITHDRAW);
_registerKnownRole(ROLE_BRIDGE_GOVERN);
_registerKnownRole(ROLE_BRIDGE_RELAY);
}
function _registerKnownRole(bytes32 role) internal {
_knownRoleRegistry[role] = true;
}
// --- IERCRoleSemantics Interface Implementation ---
function isKnownRole(bytes32 roleHash)
external view override returns (bool)
{
return _knownRoleRegistry[roleHash];
}
}
Security Considerations
- Annotation vs. runtime configuration inconsistency: NatSpec
@custom:role-typeannotations do not reflect the actual configuration of runtime role admin relationships. If a contract modifies admin relationships causing the actual authorization path to deviate from the Governor pattern (e.g.,role.token.mint’s admin is set torole.admin.rootinstead ofrole.token.govern), the annotation information remains unchanged. This inconsistency is a security anomaly that static analysis tools SHOULD detect. - NatSpec annotation forgery: NatSpec annotations are off-chain conventions that cannot be verified after compilation. Static analysis tools SHOULD cross-verify annotations against actual contract deployments and role configuration logic.
- Registry coverage incompleteness: Custom category-action pairs not in the registry cause
isKnownRole()to returnfalse, but the names still conform to the Contract Role Naming Standard’s naming syntax. This is by design: the registry is a subset of known safe combinations, not the full set of valid names. - Govern-Atomic mutual exclusion: Govern and atomic roles under the same category cannot be granted to the same address, avoiding overlapping governor and operator identities. The mutual exclusion constraint is defined and enforced by the security baseline framework; this standard only declares the semantic mutual exclusion relationship.
- Out-of-registry combination risk: Role names conforming to the naming syntax do not necessarily belong to the registry.
isKnownRole()returningfalsedoes not indicate unsafety — it only indicates the need for manual review.
Copyright
Copyright and related rights waived via CC0.