diff --git a/README.md b/README.md index 531ce01..74bfc0e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ forge install base/base-std src ├── StdPrecompiles.sol: Collection of precompiles and their interfaces └── interfaces + ├── IActivationRegistry.sol: Runtime feature activation registry (Beryl+) ├── IB20.sol: Core Token Standard ├── IB20Stablecoin.sol: Stablecoin variant of B20 ├── IB20Security.sol: Security variant of B20 diff --git a/src/interfaces/IActivationRegistry.sol b/src/interfaces/IActivationRegistry.sol index e138ad4..191ed81 100644 --- a/src/interfaces/IActivationRegistry.sol +++ b/src/interfaces/IActivationRegistry.sol @@ -2,88 +2,94 @@ pragma solidity >=0.8.20 <0.9.0; /// @title IActivationRegistry -/// @notice Singleton precompile that gates Base-native features behind -/// an activation admin. Each feature is identified by an opaque -/// `bytes32` feature id and is either activated or not; -/// consumers (other precompiles, system configuration, or -/// downstream contracts) consult `isActivated` to gate behavior. +/// @notice Singleton precompile that gates Base-native features behind an +/// activation admin. Each feature is identified by an opaque `bytes32` +/// id and is either enabled or disabled; all features default to +/// disabled. The admin can call `enable` or `disable`; any other +/// caller reverts with `Unauthorized`. No-op transitions (enabling an +/// already-enabled feature or disabling an already-disabled feature) +/// also revert. /// -/// The activation admin is the only address authorized to call -/// `activate`; all other callers revert with `Unauthorized`. -/// Activation is one-way: once a feature is activated it cannot -/// be deactivated. +/// @dev The precompile enforces two call-context invariants surfaced as ABI +/// reverts: +/// - `DelegateCallNotAllowed`: entry points require a direct `CALL`. +/// `DELEGATECALL` / `CALLCODE` are rejected so the caller identity is +/// bound to `msg.sender`, not the calling contract's storage context. +/// - `StaticCallNotAllowed`: `enable` and `disable` mutate state and +/// cannot be invoked from a `STATICCALL` frame. /// -/// @dev The precompile enforces two call-context invariants that are -/// surfaced as reverts but cannot originate from normal Solidity -/// consumers: -/// - `DelegateCallNotAllowed`: the precompile MUST be invoked -/// via `CALL` (not `DELEGATECALL` or `CALLCODE`), so the admin -/// identity is bound to `msg.sender` rather than the calling -/// contract's storage context. -/// - `StaticCallNotAllowed`: `activate` mutates state and cannot -/// be invoked from a `STATICCALL` frame. +/// Feature ids are opaque to the registry: any `bytes32` is a valid id. +/// By convention the producing component picks a stable id derived from +/// a human-readable feature name (the chain-node source uses 32-byte +/// digests for this purpose). /// -/// Feature ids are opaque to the registry: it does not interpret -/// them, and any `bytes32` is a valid id. By convention the -/// producing component picks a stable id derived from a -/// human-readable feature name (the chain-node source uses -/// 32-byte digests for this purpose). +/// `FeatureNotEnabled` is raised by consumers that call an +/// `assertEnabled`-style gate on a disabled feature, not by `isEnabled` +/// itself (which returns `false` instead). interface IActivationRegistry { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - /// @notice `caller` is not the activation admin and is therefore - /// not authorized to call `activate`. + /// @notice `caller` is not the activation admin and is therefore not + /// authorized to call `enable` or `disable`. error Unauthorized(address caller); - /// @notice `activate` was called on a feature that is already - /// activated. - error AlreadyActivated(bytes32 feature); + /// @notice `enable` was called on a feature that is already enabled. + error AlreadyEnabled(bytes32 feature); - /// @notice `feature` is not activated. Returned by precompiles that - /// consult the registry as a hard gate (the chain node - /// uses an `assertActivated`-style flow for this); not - /// raised by `isActivated`, which returns `false` instead. - error FeatureNotActivated(bytes32 feature); + /// @notice `disable` was called on a feature that is already disabled. + error AlreadyDisabled(bytes32 feature); - /// @notice The precompile was invoked via `DELEGATECALL` or - /// `CALLCODE`. All entry points require a direct `CALL`. + /// @notice `feature` is not enabled. Raised by consumers that use the + /// registry as a hard gate (e.g. an `assertEnabled` pattern); + /// not raised by `isEnabled`, which returns `false` instead. + error FeatureNotEnabled(bytes32 feature); + + /// @notice The precompile was invoked via `DELEGATECALL` or `CALLCODE`. + /// All entry points require a direct `CALL`. error DelegateCallNotAllowed(); - /// @notice A state-mutating entry point (`activate`) was invoked - /// from a `STATICCALL` frame. + /// @notice A state-mutating entry point (`enable` or `disable`) was + /// invoked from a `STATICCALL` frame. error StaticCallNotAllowed(); /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - /// @notice Emitted when `feature` transitions from inactive to - /// activated. `caller` is the activation admin. - event FeatureActivated(bytes32 indexed feature, address indexed caller); + /// @notice Emitted when `feature` transitions from disabled to enabled. + /// `caller` is the activation admin. + event FeatureEnabled(bytes32 indexed feature, address indexed caller); + + /// @notice Emitted when `feature` transitions from enabled to disabled. + /// `caller` is the activation admin. + event FeatureDisabled(bytes32 indexed feature, address indexed caller); /*////////////////////////////////////////////////////////////// ACTIVATION QUERIES //////////////////////////////////////////////////////////////*/ - /// @notice Whether `feature` is currently activated. Returns - /// `false` for any feature id that has never been - /// activated (the default state). - function isActivated(bytes32 feature) external view returns (bool); + /// @notice Whether `feature` is currently enabled. Returns `false` for + /// any feature id that has never been enabled (the default state). + function isEnabled(bytes32 feature) external view returns (bool); - /// @notice The address authorized to call `activate`. - function admin() external view returns (address); + /// @notice The address authorized to call `enable` and `disable`. + function activationAdmin() external view returns (address); /*////////////////////////////////////////////////////////////// ACTIVATION CONTROL //////////////////////////////////////////////////////////////*/ - /// @notice Activates `feature`. Caller MUST equal `admin()` (else - /// `Unauthorized`). Reverts with `AlreadyActivated` if the - /// feature is already activated; reverts with - /// `StaticCallNotAllowed` if invoked under `STATICCALL`. - /// Emits `FeatureActivated` on success. Activation is - /// one-way: there is no `deactivate` counterpart. - function activate(bytes32 feature) external; + /// @notice Enables `feature`. Caller MUST equal `activationAdmin()` (else + /// `Unauthorized`). Reverts with `AlreadyEnabled` if the feature + /// is already enabled; reverts with `StaticCallNotAllowed` if + /// invoked under `STATICCALL`. Emits `FeatureEnabled` on success. + function enable(bytes32 feature) external; + + /// @notice Disables `feature`. Caller MUST equal `activationAdmin()` (else + /// `Unauthorized`). Reverts with `AlreadyDisabled` if the feature + /// is already disabled; reverts with `StaticCallNotAllowed` if + /// invoked under `STATICCALL`. Emits `FeatureDisabled` on success. + function disable(bytes32 feature) external; }