Smart contract development is as much about process as it is about code. The workflow you choose—how you organize, upgrade, and maintain your on-chain logic—determines your project's agility, security surface, and long-term maintainability. This guide compares the most common contract architecture patterns, helping you match a workflow to your project's actual constraints: team size, upgrade frequency, gas budget, and risk appetite.
Why Workflow Choices Matter More Than Ever
In the early days of Ethereum, most projects deployed a single contract and hoped for the best. Upgrades meant migrating state to a new address, which fragmented users and liquidity. Today, the landscape is more nuanced. Teams face a maze of patterns—transparent proxies, UUPS, diamonds, beacon proxies, and modular registries—each with trade-offs that ripple into gas costs, security audits, and developer ergonomics.
Choosing poorly can be expensive. A DeFi protocol that picks a rigid single-contract architecture may find itself unable to fix a critical bug without a painful migration. Conversely, a small NFT project that adopts a complex diamond pattern may waste audit budget on unused flexibility. The goal is to align workflow complexity with project maturity and risk profile.
We see three common scenarios driving workflow decisions: early-stage projects that need rapid iteration, established protocols that prioritize stability and gas efficiency, and infrastructure layers that must support diverse upgrade policies across many contracts. Each demands a different pattern.
The Cost of Getting It Wrong
Consider a lending protocol that launched with a transparent proxy but later wanted to change its storage layout. The team discovered that the proxy's fixed storage slots conflicted with new variables, forcing a complex migration. Had they chosen UUPS, they could have upgraded the logic contract more cleanly—but they would have paid higher deployment gas each time. The lesson: the best workflow depends on how often you expect to upgrade and how much state you carry.
Three Core Dimensions
Every workflow decision boils down to three dimensions: upgradeability mechanism, storage management, and access control. Upgradeability can be built-in (proxy), modular (diamond facets), or absent (immutable). Storage can be delegated (proxy storage), partitioned (diamond storage), or encapsulated (module state). Access control can be role-based, multi-sig governed, or timelocked. Understanding these primitives helps you evaluate any pattern.
Core Workflow Patterns in Plain Language
At a high level, smart contract workflows fall into four families: the single-contract monolith, the proxy upgrade pattern, the diamond standard (EIP-2535), and the module-based registry. Each represents a different answer to the question: how do we change the code without losing the state?
Single-Contract Monolith
The simplest workflow: deploy one contract with all logic and state. Upgrades are not supported—you deploy a new contract and migrate users manually. This is ideal for simple tokens, one-time events, or projects where immutability is a feature (e.g., a vesting contract). The downside is obvious: any bug becomes a permanent liability unless you include an emergency pause or migration function from the start.
Proxy Upgrade Pattern
The proxy pattern separates logic from state. A proxy contract holds the state and delegates calls to a logic contract. To upgrade, you deploy a new logic contract and point the proxy to it. Variants include transparent proxies (where the proxy checks if the caller is the admin) and UUPS (where the upgrade function lives in the logic contract, saving gas on every call). Transparent proxies are simpler for multi-sig governance, while UUPS reduces delegation overhead—at the cost of requiring the logic contract to include upgrade code.
Diamond Standard (EIP-2535)
Diamonds extend the proxy idea to multiple logic contracts (facets) sharing one proxy's storage. Each facet implements a set of functions, and the proxy uses a fallback to dispatch calls based on a function selector mapping. This allows you to add, replace, or remove facets independently, making it attractive for large codebases that evolve over time. The trade-off is complexity: storage must be managed via diamond storage patterns to avoid collisions, and the initial setup requires careful planning.
Module-Based Registry
Instead of a single proxy, a registry contract holds pointers to multiple module contracts. Each module is a standalone contract with its own state, and the registry routes calls based on function selectors or module IDs. This pattern is common in DAO frameworks and plugin architectures. It offers maximum modularity—each module can be upgraded independently—but introduces cross-module communication overhead and requires consistent access control across modules.
How Each Workflow Works Under the Hood
Let's open the hood on the proxy pattern first, since it's the most widely used. A proxy contract has a delegatecall in its fallback function that forwards all calls to a logic contract. The logic contract's code runs in the proxy's storage context. This means storage layout must remain compatible across upgrades—you can only append new variables to the end of the existing struct, not reorder or delete them. Violating this rule corrupts state.
Transparent Proxy Mechanics
In a transparent proxy, the proxy contract checks whether the caller is the admin. If yes, it executes admin functions (like upgrade) directly on the proxy; otherwise, it delegates to the logic contract. This prevents the logic contract from accidentally hijacking admin functions. The gas cost per call includes an extra SLOAD for the admin check, which adds ~800 gas per transaction—negligible for most use cases but worth noting for high-frequency operations.
UUPS Mechanics
UUPS moves the upgrade function into the logic contract itself. The proxy only delegates; it has no upgrade logic. This saves gas on every non-upgrade call because the proxy doesn't check the admin. However, the logic contract must implement the upgrade function, and if a bug in a future version removes it, the contract becomes permanently frozen. UUPS is favored by teams that expect frequent upgrades and want to minimize per-call costs.
Diamond Storage
Diamonds avoid storage collisions by using a diamondStorage mapping keyed by a unique identifier per facet. Each facet declares its own storage struct and accesses it via sload/sstore through that mapping. The proxy's fallback uses a function selector lookup to find the correct facet. This adds a mapping lookup (one SLOAD) per call, plus the delegation overhead. For contracts with dozens of functions, the diamond pattern can reduce deployment costs because you only deploy new facets, not the entire codebase.
Registry Dispatch
In a module registry, the registry contract maintains a mapping from function selectors to module addresses. When a call comes in, the registry looks up the module and delegates via delegatecall or call. If using call, each module has its own state, which simplifies upgrades but requires cross-module calls to share data—often via a shared storage contract. This pattern is common in plugin systems where each module is developed independently.
Worked Example: Choosing a Workflow for a DeFi Lending Protocol
Let's walk through a composite scenario. A team is building a lending protocol that will launch with basic supply/borrow functionality, but plans to add flash loans, liquidation automation, and governance voting over the next year. They expect 3–5 upgrades in the first six months. Their team has two Solidity developers and a security budget of $50,000 for audits.
Option 1: Transparent Proxy
The team considers a transparent proxy with a single logic contract. Initial deployment is straightforward. They can upgrade the logic contract by deploying a new version and calling upgradeTo via a multi-sig. However, after three upgrades, they realize that adding new storage variables requires careful ordering to avoid slot collisions. They also find that each upgrade requires a full audit of the entire logic contract, even if only a small change was made. The transparent proxy's admin check adds a small but consistent gas overhead on every user transaction.
Option 2: Diamond Standard
They evaluate a diamond with facets for core lending, flash loans, and liquidation. Each facet can be audited independently, and upgrades only require deploying a new facet and updating the selector mapping. Storage management via diamond storage is more complex initially—they need to define a shared storage struct for cross-facet data (like user balances). The team spends two weeks learning the pattern, but after that, upgrades become faster. The gas cost per call is slightly higher due to the facet lookup, but the team calculates it adds only ~200 gas per transaction, acceptable for their expected volume.
Option 3: Module Registry
A registry pattern with separate contracts for each feature seems attractive for modularity. However, the team realizes that cross-module calls (e.g., liquidation needs to read supply data) require either a shared storage contract or passing data via the registry. This adds complexity and gas for nested calls. They decide the registry is overkill for a protocol that needs tight integration between features.
Decision
The team chooses the diamond pattern. The independent auditability of facets saves them money in the long run, and the ability to add new features without touching core logic reduces upgrade risk. They accept the upfront learning curve and the small per-call gas increase. For a larger team with more resources, the transparent proxy might have been simpler, but for their size, the diamond's modularity wins.
Edge Cases and Exceptions
No pattern is universal. Here are situations where the common advice flips.
Immutable Contracts with Emergency Stops
Some projects deliberately choose immutability for trust reasons—e.g., a token with a fixed supply. But what if a vulnerability is discovered? The solution is to include a circuit breaker (pause function) controlled by a multi-sig or timelock. This preserves immutability of the core logic while allowing a safety halt. The workflow here is a single contract with a pause mechanism, not a proxy.
High-Frequency State Changes
For contracts that process thousands of transactions per block, like a DEX's swap router, every gas unit matters. Transparent proxies and diamonds add overhead. In this case, the best workflow is often a single immutable contract with no upgrade path, or a UUPS proxy that minimizes per-call cost. Some projects deploy a minimal proxy (EIP-1167) for clones, but that's a different pattern for factory deployments.
Permissioned vs. Permissionless Upgrades
If your project is governed by a DAO, you need an upgrade mechanism that supports timelocks and proposal voting. Transparent proxies work well because the admin can be a multi-sig or a DAO contract. Diamonds also work, but the governance must be able to update the facet mapping. A common mistake is using an EOA as the admin for a proxy—if that key is compromised, the entire contract is compromised. Always use a multi-sig or DAO as the admin.
Cross-Chain Deployments
When deploying the same logic on multiple chains, consider using beacon proxies. A beacon contract stores the logic address, and multiple proxy contracts point to the same beacon. Upgrading the beacon updates all proxies at once. This is efficient for multi-chain projects but adds a layer of indirection that can be confusing for debugging.
Limits of Each Approach
Every pattern has a ceiling beyond which it becomes a liability.
Proxy Pattern Limits
Proxy patterns suffer from the storage collision problem when upgrading. Even with careful planning, if you add a new variable that shifts the layout of inherited contracts, you can corrupt state. Tools like OpenZeppelin's upgradeable contracts plugin enforce storage gap patterns, but they are not foolproof. Additionally, the proxy itself is a single point of failure: if the proxy contract is compromised, all logic contracts are at risk.
Diamond Pattern Limits
Diamonds introduce complexity that can lead to bugs. The diamond storage pattern requires discipline—if two facets accidentally use the same storage key, data can be overwritten. The fallback function also makes it harder to use static analysis tools, as the dispatch logic is opaque. Gas costs for the fallback lookup are higher than a direct call, and the pattern is overkill for small contracts with fewer than 10 functions.
Module Registry Limits
Registries that use call instead of delegatecall create isolated state, which is good for modularity but bad for composability. Modules that need to share state must use a central storage contract, which becomes a bottleneck. The registry itself can become a governance target—if the registry is upgraded maliciously, all modules are affected.
Single-Contract Limits
Immutable contracts are safe but inflexible. If a bug is found, the only recourse is to deploy a new contract and migrate users—a process that can break integrations and lose user trust. This pattern is best for simple, well-audited logic that is unlikely to change.
Reader FAQ
What is the cheapest workflow in terms of gas?
A single immutable contract is cheapest because there is no delegation overhead. Among upgradeable patterns, UUPS is the cheapest per call because the proxy does not check the admin. Transparent proxies add a small overhead, and diamonds add a bit more due to the facet lookup. For deployment gas, diamonds can be cheaper if you deploy many facets separately instead of one large contract.
Can I use a transparent proxy with a multi-sig admin?
Yes, that is the recommended setup. Set the admin address to a multi-sig contract. The proxy's upgradeTo function will then require multi-sig approval. This is a standard pattern for DeFi protocols.
How do I handle storage upgrades in a diamond?
Use the diamond storage pattern: define a struct inside a library with a unique storage slot (e.g., keccak256('my.storage')). Access it via sload and sstore. Never use regular state variables in facets. This prevents collisions and allows each facet to have its own storage.
What happens if I deploy a buggy logic contract to a proxy?
If the bug is in the logic contract, you can upgrade to a new logic contract. However, if the bug corrupts the proxy's storage (e.g., by writing to the wrong slot), the state may be unrecoverable. Always test upgrades on a testnet and use a timelock to allow users to exit before the upgrade takes effect.
Is the diamond standard worth the complexity for a small project?
Generally, no. For a project with fewer than 10 functions and a small team, a transparent proxy or UUPS is simpler and less error-prone. Diamonds shine when you have a large codebase with many functions that evolve independently, or when you want to deploy facets incrementally to save deployment gas.
How do I choose between transparent proxy and UUPS?
Choose transparent proxy if you want the simplest upgrade mechanism and don't mind a small gas overhead on every call. Choose UUPS if you want to minimize per-call gas and are comfortable having the upgrade logic in the logic contract. UUPS is also slightly more secure against certain types of attacks because the proxy has no admin functions to exploit.
Ultimately, the best workflow is the one that fits your project's current stage and expected evolution. Start simple, but plan for upgrades. Use a proxy if you anticipate changes, and always test your upgrade path before mainnet deployment. The right choice today will save you from a painful migration tomorrow.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!