Non-Proxy Upgradability
System Architecture Overview
Our system is structured around a set of core manager components. These managers are integral to facilitating smooth and seamless upgradeability throughout the system, with their functionality encompassing the following key operations:
Enable: Activates a logic contract.
Disable: Deactivates a logic contract.
Execution: Directs calls to a logic contract, ensuring they're always routed to the latest version.
To manage the activation, deactivation, and execution within this framework, we utilize manager contracts. Initially, to activate a logic contract, a manager contract calls Enable which requires the name of the module, blueprint, or action, the parent address, and its deployment address. As the lifecycle progresses, the system simply retrieves the current module, blueprint, or action by name to get its respective address from storage and updates it with the new desired address for use. When initiating Execution, we delegate the call to the logic contract, this is not to be confused with the proxy pattern delegate call. It's important to note that neither the manager nor the logic contract holds any state; all state information resides within our dedicated storage contract. To deactivate a contract the manager will call Disable which will remove the contract from its parent component.


As we move forward in development this setup is designed to evolve, intending to map names and chain IDs to addresses to facilitate upgrades across different blockchain networks. This approach not only enhances our system's flexibility but also ensures its adaptability for cross-chain functionality.
Security Motivation
Our architecture is designed to address and mitigate several well-documented vulnerabilities associated with traditional upgradability standards. Here, we outline common proxy-related issues and a common issue with upgradability and detail the system's mechanisms for overcoming them, ensuring enhanced security, reliability, and flexibility.
Uninitialized Proxies
Problem: Proxies typically lack a constructor, relying instead on an initialize()
function to set key parameters. There's a risk that this function may not be called upon deployment, potentially leaving the contract open to anyone setting themselves as the owner.
Upgradeability Flaws
Problem: Incorrectly implemented upgrade mechanisms can result in contracts becoming un-upgradeable, locking the system into a single implementation with no possibility for improvement or bug fixes.
Multiple Initializations
Problem: If not properly safeguarded, an initialize()
function could be executed multiple times or by unauthorized parties, potentially leading to security breaches or data corruption.
Frontrunning Initialization Risks
Problem: Public initialization functions are vulnerable to frontrunning, where malicious actors could preempt the legitimate initialization transaction, seizing control of the contract.
How we mitigate these common issues - Every contract within our system is required to utilize a constructor for initial setup. This approach ensures that all contracts are properly initialized upon deployment, eliminating the risk associated with initialization. Our system is designed such that any enabled contract can be modified or updated at any time. The primary limitation one might encounter involves appropriate execution functions, which would restrict the ability to interact with subsequent components within the architecture.
Function Selector Clash
A function selector clash arises if a newly added function in a contract results in the same four-byte hash as a pre-existing function. To counter this issue, our system utilizes explicit encoding of function signatures via abi.encodeWithSignature
. This approach substantially reduces the likelihood of unintended behavior due to a function selector clash. It fundamentally depends on accurately encoding the function signature, incorporating both the function name and its parameters, to accurately identify and call the intended function.
How It Works & Solution
Explicit Signature Specification: When you encode a function call with
abi.encodeWithSignature
, you're specifying exactly which function you intend to call, including its parameters. This direct specification helps to avoid unintended clashes, as the encoded data will only match the intended function's signature.Dynamic Execution: The
execute
function dynamically directs the call to a specified addressto_
with the provideddata_
(the encoded function call). Because this process uses the encoded signature, it relies on the caller to specify the correct function. If the encoded signature doesn't exactly match a function on the target contract, the call will fail.
Backward Compatibility
Our approach to smart contract upgradability is future-proof by design. Our system integrates smoothly with both current and future blockchain technologies. It's built for adaptability, allowing for the easy addition, modification, or removal of functionalities without compromising system integrity.
Specification
For a contract to integrate into the system architecture, it must adhere to the following guidelines:
The logic contract should implement the designated execution function as dictated by the hierarchical structure of sub-managers.
Each logic contract must inherit from its respective base contract, such as a Blueprint contract deriving from BlueprintBase.
The constructor of the logic contract needs to receive the address of the storage contract, which should then be passed along to its base contract.
Last updated