EIP-6224: Contracts Dependencies Registry

Abstract

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:

pragma solidity ^0.8.0;

interface IContractsRegistry {
   /**
    *  @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
    */
   event AddedContract(string name, address contractAddress, bool isProxy);

   /**
    *  @notice REQUIRED The event that is emitted when the contract get removed from the registry
    *  @param name the name of the removed contract
    */
   event RemovedContract(string name);

   /**
    *  @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
    */
   function getContract(string memory name) external view returns (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
    */
   function hasContract(string memory name) external view returns (bool);

   /**
    *  @notice RECOMMENDED The function that returns the admin of the added proxy contracts
    *  @return the proxy admin address
    */
   function getProxyUpgrader() external view returns (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
    */
   function getImplementation(string memory name) external view returns (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
    */
   function injectDependencies(string memory name) 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
    */
   function injectDependenciesWithData(
        string calldata name,
        bytes calldata data
    ) 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
    */
   function upgradeContract(string memory name, address newImplementation) 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
    */
   function upgradeContractAndCall(
       string memory name,
       address newImplementation,
       bytes memory data
   ) 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
    */
   function addContract(string memory name, address contractAddress) 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
    */
   function addProxyContract(string memory name, address contractAddress) 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
    */
   function justAddProxyContract(string memory name, address contractAddress) external;

   /**
    *  @notice REQUIRED The function to remove contracts from the ContractsRegistry
    *  @param name the associated name with the contract
    */
   function removeContract(string memory name) 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:

pragma solidity ^0.8.0;

interface IDependant {
   /**
    *  @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
    */
   function setDependencies(address contractsRegistry, bytes calldata data) 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
    */
   function setInjector(address injector) external;

   /**
    *  @notice The function that gets the current dependency injector
    *  @return the current dependency injector
    */
   function getInjector() external view returns (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.
4 Likes

@Arvolear thank you for this proposal. It looks very important. Two questions

  1. Do you anticipate the addProxyContract and justAddProxyContract to be guarded by access control?
  2. Do you anticipate the setInjector to carry additional context at any case?

Hey, @xinbenlv, thank you very much for the feedback.

  1. Yes, to be precise, all the non-view functions should be guarded by access control. It may be as simple as Ownable from Openzeppelin or as complex as a custom DAO. You can find the Ownable preset right here.

  2. Actually it might. The injector is a special account that has permission of injecting dependencies into the dependant contracts. Here, the ContractsRegistry possesses both roles: injector and registry. However, these roles may be split and used separately. There is a case with a factory that deploys dependant contracts where the separation becomes visible. The factory would temporarily become the injector to provide the necessary dependencies upon deployment and set the injector to a proper contract afterward.

1 Like

Cool, in that case, consider EIP-5750: General Extensibility for Method Behaviors to give it extra data for future expansion. For example, when using EIP-5750 it opens up future possibility for your ERC adopter to also use a Commit-Reveal (EIP-5732: Commit Interface) for sealed action or Commit-Delay for timelock action? It also provides future possibility for support a general permit based on EIP-5453: Endorsement - Permit for Any Functions which allow you to do

  • Admin Alice: sign(offline) an endorsement and give it to another Bob then
  • User Bob: sends TX with Alice’s endorsement to conduct the action

I am also working on a more general Access Control EIP which hopefully generalize access control Add EIP-5982: Role-based Access Control by xinbenlv · Pull Request #5982 · ethereum/EIPs · GitHub. Your feedback will be appreciated.

Then consider how do this interface represent passing in or out the context. For example, if you use r EIP-5750: General Extensibility for Method Behaviors for setInjector and any functions that might take context. EIP-5750 is a way to allow interface designer and caller to agree with each other how to pass in more context in the same function call as extra parameter. But I could imagine you might also consider other methods / additional metheds like setContext(...)

Thanks, will take a look. The EIP-5750: General Extensibility for Method Behaviors might be useful for the setDependencies method.

1 Like

Hey, @xinbenlv, I have updated the sheet with the new method injectDependenciesWithData() and an extra parameter in setDependencies() function. Would be amazing if you could take a look.

1 Like

Cool. that sounds great!