Discussion Topic for ERC: Versioned Proxy Contract Interface
Simple Summary
This ERC standardizes a proxy interface that lets users call specific contract versions, enabling backward compatibility and opt-in upgrades.
Abstract
This EIP standardizes an interface for proxy contracts that allows callers to explicitly select which version of an implementation contract they want to interact with. Unlike traditional proxy patterns that only expose the latest implementation, this standard enables backward compatibility by maintaining access to previous implementations while supporting upgrades. The versioned proxy maintains a registry of implementation addresses mapped to version identifiers, allowing callers to specify their desired version at call time.
Motivation
Smart contract upgrades are essential for fixing bugs and adding features. Current proxy patterns typically force all callers to use the latest implementation, which can break existing integrations when interfaces change.
Furthermore, traditional proxy patterns expose all users to risk if an upgrade is malicious, as they have no choice but to use the latest implementation. This standard allows users to remain on verified versions they trust, mitigating the risk of a compromised admin key or governance process deploying harmful code.
This EIP addresses several key problems:
- Breaking Changes: Interface changes in new implementations can break existing integrations.
- Gradual Adoption: There is no standard way to allow gradual adoption of new contract versions.
- Malicious Upgrades: Users today must trust proxy admins indefinitely, as they can’t opt out of potentially harmful upgrades without ceasing use of the contract entirely.
- Trust Assumptions: Contract users must maintain perpetual trust in governance or admin keys, with no ability to selectively trust specific, audited implementations.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Interface
interface IVersionedProxy {
/// @notice Emitted when a new implementation version is registered
/// @param version The version identifier
/// @param implementation The address of the implementation contract
event VersionRegistered(bytes32 version, address implementation);
/// @notice Emitted when the default version is changed
/// @param oldVersion The previous default version
/// @param newVersion The new default version
event DefaultVersionChanged(bytes32 oldVersion, bytes32 newVersion);
/// @notice Registers a new implementation version
/// @param version The version identifier (e.g., "1.0.0")
/// @param implementation The address of the implementation contract
function registerVersion(bytes32 version, address implementation) external;
/// @notice Removes a version from the registry
/// @param version The version identifier to remove
function removeVersion(bytes32 version) external;
/// @notice Sets the default version to use when no version is specified
/// @param version The version identifier to set as default
function setDefaultVersion(bytes32 version) external;
/// @notice Gets the implementation address for a specific version
/// @param version The version identifier
/// @return The implementation address for the specified version
function getImplementation(bytes32 version) external view returns (address);
/// @notice Gets the current default version
/// @return The current default version identifier
function getDefaultVersion() external view returns (bytes32);
/// @notice Gets all registered versions
/// @return An array of all registered version identifiers
function getVersions() external view returns (bytes32[] memory);
/// @notice Executes a call to a specific implementation version
/// @param version The version identifier of the implementation to call
/// @param data The calldata to forward to the implementation
/// @return The return data from the implementation call
function executeAtVersion(bytes32 version, bytes calldata data) external payable returns (bytes memory);
}
Behavior Requirements
- The proxy contract MUST maintain a mapping of version identifiers to implementation addresses.
- The proxy contract MUST maintain a default version that is used when no version is specified.
- When
executeAtVersion
is called, the proxy MUST:- Verify the specified version exists
- Forward the call to the corresponding implementation
- Return any data returned by the implementation
- The proxy contract MUST emit appropriate events when versions are registered, or when the default version changes.
- The proxy contract SHOULD implement access control for administrative functions (registering versions, setting default).
- The proxy contract MAY implement EIP-1967 storage slots for compatibility with existing tools.
Fallback Function
The proxy contract SHOULD implement a fallback function that forwards calls to the default implementation version when no version is specified. This maintains compatibility with traditional proxy patterns.
Rationale
Version Identifiers as bytes32
Version identifiers are specified as bytes32
rather than semantic versioning strings to:
- Provide flexibility in versioning schemes
- Reduce gas costs for storage and comparison
- Allow for both string-based versions (converted to bytes32) and numeric versions
- Allow for storing a Git commit identifier in SHA-1 or SHA-256
Explicit Version Selection
The standard requires callers to explicitly select a version through executeAtVersion
rather than encoding version information in the call data to:
- Maintain a clean separation between version selection and function calls
- Avoid modifying existing function signatures
- Make version selection explicit and auditable
Registry Pattern
The registry pattern was chosen over alternatives like:
- Multiple Proxies: Having separate proxies for each version would increase deployment costs and complexity
- Version in Storage: Storing a single “current version” would not allow different callers to use different versions simultaneously
Default Version
The default version mechanism allows the proxy to maintain compatibility with traditional proxy patterns and supports callers that don’t need to specify a version.
Backwards Compatibility
This EIP is designed to enhance backward compatibility for smart contracts. It does not introduce any backward incompatibilities with existing Ethereum standards or implementations.
Existing contracts that interact with proxy contracts can continue to do so without modification, as the fallback function will route calls to the default implementation.
Reference Implementation
TBD