This ERC is designed for observing smart contract emergency states, enabling interoperable detection across protocols.
Abstract
This proposal defines a standard interface for observing emergency states in smart contracts. The interface provides a multi-level emergency state indicator and a timestamp of the most recent state change.
Motivation
Smart contracts implement emergency mechanisms in incompatible ways: some expose a binary paused flag, others use bespoke state variables with custom names and semantics, and many expose no observable emergency state at all. There is no standard interface for external systems to query whether a contract is in an emergency condition or how severe it is.
This creates concrete gaps:
- Cross-Protocol Risk Management: Many contracts expose no observable emergency state. Those that do use incompatible approaches, making cross-protocol detection costly and automated response to third-party risk effectively impossible.
- User Safety: Without a standard interface, wallets and frontends cannot generically detect emergency conditions. Users interact with distressed contracts with no warning.
- Automated Monitoring: Security infrastructure cannot query emergency status across contracts in a uniform way. Monitoring systems must be individually configured for each protocol, increasing operational overhead and the chance of missed alerts.
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.
Core Interface: IEmergencyState
The interface MUST include the following elements:
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;
/// @title IEmergencyState
/// @notice Standard interface for smart contract emergency states.
/// @dev Contracts implementing this interface MUST also implement ERC-165 supportsInterface.
interface IEmergencyState /* is IERC165 */ {
/// @notice Emitted when the emergency state is set to a new value.
/// @param executor The address that triggered the state change.
/// @param previousState The emergency state before the transition.
/// @param newState The emergency state after the transition.
/// @dev MUST NOT be emitted when previousState equals newState.
event EmergencyStateChanged(address indexed executor, uint8 previousState, uint8 newState);
/// @notice Returns the current emergency state of the contract
/// @dev MUST return a value in the range [0, 255]
/// State 0 MUST indicate normal operation
/// States 1-255 MAY be defined by the implementer
/// @return emergencyState The current emergency state
function emergencyState() external view returns (uint8 emergencyState);
/// @notice Returns the timestamp of the most recent emergency state change.
/// @dev MUST be updated to block.timestamp on every state change, regardless of direction.
/// MUST return 0 if no state change has ever occurred.
/// @return timestamp The Unix timestamp of the most recent state change
function lastEmergencyStateUpdateAt() external view returns (uint256 timestamp);
}
Standard Emergency States
| State | Description |
|---|---|
| 0 | Normal operation. MUST be the default state. |
| 1-255 | Implementer-defined. Semantics MUST be documented by the implementer. |
External systems SHOULD treat any state greater than 0 as indicating an emergency condition. Consumers SHOULD query protocol-specific documentation for state interpretation.
emergencyState()
- MUST be implemented as a
viewfunction. - MUST return a value in the range [0, 255].
- State 0 MUST indicate normal operation.
lastEmergencyStateUpdateAt()
- MUST be implemented as a
viewfunction. - MUST return the
block.timestampat the time of the most recent state change.
EmergencyStateChanged
- The event MUST be emitted once per successful state transition.
- The event MUST NOT be emitted when
previousStateequalsnewState. - The
executorfield MUST be the address of the immediate caller (msg.sender). - The
previousStatefield MUST be the emergency state value before the transition. - The
newStatefield MUST be the emergency state value after the transition.
Rationale
No state transition function in the interface
This interface standardizes what you observe, not how you change it. External systems need to read emergency state and they do not need to set it. State transition functions are deliberately omitted because authorization patterns vary across protocols. Some use a single owner, others a multisig or governance vote. Mandating a transition function would force a specific authorization pattern, excluding valid implementations without improving interoperability.
Implementer-defined states
A binary paused/unpaused flag is insufficient for protocols that need to distinguish between different emergency scenarios. States 1-255 are left open so each protocol can define states that represent whatever conditions are meaningful for their use case, whether those are attack scenarios, risk levels, operational modes, or anything else. The critical requirement is that implementers document all states they use, so external systems can correctly interpret them.
Separation of action and observation
This proposal defines only the observation layer of emergency conditions. The state transition function and its authorization are implementation concerns, not standardized by this interface. Contracts that also implement an emergency response interface may wire the two together internally, where the response hook calls the state transition function and the recovery hook resets the state to 0. This proposal does not depend on any action-layer standard.
Backwards Compatibility
This interface can be added to new contracts or to upgradeable contracts via a proxy upgrade. Non-upgradeable contracts that do not already implement this interface cannot adopt it retroactively.
Security Considerations
Emergency State is Not Guaranteed to Be Trustworthy
Contracts that read emergency state from an external contract and take actions based on it must consider the consequences of acting on a wrong or manipulated state. The state transition function of the observed contract is outside the scope of this interface and may be compromised, misconfigured, or maliciously triggered. A falsely set emergency state could cause a consuming contract to take irreversible actions it would not otherwise take.
For example, a lending protocol that liquidates positions when a dependency reports an emergency state could cause significant harm to users if that state was set incorrectly. Contracts consuming emergency state SHOULD carefully assess the impact of acting on a potentially incorrect state and SHOULD apply safeguards such as additional verification, time delays, or human confirmation for high-consequence actions.
Centralization of State Transition Authority
The state transition function is not defined by this interface and is left to each implementation. In practice, the authority to trigger emergency states is often concentrated in a single address or a small set of addresses. If the key controlling this authority is compromised, an attacker could set a false emergency state to disrupt operations, or fail to set it during a genuine emergency. Implementations should consider mitigations such as multi-sig wallets, time-locked governance, role separation between emergency triggers and resolvers, and monitoring alerts for state transition transactions.
Reference Implementation
The following is a reference implementation of IEmergencyState. Authorization and the setEmergencyState() function are deliberately omitted; concrete contracts define them according to their own authorization requirements.
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;
import "../interface/IEmergencyState.sol";
/// @title EmergencyState
/// @notice Reference implementation of IEmergencyState with storage for state and timestamp.
/// Authorization for state transitions and the setEmergencyState() function are
/// intentionally not defined here; concrete contracts MUST implement them.
abstract contract EmergencyState is IEmergencyState {
uint8 internal _currentEmergencyState;
uint256 internal _lastEmergencyStateUpdateAtTimestamp;
/// @inheritdoc IEmergencyState
function emergencyState() external view override returns (uint8) {
return _currentEmergencyState;
}
/// @inheritdoc IEmergencyState
function lastEmergencyStateUpdateAt() external view override returns (uint256 timestamp) {
return _lastEmergencyStateUpdateAtTimestamp;
}
/// @notice Transitions to a new emergency state and records the timestamp of the change.
/// @dev Updates the timestamp to block.timestamp on every state change.
/// No-op when newState equals the current state (no event, no timestamp update).
/// Emits EmergencyStateChanged only when the state actually changes.
function _setEmergencyState(uint8 newState) internal virtual {
uint8 previousState = _currentEmergencyState;
if (previousState == newState) {
return;
}
_lastEmergencyStateUpdateAtTimestamp = block.timestamp;
_currentEmergencyState = newState;
emit EmergencyStateChanged(msg.sender, previousState, newState);
}
/**
* @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(IEmergencyState).interfaceId;
}
}
Example state definitions
The following is a non-normative example of how states 1-3 might be defined. Implementers are free to define any state values and semantics appropriate for their protocol.
| State | Name | Example Behavior |
|---|---|---|
| 0 | Normal | All contract functions operate normally |
| 1 | Restricted | High-value or risky operations are limited or require additional authorization |
| 2 | HighRisk | Only emergency recovery operations are permitted; normal operations are suspended |
| 3 | Frozen | All external interactions are suspended; only internal administrative functions may operate |
Example standalone implementation
The following example demonstrates a token contract that implements IEmergencyState alone, without any emergency response interface. The contract defines its own EMERGENCY_ROLE and setEmergencyState() function.
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./EmergencyState.sol";
contract TokenWithEmergencyState is ERC20, Pausable, AccessControl, EmergencyState {
bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE");
constructor(address emergencyAdmin)
ERC20("TokenWithEmergencyState", "TES")
{
_grantRole(EMERGENCY_ROLE, emergencyAdmin);
}
function transfer(address to, uint256 amount) public override whenNotPaused returns (bool) {
return super.transfer(to, amount);
}
function transferFrom(address from, address to, uint256 amount) public override whenNotPaused returns (bool) {
return super.transferFrom(from, to, amount);
}
function setEmergencyState(uint8 newState) external onlyRole(EMERGENCY_ROLE) {
_setEmergencyState(newState);
}
}
Example combining with an emergency response interface
The following example demonstrates a token contract that implements both IEmergencyState (this proposal) and IEmergencyResponse (an emergency response interface). Because EmergencyState no longer brings its own AccessControl, both abstracts can be inherited directly without conflict. State is managed exclusively through triggerEmergency() and resolveEmergency() to keep state and side effects (pause/unpause) consistent.
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "./EmergencyResponse.sol";
import "./EmergencyState.sol";
contract TokenWithEmergencyResponseAndState is ERC20, Pausable, EmergencyResponse, EmergencyState {
constructor(address emergencyTriggerer, address emergencyResolver)
ERC20("TokenWithEmergencyResponseAndState", "TERS")
{
_grantRole(EMERGENCY_TRIGGER_ROLE, emergencyTriggerer);
_grantRole(EMERGENCY_RESOLVE_ROLE, emergencyResolver);
}
function transfer(address to, uint256 amount) public override whenNotPaused returns (bool) {
return super.transfer(to, amount);
}
function transferFrom(address from, address to, uint256 amount) public override whenNotPaused returns (bool) {
return super.transferFrom(from, to, amount);
}
/// @dev Only emergencyState 3 (Frozen) is supported: sets the state then pauses transfers.
function _executeEmergencyTrigger(uint8 _emergencyState) internal override {
if (_emergencyState == 3) {
_setEmergencyState(_emergencyState);
_pause();
return;
}
revert("unsupported emergencyState");
}
/// @dev Resets the emergency state to 0 and unpauses transfers.
/// Only emergencyState 3 (Frozen) recovery is supported; other values revert.
function _executeEmergencyResolve(uint8 _emergencyState) internal override {
if (_emergencyState == 3) {
_setEmergencyState(0);
_unpause();
return;
}
revert("unsupported emergencyState for recovery");
}
}
Copyright
Copyright and related rights waived via CC0.