ethereum/contract-upgrade-patterns

Contract Upgrade Pattern Comparison: UUPS vs Transparent vs Diamond

ethereumguide👥 Communityconfidence highhealth 100%
v1.0.0·Updated 3/20/2026

Quick Overview of Three Mainstream Patterns

PatternGas (Deploy)Gas (Call)Upgrade AuthorityComplexityRecommended Use Case
Transparent ProxyHighMediumAdmin accountMediumGeneral projects
UUPSLowLowImplementation contractLowFirst choice for new projects
Diamond (EIP-2535)Very HighLowCustomVery HighLarge-scale protocols

1. Transparent Proxy (Most Traditional)

Principle: Proxy contract intercepts all calls; admin calls go to proxy management functions, other calls delegatecall to implementation contract.

// Deploy
const proxy = await upgrades.deployProxy(MyContractV1, [initArgs], { kind: 'transparent' })
// Upgrade
await upgrades.upgradeProxy(proxy.address, MyContractV2)

Drawbacks:

  • Every call must check if msg.sender is admin (extra gas)
  • Admin and regular users must be different addresses (otherwise admin operations cannot delegate)

Principle: Upgrade logic is placed in the implementation contract, proxy itself is minimal (only does delegatecall).

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyContractV1 is UUPSUpgradeable, OwnableUpgradeable {
  /// @custom:oz-upgrades-unsafe-allow constructor
  constructor() { _disableInitializers(); }
  
  function initialize(address initialOwner) public initializer {
    __Ownable_init(initialOwner);
    __UUPSUpgradeable_init();
  }
  
  // Control who can upgrade
  function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
# Deploy
const proxy = await upgrades.deployProxy(MyContractV1, [owner], { kind: 'uups' })
# Upgrade
await upgrades.upgradeProxy(proxy.address, MyContractV2)

Advantages:

  • Smaller proxy contract (lower deployment gas)
  • Shorter call path (lower gas)
  • More flexible upgrade authority

Risk: Implementation contract must include _authorizeUpgrade, otherwise upgrades are permanently locked.


3. Diamond Pattern (EIP-2535)

Principle: One proxy + multiple facets (functional modules), each selector routes to a different facet. Breaks through the 24KB contract size limit.

// Diamond structure
Diamond (proxy)
  └── DiamondCut Facet    // Manage facet addition/removal/modification
  └── DiamondLoupe Facet  // Query current facet information
  └── Ownership Facet     // Permission management
  └── YourFeature Facet   // Business functionality
  └── ...

Use Cases:

  • Contract logic exceeds 24KB limit
  • Need fine-grained feature upgrades (upgrade only specific modules)
  • Long-term large-scale protocols (Aavegotchi, etc.)

Not recommended for: Regular projects, complexity cost far exceeds benefits.


Storage Layout Safety

Rules that must be followed:

// V1
contract MyContractV1 {
  uint256 public value;    // slot 0
  address public owner;   // slot 1
}

// ✅ V2 can only append at the end
contract MyContractV2 is MyContractV1 {
  uint256 public newValue; // slot 2 (safe)
}

// ❌ Never insert in the middle
contract MyContractV2BAD {
  uint256 public newValue; // slot 0 (will overwrite value!)
  uint256 public value;   // slot 1 (will overwrite owner!)
}

Validation tools:

npx hardhat check --network mainnet  # hardhat-upgrades automatically checks storage compatibility

Upgrade Best Practices

  1. Fully test on fork testnet before upgrading
  2. Use multisig (Gnosis Safe) to manage upgrade authority, not EOA
  3. Set timelock (24-48h delay) to give community reaction time
  4. Immediately verify new implementation contract after each upgrade