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
| Pattern | Gas (Deploy) | Gas (Call) | Upgrade Authority | Complexity | Recommended Use Case |
|---|---|---|---|---|---|
| Transparent Proxy | High | Medium | Admin account | Medium | General projects |
| UUPS | Low | Low | Implementation contract | Low | First choice for new projects |
| Diamond (EIP-2535) | Very High | Low | Custom | Very High | Large-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)
2. UUPS (Recommended)
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
- Fully test on fork testnet before upgrading
- Use multisig (Gnosis Safe) to manage upgrade authority, not EOA
- Set timelock (24-48h delay) to give community reaction time
- Immediately verify new implementation contract after each upgrade