The EIP standardizes the management of smart contracts within the decentralized application ecosystem. It enables protocols to become upgradeable and reduces their maintenance threshold. This EIP additionally introduces a smart contract dependency injection mechanism to audit dependency usage, to aid larger composite projects.
Motivation
In the ever-growing Ethereum, 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 facade and transparency upon their dependencies are easier to develop and maintain. The given EIP tries to solve the aforementioned problems by presenting two concepts: the contracts registry and the dependant.
The advantages of using the provided pattern might be:
Structured smart contracts management via specialized contract.
Ad-hoc upgradeability provision.
Runtime smart contracts addition, removal, and substitution.
Dependency injection mechanism to keep smart contracts’ dependencies under control.
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.
ContractsRegistry
The ContractsRegistry MUST implement the following interface:
pragmasolidity^0.8.0;interfaceIContractsRegistry{/**
* @notice REQUIRED 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
* @param isProxy whether the added contract is a proxy
*/eventAddedContract(stringname,addresscontractAddress,boolisProxy);/**
* @notice REQUIRED The event that is emitted when the contract get removed from the registry
* @param name the name of the removed contract
*/eventRemovedContract(stringname);/**
* @notice REQUIRED The function that returns an associated contract by the name
* @param name the name of the contract
* @return the address of the contract
*/functiongetContract(stringmemoryname)externalviewreturns(address);/**
* @notice OPTIONAL 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 RECOMMENDED The function that returns the admin of the added proxy contracts
* @return the proxy admin address
*/functiongetProxyUpgrader()externalviewreturns(address);/**
* @notice RECOMMENDED The function that returns an implementation of the given proxy contract
* @param name the name of the contract
* @return the implementation address
*/functiongetImplementation(stringmemoryname)externalviewreturns(address);/**
* @notice REQUIRED The function that injects dependencies into the given contract.
* MUST call the setDependencies() with address(this) and bytes("") as arguments on the substituted contract
* @param name the name of the contract
*/functioninjectDependencies(stringmemoryname)external;/**
* @notice REQUIRED The function that injects dependencies into the given contract with extra data.
* MUST call the setDependencies() with address(this) and given data as arguments on the substituted contract
* @param name the name of the contract
* @param data the extra context data
*/functioninjectDependenciesWithData(stringcalldataname,bytescalldatadata)external;/**
* @notice REQUIRED The function that upgrades added proxy contract with a new implementation
* @param name the name of the proxy contract
* @param newImplementation the new implementation the proxy will be upgraded to
*
* It is the Owner's responsibility to ensure the compatibility between implementations
*/functionupgradeContract(stringmemoryname,addressnewImplementation)external;/**
* @notice RECOMMENDED The function that upgrades added proxy contract with a new implementation, providing data
* @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 new implementation will be called with. This can be an ABI encoded function call
*
* It is the Owner's responsibility to ensure the compatibility between implementations
*/functionupgradeContractAndCall(stringmemoryname,addressnewImplementation,bytesmemorydata)external;/**
* @notice REQUIRED 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 the ones that are not upgradeable by design
* @param name the name to associate the contract with
* @param contractAddress the address of the contract
*/functionaddContract(stringmemoryname,addresscontractAddress)external;/**
* @notice REQUIRED The function that adds the contracts and deploys the Transaprent proxy above them.
* It MAY be used to add contract that the ContractsRegistry has to be able to upgrade
* @param name the name to associate the contract with
* @param contractAddress the address of the implementation
*/functionaddProxyContract(stringmemoryname,addresscontractAddress)external;/**
* @notice RECOMMENDED 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 ProxyUpgrader MUST have the
* credentials to upgrade the newly added proxies
* @param name the name to associate the contract with
* @param contractAddress the address of the proxy
*/functionjustAddProxyContract(stringmemoryname,addresscontractAddress)external;/**
* @notice REQUIRED The function to remove contracts from the ContractsRegistry
* @param name the associated name with the contract
*/functionremoveContract(stringmemoryname)external;}
The ContractsRegistry MUST deploy the ProxyUpgrader contract in the constructor that MUST be set as an admin of Transparent proxies deployed via addProxyContract method.
It MUST NOT be possible to add the zero address to the ContractsRegistry.
The ContractsRegistry MUST use the IDependant interface in the injectDependencies and injectDependenciesWithData methods.
Dependant
The Dependant contract is the one that depends on other contracts present in the system. In order to support dependency injection mechanism, the dependant contract MUST implement the following interface:
pragmasolidity^0.8.0;interfaceIDependant{/**
* @notice The function that is called from the ContractsRegistry (or factory) to inject dependencies.
* @param contractsRegistry the registry to pull dependencies from
* @param data the extra data that might provide additional application-specific context/behavior
*
* The Dependant MUST perform a dependency injector access check to this method
*/functionsetDependencies(addresscontractsRegistry,bytescalldatadata)external;/**
* @notice The function that sets the new dependency injector.
* @param injector the new dependency injector
*
* The Dependant MUST perform a dependency injector access check to this method
*/functionsetInjector(addressinjector)external;/**
* @notice The function that gets the current dependency injector
* @return the current dependency injector
*/functiongetInjector()externalviewreturns(address);}
The Dependant contract MUST pull its dependencies in the setDependencies method from the passed contractsRegistry address.
The Dependant contract MAY store the dependency injector 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 specified explicitly:
ContractsRegistry Rationale
Usage
The extensions of this EIP SHOULD add proper access control checks to the described non-view methods.
The getContract and getImplementation methods MUST revert if the nonexistent contracts are queried.
The ContractsRegistry MAY be set behind the proxy to enable runtime addition of custom methods. Applications MAY also leverage the pattern to develop custom tree-like ContractsRegistry data structures.
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, it MAY be typical for the users to interact with it via block explorers or DAOs. Clarity was prioritized over gas usage.
Proxy
The Transparent proxy is chosen over the UUPS proxy to hand the upgradeability responsibility to the ContractsRegistry itself. The extensions of this EIP MAY use the proxy of their choice.
Dependant Rationale
Dependencies
The required dependencies MUST be set in the overridden setDependencies method, not in the constructor or initializer methods.
The data parameter is provided to carry additional application-specific context. It MAY be used to extend the method’s behavior.
Injector
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 setInjector function is made external to support the dependency injection mechanism for factory-made contracts. However, the method SHOULD be used with extra care.
The injector address MAY be stored in the dedicated slot 0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583 to exclude the chances of storage collision.
Reference Implementation
0xdistributedlab-solidity-library dev-modules provides a reference implementation.
Security Considerations
The described EIP must be used with extra care as the loss/leakage of credentials to the ContractsRegistry leads to the application’s point of no return. The ContractRegistry is a cornerstone of the protocol, access must be granted to the trusted parties only.
ContractsRegistry Security Considerations
The non-view methods of ContractsRegistry contract MUST be overridden with proper access control checks.
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 Considerations
The non-view methods of Dependant contract MUST be overridden with proper access control checks. Only the dependency injector MUST be able to call them.
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.