Smart Contract Upgradeability: Patterns and Best Practices
A deep dive into upgradeable smart contracts, proxy patterns, and storage management

Introduction
Smart contract upgradeability is a crucial consideration in blockchain development. While immutability is a core feature of blockchain technology, there are legitimate reasons why you might want to make your contracts upgradeable:
- Fixing critical bugs or security vulnerabilities
- Adding new features or functionality
- Optimizing gas costs
- Adapting to changing requirements
Upgradeability Patterns
There are several patterns for implementing upgradeable smart contracts. Let's explore the most common ones:
1. Proxy Pattern
The proxy pattern is the most widely used approach for upgradeable contracts. It involves two main components:
- Proxy Contract: Stores the implementation address and delegates all calls to it
- Implementation Contract: Contains the actual business logic
How the Proxy Pattern Works:
- When a user calls the proxy contract, the call is forwarded to the implementation contract using
delegatecall - The implementation contract executes the logic but uses the proxy's storage
- To upgrade, we simply change the implementation address in the proxy
- All state is preserved because it's stored in the proxy contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Proxy {
address public implementation;
address public admin;
constructor(address _implementation) {
implementation = _implementation;
admin = msg.sender;
}
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
function upgrade(address _newImplementation) external onlyAdmin {
implementation = _newImplementation;
}
fallback() external payable {
address _impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}Key Components Explained:
implementation: Stores the address of the current implementation contractadmin: Address that has permission to upgrade the implementationupgrade(): Function to change the implementation addressfallback(): Forwards all calls to the implementation contract usingdelegatecall
2. Storage Patterns
When using proxy patterns, it's crucial to maintain storage compatibility between implementations. Here are the key storage patterns:
3. Function Selector Clashes
Function selector clashes can occur when upgrading contracts. Here's how to prevent them:
Best Practices
Security Considerations
- Always use a timelock for upgrades
- Implement multi-sig for admin functions
- Test upgrades thoroughly before deployment
- Maintain storage compatibility
- Document all storage slots and their purposes
When to Use Upgradeable Contracts
While upgradeability can be useful, it's not always the best choice. Consider these factors:
- Use upgradeable contracts when:
- You need to fix critical bugs
- Your protocol requires frequent updates
- You're building a complex system that needs iteration
- Avoid upgradeable contracts when:
- Security is the absolute priority
- Your contract is simple and well-tested
- You want to maximize trustlessness
Implementation Example
Here's a complete example of an upgradeable contract using the proxy pattern:
Conclusion
Smart contract upgradeability is a powerful feature that can help maintain and improve your contracts over time. However, it comes with significant security considerations and complexity. Always carefully evaluate whether upgradeability is necessary for your use case and implement it with proper security measures.
Key Takeaways
- Proxy patterns are the most common approach to upgradeability
- Storage compatibility is crucial for successful upgrades
- Function selector clashes must be avoided
- Security measures like timelocks and multi-sig are essential
- Not all contracts need to be upgradeable