Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

How to Architect a Modular Smart Contract System for Easy Upgrades

A technical guide on designing upgradeable smart contracts using proxies, EIP-2535 diamonds, and module separation for secure, governed protocol evolution.
Chainscore © 2026
introduction
INTRODUCTION

How to Architect a Modular Smart Contract System for Easy Upgrades

A modular architecture separates core logic from implementation details, enabling secure, gas-efficient upgrades without compromising user trust or assets.

Smart contracts are immutable by design, but application requirements evolve. A modular architecture solves this by separating a system's persistent state and core security logic from its changeable business logic. This pattern, often implemented via proxy contracts, allows developers to deploy new implementations while preserving the contract's address, storage, and user interactions. Key standards like Ethereum's EIP-1967 and EIP-1822 formalize these patterns, providing a reliable blueprint for upgradeable systems used by major protocols like Aave and Compound.

The core of this architecture is the Proxy Pattern. A user interacts with a proxy contract (the storage layer) which delegates all logic calls to a separate implementation contract via delegatecall. The proxy stores the address of the current implementation, which can be updated by an admin. Crucially, delegatecall executes the logic in the context of the proxy's storage, meaning user data and balances persist across upgrades. This separation is fundamental: the proxy holds the state, while the implementation holds the executable code.

To manage upgrades safely, you need an upgrade mechanism. A common approach uses an UpgradeableProxy with an associated ProxyAdmin contract. The admin contract owns the proxy and is the only address authorized to call upgradeTo(address newImplementation). This multi-step ownership adds a security layer, preventing a single compromised private key from unilaterally upgrading the system. The upgrade function should include checks and potentially a timelock to allow users to review changes.

Storage layout compatibility is the most critical technical constraint. When writing a new implementation, you must not modify the order, type, or removal of existing state variables declared in previous versions. Adding new variables is safe only at the end of the inherited storage layout. Incompatible changes can corrupt the proxy's storage, leading to catastrophic loss of funds. Using structured storage patterns, like the EIP-7201 Diamond Standard or unstructured storage with bytes32 slots, can mitigate these risks.

Best practices extend beyond the core pattern. Always use transparent proxies (EIP-1967) to avoid function selector clashes between the proxy and implementation. Employ comprehensive testing with tools like Hardhat or Foundry to simulate upgrades and verify storage integrity. Implement a timelock controller for production deployments, giving the community a window to exit if they disagree with an upgrade. Finally, thoroughly document all changes and use on-chain verification services like Sourcify to maintain transparency for users and auditors.

prerequisites
PREREQUISITES

How to Architect a Modular Smart Contract System for Easy Upgrades

This guide outlines the foundational concepts and design patterns required to build a smart contract system that can be securely and efficiently upgraded.

Before designing an upgradeable system, you must understand the core limitation of traditional smart contracts: their code is immutable once deployed. This immutability ensures trust but hinders bug fixes and feature additions. To enable upgrades, you must separate your application's logic from its storage. This is the fundamental principle behind patterns like the Proxy Pattern, where a lightweight proxy contract holds the state and delegates function calls to a separate logic contract. The proxy's storage is persistent, while the logic contract can be replaced.

Familiarity with delegatecall is essential. This low-level EVM opcode allows a contract to execute code from another contract's logic while preserving the context (like msg.sender and storage) of the caller. In a proxy system, the proxy uses delegatecall to the logic contract. This means the logic contract's code runs as if it were inside the proxy, allowing it to read and write to the proxy's storage. Understanding how delegatecall works, including its risks like storage layout collisions, is a non-negotiable prerequisite.

You must also grasp storage layout management. Since multiple logic contracts will sequentially write to the same proxy storage, their variable declarations must be append-only. You cannot change the order or types of existing state variables in an upgrade, as this will corrupt the stored data. Tools like OpenZeppelin's StorageSlot library or unstructured storage patterns help mitigate this risk. Planning your storage layout for future expansion from day one is critical for long-term upgradeability.

Knowledge of contract initialization is required. Unlike constructors, which run only on deployment, the code in a logic contract deployed for proxies never runs in its own context. Therefore, you need a separate initialization function, often called initialize, to set up the proxy's initial state. It's vital to protect this function from being called more than once, typically using an initializer modifier, to prevent reinitialization attacks that could reset critical state variables.

Finally, you should be aware of the main upgrade patterns and their trade-offs. The Transparent Proxy Pattern (used by OpenZeppelin) uses a ProxyAdmin to manage upgrades and prevent selector clashes between admin and user functions. The UUPS (Universal Upgradeable Proxy Standard) pattern bakes the upgrade logic into the logic contract itself, making it more gas-efficient but requiring each new implementation to include upgrade functionality. Choosing the right pattern depends on your gas budget and desired security model.

key-concepts-text
CORE UPGRADEABILITY CONCEPTS

How to Architect a Modular Smart Contract System for Easy Upgrades

A modular architecture separates a smart contract's core logic from its data storage and upgrade mechanisms, enabling secure and controlled updates without compromising user assets or state.

The most robust upgradeable smart contract systems are built on the proxy pattern. This pattern uses two main contracts: a Proxy and an Implementation (or Logic contract). The Proxy is the contract users interact with; it holds the system's state (data storage). The Implementation contains the executable code. When a user calls the Proxy, it delegates the call to the current Implementation using delegatecall, which executes the logic in the context of the Proxy's storage. This separation is fundamental, as it allows you to deploy a new Implementation contract and point the Proxy to it, upgrading the logic while preserving all existing data.

To manage the upgrade process securely, you need an upgrade mechanism. This is typically governed by an Upgrade Admin (like a multi-sig wallet or DAO) that can authorize changes. The most common standard is the Transparent Proxy Pattern, which prevents function selector clashes between the proxy admin and the implementation. More advanced systems use the UUPS (Universal Upgradeable Proxy Standard), where the upgrade logic is built into the Implementation contract itself, making proxies lighter and allowing for more granular upgrade control. Using established libraries like OpenZeppelin's Upgrades Plugins is critical, as they provide battle-tested contracts and tools that prevent common storage collision errors.

A modular architecture extends beyond the proxy pattern by breaking the Implementation into discrete, reusable components. Instead of one monolithic logic contract, you design a system of separate modules or facets that handle specific functions (e.g., a token transfer module, a staking module, an access control module). These modules interact with a shared storage contract. This approach, exemplified by Diamond Pattern (EIP-2535), allows for granular upgrades. You can replace or add a single module without redeploying the entire system, reducing gas costs and limiting the scope of changes, which simplifies testing and audit requirements.

Storage management is a critical consideration. You must ensure that new logic versions do not corrupt existing data. The unstructured storage pattern is a common solution, where the proxy stores the implementation address and admin in specific, pseudo-random storage slots calculated with keccak256. This prevents the logic contract from accidentally overwriting these critical variables. For modular systems, a structured storage contract that defines explicit struct layouts for data, which all modules reference, provides a clear and collision-resistant data model. Always initialize storage variables in a dedicated function, like an initialize function, which acts as a constructor for upgradeable contracts.

When architecting the system, plan for upgrade safety and governance. Key steps include: conducting thorough testing of upgrade paths on a testnet, implementing timelocks for administrative actions to allow community review, and maintaining clear versioning and changelogs. Security tools like Slither or MythX can help detect storage layout incompatibilities. Remember, upgradeability introduces trust assumptions in the upgrade admin, so the governance model is as important as the technical design. The end goal is a system that remains secure, maintainable, and adaptable to future needs without requiring costly migrations.

upgrade-patterns
ARCHITECTURE

Upgradeability Patterns

Smart contract upgradeability is critical for long-term protocol security and feature development. This guide covers the core patterns, their trade-offs, and implementation details.

ARCHITECTURE

Upgrade Pattern Comparison

A comparison of common smart contract upgrade patterns, detailing their security, complexity, and operational trade-offs.

FeatureTransparent ProxyUUPS (EIP-1822)Diamond Standard (EIP-2535)

Upgrade Logic Location

Proxy Contract

Implementation Contract

Diamond Contract

Proxy Storage Clashes

Implementation Contract Size Limit

< 24KB

No Limit

No Limit

Gas Cost for Upgrade

~100k gas

~50k gas

~200k+ gas

Initialization Mechanism

Constructor or initializer

Constructor or initializer

diamondCut initializer

Selective Function Upgrades

Implementation Immutability Risk

Low (admin can't selfdestruct)

High (implementation can selfdestruct)

Medium (facet can be removed)

Audit Complexity

Low

Medium

High

proxy-implementation
SMART CONTRACT ARCHITECTURE

Implementing a Transparent Proxy System

A guide to designing upgradeable smart contracts using the Transparent Proxy Pattern, separating logic from storage for secure and manageable deployments.

The Transparent Proxy Pattern is a foundational upgradeability architecture for Ethereum smart contracts. It separates your system into two core components: a Proxy Contract that holds the state (storage) and a Logic Contract that contains the executable code. User interactions are always with the proxy, which delegates all function calls to the current logic contract via the delegatecall opcode. This pattern is widely adopted, forming the basis for systems like OpenZeppelin's upgradeable contracts and many major DeFi protocols, allowing developers to fix bugs or add features without migrating user data or funds.

Architecting this system requires careful management of storage layouts and initialization. Because delegatecall executes logic in the context of the proxy's storage, the storage variables defined in your logic contract must be append-only; you cannot change the order or type of existing variables in an upgrade. Malicious storage collisions are a primary risk. The constructor in a logic contract is not used; instead, you must implement an initializer function protected by an initializer modifier to set up the contract's initial state. This function is called atomically after the proxy links to the logic contract.

Here is a simplified example of a proxy's fallback function that handles delegation:

solidity
fallback() external payable {
    address _implementation = implementation;
    assembly {
        calldatacopy(0, 0, calldatasize())
        let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
        returndatacopy(0, 0, returndatasize())
        switch result
        case 0 { revert(0, returndatasize()) }
        default { return(0, returndatasize()) }
    }
}

This low-level assembly code forwards the call and handles the return data or revert. The implementation variable is a storage slot in the proxy that holds the address of the current logic contract.

The "transparent" aspect refers to a critical security feature: differentiating between admin calls and regular user calls. If an admin's address tried to call a function that only exists on the proxy (like upgradeTo(address newImplementation)), it should execute on the proxy. If a regular user calls the same function signature, it should be delegated to the logic contract (and likely revert). This prevents a malicious admin from tricking users into executing proxy-admin functions. Modern libraries like OpenZeppelin's TransparentUpgradeableProxy handle this access control automatically.

To implement a robust system, follow these steps: 1) Write your logic contract using upgradeable-safe patterns (e.g., inheriting from OpenZeppelin's Initializable). 2) Deploy the logic contract first. 3) Deploy your proxy contract, passing the logic contract address and an admin address to its constructor. 4) Call the initializer function on the proxy to set up state. For upgrades, the admin deploys a new logic contract and calls upgradeTo on the proxy. Always use established libraries and conduct thorough testing, including storage layout checks, on a testnet before mainnet deployment.

diamond-implementation
MODULAR ARCHITECTURE

Building with the Diamond Standard (EIP-2535)

EIP-2535 introduces a modular proxy pattern for smart contracts, enabling on-chain upgrades without migration. This guide explains its core components and how to architect a system for long-term maintainability.

The Diamond Standard (EIP-2535) solves the upgradeability problem for complex smart contracts. Traditional upgradeable proxies replace a single logic contract, forcing a monolithic redeployment. A Diamond is a proxy contract that delegates function calls to multiple, independent logic contracts called facets. This allows developers to add, replace, or remove functionality in a granular, gas-efficient manner. The Diamond maintains a central mapping of function selectors to facet addresses, managed by a diamondCut function.

A Diamond system has three core components. The Diamond Proxy is the main contract that holds state and routes calls. Facets are stateless libraries that contain the executable logic; popular implementations like OpenZeppelin's use the LibDiamond library to manage storage. The Diamond Loupe is a standard set of view functions that introspect the Diamond, listing its facets and the functions each provides. This modularity enables teams to develop and audit features independently before attaching them to the live system.

Storage management is critical. Facets should not define their own state variables, as this causes storage collisions. Instead, use a structured approach like Diamond Storage. This involves defining a struct for a facet's data and using a unique namespace (a bytes32 slot key) to store it. For example: bytes32 constant MY_FACET_STORAGE_POSITION = keccak256("my.facet.storage");. This ensures each facet's data is isolated and can be safely modified across upgrades, preventing the silent corruption of state that plagues other patterns.

The diamondCut function is the upgrade mechanism. It takes an array of FacetCut structs, each specifying a facet address, an action (ADD, REPLACE, REMOVE), and the function selectors involved. When executed, it updates the internal selector-to-facet mapping and emits an event. This operation is permissioned, typically restricted to a multi-sig or DAO. Because only the mapping changes, upgrades are gas-efficient compared to full contract redeployment, and existing storage layouts remain untouched, preserving user data and contract addresses.

To architect a system, start by defining core domains (e.g., TokenFacet, StakingFacet, GovernanceFacet). Implement each as a separate contract using Diamond Storage. Use the Diamond-3 reference implementation for the base proxy and loupe. For testing, tools like hardhat-deploy can simulate diamondCut operations. A well-designed Diamond enables continuous iteration: you can patch a bug in one facet, add a new AMM integration, or deprecate an old function without disrupting the entire protocol's operation or requiring user migration.

governance-integration
ARCHITECTURE GUIDE

Integrating Governance for Upgrades

Learn how to design a modular smart contract system that enables secure, community-driven protocol evolution through on-chain governance.

A modular architecture is the foundation for upgradeable smart contracts. The core principle is separation of concerns: separate your contract's logic from its data storage and its upgrade mechanism. This is typically achieved using a proxy pattern, where a lightweight Proxy contract holds the state and delegates all function calls to a separate Logic contract. The proxy stores a single address pointing to the current logic implementation. When you need an upgrade, you deploy a new logic contract and propose updating the proxy's pointer via governance. This allows you to fix bugs, add features, or optimize gas without migrating user data or disrupting the protocol's operation.

On-chain governance integrates the upgrade decision-making process directly into the protocol. Instead of a single admin key, upgrade authority is vested in a governance token or a DAO (Decentralized Autonomous Organization). Common implementations use contracts like OpenZeppelin's Governor with a companion Timelock. A standard flow is: 1) A community member creates a proposal to upgrade the proxy to a new logic contract address. 2) Token holders vote on the proposal during a specified period. 3) If the vote passes, the proposal is queued in a Timelock contract, introducing a mandatory delay (e.g., 48 hours). 4) After the delay, anyone can execute the proposal, finalizing the upgrade. The timelock is critical for security, giving users time to exit if they disagree with the changes.

Your logic contracts must be designed for upgrade safety. Use inheritance and initializer functions instead of constructors, as a proxy cannot call a constructor. Libraries like OpenZeppelin's Initializable and UUPSUpgradeable provide the scaffolding. Crucially, you cannot change the storage layout between upgrades; new variables must always be appended. A flawed upgrade can permanently corrupt storage and brick your protocol. Thorough testing with tools like Hardhat or Foundry, including storage layout checks and simulating the full governance flow, is non-negotiable before deploying any upgrade proposal to a live network.

Consider the trade-offs between upgradeability patterns. The Transparent Proxy pattern (different admin and logic) avoids selector clash issues but is more gas-intensive for users. The UUPS (EIP-1822) pattern bakes the upgrade logic into the logic contract itself, making it more gas-efficient but requiring that upgrade functionality is included and preserved in every subsequent version. UUPS proxies are now the recommended standard for most new projects. Regardless of pattern, always implement a pause mechanism in your logic contract and grant the timelock the pauser role. This allows the DAO to emergency-pause the protocol if a catastrophic bug is discovered in a newly upgraded contract, providing a critical safety net.

A real-world example is Uniswap's migration to V3. While not a simple proxy upgrade, it demonstrates governance-led evolution. UNI token holders voted on and approved the deployment of the new, incompatible V3 core contracts. For in-place upgrades, Compound's Governor Bravo and the associated Timelock control the Comptroller and CToken proxy contracts. Their architecture shows how a complex DeFi system can evolve through hundreds of executed proposals. Your upgrade system should be as simple as possible while being robust enough to handle both routine improvements and emergency responses, always prioritizing the security of user funds over development convenience.

common-mistakes-grid
MODULAR ARCHITECTURE

Common Pitfalls and Security Risks

A modular smart contract system enables upgrades, but introduces critical design challenges. Avoiding these pitfalls is essential for security and maintainability.

06

Ignoring Proxy-Specific Gas Costs

Every call to an upgradeable contract via a proxy incurs an extra ~2,700 gas for the delegatecall overhead. Complex architectures with many cross-module calls can make functions prohibitively expensive, potentially causing out-of-gas errors for users.

Optimization strategies:

  • Minimize cross-contract calls within a single transaction.
  • Batch operations where possible.
  • Profile gas usage specifically in the proxy context, not just the standalone implementation.
~2.7k gas
Delegatecall Overhead
testing-strategy
ARCHITECTURE

Testing and Verification Strategy

A robust testing and verification strategy is essential for secure, maintainable smart contract systems. This guide outlines a multi-layered approach for modular architectures.

The core principle is to test each component in isolation before integration. For a modular system using the Proxy Pattern with an Implementation contract, you must write unit tests for the logic contract's functions independently of the proxy. Use a testing framework like Foundry or Hardhat to simulate calls directly to the implementation. This verifies the core business logic works as intended. For example, test a transfer function's math and state changes without the overhead of delegatecall mechanics. Isolating logic contracts prevents proxy-specific bugs from obscuring functional issues.

Integration testing validates how components interact. The critical integration is between the Proxy and its Implementation. Write tests that deploy a proxy pointing to your logic contract and execute transactions through the proxy address. Verify that storage is correctly persisted across upgrades by initializing state in v1, upgrading to v2, and asserting the state remains accessible. Test upgrade scenarios explicitly: simulate an upgrade via the upgradeTo function from a ProxyAdmin contract and confirm the new logic executes correctly. Tools like OpenZeppelin's Upgrades Plugins can automate much of this safety checking.

Upgrade-specific tests are non-negotiable. They ensure new versions don't corrupt existing storage. Storage layout compatibility is paramount; use slither-upgradeability-checker or surya to analyze changes between versions. Write migration tests for any required state transformations. For instance, if v2 adds a new mapping, a test should deploy v1, populate old data, upgrade, run a migration function, and verify the new structure holds the old data. Always test the upgrade rollback path to ensure you can revert to a previous version if a bug is discovered post-deployment.

Formal verification and invariant testing provide higher assurance. Tools like Certora or SMTChecker can mathematically prove that certain properties hold for all inputs and states, which is ideal for critical modules like upgrade authorization. For complex systems, use fuzzing (e.g., Foundry's forge fuzz) to generate random inputs and uncover edge cases in your logic. Define invariants—properties that should always be true, such as "total supply must equal the sum of all balances"—and run fuzz tests to check they hold before and after simulated upgrades.

Finally, establish a verification checklist for each release. This should include: 1) All unit and integration tests pass, 2) Storage layout checks show no collisions, 3) Upgrade simulation on a testnet (like Sepolia) succeeds, 4) Key system invariants are fuzzed for at least 1 million runs, and 5) A time-locked upgrade process is tested in a staging environment. Documenting and automating this process reduces human error and creates a reproducible, secure pipeline for deploying upgrades to your modular smart contract system.

MODULAR ARCHITECTURE

Frequently Asked Questions

Common questions and solutions for developers designing upgradeable smart contract systems using patterns like the Proxy Pattern, Diamond Standard, and Data Separation.

Both patterns use a proxy contract to delegate calls to a logic contract, but they differ in where the upgrade logic resides.

Transparent Proxy: The upgrade logic (the upgradeTo function) is built into the proxy contract itself. This pattern requires a separate admin address to manage upgrades, preventing function selector clashes between the admin and logic contract.

UUPS (EIP-1822): The upgrade logic is part of the logic (implementation) contract. The proxy is simpler and cheaper to deploy, but each new logic contract version must include the upgrade functionality. If this is omitted in a future version, the system becomes non-upgradeable.

Key Trade-off: Transparent proxies have higher initial gas costs but are more foolproof. UUPS proxies are more gas-efficient but place the burden of maintaining upgradeability on the developer.

How to Architect a Modular Smart Contract System for Easy Upgrades | ChainScore Guides