Smart contract immutability is a core security feature of blockchains, but it creates a significant development challenge: how do you fix bugs or add features after deployment? Upgradability patterns solve this by separating a contract's logic from its storage. The most common approach uses a Proxy Contract that delegates all function calls to a separate Logic Contract. Users interact with the proxy, which holds the data, while the logic contract contains the executable code. This allows developers to deploy a new logic contract and point the proxy to it, effectively upgrading the system without migrating user data or changing the contract address. Popular implementations include OpenZeppelin's TransparentUpgradeableProxy and the UUPS (EIP-1822) standard.
How to Plan for Smart Contract Upgradability and Migration
Introduction to Smart Contract Upgradability
A guide to designing and implementing upgradeable smart contracts using established patterns like Proxies and Diamonds.
Planning for upgrades requires a disciplined approach to storage layout. When you upgrade a logic contract, the new version must be storage-compatible with the old one. This means you cannot change the order, type, or size of existing state variables, as it would corrupt the data persisted in the proxy's storage slots. New variables must always be appended. Using structured storage contracts or the @openzeppelin/upgrades plugin helps manage this. A critical security consideration is initialization. Constructors are ineffective in proxy patterns, so you must use a separate initialize function. It is essential to protect this function with access controls and ensure it can only be called once to prevent re-initialization attacks.
For complex systems, the Diamond Pattern (EIP-2535) offers a more modular approach. Instead of a single logic contract, a Diamond proxy can route calls to multiple, smaller logic contracts called facets. This solves the 24KB maximum contract size limit on Ethereum and allows for more granular, gas-efficient upgrades where only specific facets are replaced. However, it introduces complexity in managing the routing table (diamondCut). Regardless of the pattern, you must establish a clear governance and timelock process for upgrades. Multisig wallets or DAO votes should control the upgrade function, and changes should be executed through a timelock to give users a window to exit if they disagree with the changes, preserving decentralization and trust.
How to Plan for Smart Contract Upgradability and Migration
A guide to the core concepts and strategic decisions required before implementing an upgradeable smart contract system.
Smart contract upgradability is the ability to modify a deployed contract's logic while preserving its state and address. This is essential for fixing bugs, adding features, and responding to evolving protocol requirements. The primary architectural patterns are the Proxy Pattern and the Diamond Pattern (EIP-2535). In the proxy pattern, a lightweight proxy contract delegates all calls to a separate logic contract. Users interact with the proxy, which can be pointed to a new logic contract during an upgrade. The diamond pattern extends this concept, allowing a single proxy to delegate calls to multiple logic contracts (facets) for modularity.
Planning begins with a clear upgrade governance model. You must decide who can authorize an upgrade. Common models include: a single admin key (for early development), a multi-signature wallet controlled by team members, a decentralized autonomous organization (DAO) governed by token holders, or a timelock contract that enforces a delay between proposal and execution. The chosen model directly impacts security and decentralization. For production systems, moving away from a single admin to a multi-sig or DAO is a critical step to mitigate centralization risks.
You must also plan your data storage strategy. In upgradeable contracts, storage layout compatibility between logic contract versions is paramount. Incompatible layouts can lead to critical state corruption. The standard approach is to use inherited storage or unstructured storage patterns, often facilitated by libraries like OpenZeppelin's Initializable and UUPSUpgradeable or TransparentUpgradeableProxy. The EIP-1967 standard defines specific storage slots for the logic contract address and admin, preventing storage collisions. Never modify the order or type of existing state variables in an upgrade.
Consider the migration path for major, breaking changes. Some upgrades require a full state migration, which a simple proxy swap cannot handle. This involves deploying a new contract system and moving all user balances and permissions from the old system. Tools for this include custom migration scripts or using the CREATE2 opcode to deploy new contracts at predetermined addresses. Planning for this possibility from the start, by keeping state structures modular and maintaining clear data access functions, simplifies future migrations significantly.
Finally, establish a rigorous testing and security process. Upgradeable contracts introduce unique attack vectors, such as storage collisions, function selector clashes, and initialization vulnerabilities. Your testing suite must include specific upgrade simulations. Use tools like OpenZeppelin Upgrades Plugins for Hardhat or Foundry to automate safety checks for storage layout. Always conduct audits on both the initial implementation and any proposed upgrade. A failed upgrade can permanently lock funds or break core protocol functionality, making pre-deployment diligence non-negotiable.
How to Plan for Smart Contract Upgradability and Migration
A strategic guide to designing, testing, and executing secure upgrades and migrations for on-chain protocols.
Planning for smart contract upgradability begins with a clear separation of concerns. The most common pattern, using proxy contracts, relies on a storage proxy that delegates logic calls to a separate implementation contract. This allows you to deploy a new implementation (V2) and point the proxy to it, upgrading the logic while preserving the contract's address and state. Key decisions include choosing an upgrade pattern—such as the Transparent Proxy (OpenZeppelin) or UUPS (EIP-1822)—and a proxy admin model, which dictates who can authorize an upgrade.
A robust upgrade plan requires rigorous testing and simulation. Before any mainnet deployment, you must run the upgrade on a forked mainnet or a testnet that mirrors production conditions. This process verifies that: the new bytecode deploys correctly, all existing storage variables are compatible (avoiding storage collisions), the proxy's upgrade function executes without reverting, and all user-facing functions behave as expected post-upgrade. Tools like OpenZeppelin Upgrades Plugins automate much of this validation.
For data migration, you must decide between in-place storage updates and explicit migration contracts. Simple additions can often be handled by initializer functions in the new implementation. For complex restructuring—like moving from a mapping to a struct array—you may need a one-time migration contract that the upgraded logic can call to transform the old data layout. This contract should be thoroughly audited and include a mechanism to prevent re-execution.
Establish a formal governance and security process. Define who holds the upgrade keys (a multi-sig, a DAO) and require multiple confirmations. Create a timelock contract to enforce a delay between a proposal and its execution, giving users time to react. Document a rollback procedure, including how to redeploy a previous version if a critical bug is discovered post-upgrade. Transparency with users about the upgrade's scope and schedule is critical for trust.
Finally, consider the endgame: contract migration. Sometimes, a full upgrade isn't sufficient, and you need to move users and funds to an entirely new contract system. This involves creating a new proxy or non-upgradeable contract, writing migration scripts to snapshot and transfer state, and incentivizing users to migrate via the old contract's UI. Plan for gas costs, provide clear user instructions, and ensure the old contract has a final, secure shutdown mechanism.
Comparison of Upgradeability Patterns
A feature and risk comparison of common smart contract upgrade patterns used in production.
| Feature / Metric | Transparent Proxy (OpenZeppelin) | UUPS (EIP-1822) | Diamond Standard (EIP-2535) |
|---|---|---|---|
Upgrade Logic Location | Proxy Contract | Implementation Contract | Diamond Facets |
Proxy Storage Overhead | ~0.5k gas | ~0.5k gas | ~2-5k gas |
Implementation Size Limit | 24KB | 24KB | No practical limit |
Upgrade Authorization Complexity | Medium | High | Very High |
Storage Collision Risk | High | High | None |
Gas Cost for First Call | ~40k gas | ~27k gas | ~55k gas |
Battle-Tested Adoption | |||
Requires Initializer Function |
Implementing the Transparent Proxy Pattern
A guide to planning for smart contract upgradability using the Transparent Proxy pattern, a standard for separating logic and storage.
The Transparent Proxy pattern is a core upgradeability standard used by protocols like OpenZeppelin and Aave. It separates contract logic from data storage using a proxy-forwarding mechanism. The user interacts with a Proxy contract that holds the state (storage), while all logic execution is delegated to a separate Implementation contract. This separation allows developers to deploy a new implementation contract and point the proxy to it, upgrading the system's logic without migrating user data or changing the contract address. The pattern's name comes from its goal of being transparent to end-users, who continue to call the same proxy address.
Planning for an upgrade requires a clear separation of concerns in your contract architecture. Your storage layout—the variables declared in the implementation—must be append-only and never reorganized between upgrades. You cannot remove existing state variables or change their order; you can only add new ones at the end. This is because the proxy's storage slots are fixed, and a new implementation must interpret them exactly as the previous one did. Use uint256 for gas efficiency and consider storage gaps in base contracts to reserve slots for future variables. Tools like the OpenZeppelin Upgrades Plugins help validate these constraints.
The upgrade process involves several key steps. First, you develop and thoroughly test the new implementation contract (V2). Next, you propose the upgrade, often through a decentralized governance vote for permissioned upgrades. Finally, you execute the upgrade by calling the upgradeTo(address newImplementation) function on the proxy's admin. Crucially, you must also initialize the new implementation contract if it has setup logic, using a dedicated initialize function to avoid conflicts with the proxy's constructor. Always conduct the upgrade on a testnet first, simulating all user interactions to ensure state integrity and functionality.
Security is paramount. The proxy admin address holds immense power and must be secured, often as a multi-sig wallet or a Timelock contract. Use the TransparentUpgradeableProxy from OpenZeppelin, which includes safeguards against storage collisions and a clear separation between admin and user calls. Be aware of function selector clashes: if the admin and the implementation have a function with the same selector, the proxy will always give the admin priority. This design prevents an attacker from hijacking admin functions. Always audit both the implementation logic and the upgrade mechanism itself.
Migration planning extends beyond a simple code swap. Consider how the upgrade affects integrations: oracles, other smart contracts, and front-end applications. Update your ABI and interface files. Document the changes and communicate them to users. For complex state migrations that require data transformation, you may need to write a migration contract that the new implementation can call once to reorganize storage. Remember, while the Transparent Proxy pattern enables upgrades, it also introduces complexity and centralization risk; use it only when the benefits of future flexibility clearly outweigh these costs.
Implementing UUPS (EIP-1822)
Learn how to implement the Universal Upgradeable Proxy Standard (EIP-1822) for secure, gas-efficient smart contract upgrades.
The Universal Upgradeable Proxy Standard (UUPS) is an Ethereum Improvement Proposal (EIP-1822) that defines a pattern for upgradeable smart contracts. Unlike the more common Transparent Proxy Pattern, the upgrade logic in UUPS is built directly into the implementation contract itself, not the proxy. This design eliminates the need for a separate proxy admin contract, reducing deployment gas costs and complexity. The key function is upgradeTo(address newImplementation), which must be called to point the proxy to a new logic contract.
Planning for upgradability starts before writing the first line of code. You must decide which contract state—stored variables—needs to persist across upgrades. State variables are stored in the proxy's storage, not the implementation. Therefore, you cannot change the order, type, or remove existing state variables in a new implementation; you can only append new ones. A flawed storage layout is a primary cause of critical upgrade failures. Tools like the OpenZeppelin Upgrades Plugins can help validate storage compatibility.
To implement UUPS, your initial logic contract must inherit from a UUPS-compliant base contract, such as OpenZeppelin's UUPSUpgradeable. Crucially, you must include and properly secure the _authorizeUpgrade(address newImplementation) function. This is an internal function where you define the authorization logic, typically using OpenZeppelin's Ownable or Access Control. Failure to override this function will leave your contract permanently un-upgradeable. Here's a minimal skeleton:
solidityimport "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract MyContract is Initializable, OwnableUpgradeable, UUPSUpgradeable { function initialize() public initializer { __Ownable_init(); } function _authorizeUpgrade(address newImpl) internal override onlyOwner {} }
The migration process involves deploying a new version of your implementation contract and then executing the upgrade. Always test upgrades exhaustively on a testnet using a scripted flow: 1) Deploy the new implementation (V2), 2) Call upgradeTo(address(V2)) on the proxy contract (this call must be authorized, e.g., by the owner). After the transaction confirms, all subsequent calls to the proxy will delegate to the new logic. It is critical to verify the new contract's bytecode and conduct comprehensive integration tests in a forked mainnet environment before executing the live upgrade.
While UUPS is more gas-efficient, it introduces a unique risk: if an upgrade removes the upgradeTo function from a future implementation, the proxy becomes frozen and cannot be upgraded again. This is a deliberate security feature but requires careful planning. Best practices include: using the OpenZeppelin Upgrades Plugins for validation, implementing timelocks for authorization, having a rollback plan, and thoroughly documenting all storage variables and changes. Always refer to the official EIP-1822 specification and OpenZeppelin documentation.
Implementing the Diamond Standard (EIP-2535)
The Diamond Standard (EIP-2535) is a modular smart contract system that enables unlimited upgrades without storage collisions. This guide explains how to plan for upgradability and migration using its core components.
Traditional proxy upgrade patterns have significant limitations: a single logic contract address, potential storage clashes, and a hard 24KB contract size limit. The Diamond Standard solves this by introducing a Diamond proxy contract that delegates function calls to multiple, independent logic contracts called Facets. This architecture allows you to add, replace, or remove functions without ever touching the Diamond's core storage layout, represented by a central Diamond Storage pattern. Planning begins by mapping your system's functions into discrete, upgradeable modules.
Diamond Storage is the cornerstone of safe upgrades. Instead of inheriting storage variables, facets declare a struct within a specific namespace (e.g., AppStorage) and use a unique position in contract storage via bytes32 constants. This prevents two facets from overwriting each other's data. For example:
soliditylibrary LibAppStorage { bytes32 constant APP_STORAGE_POSITION = keccak256("diamond.app.storage"); struct AppStorage { uint256 totalSupply; mapping(address => uint256) balances; } function appStorage() internal pure returns (AppStorage storage ds) { assembly { ds.slot := APP_STORAGE_POSITION } } }
All facets that need this data import and use this library.
The DiamondCut facet manages upgrades. It contains the diamondCut function, which takes an array of FacetCut structs. Each struct defines a facet address, the action (ADD, REPLACE, REMOVE), and the function selectors to modify. A crucial planning step is maintaining an off-chain function selector map to track which facet owns each function. During a cut, the Diamond updates its internal selectorToFacet mapping and emits an event. You must rigorously test each diamondCut on a testnet to ensure new facets integrate correctly and no selectors are duplicated or orphaned.
Migration planning requires managing state across upgrades. Since storage is persistent, new facets must be backward compatible with existing storage structs. To add a new data field, you append it to the existing struct in its library—never reorder or remove existing fields. For complex migrations that require data transformation, deploy a one-time Migration Facet with a function that repackages old storage into a new format. Execute this migration in a single diamondCut transaction that adds the migration facet, runs the migration function, and then removes the facet, ensuring atomicity and leaving the Diamond in a clean, new state.
Key tools for development include Louper (a block explorer for Diamonds) to inspect facets and selectors, and the Diamond Reference Implementation from the official EIP-2535 repository. Start by deploying a Diamond with a DiamondCutFacet, DiamondLoupeFacet (for introspection), and an initial OwnershipFacet. Use an established implementation like SolidState Solidity's Diamond contracts to avoid low-level bugs. Your upgrade checklist should include: verifying selector uniqueness, testing storage compatibility, estimating gas costs for the cut, and ensuring all init functions for new facets are called correctly during the upgrade process.
Managing Storage Layouts During Upgrades
A guide to preserving state and preventing critical errors when deploying new versions of upgradeable smart contracts.
Smart contract upgrades are a powerful tool for fixing bugs and adding features, but they introduce a critical constraint: storage layout incompatibility. When you deploy a new implementation contract, it must read from and write to the same storage slots as the previous version. Changing the order, type, or packing of state variables will corrupt your protocol's data, leading to permanent loss of funds or a broken contract. This guide explains the rules for managing storage layouts to ensure safe, state-preserving upgrades using proxies like the OpenZeppelin Transparent Proxy Pattern.
The core principle is that storage is accessed by slot number, not variable name. The compiler assigns slots sequentially based on the order of declaration. For example, a contract with uint256 public totalSupply; followed by address public owner; will store totalSupply at slot 0 and owner at slot 1. If you swap these variables in a new version, the new logic will read the owner address from slot 0 (which holds the totalSupply value), causing catastrophic failure. You can append new variables after existing ones, but you cannot insert, delete, or reorder existing state variables.
Complex data types like mapping, dynamic arrays, and structs have specific storage layout rules. A mapping or dynamic array does not store its data contiguously; instead, it uses a keccak256 hash of the slot and key to determine a pseudo-random storage location. This means adding a new mapping is always safe, as it uses a unique slot. However, altering a struct's internal field order within a state variable will corrupt that struct's data. Understanding these rules is essential for planning upgrades involving complex state.
To manage upgrades systematically, use tools like the OpenZeppelin Upgrades Plugins for Hardhat or Foundry. These tools will simulate a storage layout check before deployment, comparing the new implementation against the previous one and throwing an error if incompatibilities are detected. For manual verification, you can inspect the storage layout output from the Solidity compiler using solc --storage-layout. Always run these checks in a test environment before proceeding to mainnet.
When you need to deprecate an old variable, you should not delete it from the contract. Instead, mark it as unused and create a new variable at the end of the layout. For example, change uint256 public oldVar; to uint256 private __deprecated_oldVar; and add your new variable after it. This "gap" technique preserves slot numbering. For more complex migrations, consider writing a one-time migration function in the new implementation that can reorganize data from old formats to new ones upon upgrade initialization.
Ultimately, managing storage is about discipline and verification. Always document your storage layout changes, use automated upgrade tools for validation, and thoroughly test state persistence in a forked environment. A failed upgrade can be irreversible, making careful planning the most important step in building resilient, upgradeable protocols.
Choosing an Upgrade Pattern: A Decision Framework
A systematic guide to evaluating and selecting the right upgrade strategy for your decentralized application, balancing security, decentralization, and developer experience.
Smart contract immutability is a foundational security principle, but it can hinder protocol evolution and bug fixes. Upgrade patterns provide a controlled mechanism for introducing changes. The core challenge is selecting a pattern that aligns with your project's specific needs for security guarantees, decentralization level, and developer overhead. This framework helps you evaluate the three primary approaches: Transparent Proxies, UUPS Proxies, and Diamond Pattern.
The Transparent Proxy pattern, standardized in OpenZeppelin's contracts, uses a proxy contract that delegates all logic calls to a separate implementation contract. Upgrades are performed by a designated admin account calling upgradeTo(address newImplementation). This pattern clearly separates proxy admin privileges from regular users, preventing function selector clashes. However, it introduces gas overhead on every call due to its access control checks and is susceptible to the initialization reentrancy vulnerability if initialize functions are not properly protected.
The UUPS (Universal Upgradeable Proxy Standard) pattern moves the upgrade logic into the implementation contract itself. The implementation contains the upgradeTo function. This makes the proxy contract simpler and reduces gas costs for regular users, as the proxy doesn't perform admin checks on every call. The critical trade-off is that if an implementation is deployed without the upgrade function, it becomes permanently frozen. UUPS is often preferred for its efficiency and is used by major protocols like Compound V3.
For complex systems requiring modular upgrades, the Diamond Pattern (EIP-2535) is the most advanced option. Instead of a single implementation, a Diamond uses a facets system—multiple logic contracts that are mapped to specific function selectors. This allows you to add, replace, or remove functions without needing a full storage migration. It's ideal for large, evolving protocols but introduces significant implementation complexity. The reference implementation is Louper, a tool for visualizing and interacting with Diamonds.
Your decision should be guided by key questions: Who controls upgrades (multi-sig, DAO, single admin)? How frequently will you upgrade? What is your tolerance for gas overhead? For most applications starting out, a UUPS proxy offers the best balance of simplicity, safety, and gas efficiency. Reserve the Diamond pattern for when you have a clear need for modular, granular upgrades. Always use established, audited libraries like OpenZeppelin Upgrades Plugins to deploy and manage your upgradeable contracts securely.
Regardless of the pattern chosen, rigorous practices are non-negotiable. Always use a timelock controller for production upgrades to give users a window to exit. Maintain comprehensive storage layout compatibility between versions. Conduct thorough testing on a testnet fork before any mainnet deployment. The goal of an upgrade pattern is not to make changes easy, but to make them safe, transparent, and predictable for your users.
Planning for a Full Contract Migration
A full smart contract migration is a high-risk operation that moves a protocol's entire state and logic to a new contract address. This guide outlines the strategic planning required to execute it securely.
A full migration is distinct from an in-place upgrade using a proxy pattern. It involves deploying a new, standalone contract (V2) and permanently moving all user funds, data, and protocol control from the old contract (V1). This approach is necessary when the existing contract architecture is fundamentally flawed, requires changes incompatible with the old storage layout, or when moving away from a non-upgradeable contract. It is a last-resort option due to its complexity and the need for coordinated user action.
The planning phase begins with a comprehensive audit of the V1 contract's state. You must create a complete inventory of all storage variables, mappings, and user balances. For ERC-20 tokens, this includes the total supply and each holder's balance. For more complex protocols like lending markets or AMMs, you need to map user deposits, collateral positions, liquidity provider shares, and accrued rewards. This data snapshot becomes the source of truth for the migration. Tools like Etherscan's contract read functions or custom scripts using ethers.js or web3.py are essential for this data extraction.
Next, design the V2 contract with migration as a first-class feature. It should include a privileged migrateFromV1 function (or similar) that accepts user-signed data or proofs. A common pattern is to allow users to call this function themselves, submitting their V1 state (e.g., their balance) along with a cryptographic proof (like a Merkle proof) that validates this data against a Merkle root of the V1 state snapshot stored in the V2 contract. This design puts the gas cost on the user but gives them control and eliminates a single privileged migration transaction that could be a bottleneck or failure point.
Communication and incentivization are critical. You must announce the migration well in advance, provide clear instructions, and run the process for a significant window (e.g., 30-90 days). Consider implementing a deadline after which un-migrated V1 assets may be permanently inaccessible or have reduced utility. To encourage participation, you can offer migration incentives in the V2 token or temporarily boost rewards for early migrators. All communication should happen through official channels: the project's website, Twitter, Discord, and directly via on-chain events emitted by the V1 contract.
Finally, execute a phased rollout. 1) Deploy V2 with the migration function disabled. 2) Publish the V1 state Merkle root on-chain after community verification. 3) Enable user-initiated migration and monitor participation. 4) Provide a fallback where the team can, after a long timelock, migrate any remaining dust for users. Post-migration, you must update all front-end interfaces, API endpoints, and partner integrations (like DEX listings and wallets) to point to the new V2 contract address, and clearly mark the old V1 contract as deprecated.
Essential Tools and Resources
Planning for smart contract upgradability and migration requires explicit design choices, verified tooling, and operational discipline. These tools and references help developers design upgrade-safe architectures, execute migrations without breaking state, and reduce governance and security risk.
Hardhat and Foundry Upgrade Tooling
Local development frameworks provide simulation and verification for upgrade and migration workflows before touching mainnet state.
Hardhat:
- OpenZeppelin Hardhat Upgrades plugin for proxy deployments
- Network forking to rehearse upgrades against live chain state
- Scriptable migrations with explicit transaction ordering
Foundry:
forge scriptfor deterministic, replayable upgrade scripts- On-chain dry runs using forked RPC endpoints
- Storage layout inspection via build artifacts
Actionable guidance:
- Always test upgrades against a fork of mainnet or target L2
- Include rollback tests to confirm upgrade reversibility
- Store migration scripts in version control alongside contracts
These tools reduce the risk of irreversible mistakes by letting teams observe exact state transitions before executing upgrades in production.
Protocol-Level Migration Playbooks
Some upgrades cannot be handled by proxies alone and require state migration or coordinated contract replacement. Established protocols publish migration playbooks that are valuable references.
Common migration patterns:
- Deploy new contracts and migrate state via batched transactions
- Snapshot balances and rehydrate state using Merkle proofs
- Dual-running old and new contracts during a transition window
Real-world examples:
- MakerDAO system upgrades with staged contract replacement
- Uniswap v2 to v3 migration with opt-in liquidity movement
- L2 protocol upgrades involving escrow and bridge contracts
Actionable guidance:
- Define explicit cutover conditions and rollback paths
- Communicate migration timelines to integrators early
- Budget gas costs for worst-case migration scenarios
Studying real migrations helps teams plan for edge cases that tooling alone does not cover.
On-Chain Governance and Upgrade Controls
Upgradability introduces governance risk. Tooling must be paired with clear upgrade authority and delay mechanisms.
Common controls:
- Timelocks to delay upgrades and allow community review
- Multisig-controlled upgrade admins with hardware key separation
- On-chain governance proposals for major logic changes
Best practices:
- Separate upgrade authority from day-to-day operational roles
- Emit events for every upgrade and admin change
- Document emergency upgrade procedures in advance
Actionable guidance:
- Enforce minimum timelock delays for non-critical upgrades
- Rotate admin keys on a fixed schedule
- Monitor upgrade events with automated alerts
Planning governance alongside technical upgrade paths reduces the likelihood of rushed or compromised migrations.
Frequently Asked Questions
Common questions and solutions for developers implementing upgradeable smart contracts, covering patterns, tools, and migration strategies.
The Transparent Proxy and UUPS (Universal Upgradeable Proxy Standard) are the two primary patterns for upgradeability, differing in where the upgrade logic resides.
In the Transparent Proxy pattern, an external ProxyAdmin contract manages the upgrade logic. The proxy itself only contains a fallback function to delegate calls to the implementation. This separation prevents function selector clashes but adds gas overhead for admin calls.
In the UUPS pattern, the upgrade logic is built directly into the implementation contract itself. The proxy is simpler and delegates all calls, including upgrades, to the implementation. This makes deployments cheaper, but requires the implementation to always include the upgrade function, adding complexity and a critical security consideration: if you accidentally omit the upgrade function in a new version, you permanently lose upgradeability.
Key Trade-off: Transparent proxies are more straightforward and secure for admin management, while UUPS proxies are more gas-efficient but place the upgrade burden on the implementation logic.