3 min read

Diamond (EIP-2535) vs transparent proxy: choosing an upgrade pattern

How transparent proxies, UUPS and EIP-2535 diamonds work, what each costs you, and when I use a diamond for a factory and a transparent proxy for a router.

Deployed contract code can’t change. Upgradeability is a pattern you build: a proxy contract holds the state and the address users interact with, and forwards calls with delegatecall to implementation code that can be swapped. The proxy pattern you pick decides how upgrades work, who can trigger them and what can go wrong.

In my Multi-DEX Swap Router I used two patterns side by side: a diamond for the factory and a transparent proxy for the router. Here’s why.

Transparent proxy

The transparent proxy (popularised by OpenZeppelin) stores the implementation address and the admin address in fixed storage slots defined by EIP-1967, so they can’t collide with your contract’s own variables.

Its key rule: calls from the admin are handled by the proxy itself (for example, upgrading), and calls from everyone else are forwarded to the implementation. That avoids “selector clashes”, where a proxy function and an implementation function share the same 4-byte selector. In OpenZeppelin Contracts 5.x, each transparent proxy deploys its own ProxyAdmin contract, which owns the right to upgrade.

Good for: a single contract with a clear, stable interface that you may need to patch or extend later, like a router.

Watch out for: storage layout. New versions must only append state variables, never reorder or remove them. Use initializers instead of constructors, and lock the implementation contract itself.

UUPS (for comparison)

UUPS (ERC-1822 style) moves the upgrade function into the implementation rather than the proxy. The proxy is cheaper and simpler, but if you ever deploy an implementation without a working, access-controlled upgrade function, the contract can no longer be upgraded. It’s a solid choice when you want lighter proxies and are disciplined about every release.

Diamond (EIP-2535)

A diamond is a single proxy that forwards each function to one of many implementation contracts called facets. It keeps a mapping from function selector to facet address.

  • Upgrades happen through diamondCut, which can add, replace or remove functions in one transaction and emits a DiamondCut event.
  • The standard requires loupe functions (facets(), facetFunctionSelectors(), facetAddresses(), facetAddress()) so anyone can inspect which code currently backs which function.
  • Because logic is spread across facets, a diamond isn’t limited by the 24 KB contract size limit the way a single implementation is.

Good for: systems that will keep growing by feature, like a factory where new capabilities arrive over time. You ship a new facet and cut it in; you don’t redeploy the whole thing or migrate users to a new address.

Watch out for:

  • Shared storage. Facets share the diamond’s storage. Use a disciplined layout such as Diamond Storage (a struct at a hashed slot per feature) or AppStorage, and never let two facets claim the same slot.
  • Who can cut. diamondCut can replace any function. Protect it with strict access control, ideally a multisig and a timelock.
  • Tooling and review cost. Auditors and tooling handle simple proxies more easily. Document every facet and keep the cut history readable.

How I choose

Question Transparent / UUPS Diamond
One contract with a stable interface? Yes Overkill
Will features keep being added as modules? Gets awkward Yes
Near the contract size limit? Hits it Splits across facets
Simplest to review and audit? Yes More work

A practical rule: default to a transparent or UUPS proxy, and reach for a diamond only when the system is genuinely modular and will grow by feature. Whatever you pick, test upgrades explicitly: deploy v1, write state, upgrade to v2, and assert that the state survived.

Upgrade safety checklist

  • Storage layout checked between versions (append only).
  • Initializers protected and the implementation locked.
  • Upgrade rights behind a multisig, ideally with a timelock.
  • Upgrade path covered by tests, not just the happy path.
  • Every upgrade emits events and is documented for users.

Sources