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 like OpenZeppelin’sPausable). 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 use role-based access control (e.g., OpenZeppelin’s
OwnableorAccessControl) to restrictregisterContract(),modifyContractStatus(),modifyContractExtraData()andremoveContract()to authorized addresses. -
Events: MUST emit events for all state changes to enable off-chain indexing by tools like block explorers.
-
Input Validation:
registerContract()MUST revert ifcontractAddressis already registered or zero.modifyContractStatus(),modifyContractExtraData()andremoveContract()MUST revert ifcontractAddressis not registered. -
Extensibility: MAY include additional fields or functions, but core functionality MUST remain compatible.
-
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 (e.g., immutable contracts, multisigs, or role-based systems like OpenZeppelin’sAccessControl). 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., ERC-1967 with a single implementation vs. ERC-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. Examples include:-
ERC-1967 Proxy: The proxy (0xABC) and implementation (0xDEF) are registered separately. Proxy:
Active,extraData: “EIP-1967 proxy using implementation 0xDEF.” Implementation:ActiveorDeprecated,extraData: “Implementation for proxy 0xABC.” -
ERC-2535 Diamond Proxy: The proxy (0x789) and facets (0x123, 0x456) are registered separately. Proxy:
Active,extraData: “Diamond proxy with facets 0x123 (governance), 0x456 (staking).” Facet:ActiveorDeprecated,extraData: “Facet for governance in Diamond 0x789.” -
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
registerContract(),modifyContractStatus()andmodifyContractExtraData()functions are restricted to authorized addresses (e.g., viaOwnableorAccessControl). Implementers MUST ensure only trusted parties can update metadata to prevent unauthorized status changes. -
Data Integrity: The fields in
ContractMetadatamay contain misleading information. Implementers SHOULD verify using trusted sources before adding to registry to ensure accuracy and prevent misuse. -
State Transition Validation: Implementers SHOULD add additional checks to restrict invalid state transitions and preserve important states (e.g.,
Terminatednot supposed to be changed to any status) when usingmodifyContractStatus()to maintain lifecycle integrity. -
Off-Chain Verification for Sensitive Statuses: Before updating a contract’s status to
Terminated, implementers SHOULD validate claims using off-chain audits, public reports, or official announcements to ensure accuracy and prevent misuse. TheextraDatafield MAY include references to such documentation for transparency. -
Legitimacy of Registry: Registry owners are RECOMMENDED to provide official channel for the public to verify the registry address (e.g., maintain registry address in official website) to prevent phishing by creating a fake registry contract.
-
Non-Contract Address Registration: Registering non-contract addresses (e.g., externally owned accounts) could mislead users or tools, compromising the registry’s integrity. Implementers MUST verify that an address contains deployed code in
registerContract()at registration time.
Copyright
Copyright and related rights waived via CC0.