Diamonds vs Transparent and UUPS Proxies
An honest comparison of EIP-2535 against the more common proxy patterns, including the real trade-offs in gas, tooling and audit burden.
Diamonds are often presented as the natural evolution of proxy patterns. They are not — they are a different set of trade-offs, better for some systems and worse for others. This guide compares EIP-2535 against the two patterns most teams actually use.
The three patterns in one paragraph each
Transparent proxy (EIP-1967 + OpenZeppelin)
A proxy holds an implementation address in a fixed storage slot and delegatecalls it for every
call. A separate ProxyAdmin contract performs upgrades. The proxy inspects msg.sender on each call to decide whether it is an admin call or a user call, which is
where the "transparent" name and a small permanent gas overhead come from.
// Transparent proxy: one implementation, all calls
contract TransparentUpgradeableProxy {
address implementation; // stored at a fixed EIP-1967 slot
fallback() external payable {
// admin calls go to the proxy's own admin functions,
// everything else delegatecalls the single implementation
_delegate(implementation);
}
} UUPS (EIP-1822 style)
Same single-implementation idea, but the upgrade function lives in the implementation rather than the proxy. That makes the proxy smaller and cheaper to call, at the cost of a sharp edge: ship an implementation without upgrade logic and the contract is frozen forever.
// UUPS: upgrade logic lives in the implementation
contract MyContractV1 is UUPSUpgradeable, OwnableUpgradeable {
function _authorizeUpgrade(address) internal override onlyOwner {}
// ... protocol logic, all in this one contract, all under 24 KB
} Diamond (EIP-2535)
A proxy with a mapping of implementations rather than a single one. Each function
selector routes to its own facet, and diamondCut edits that mapping.
// Diamond: many implementations, routed per selector
fallback() external payable {
address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
require(facet != address(0), "Diamond: Function does not exist");
_delegate(facet);
} Head to head
| Dimension | Transparent | UUPS | Diamond |
|---|---|---|---|
| 24 KB size limit | Applies to the whole implementation | Applies to the whole implementation | Applies per facet — effectively unbounded |
| Upgrade granularity | All-or-nothing | All-or-nothing | Per function selector |
| Per-call gas overhead | Highest (admin check + delegatecall) | Lowest (delegatecall only) | Middle (storage lookup + delegatecall) |
| Deployment cost | Low | Low | High — many facets plus the cut transaction |
| Explorer support | Good | Good | Poor without loupe-aware tooling |
| Storage discipline required | Moderate (append-only) | Moderate (append-only) | High (append-only + cross-facet isolation) |
| Audit surface | One implementation | One implementation | Every facet plus their interactions |
| Ecosystem familiarity | Very high | High | Low |
Where diamonds genuinely win
- You are over 24 KB and cannot reasonably slim down. This is the strongest and least arguable reason. No amount of optimiser tuning fixes a protocol that is fundamentally too large.
- Independent release cadences. A bug fix in one module should not require redeploying and re-auditing an unrelated one.
- Multiple owners. Different teams, or different multisigs, can be given authority over different selectors.
- Smaller upgrade blast radius. Replacing one selector changes exactly one code path, which is far easier to reason about than swapping an entire implementation.
Where a plain proxy wins
- You fit in 24 KB. Most protocols do. Adopting a diamond "just in case" buys complexity you are not using.
- Your team is small. The storage discipline diamonds demand scales badly when one person is holding the whole layout in their head at 2am.
- You want off-the-shelf tooling. OpenZeppelin's upgrade plugins will refuse an unsafe storage change automatically. Equivalent tooling for diamonds is thinner and often bespoke.
- Auditor availability and cost. Far more reviewers are fluent in transparent/UUPS proxies, and a diamond audit is simply a bigger job.
- Users need to read your contract. On a plain proxy, a block explorer shows the full ABI. On a diamond it shows a fallback and nothing else.
The honest summary
Diamonds trade simplicity for modularity. If the 24 KB limit is a real constraint for you, or if independent module upgrades are a genuine organisational requirement, that trade is worth making and EIP-2535 is a well-designed way to make it. If neither is true, a UUPS proxy will serve you better, and choosing one is not a sign of a less sophisticated team.
A pragmatic middle path that several protocols use: start with a UUPS proxy, and migrate to a diamond only when you actually hit the size wall. The migration is not trivial, but it is far cheaper than carrying diamond complexity through the entire life of a project that never needed it.
If you are reviewing a diamond that someone else deployed — whether to integrate with it or to audit it — start with inspecting it in Louper and then work through the security checklist.