This EIP standardizes secure time-bound roles with automatic expiration, eliminating manual revocation risks.
Abstract
This EIP introduces a minimal interface for enforcing time-bound role permissions management in contracts.
Specifically, it provides interfaces for granting time-bound roles and verifying active permissions, enabling automatic deactivation of expired roles without requiring manual intervention.
Motivation
The permission system of smart contracts is crucial for the operation and management. Role-based access control (RBAC) systems are widely adopted in smart contracts to manage permissions effectively.
However, the absence of a standardized mechanism for automatically revoking roles after predefined durations introduces significant security challenges, particularly in dynamic environments such as complex organizational structures and multi-party business scenarios. Permanent role assignments exacerbate these risks in common use cases, including:
- Temporary access needs, such as third-party supplier or vendors integrations.
- Project- or task-specific permissions that should not persist beyond their scope.
- Employee offboarding or role changes.
In these cases, permanent roles create unnecessary attack surfaces. To address these systematic issues, this EIP introduced verifiable time constraints directly into permission management.
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.
Every contract compliant with this EIP MUST implement the EIP-XXXX interface. Contracts SHOULD also implement EIP-165 to support interface detection.
pragma solidity ^0.8.13;
interface ERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
/// @title Time-Bound Access Control Interface
interface EIPXXXX /* is ERC165 */ {
/// @dev Emitted when role expiration is changed
event RoleExpirationChanged(
bytes32 indexed role,
address indexed account,
uint256 previousExpiryTimestamp,
uint256 expiryTimestamp
);
/// @dev set role expiration at specified timestamp
function setRoleExpiration(
bytes32 role,
address account,
uint256 expiryTimestamp
) external;
/// @dev Query the expiry timestamp of the role
function getRoleExpiration(bytes32 role, address account) external view returns (uint256);
/// @dev Checks if role is active at current timestamp
function hasActiveRole(bytes32 role, address account) external view returns (bool);
}
- The
setRoleExpiration(bytes role, address account, uint256 expiryTimestamp)MUST have reasonable access control. - The
expiryTimestampparameter MUST be represented as seconds, the role expiration time, using Unix timestamp format. - All operations involving changes in the role expiration MUST emit
RoleExpiryChangedevents.
Rationale
-
Timestamp-Only: A single timestamp parameter simplifies implementation while supporting calendar-based expiration that aligns with real-world use cases.
-
Minimal surface: The design enforces a clear security boundary with the function
hasActiveRole, avoids auxiliary functions to reduce the attack surface, and provides a fully self-contained specification.
Backwards Compatibility
No backward compatibility issues are introduced. This proposal is fully backward-compatible with existing access control systems and supports EIP-165 interface detection.
Reference Implementation
pragma solidity 0.8.13;
import { EIPXXXX } from "./EIPXXXX.sol";
/**
* @title EIPXXXX Implementation
* @dev Implementation of the EIPXXXX Time-Bound Access Control Interface
*/
contract EIPXXXXImpl is EIPXXXX {
mapping(bytes32 => bytes32) private _roleAdmin;
mapping(bytes32 => mapping(address => uint256)) private _roleExpiryTimestamps;
error NotActiveRole(bytes32 role, address account);
event RoleAdminChanged(bytes32 indexed role, bytes32 indexed prevRole, bytes32 adminRole);
/**
* @dev Sets the admin role for a given role
* @param role The role to set admin for
* @param adminRole The admin role to set
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
bytes32 previousAdminRole = getRoleAdmin(role);
_roleAdmin[role] = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Returns the admin role for a given role
* @param role The role to query
* @return The admin role
*/
function getRoleAdmin(bytes32 role) public view returns(bytes32) {
return _roleAdmin[role];
}
/**
* @dev Modifier to check if an account has an active role
* @param role The role to check
* @param account The account to check
*/
modifier onlyActiveRole(bytes32 role, address account) {
if (!hasActiveRole(role, account)) {
revert NotActiveRole(role, account);
}
_;
}
/**
* @dev Sets the expiration timestamp for a role-account pair
* @param role The role to set expiration for
* @param account The account to set expiration for
* @param expiryTimestamp The expiration timestamp
*/
function setRoleExpiration(
bytes32 role,
address account,
uint256 expiryTimestamp
) external onlyActiveRole(getRoleAdmin(role), msg.sender) {
uint256 lastExpiryTimestamp = _roleExpiryTimestamps[role][account];
_roleExpiryTimestamps[role][account] = expiryTimestamp;
emit RoleExpirationChanged(role, account, lastExpiryTimestamp, expiryTimestamp);
}
/**
* @dev Returns the expiration timestamp for a role-account pair
* @param role The role to query
* @param account The account to query
* @return The expiration timestamp
*/
function getRoleExpiration(bytes32 role, address account) external view returns(uint256) {
return _roleExpiryTimestamps[role][account];
}
/**
* @dev Checks if an account has an active role (not expired)
* @param role The role to check
* @param account The account to check
* @return Whether the role is active
*/
function hasActiveRole(bytes32 role, address account) public view returns (bool) {
return block.timestamp < _roleExpiryTimestamps[role][account];
}
/**
* @dev Checks if the contract supports a given interface
* @param interfaceId The interface identifier, as specified in ERC-165
* @return true if the contract supports the interface, false otherwise
*/
function supportsInterface(bytes4 interfaceID) external pure returns (bool) {
return interfaceID == this.supportsInterface.selector ||
interfaceID == type(EIPXXXX).interfaceId;
}
}
Security Considerations
- Timestamp Variability and Safety Margins: Ethereum block timestamps are determined by miners and MAY deviate from real-world time. Malicious or accidental manipulation could lead to premature role expirations or unintended delays in revocation. Contracts managing high-value permissions or time-sensitive roles SHOULD incorporate safety margins to mitigate timestamp variances. For short-duration roles, larger margins are RECOMMENDED to account for potential network congestion or delays. ďźLayer 2ďź
- Temporal Consistency: Role permissions MUST remain valid until their exact expiry time and MUST be automatically invalidated immediately thereafter, ensuring precise alignment between granted duration and effective access period.
- Enforcement of Active Role Checks: Expired roles are not actively monitored or processed after expiry. Instead, all permission validations MUST strictly rely on the
hasActiveRole(address account, bytes32 role)function. - Mandatory Pre-Action Validation: Every permission check is REQUIRED to call
hasActiveRole(address account, bytes32 role)prior to executing any privileged operation, irrespective of previous role grants, to maintain consistent and real-time enforcement. - Permanent Root Roles: Root or default admin roles are RECOMMENDED be assigned infinite expiry times (e.g., type(uint256).max). This ensures these roles remain permanent and prevents the risk of irreversible lockout from the entire role-based access control system.
- Transaction Order Dependency Attacks: Due to miner extractable value (MEV) and transaction reordering within blocks, attackers MAY front-run role renewals or extensions to execute privileged actions just before expiry updates. Time-sensitive roles SHOULD implement safeguards to mitigate transaction ordering risks.
Copyright
Copyright and related rights waived via CC0.