Appearance
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:
solidity
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 theProxyUpgrader
contract in the constructor that MUST be set as an admin ofTransparent
proxies deployed viaaddProxyContract
method. - It MUST NOT be possible to add the zero address to the
ContractsRegistry
. - The
ContractsRegistry
MUST use theIDependant
interface in theinjectDependencies
andinjectDependenciesWithData
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:
solidity
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 thesetDependencies
method from the passedcontractsRegistry
address. - The
Dependant
contract MAY store the dependency injector address in the special slot0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583
(obtained asbytes32(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 thesetDependencies
function is made. That being said, it is possible to front-run the first dependency injection.
Copyright ​
Copyright and related rights waived via CC0.