ERC-6224 Contract dependency registry (Contracts Dependencies Registry)

Last Call · Other · created 2022-12-27

A management standard that registers the addresses of all smart contracts used by a protocol under names in a single registry, so each contract can fetch the dependency contracts it needs from there. Registered contracts can also be wrapped in a proxy for upgrades.

Why it is needed

As the Ethereum ecosystem grows, projects become increasingly complex, and continuing to ship new features requires the flexibility to change things. However, blockchains and smart contracts are immutable once deployed, which makes this requirement hard to meet. In a complex system, when contracts reference each other in tangled relationships, bugs become easy to introduce and maintenance becomes difficult. If which contract uses which contract is scattered throughout the code, replacing one contract means hunting down and fixing every contract that holds its address. This standard solves the problem with two contracts: a registry that collects addresses in one place (ContractsRegistry) and a mixin that receives injected dependencies (Dependant). The stated advantages are a clear structure, the ability to add, remove, or replace contracts at runtime, upgrades that can be enabled when needed, and transparent dependency relationships.

What to implement

In depth

The role of ContractsRegistry and its required rules

ContractsRegistry is a singleton registry that must store references to every standalone contract used within a protocol. On registration it MAY be configured to deploy a chosen proxy contract on top. It must reject registering the zero address, and getContract must revert if the requested contract is address(0). This is meant to reduce the risk of protocol initialization mistakes, since the correct contract must be registered before dependency injection takes place. addContract, addProxyContract, addProxyContractAndCall, and justAddProxyContract likewise revert on the zero address for the same reason.

Three branches of registration functions

addContract is for contracts used without a proxy, registering contracts over which the system does not have upgrade authority or that are not upgradeable by design. addProxyContract and addProxyContractAndCall newly deploy a proxy over the given implementation and register it, and are used for contracts the registry must be able to upgrade. The AndCall variant takes call data to invoke on the proxy immediately after deployment (for example, an ABI-encoded initializer call). justAddProxyContract registers an already-deployed proxy as is, and can be used when a system migrates to a new ContractsRegistry, in which case the new registry must have upgrade authority over that proxy.

The dependency injection mechanism

injectDependencies(name) must call the registered contract's setDependencies() with address(this) and bytes("") as arguments, and injectDependenciesWithData passes the supplied data instead of bytes(""). The data parameter is meant to carry application-specific additional context and can be used to extend the method's behavior. Every standalone contract must inherit Dependant, and required dependencies must be set in an overridden setDependencies method rather than in a constructor or initializer. This way, references can be refreshed by simply re-running injection whenever a dependency changes.

Injector access control and the storage slot

setDependencies and setInjector must only be callable by the injector. The initial injector is the zero address, in which case the access control check must not revert. In the reference implementation, _checkInjector passes only when the injector is address(0) or equal to msg.sender. The injector address (typically ContractsRegistry) MAY be stored in the special slot 0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583, which is derived as bytes32(uint256(keccak256("eip6224.dependant.slot")) - 1). setInjector is external so that dependency injection can also be supported for contracts minted by a factory, and it is explicitly noted that this must be used with particular care.

Upgrade path and reference implementation

upgradeContract and upgradeContractAndCall upgrade a registered proxy to a new implementation and must emit the ProxyContractUpgraded event. Verifying compatibility between implementations is the Owner's responsibility. In the reference implementation (based on OpenZeppelin 4.9.2), the registry deploys a ProxyUpgrader during initialization and designates this ProxyUpgrader as the admin of the TransparentUpgradeableProxy. Upgrade requests go through an onlyOwner registry function and are executed via ProxyUpgrader.upgrade, which reverts with "not a proxy contract" for an address that is not registered as a proxy.

Why names use string

Contract identifiers use string rather than uint256 or bytes32. Because the registry is the top-level contract of the protocol, users commonly interact with it directly through block explorers or a DAO, so code readability and reducing human error were prioritized over gas cost. Because identifiers are strings, event parameters are not made indexed. A string indexed parameter longer than 32 bytes is converted into the keccak256 hash of the name, which makes it hard to read. This proposal is currently in Last Call status (deadline 2025-07-31), a specification in its final comment period before finalization.

Things to watch out for

Original ERC · ← ERCs