All guides

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.

9 min read

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

DimensionTransparentUUPSDiamond
24 KB size limitApplies to the whole implementationApplies to the whole implementationApplies per facet — effectively unbounded
Upgrade granularityAll-or-nothingAll-or-nothingPer function selector
Per-call gas overheadHighest (admin check + delegatecall)Lowest (delegatecall only)Middle (storage lookup + delegatecall)
Deployment costLowLowHigh — many facets plus the cut transaction
Explorer supportGoodGoodPoor without loupe-aware tooling
Storage discipline requiredModerate (append-only)Moderate (append-only)High (append-only + cross-facet isolation)
Audit surfaceOne implementationOne implementationEvery facet plus their interactions
Ecosystem familiarityVery highHighLow

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.