Abstract
This EIP introduces a minimal registry interface for enforcing maintenance of smart contract lifecycle status. The status indicates whether a smart contract deployed is Active (in use), Deprecated (functional but unmaintained), Frozen (paused but reversible), or Terminated (permanently deactivated). The registry allows implementers to record and update the status and important metadata about contracts. The goal is to improve security and simplify inventory management for protocols by providing a minimal, interoperable mechanism for users and tools (e.g., block explorers) to monitor contract states.
Motivation
The absence of standardized tracking in the smart contract lifecycle introduces critical challenges for Ethereum entities:
-
Loss of Inventory: Entities cannot enumerate or monitor deployed contracts and avoid maintaining contract inventories due to perceived inconvenience, leading to disorganized asset management.
-
Security Risks: Without an inventory, entities cannot promptly identify their deployed smart contracts and respond quickly to attacks due to discrepancy of statuses.
-
Inefficient Status Validation: Security researchers require inefficient coordination with teams to discover discrepancies of smart contract state (e.g., contracts marked inactive internally but still callable).
-
Lack of Interoperable Tracking: Custom or off-chain registries lack interoperability, isolating contract data and limiting integration with ecosystem tools.
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.
The registry MUST include the following elements:
Interface
The IContractRegistry interface defines the standard functions and events for the registry, ensuring interoperability and consistency across implementation.
/// @title Smart Contract Lifecycle Registry
pragma solidity ^0.8.0;
interface IContractRegistry {
enum ContractStatus { Default, Active, Deprecated, Frozen, Terminated }
struct ContractMetadata {
ContractStatus status;
string extraData;
uint256 statusUpdateTimestamp;
}
// MUST trigger when a new contract is added to the registry
event ContractRegistered(address indexed contractAddress);
// MUST trigger when a registered contract’s status field is modified
event ContractStatusModified(address indexed contractAddress, ContractStatus oldStatus, ContractStatus newStatus);
// MUST trigger when a registered contract’s extraData field is modified
event ContractExtraDataModified(address indexed contractAddress, string newExtraData, string newExtraData);
// MUST trigger when a contract is removed from the registry
event ContractRemoved(address indexed contractAddress);
// MUST be called only by AUTHORIZED users (e.g., the registry owner) to add a new contract to the registry and MUST emit the ContractRegistered event
function registerContract(address contractAddress, ContractStatus status, string memory extraData) external;
// MUST be called only by AUTHORIZED users to modify a registered contract’s status and MUST emit the ContractStatusModified event
function modifyContractStatus(address contractAddress, ContractStatus newStatus) external;
// MUST be called only by AUTHORIZED users to modify a registered contract’s extraData and MUST emit the ContractExtraDataModified event
function modifyContractExtraData(address contractAddress, string newExtraData) external;
// MUST called only by AUTHORIZED users to remove a contract from the registry and MUST emit the ContractRemoved event
function removeContract(address contractAddress) external;
// Provides public read-only access to a contract’s metadata to inspect its current status, extraData, and timestamps
function getContractMetadata(address contractAddress) external view returns (ContractMetadata memory);
// Returns a list of all registered contract addresses, enabling to enumerate the entire inventory of contracts managed by the registry
function getAllContracts() external view returns (address[] memory);
}Data Structures
The ContractStatus enum defines possible contract states to indicate the operational status of a registered contract. The enum values are:
-
Default: Default value for uninitialized contract status. -
Active: The contract is actively used, fully functional, and intended for regular operation. Suitable for contracts in production, signaling safe interaction to users. -
Deprecated: The contract is no longer actively used or recommended but remains fully functional, allowing user interactions (e.g., Outdated contract version but the contract is still callable by users). -
Frozen: The contract cannot be used due to a temporary, reversible halt (e.g., via a pause mechanism). Used for signaling temporary unavailability with potential reactivation (e.g., The contract is paused and can't be interacted with temporarily but it can be recovered to fully operational status by unpausing the contract). -
Terminated: The contract cannot be used, and its usability is not recoverable. Used for obsolete or abandoned contracts to signal they should be ignored or archived (e.g., The contract is paused and revoked pauser role, resulting no one can revert the pause state and the contracts is completely not callable).
The ContractMetadata struct stores metadata for each contract, including:
-
status: The current operational status, REQUIRED to track lifecycle state. -
extraData: An extra data field to store additional information of the contract, OPTIONAL for context (e.g., IPFS storing additional data to store details of audit reports, proxy implementation address, etc). -
statusUpdateTimestamp: The block timestamp of the last modification time of status, REQUIRED to track maintain auditability.
Implementation Requirements
-
Access Control: MUST restrict
registerContract(),modifyContractStatus(),modifyContractExtraData()andremoveContract()to authorized addresses. -
Events: MUST emit events for all state changes to enable off-chain indexing by tools like block explorers.
-
Implementers SHOULD deploy one registry per entity to manage their contracts, ensuring scalability and customization.
Rationale
The design choices reflect the following considerations:
-
Simplified
ContractMetadataStruct: TheContractMetadatastruct includes onlystatus,extraDataandstatusUpdateTimestampto keep the registry lightweight and focused on tracking contract deployment and lifecycle states. This minimizes gas costs and adoption barriers. -
Exclusion of Privileged Accounts: Fields for tracking ownership (e.g.,
owner) were considered but excluded due to the complexity of smart contract ownership models. Not all contracts have a single privileged account. Tracking privileged accounts accurately requires a dedicated registry to handle diverse models, which is better addressed in a separate EIP. -
Exclusion of Upgrade-Specific Fields: Fields like
upgradeProxywere considered but removed due to the diversity of upgrade patterns (e.g., EIP-1967 with a single implementation vs. EIP-2535 Diamonds with multiple facets). Instead, users can register each implementation contract (e.g., proxies, facets) as separate entries in the registry, using theextraDatafield to clarify relationships. This approach ensures flexibility for all upgradeable contract types while keeping the struct simple. -
Flexible Extra Data Field: The
extraDatafield also allows users to provide context for complex scenarios, such as proxy implementations, without imposing rigid fields that may not suit all contracts. These extra data can also be any form like an IPFS link which stored extra information of the contracts. -
Exclusion of a Global Registry: A global registry per chain was considered but rejected due to governance challenges. A single registry would require centralized or complex decentralized governance to manage updates, risking centralization, disputes, or unauthorized changes. Instead, this EIP prefers each entity deploy their own registry, ensuring autonomous control over their contract metadata and avoiding governance-related risks. However, global registry could also be implemented if a legitimate governance to manage updates can be ensured.
Backwards Compatibility
No backward compatibility issues are introduced. This EIP introduces a new registry contract and does not affect existing Ethereum standards or contracts.
Reference Implementation
The reference implementation of the ContractRegistry provided in this EIP is for illustrative purposes only, demonstrating the proposed interface and functionality.
Below is a reference implementation of the registry contract in Solidity:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
/// @title EIP8089 Implementation
/// @notice Implementation of EIP-8089 Smart Contract Lifecycle Registry Standard
/// @dev This registry allows operators to record and update metadata about contracts, including their operational status
contract ContractRegistry is Ownable {
using EnumerableSet for EnumerableSet.AddressSet;
/// @notice Status of a registered contract
/// @dev Used to track the lifecycle status of registered contracts
enum ContractStatus {
Default, /// Initial or reset state
Active, /// Contract is operational
Deprecated, /// Contract is outdated but still functional
Frozen, /// Contract operations are temporarily suspended but recoverable
Terminated /// Contract is permanently disabled and not recoverable
}
/// @notice Metadata associated with a registered contract
/// @dev Contains status, extra data, and timestamp information
struct ContractMetadata {
ContractStatus status; /// Current status of the contract
string extraData; /// Additional information about the contract
uint256 statusUpdateTimestamp; /// Timestamp of last status update
}
/// @dev Mapping of contract addresses to their metadata
mapping(address => ContractMetadata) private _metadata;
/// @dev Set of all registered contract addresses
EnumerableSet.AddressSet private _contracts;
/// @notice Emitted when a new contract is registered
/// @param contractAddress The address of the registered contract
event ContractRegistered(address indexed contractAddress);
/// @notice Emitted when a contract's status is modified
/// @param contractAddress The address of the contract
/// @param oldStatus The previous status of the contract
/// @param newStatus The new status of the contract
event ContractStatusModified(address indexed contractAddress, ContractStatus oldStatus, ContractStatus newStatus);
/// @notice Emitted when a contract's extra data is modified
/// @param contractAddress The address of the contract
/// @param oldExtraData The previous extra data
/// @param newExtraData The new extra data
event ContractExtraDataModified(address indexed contractAddress, string oldExtraData, string newExtraData);
/// @notice Emitted when a contract is removed from the registry
/// @param contractAddress The address of the removed contract
event ContractRemoved(address indexed contractAddress);
/// @dev Maximum length for extraData field (256 bytes)
uint256 private constant MAX_EXTRADATA_LENGTH = 256;
/// @notice Constructs the ContractRegistry contract
/// @param initialOwner The address that will be set as the owner of the contract
constructor(address initialOwner) Ownable(initialOwner) {}
/// @dev Private function to update contract status and emit ContractStatusModified event
/// @param contractAddress The address of the contract to update
/// @param newStatus The new status to assign to the contract
function _modifyContractStatus(address contractAddress, ContractStatus newStatus) private {
ContractStatus oldStatus = _metadata[contractAddress].status;
_metadata[contractAddress].status = newStatus;
_metadata[contractAddress].statusUpdateTimestamp = block.timestamp;
emit ContractStatusModified(contractAddress, oldStatus, newStatus);
}
/// @dev Private function to update extra data field and emit ContractExtraDataModified event
/// @param contractAddress The address of the contract to update
/// @param newExtraData The new extra data to assign to the contract
function _modifyContractExtraData(address contractAddress, string memory newExtraData) private {
string memory oldExtraData = _metadata[contractAddress].extraData;
require(bytes(newExtraData).length <= MAX_EXTRADATA_LENGTH, "ExtraData too long");
_metadata[contractAddress].extraData = newExtraData;
emit ContractExtraDataModified(contractAddress, oldExtraData, newExtraData);
}
/// @notice Registers a new contract in the registry
/// @dev Only callable by the owner
/// @param contractAddress The address of the contract to register
/// @param newStatus The initial status to assign to the contract
/// @param newExtraData Additional information about the contract
function registerContract(
address contractAddress,
ContractStatus newStatus,
string calldata newExtraData
) public onlyOwner {
require(contractAddress != address(0), "Invalid address");
require(!_contracts.contains(contractAddress), "Contract already registered");
_contracts.add(contractAddress);
_modifyContractStatus(contractAddress, newStatus);
_modifyContractExtraData(contractAddress, newExtraData);
emit ContractRegistered(contractAddress);
}
/// @notice Updates the status of a registered contract
/// @dev Only callable by the owner
/// @param contractAddress The address of the contract to update
/// @param newStatus The new status to assign to the contract
function modifyContractStatus(
address contractAddress,
ContractStatus newStatus
) public onlyOwner {
require(_contracts.contains(contractAddress), "Contract not registered");
require(newStatus != ContractStatus.Default, "Invalid contract status");
_modifyContractStatus(contractAddress, newStatus);
}
/// @notice Updates the extra data of a registered contract
/// @dev Only callable by the owner
/// @param contractAddress The address of the contract to update
/// @param newExtraData The new extra data to assign to the contract
function modifyContractExtraData(
address contractAddress,
string calldata newExtraData
) public onlyOwner {
require(_contracts.contains(contractAddress), "Contract not registered");
_modifyContractExtraData(contractAddress, newExtraData);
}
/// @notice Removes a contract from the registry
/// @dev Only callable by the owner. Resets contract metadata to default values.
/// @param contractAddress The address of the contract to remove
function removeContract(address contractAddress) public onlyOwner {
require(_contracts.contains(contractAddress), "Contract not registered");
_contracts.remove(contractAddress);
_modifyContractStatus(contractAddress, ContractStatus.Default);
_modifyContractExtraData(contractAddress, "");
emit ContractRemoved(contractAddress);
}
/// @notice Retrieves the metadata for a registered contract
/// @dev Reverts if the contract is not registered
/// @param contractAddress The address of the contract
/// @return The metadata associated with the contract
function getContractMetadata(address contractAddress) public view returns (ContractMetadata memory) {
require(_contracts.contains(contractAddress), "Contract not registered");
return _metadata[contractAddress];
}
/// @notice Retrieves all registered contract addresses
/// @return An array of all registered contract addresses
function getAllContracts() public view returns (address[] memory) {
return _contracts.values();
}
}Security Considerations
-
Access Control: The registry’s write functions are a critical attack surface. If access control is misconfigured or compromised, an attacker could falsely mark active contracts as
Terminatedor register fraudulent entries, misleading users and dependent tools. -
Data Integrity: The registry does not verify the accuracy of metadata it stores. A registry owner could intentionally or accidentally record incorrect statuses or misleading
extraData, causing users to interact with unsafe or obsolete contracts. -
State Transition Validation: The base interface does not enforce valid state transitions. Without additional guards, a
Terminatedcontract could be reactivated. -
Off-Chain Verification for Sensitive Statuses: The
Terminatedstatus signals permanent deactivation. If applied without sufficient evidence, it could incorrectly signal that a live contract is defunct, or conversely, delay marking a genuinely compromised contract. TheextraDatafield can serve as an audit trail by referencing supporting off-chain documentation. -
Legitimacy of Registry: Anyone can deploy a contract implementing this interface. A malicious actor could create a fake registry that misrepresents the status of contracts they do not own, deceiving users or block explorers.
Copyright
Copyright and related rights waived via CC0.
