This EIP introduces an on-chain registry system that a decentralized protocol may use to manage its smart contracts.
The proposed system consists of two components: ContractsRegistry and Dependant. The ContractsRegistry contract stores references to every smart contract used within a protocol, optionally making them upgradeable by deploying self-managed proxies on top, and acts as a hub the Dependant contracts query to fetch their required dependencies from.
Motivation
In the ever-growing Ethereum ecosystem, projects tend to become more and more complex. Modern protocols require portability and agility to satisfy customer needs by continuously delivering new features and staying on pace with the industry. However, the requirement is hard to achieve due to the immutable nature of blockchains and smart contracts. Moreover, the increased complexity and continuous delivery bring bugs and entangle the dependencies between the contracts, making systems less supportable.
Applications that have a clear architectural facade; which are designed with forward compatibility in mind; which dependencies are transparent and clean are easier to develop and maintain. The given EIP tries to solve the aforementioned problems by presenting two smart contracts: the ContractsRegistry and the Dependant.
The advantages of using the provided system might be:
Structured smart contracts management via specialized contracts.
Ad-hoc upgradeability provision of a protocol.
Runtime addition, removal, and substitution of smart contracts.
Dependency injection mechanism to keep smart contracts’ dependencies under control.
Ability to specify custom access control rules to maintain the protocol.
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.
Overview
The system consists of two smart contracts:
ContractsRegistry that is a singleton registry to manage and upgrade a protocol’s smart contracts.
Dependant that is a mix-in which enables a dependency injection mechanism.
The following diagram depicts the relationship between the registry and its dependants:
ContractsRegistry
The ContractsRegistry is the main contract of the proposed system. It MUST store the references to every standalone contract used within a protocol. The ContractRegistry MAY be configured to deploy a proxy contract of choice on top of the registered contracts.
Additionally, the ContractsRegistry MUST reject the registration of zero addresses.
The ContractsRegistry MUST implement the following interface:
pragmasolidity^0.8.0;interfaceIContractsRegistry{/**
* @notice The event that is emitted when the contract gets added to the registry
* @param name the name of the contract
* @param contractAddress the address of the added contract
*/eventContractAdded(stringname,addresscontractAddress);/**
* @notice The event that is emitted when the proxy contract gets added to the registry
* @param name the name of the contract
* @param contractAddress the address of the proxy contract
* @param implementation the address of the implementation contract
*/eventProxyContractAdded(stringname,addresscontractAddress,addressimplementation);/**
* @notice The event that is emitted when the proxy contract gets upgraded through the registry
* @param name the name of the contract
* @param newImplementation the address of the new implementation contract
*/eventProxyContractUpgraded(stringname,addressnewImplementation);/**
* @notice The event that is emitted when the contract gets removed from the registry
* @param name the name of the removed contract
*/eventContractRemoved(stringname);/**
* @notice The function that returns an associated contract by the name.
*
* MUST revert if the requested contract is `address(0)`
*
* @param name the name of the contract
* @return the address of the contract
*/functiongetContract(stringmemoryname)externalviewreturns(address);/**
* @notice The function that checks if a contract with a given name has been added
* @param name the name of the contract
* @return true if the contract is present in the registry
*/functionhasContract(stringmemoryname)externalviewreturns(bool);/**
* @notice The function that injects dependencies into the given contract.
*
* MUST call the `setDependencies()` with `address(this)` and `bytes("")` as arguments on the provided contract
*
* @param name the name of the contract
*/functioninjectDependencies(stringmemoryname)external;/**
* @notice The function that injects dependencies into the given contract with extra data.
*
* MUST call the `setDependencies()` with `address(this)` and `data` as arguments on the provided contract
*
* @param name the name of the contract
* @param data the extra context data that will be passed to the dependant contract
*/functioninjectDependenciesWithData(stringmemoryname,bytesmemorydata)external;/**
* @notice The function that upgrades added proxy contract with a new implementation.
*
* It is the Owner's responsibility to ensure the compatibility between implementations.
*
* MUST emit `ProxyContractUpgraded` event
*
* @param name the name of the proxy contract
* @param newImplementation the new implementation the proxy will be upgraded to
*/functionupgradeContract(stringmemoryname,addressnewImplementation)external;/**
* @notice The function that upgrades added proxy contract with a new implementation, providing data
*
* It is the Owner's responsibility to ensure the compatibility between implementations.
*
* MUST emit `ProxyContractUpgraded` event
*
* @param name the name of the proxy contract
* @param newImplementation the new implementation the proxy will be upgraded to
* @param data the data that the proxy will be called with after upgrade. This can be an ABI encoded function call
*/functionupgradeContractAndCall(stringmemoryname,addressnewImplementation,bytesmemorydata)external;/**
* @notice The function that adds pure (non-proxy) contracts to the `ContractsRegistry`. The contracts MAY either be
* the ones the system does not have direct upgradeability control over or those that are not upgradeable by design.
*
* MUST emit `ContractAdded` event. Reverts if the provided address is `address(0)`
*
* @param name the name to associate the contract with
* @param contractAddress the address of the contract to be added
*/functionaddContract(stringmemoryname,addresscontractAddress)external;/**
* @notice The function that adds the proxy contracts to the registry by deploying them above the provided implementation.
*
* The function may be used to add a contract that the `ContractsRegistry` has to be able to upgrade.
*
* MUST emit `ProxyContractAdded` event. Reverts if implementation address is `address(0)`
*
* @param name the name to associate the contract with
* @param contractAddress the address of the implementation to point the proxy to
*/functionaddProxyContract(stringmemoryname,addresscontractAddress)external;/**
* @notice The function that adds the proxy contracts to the registry by deploying them above the provided implementation,
* providing data.
*
* The function may be used to add a contract that the `ContractsRegistry` has to be able to upgrade.
*
* MUST emit `ProxyContractAdded` event. Reverts if implementation address is `address(0)`
*
* @param name the name to associate the contract with
* @param contractAddress the address of the implementation
* @param data the data that the proxy will be called with. This can be an ABI encoded initialization call
*/functionaddProxyContractAndCall(stringmemoryname,addresscontractAddress,bytesmemorydata)external;/**
* @notice The function that adds an already deployed proxy to the `ContractsRegistry`. It MAY be used
* when the system migrates to the new `ContractRegistry`. In that case, the new registry MUST have the
* credentials to upgrade the newly added proxies.
*
* MUST emit `ProxyContractAdded` event. Reverts if implementation address is `address(0)`
*
* @param name the name to associate the contract with
* @param contractAddress the address of the proxy
*/functionjustAddProxyContract(stringmemoryname,addresscontractAddress)external;/**
* @notice The function to remove contracts from the ContractsRegistry.
*
* MUST emit `ContractRemoved` event. Reverts if the contract is already removed
*
* @param name the associated name with the contract
*/functionremoveContract(stringmemoryname)external;}
Dependant
The ContractsRegistry works together with the Dependant contract. Every standalone contract of a protocol MUST inherit Dependant in order to support the dependency injection mechanism.
The required dependencies MUST be set in the overridden setDependencies method, not in the constructor or initializer methods.
Only the injector MUST be able to call the setDependencies and setInjector methods. The initial injector will be a zero address, in that case, the call MUST NOT revert on access control checks.
The Dependant contract MUST implement the following interface:
pragmasolidity^0.8.0;interfaceIDependant{/**
* @notice The function that is called from the `ContractsRegistry` to inject dependencies.
*
* The contract MUST perform a proper access check of `msg.sender`. The calls should only be possible from `ContractsRegistry`
*
* @param contractsRegistry the registry to pull dependencies from
* @param data the extra data that might provide additional application-specific context
*/functionsetDependencies(addresscontractsRegistry,bytesmemorydata)external;/**
* @notice The function that sets the new dependency injector.
*
* The contract MUST perform a proper access check of `msg.sender`
*
* @param injector the new dependency injector
*/functionsetInjector(addressinjector)external;/**
* @notice The function that gets the current dependency injector
* @return the current dependency injector
*/functiongetInjector()externalviewreturns(address);}
The Dependant contract MAY store the dependency injector (usually ContractsRegistry) address in the special slot 0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583 (obtained as bytes32(uint256(keccak256("eip6224.dependant.slot")) - 1)).
Rationale
There are a few design decisions that have to be explicitly specified:
ContractsRegistry Rationale
Contracts Identifier
The string contracts identifier is chosen over the uint256 and bytes32 to maintain code readability and reduce the human error chances when interacting with the ContractsRegistry. Being the topmost smart contract of a protocol, it MAY be typical for the users to interact with it via block explorers or DAOs. Clarity was prioritized over gas usage.
Due to the string identifier, the event parameters are not indexed. The string indexed parameter will become the keccak256 hash of the contract name if it is larger than 32 bytes. This fact reduces readability, which was prioritized.
Reverts
The getContract view function reverts if the requested contract is address(0). This is essential to minimize the risks of misinitialization of a protocol. Correct contracts SHOULD be added to the registry prior to any dependency injection actions.
The addContract, addProxyContract, addProxyContractAndCall, and justAddProxyContract methods revert if the provided address is address(0) for the same risk minimization reason.
Dependant Rationale
Dependencies
The data parameter is provided to carry additional application-specific context. It MAY be used to extend the method’s behavior.
Injector
The setInjector function is made external to support the dependency injection mechanism for factory-made contracts. However, the method SHOULD be used with extra care.
Reference Implementation
Note that the reference implementation depends on OpenZeppelin contracts 4.9.2.
ContractsRegistry Implementation
pragmasolidity^0.8.0;import{Address}from"@openzeppelin/contracts/utils/Address.sol";import{TransparentUpgradeableProxy}from"@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";import{OwnableUpgradeable}from"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";import{Dependant}from"./Dependant.sol";interfaceIContractsRegistry{eventContractAdded(stringname,addresscontractAddress);eventProxyContractAdded(stringname,addresscontractAddress,addressimplementation);eventProxyContractUpgraded(stringname,addressnewImplementation);eventContractRemoved(stringname);functiongetContract(stringmemoryname)externalviewreturns(address);functionhasContract(stringmemoryname)externalviewreturns(bool);functioninjectDependencies(stringmemoryname)external;functioninjectDependenciesWithData(stringmemoryname,bytesmemorydata)external;functionupgradeContract(stringmemoryname,addressnewImplementation)external;functionupgradeContractAndCall(stringmemoryname,addressnewImplementation,bytesmemorydata)external;functionaddContract(stringmemoryname,addresscontractAddress)external;functionaddProxyContract(stringmemoryname,addresscontractAddress)external;functionaddProxyContractAndCall(stringmemoryname,addresscontractAddress,bytesmemorydata)external;functionjustAddProxyContract(stringmemoryname,addresscontractAddress)external;functionremoveContract(stringmemoryname)external;}contractProxyUpgrader{usingAddressforaddress;addressprivateimmutable_OWNER;modifieronlyOwner(){_onlyOwner();_;}constructor(){_OWNER=msg.sender;}functionupgrade(addresswhat_,addressto_,bytescalldatadata_)externalonlyOwner{if(data_.length>0){TransparentUpgradeableProxy(payable(what_)).upgradeToAndCall(to_,data_);}else{TransparentUpgradeableProxy(payable(what_)).upgradeTo(to_);}}functiongetImplementation(addresswhat_)externalviewonlyOwnerreturns(address){// bytes4(keccak256("implementation()")) == 0x5c60da1b
(boolsuccess_,bytesmemoryreturndata_)=address(what_).staticcall(hex"5c60da1b");require(success_,"ProxyUpgrader: not a proxy");returnabi.decode(returndata_,(address));}function_onlyOwner()internalview{require(_OWNER==msg.sender,"ProxyUpgrader: not an owner");}}contractContractsRegistryisIContractsRegistry,OwnableUpgradeable{ProxyUpgraderprivate_proxyUpgrader;mapping(string=>address)private_contracts;mapping(address=>bool)private_isProxy;function__ContractsRegistry_init()publicinitializer{_proxyUpgrader=newProxyUpgrader();__Ownable_init();}functiongetContract(stringmemoryname_)publicviewreturns(address){addresscontractAddress_=_contracts[name_];require(contractAddress_!=address(0),"ContractsRegistry: this mapping doesn't exist");returncontractAddress_;}functionhasContract(stringmemoryname_)publicviewreturns(bool){return_contracts[name_]!=address(0);}functiongetProxyUpgrader()externalviewreturns(address){returnaddress(_proxyUpgrader);}functioninjectDependencies(stringmemoryname_)publicvirtualonlyOwner{injectDependenciesWithData(name_,bytes(""));}functioninjectDependenciesWithData(stringmemoryname_,bytesmemorydata_)publicvirtualonlyOwner{addresscontractAddress_=_contracts[name_];require(contractAddress_!=address(0),"ContractsRegistry: this mapping doesn't exist");Dependantdependant_=Dependant(contractAddress_);dependant_.setDependencies(address(this),data_);}functionupgradeContract(stringmemoryname_,addressnewImplementation_)publicvirtualonlyOwner{upgradeContractAndCall(name_,newImplementation_,bytes(""));}functionupgradeContractAndCall(stringmemoryname_,addressnewImplementation_,bytesmemorydata_)publicvirtualonlyOwner{addresscontractToUpgrade_=_contracts[name_];require(contractToUpgrade_!=address(0),"ContractsRegistry: this mapping doesn't exist");require(_isProxy[contractToUpgrade_],"ContractsRegistry: not a proxy contract");_proxyUpgrader.upgrade(contractToUpgrade_,newImplementation_,data_);emitProxyContractUpgraded(name_,newImplementation_);}functionaddContract(stringmemoryname_,addresscontractAddress_)publicvirtualonlyOwner{require(contractAddress_!=address(0),"ContractsRegistry: zero address is forbidden");_contracts[name_]=contractAddress_;emitContractAdded(name_,contractAddress_);}functionaddProxyContract(stringmemoryname_,addresscontractAddress_)publicvirtualonlyOwner{addProxyContractAndCall(name_,contractAddress_,bytes(""));}functionaddProxyContractAndCall(stringmemoryname_,addresscontractAddress_,bytesmemorydata_)publicvirtualonlyOwner{require(contractAddress_!=address(0),"ContractsRegistry: zero address is forbidden");addressproxyAddr_=_deployProxy(contractAddress_,address(_proxyUpgrader),data_);_contracts[name_]=proxyAddr_;_isProxy[proxyAddr_]=true;emitProxyContractAdded(name_,proxyAddr_,contractAddress_);}functionjustAddProxyContract(stringmemoryname_,addresscontractAddress_)publicvirtualonlyOwner{require(contractAddress_!=address(0),"ContractsRegistry: zero address is forbidden");_contracts[name_]=contractAddress_;_isProxy[contractAddress_]=true;emitProxyContractAdded(name_,contractAddress_,_proxyUpgrader.getImplementation(contractAddress_));}functionremoveContract(stringmemoryname_)publicvirtualonlyOwner{addresscontractAddress_=_contracts[name_];require(contractAddress_!=address(0),"ContractsRegistry: this mapping doesn't exist");delete_isProxy[contractAddress_];delete_contracts[name_];emitContractRemoved(name_);}function_deployProxy(addresscontractAddress_,addressadmin_,bytesmemorydata_)internalvirtualreturns(address){returnaddress(newTransparentUpgradeableProxy(contractAddress_,admin_,data_));}}
Dependant Implementation
pragmasolidity^0.8.0;interfaceIDependant{functionsetDependencies(addresscontractsRegistry,bytesmemorydata)external;functionsetInjector(addressinjector)external;functiongetInjector()externalviewreturns(address);}abstractcontractDependantisIDependant{/**
* @dev bytes32(uint256(keccak256("eip6224.dependant.slot")) - 1)
*/bytes32privateconstant_INJECTOR_SLOT=0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583;modifierdependant(){_checkInjector();_;_setInjector(msg.sender);}functionsetDependencies(addresscontractsRegistry_,bytesmemorydata_)publicvirtual;functionsetInjector(addressinjector_)external{_checkInjector();_setInjector(injector_);}functiongetInjector()publicviewreturns(addressinjector_){bytes32slot_=_INJECTOR_SLOT;assembly{injector_:=sload(slot_)}}function_setInjector(addressinjector_)internal{bytes32slot_=_INJECTOR_SLOT;assembly{sstore(slot_,injector_)}}function_checkInjector()internalview{addressinjector_=getInjector();require(injector_==address(0)||injector_==msg.sender,"Dependant: not an injector");}}
Security Considerations
It is crucial for the owner of ContractsRegistry to keep their keys in a safe place. The loss/leakage of credentials to the ContractsRegistry will lead to the application’s point of no return. The ContractRegistry is a cornerstone of a protocol, access must be granted to the trusted parties only.
ContractsRegistry Security
The ContractsRegistry does not perform any upgradeability checks between the proxy upgrades. It is the user’s responsibility to make sure that the new implementation is compatible with the old one.
Dependant Security
The Dependant contract MUST set its dependency injector no later than the first call to the setDependencies function is made. That being said, it is possible to front-run the first dependency injection.