ERC-8314: Contract Role Naming

Abstract

This proposal defines a hierarchical namespace pattern for naming privileged roles in smart contracts, enabling consistent role naming across protocols with support for incremental adoption. The pattern role.{category}.{action} combined with keccak256 hash derivation provides semantically clear and hash-discoverable standardized naming. A core role set applies conditionally based on contract functionality. A standardized storage slot enables role registry discovery, and an on-chain query interface provides role name resolution and core role verification. This standard addresses the fragmentation caused by inconsistent naming conventions for equivalent privileged contract roles across protocols.

Note: Semantic derivation rules for role names will be defined by a companion EIP — the Contract Role Semantics Standard.

Motivation

Privileged role naming in smart contracts is highly inconsistent across the Ethereum ecosystem. OpenZeppelin uses DEFAULT_ADMIN_ROLE, MINTER_ROLE, PAUSER_ROLE, and over 17 other naming patterns. Aave V3 uses POOL_ADMIN, BRIDGE_ADMIN, RISK_ADMIN. Compound III uses pauseGuardian, borrowCapGuardian, baseRateOracle. Venus Protocol uses function-level pausers with no unified naming.

This inconsistency produces three critical problems:

  1. Cross-protocol auditing is infeasible: Auditors cannot mechanically verify whether an “upgrade role” is properly protected across multiple protocols, because each protocol names it differently. Manual cross-referencing of role semantics across audit reports introduces human error.

  2. Role confusion attacks: Protocol A’s ADMIN role may grant unlimited permissions, while Protocol B’s ADMIN is a secondary operator. Auditing tools cannot distinguish role permissions by name alone. As noted in the motivation of EIP-6366: “Owner, Operator, Manager, Validator and other special roles are common in many smart contracts… since these permissions are not managed in a single smart contract, auditing and maintaining these systems is difficult.”

  3. Semantic drift attacks: A custom BURNER role that actually performs minting operations can exploit auditors’ assumptions about role names, creating a false appearance.

EIP-5982 defines role-based access control (RBAC) interfaces but does not specify role naming conventions. EIP-7820 provides cross-contract role management but does not standardize naming. EIP-6366 acknowledges naming inconsistency but addresses it through bit-operation permissions rather than naming standardization. EIP-7303 recommends keccak256("MINTER") for role hash computation but does not define hierarchical naming. No existing EIP defines what canonical role names should be, or provides an extensible hierarchical naming syntax.

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.

Role Naming Syntax

Role names MUST conform to the following BNF grammar:

<role-name> ::= "role" "." <ident> "." <ident>
<ident>     ::= [a-z][a-z0-9]*

The corresponding regular expression is:

^role\.[a-z][a-z0-9]*\.[a-z][a-z0-9]*$

Valid examples: role.admin.root, role.token.mint, role.system.proxy

Invalid examples: ADMIN, MINTER_ROLE, role.Admin, role.123.mint, role.token.Mint, role.aave.token.mint

Level 1 Role Constant Definition

Contracts implementing Level 1 MUST define role constants using the naming syntax. The following example demonstrates correct role constant definitions for a contract with admin, token, and system operations:

// Level 1 — Role constant definitions
bytes32 public constant ROLE_ADMIN_ROOT = keccak256("role.admin.root");
bytes32 public constant ROLE_TOKEN_MINT = keccak256("role.token.mint");
bytes32 public constant ROLE_SYSTEM_PAUSE = keccak256("role.system.pause");

Role Hash Derivation

bytes32 role hashes MUST be computed as follows:

bytes32 roleHash = keccak256(roleNameString);

This pattern is recommended by EIP-5982 and EIP-7303. Test vectors:

  • keccak256("role.admin.root") → 0x448bc86664db33d6588225675ce4416b4011dc2042715b15896ebd54b3505eea

  • keccak256("role.token.mint") → 0x55fb6288bd8e87dee5defd2246b578e35c083751d5b87edda220600b885d438e

  • keccak256("role.system.pause") → 0x14d56b8c118883194ba501333a39231409c3c0ebf29b0dc180005569e6bc0b7b

Role Naming Format

Two-Part Canonical Configuration

The canonical configuration uses the format role.{category}.{action}. This form SHOULD be used for all role naming and serve as the recommended identifier form for role names. Names conforming to the BNF grammar MUST be two-part format; SHOULD indicates that all contracts are encouraged to adopt this naming form, but contracts may choose to use non-standard naming (in which case they are not covered by this standard).

Category Definition Principles: Category represents a functional domain — a logical grouping of related operations. A valid category SHOULD satisfy the following principles:

  1. Domain-oriented: Category describes “which operational domain”, action describes “what operation”. Category is a noun-based functional domain, action is a verb-based operation. Verbs (e.g., pause, upgrade, freeze) MUST NOT be used as categories; they MUST be used as actions within the corresponding functional domain. For Tier-0 roles (e.g., root admin), the action may be a positional noun, as its semantics represent “highest authority within the domain” rather than a specific operation.

  2. Cohesion: Actions under the same category should belong to the same functional domain, sharing authorization management semantics (the same grantor role manages all atomic roles within the domain).

  3. Extensibility: A category should accommodate multiple related actions. A category containing only a single action should consider being merged into a broader functional domain.

Validation of these principles occurs at compile time and audit time. Contracts SHOULD use linter rules to check whether categories are noun-based functional domains, and audit tools SHOULD verify naming compliance during conformance checks.

Category Action Canonical Name Domain Description
admin root role.admin.root Admin domain (root is a Tier-0 exception, representing highest authority within the domain)
token mint role.token.mint Token domain
token burn role.token.burn Token domain
token freeze role.token.freeze Token domain
system pause role.system.pause System operations domain
system upgrade role.system.upgrade System operations domain
system proxy role.system.proxy System operations domain (proxy management operation)
bridge relay role.bridge.relay Cross-chain domain
reserve audit role.reserve.audit Reserve domain
finance withdraw role.finance.withdraw Finance domain

Core Role Set (Conditional SHOULD)

Core roles are privileged roles that appear at high frequency across protocols and whose misuse can directly lead to fund loss or governance compromise. All core roles listed below satisfy the following admission criteria: (1) present in the role systems of at least two of OpenZeppelin, Aave, and Compound, and (2) erroneous authorization or missing configuration of the role can lead to irreversible fund loss or governance hijacking.

The core role set is fixed by this EIP. New core roles must be proposed through a separate EIP, with the motivation section demonstrating that they satisfy both admission criteria.

Universal Roles (MUST)

Role Name Description
role.admin.root The highest-privilege role that can grant and revoke all other roles

Functionality-Conditional Roles (SHOULD when applicable)

Role Name Condition
role.system.pause Contract implements pausable functionality
role.system.upgrade Contract is upgradeable
role.system.proxy Contract is a proxy contract
role.token.mint Contract implements token minting
role.token.burn Contract implements token burning
role.token.freeze Contract implements token freezing
role.bridge.relay Contract implements cross-chain relay
role.reserve.audit Contract implements reserve management

Audit Verification Rule: Audit tools SHOULD check the correspondence between contract functionality and role definitions. For example, if a contract implements an upgradeable pattern (e.g., UUPS or Transparent Proxy), audit verification SHOULD confirm that the contract defines the role.system.upgrade role constant and that the role is properly configured in the permission management interface; for Transparent Proxies, the definition and configuration of the role.system.proxy role constant should also be confirmed.

On-chain Query Interface

Level 2 builds upon Level 1 (naming convention) by providing on-chain role name query and core role verification capabilities. External audit tools, block explorers, and security scanners can query a contract’s role definition information through a standardized interface.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERCRoleNaming {
    /// @notice Reverse mapping from hash to name
    /// @dev Returns empty string for unregistered hashes; callers SHOULD check for non-empty return
    function roleHashToName(bytes32 roleHash) external view returns (string memory);

    /// @notice Name-to-hash computation (pure computation, no storage)
    function roleNameToHash(string calldata roleName) external pure returns (bytes32);

    /// @notice Whether the role belongs to the canonical core role set
    ///         (universal roles and functionality-conditional roles defined by this EIP)
    function isCanonicalRole(bytes32 roleHash) external view returns (bool);
}

isCanonicalRole() only verifies whether a role belongs to the core role set defined by this EIP. Broader semantic validation (Category-Action registry) is provided by the companion Contract Role Semantics Standard interface.

roleHashToName() MUST return an empty string ("") for unregistered hashes. Callers SHOULD verify hash validity through isCanonicalRole() or by checking for a non-empty return value.

Implementations SHOULD declare the IERCRoleNaming interface via EIP-165’s supportsInterface(), with interfaceId 0x8f97d349.

Adoption Levels

This standard supports progressive adoption:

Level Scope Requirement Dependency Adoption Cost
Level 1 Role naming standard definition REQUIRED None Zero (compile-time constants)
Level 2 On-chain naming query interface RECOMMENDED Level 1 Single interface contract deployment
Level 3 On-chain semantic derivation interface OPTIONAL (defined by companion EIP) Level 2 Additional semantic interface contract

Rationale

Hierarchical Naming Pattern

The role.{category}.{action} pattern was chosen over flat naming (MINTER, ADMIN) for three reasons:

  1. Semantic clarity: role.token.mint simultaneously conveys the domain (token operations) and the permission (minting).

  2. Hash discoverability: Standardized naming causes the same role name across all protocols to produce the same keccak256 hash. The role.admin.root role in an unverified contract can be identified by block explorers and security scanners through a known hash table — this is the core value of this standard. If namespace prefixes were allowed (e.g., role.aave.token.mint), the hash of identically-named roles would diverge due to different prefixes, regressing to the same opacity as custom constants like MINTER_ROLE, eliminating the core benefit of standardized naming.

  3. In-domain extensibility: New actions can be extended under existing categories (e.g., role.token.newaction), and new categories can be added by functional domain without modifying existing names.

Conditional SHOULD vs Absolute MUST

Absolute MUST requirements would force non-upgradeable contracts to declare irrelevant roles. Conditional SHOULD maintains audit enforceability while accepting contract heterogeneity. Audit tools check the correspondence between contract functionality and role definitions — for example, “if a contract implements an upgradeable pattern, it should define an upgrade role.”

Hash Discoverability and Two-Part Naming

This standard chooses strict two-part naming role.{category}.{action} primarily for hash discoverability:

  • keccak256("role.admin.root") produces a deterministic hash. Any contract using this standard name — regardless of whether it is open-source — generates the same bytes32 value. Block explorers, security scanners, and audit tools can maintain a “known role hash table” to directly reverse-lookup role semantics from on-chain storage slots.

  • A three-part name such as role.aave.token.mint produces a different hash from role.token.mint, and third parties cannot infer from the hash alone that this is a token minting role — the core benefit of standardized naming is dissipated by the namespace.

Protocol distinction does not require namespace prefixes: roles of different protocols are naturally isolated by different contract addresses. Cross-protocol comparison is achieved through the combination of contract address and role name, not by embedding protocol identifiers within role names.

Progressive Adoption

Adoption levels are designed as progressive — contracts may choose any level based on their own needs, without requiring full implementation at once:

  • Level 1 (REQUIRED): Only requires using role constant names conforming to the BNF grammar in contract code. Zero deployment cost — role hashes are compile-time constants that add no on-chain storage. All contracts MUST reach this level.

  • Level 2 (RECOMMENDED): Deploy an additional contract or library implementing IERCRoleNaming, enabling external tools to query role names through on-chain calls. Cost is a single interface contract deployment.

This contrasts with existing RBAC standards such as EIP-5982, which require full interface implementation upfront — this standard allows contracts to benefit from adopting only the naming convention, upgrading on-chain capabilities incrementally as needed.

Separation of Naming and Semantic Responsibilities

The role naming standard adopts an “identifier-first” design stance — names are identifiers (short, hashable, gas-efficient), not documentation (hierarchy and authorization source are not embedded in the name string). This is consistent with industrial practices such as K8s cluster-admin and DNS domain names.

Semantic gaps in naming are addressed by the companion Contract Role Semantics Standard: Action lexical classification (atomic/scope/grant), Tier derivation, Grantor semantics, and NatSpec semantic annotation fields.

Backwards Compatibility

EIP-5982 Compatibility

This standard is orthogonal to EIP-5982. EIP-5982 defines RBAC interface mechanisms; this standard defines what role identifiers should be. Contracts implementing EIP-5982 MAY adopt this naming standard without breaking compatibility.

EIP-7820 Compatibility

This standard is orthogonal to EIP-7820. EIP-7820 manages cross-contract roles through a centralized registry; this standard ensures those roles have consistent naming.

EIP-173 Compatibility

The role.admin.root role subsumes the owner() concept of EIP-173. Contracts MAY implement both interfaces simultaneously. When the permission subjects of role.admin.root and owner() differ, the authorization operations (grant/revoke) of role.admin.root SHALL be treated as the final authority.

OpenZeppelin Migration

OpenZeppelin Constant Standard Name Migration Phase
DEFAULT_ADMIN_ROLE role.admin.root Phase 1
MINTER_ROLE role.token.mint Phase 1
PAUSER_ROLE role.system.pause Phase 1
BURNER_ROLE role.token.burn Phase 1
UPGRADER_ROLE role.system.upgrade Phase 2
PROXY_ADMIN_ROLE role.system.proxy Phase 2

Migration strategy: dual-constant transition phase preserves storage integrity.

// Deprecated: Use ROLE_ADMIN_ROOT instead
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
bytes32 public constant ROLE_ADMIN_ROOT = keccak256("role.admin.root");

Role Admin Relationship Migration: The “role → admin role” authorization relationships established by OpenZeppelin’s AccessControl._setRoleAdmin() must be migrated in sync. After migration, ROLE_ADMIN_ROOT should be set as the adminRole for all grantor roles, ensuring authorization chain consistency.

Security Considerations

  • Role confusion attacks: Standardized naming eliminates cross-protocol semantic ambiguity. role.admin.root consistently represents the highest privilege.

  • Storage slot collision: keccak256("eip.role.registry") - 1 uses the EIP-1967/EIP-7201 pattern with negligible collision probability.

  • Proxy and factory patterns: Proxy contracts (Transparent Proxy / UUPS) and implementation contracts have independently isolated role namespaces at their respective addresses. Proxy contracts use role.system.proxy to manage the proxy itself, while implementation contracts use standard roles for business logic. Diamond patterns (EIP-2535) should adopt this standard’s naming uniformly across all facets, with the IERCRoleNaming implementation at the Diamond address providing aggregated queries. Child contracts created by contract factories independently own their own role namespaces and do not automatically inherit factory roles.

  • Upgrade role hijacking: Conditional SHOULD encourages upgradeable contracts to define role.system.upgrade.

  • ACL integration blind spots: Dual-constant transition phase preserves storage integrity, with legacy and standard constants coexisting during the transition period.

  • False standard claims: The isCanonicalRole() function can verify core role coverage. Interface implementation with incorrect behavior does not constitute conformance.

  • Asymmetric permission pairs: role.token.freeze (conditional SHOULD) has a lower applicability threshold than role.token.unfreeze (which is not a core role but a valid domain-specific extension following this standard’s naming convention), because freezing is an emergency stop-loss operation and contracts implementing token freezing are strongly encouraged to define this role. Similarly, role.system.pause (conditional SHOULD) has no corresponding unpause role, because the security impact of pause authority is more direct.

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 IERCRoleNaming {
    /// @notice Reverse mapping from hash to name
    /// @dev Returns empty string for unregistered hashes; callers SHOULD check for non-empty return
    function roleHashToName(bytes32 roleHash) external view returns (string memory);

    /// @notice Name-to-hash computation (pure computation, no storage)
    function roleNameToHash(string calldata roleName) external pure returns (bytes32);

    /// @notice Whether the role belongs to the canonical core role set
    ///         (universal roles and functionality-conditional roles defined by this EIP)
    function isCanonicalRole(bytes32 roleHash) external view returns (bool);
}

/**
 * @title RoleNamingStandard
 * @dev Reference implementation for EIP-XXXX Contract Role Naming Standard
 *      Demonstrates role naming constant definitions, hash derivation,
 *      and IERCRoleNaming interface implementation
 */
contract RoleNamingStandard is IERC165, IERCRoleNaming {

    // ─── Level 1: Role Constant Definitions ────────────────────────────

    bytes32 public constant ROLE_ADMIN_ROOT = keccak256("role.admin.root");
    bytes32 public constant ROLE_TOKEN_MINT = keccak256("role.token.mint");
    bytes32 public constant ROLE_TOKEN_BURN = keccak256("role.token.burn");
    bytes32 public constant ROLE_TOKEN_FREEZE = keccak256("role.token.freeze");
    bytes32 public constant ROLE_SYSTEM_PAUSE = keccak256("role.system.pause");
    bytes32 public constant ROLE_SYSTEM_UPGRADE = keccak256("role.system.upgrade");
    bytes32 public constant ROLE_SYSTEM_PROXY = keccak256("role.system.proxy");
    bytes32 public constant ROLE_BRIDGE_RELAY = keccak256("role.bridge.relay");
    bytes32 public constant ROLE_RESERVE_AUDIT = keccak256("role.reserve.audit");

    // ─── EIP-165 Interface Declaration ──────────────────────────────

    function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
        return interfaceId == type(IERC165).interfaceId
            || interfaceId == type(IERCRoleNaming).interfaceId;
    }

    // ─── Level 2: Name Registry ───────────────────────────────

    mapping(bytes32 => string) private _nameRegistry;
    mapping(bytes32 => bool) private _registered;

    constructor() {
        _register(ROLE_ADMIN_ROOT, "role.admin.root");
        _register(ROLE_TOKEN_MINT, "role.token.mint");
        _register(ROLE_TOKEN_BURN, "role.token.burn");
        _register(ROLE_TOKEN_FREEZE, "role.token.freeze");
        _register(ROLE_SYSTEM_PAUSE, "role.system.pause");
        _register(ROLE_SYSTEM_UPGRADE, "role.system.upgrade");
        _register(ROLE_SYSTEM_PROXY, "role.system.proxy");
        _register(ROLE_BRIDGE_RELAY, "role.bridge.relay");
        _register(ROLE_RESERVE_AUDIT, "role.reserve.audit");
    }

    function _register(bytes32 roleHash, string memory name) internal {
        _nameRegistry[roleHash] = name;
        _registered[roleHash] = true;
    }

    // ─── IERCRoleNaming Interface Implementation ─────────────────────────

    function roleHashToName(bytes32 roleHash)
        external view override returns (string memory)
    {
        if (!_registered[roleHash]) return "";
        return _nameRegistry[roleHash];
    }

    function roleNameToHash(string calldata roleName)
        external pure override returns (bytes32)
    {
        return keccak256(bytes(roleName));
    }

    function isCanonicalRole(bytes32 roleHash)
        external pure override returns (bool)
    {
        return roleHash == keccak256("role.admin.root")
            || roleHash == keccak256("role.system.pause")
            || roleHash == keccak256("role.system.upgrade")
            || roleHash == keccak256("role.system.proxy")
            || roleHash == keccak256("role.token.mint")
            || roleHash == keccak256("role.token.burn")
            || roleHash == keccak256("role.token.freeze")
            || roleHash == keccak256("role.bridge.relay")
            || roleHash == keccak256("role.reserve.audit");
    }
}

Future Work

This EIP intentionally limits its scope to role naming, to establish a minimal consensus foundation. The following capabilities are planned as companion standards:

  • Contract Role Semantics Standard: Define Action lexical classification (atomic/scope/grant), Tier derivation, Grantor semantics, and NatSpec semantic annotation fields (@custom:role-tier, etc.)

  • Role Enumeration Extension: Provide an interface for on-chain discovery of all roles declared by a contract (IERCRoleNamingEnumerable)

  • Static Analysis Toolchain: BNF grammar validation, role coverage reporting, conditional SHOULD heuristic detection

These extensions build upon the naming foundation defined herein and do not modify its core semantics.

Copyright

Copyright and related rights waived via CC0.

1 Like

Hi, the storage location definitions you proposed seem unrelated to this ERC. ** Could you clarify if they are necessary?**

It seems that the roleNameToHash function from the IERCRoleNaming interface is meaningless → it can be calculated off-chain. (keccak256(roleNameStr))