How to implement contract upgrade patterns in Solidity? What are the common upgrade solutions?
Contract upgrade is an important topic in smart contract development. Due to the immutability of blockchain, once a contract is deployed it cannot be modified, so upgrade mechanisms need to be designed to fix bugs or add new features.
1. Why Contract Upgrades Are Needed
solidity// Problem: Contract cannot be modified after deployment contract ImmutableContract { uint256 public value = 100; // If a bug is found, cannot be directly fixed function setValue(uint256 _value) external { value = _value; // Assume there is a logic error here } } // Solution: Use proxy pattern to achieve upgrades
2. Proxy Pattern
Basic Principle
Proxy pattern achieves upgrades by separating storage and logic:
- Proxy Contract: Holds state storage, delegates calls to Implementation contract
- Implementation Contract: Contains business logic, can be replaced
solidity// Simple proxy contract contract SimpleProxy { 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; } // Delegate call to implementation contract fallback() external payable { address impl = implementation; require(impl != address(0), "Implementation not set"); 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()) } } } receive() external payable {} } // Implementation contract V1 contract LogicV1 { uint256 public value; function setValue(uint256 _value) external { value = _value; } function getValue() external view returns (uint256) { return value; } } // Implementation contract V2 (upgraded version) contract LogicV2 { uint256 public value; uint256 public bonus; // New state variable function setValue(uint256 _value) external { value = _value; bonus = _value / 10; // New feature: auto calculate 10% bonus } function getValue() external view returns (uint256) { return value + bonus; } }
3. Transparent Proxy Pattern
OpenZeppelin's recommended upgrade pattern, solves function selector collision issues.
solidity// Transparent proxy contract contract TransparentUpgradeableProxy { // Storage slots: avoid conflicts with implementation contract bytes32 private constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); bytes32 private constant ADMIN_SLOT = bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1); constructor(address _logic, address _admin, bytes memory _data) { _setImplementation(_logic); _setAdmin(_admin); if (_data.length > 0) { (bool success, ) = _logic.delegatecall(_data); require(success, "Initialization failed"); } } modifier ifAdmin() { if (msg.sender == _getAdmin()) { _; } else { _fallback(); } } function admin() external ifAdmin returns (address) { return _getAdmin(); } function implementation() external ifAdmin returns (address) { return _getImplementation(); } function upgradeTo(address _newImplementation) external ifAdmin { _setImplementation(_newImplementation); } function upgradeToAndCall(address _newImplementation, bytes calldata _data) external payable ifAdmin { _setImplementation(_newImplementation); (bool success, ) = _newImplementation.delegatecall(_data); require(success, "Upgrade initialization failed"); } function _getImplementation() internal view returns (address impl) { bytes32 slot = IMPLEMENTATION_SLOT; assembly { impl := sload(slot) } } function _setImplementation(address _newImplementation) internal { bytes32 slot = IMPLEMENTATION_SLOT; assembly { sstore(slot, _newImplementation) } } function _getAdmin() internal view returns (address adm) { bytes32 slot = ADMIN_SLOT; assembly { adm := sload(slot) } } function _setAdmin(address _newAdmin) internal { bytes32 slot = ADMIN_SLOT; assembly { sstore(slot, _newAdmin) } } function _fallback() internal { address impl = _getImplementation(); require(impl != address(0), "Implementation not set"); 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()) } } } fallback() external payable { _fallback(); } receive() external payable { _fallback(); } } // Implementation contract (uses initialization instead of constructor) contract MyTokenV1 { string public name; string public symbol; uint256 public totalSupply; mapping(address => uint256) public balanceOf; bool private initialized; function initialize( string memory _name, string memory _symbol, uint256 _initialSupply ) public { require(!initialized, "Already initialized"); initialized = true; name = _name; symbol = _symbol; totalSupply = _initialSupply; balanceOf[msg.sender] = _initialSupply; } function transfer(address _to, uint256 _amount) external returns (bool) { require(balanceOf[msg.sender] >= _amount, "Insufficient balance"); balanceOf[msg.sender] -= _amount; balanceOf[_to] += _amount; return true; } } // Upgraded version V2 contract MyTokenV2 { string public name; string public symbol; uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; // New bool private initialized; function initialize( string memory _name, string memory _symbol, uint256 _initialSupply ) public { require(!initialized, "Already initialized"); initialized = true; name = _name; symbol = _symbol; totalSupply = _initialSupply; balanceOf[msg.sender] = _initialSupply; } function transfer(address _to, uint256 _amount) external returns (bool) { require(balanceOf[msg.sender] >= _amount, "Insufficient balance"); balanceOf[msg.sender] -= _amount; balanceOf[_to] += _amount; return true; } // New feature: approve transfer function approve(address _spender, uint256 _amount) external returns (bool) { allowance[msg.sender][_spender] = _amount; return true; } function transferFrom(address _from, address _to, uint256 _amount) external returns (bool) { require(balanceOf[_from] >= _amount, "Insufficient balance"); require(allowance[_from][msg.sender] >= _amount, "Insufficient allowance"); balanceOf[_from] -= _amount; balanceOf[_to] += _amount; allowance[_from][msg.sender] -= _amount; return true; } }
4. UUPS Proxy Pattern (Universal Upgradeable Proxy Standard)
More lightweight upgrade pattern, upgrade logic is in the implementation contract.
solidity// UUPS proxy contract contract ERC1967Proxy { bytes32 private constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); constructor(address _logic, bytes memory _data) { _setImplementation(_logic); if (_data.length > 0) { (bool success, ) = _logic.delegatecall(_data); require(success, "Initialization failed"); } } function _getImplementation() internal view returns (address impl) { bytes32 slot = IMPLEMENTATION_SLOT; assembly { impl := sload(slot) } } function _setImplementation(address _newImplementation) internal { bytes32 slot = IMPLEMENTATION_SLOT; assembly { sstore(slot, _newImplementation) } } fallback() external payable { address impl = _getImplementation(); require(impl != address(0), "Implementation not set"); 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()) } } } receive() external payable {} } // UUPS implementation contract base class abstract contract UUPSUpgradeable { bytes32 private constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); modifier onlyProxy() { require(address(this) != __self, "Function must be called through delegatecall"); _; } address private immutable __self = address(this); function proxiableUUID() external view returns (bytes32) { return IMPLEMENTATION_SLOT; } function upgradeTo(address _newImplementation) external onlyProxy { _authorizeUpgrade(_newImplementation); _upgradeToAndCall(_newImplementation, "", false); } function upgradeToAndCall(address _newImplementation, bytes memory _data) external payable onlyProxy { _authorizeUpgrade(_newImplementation); _upgradeToAndCall(_newImplementation, _data, true); } function _authorizeUpgrade(address _newImplementation) internal virtual; function _upgradeToAndCall( address _newImplementation, bytes memory _data, bool _forceCall ) internal { _setImplementation(_newImplementation); if (_data.length > 0 || _forceCall) { (bool success, ) = _newImplementation.delegatecall(_data); require(success, "Upgrade initialization failed"); } } function _setImplementation(address _newImplementation) private { bytes32 slot = IMPLEMENTATION_SLOT; assembly { sstore(slot, _newImplementation) } } } // Contract using UUPS contract MyUUPSTokenV1 is UUPSUpgradeable { string public name; uint256 public value; address public owner; bool private initialized; function initialize(string memory _name) public { require(!initialized, "Already initialized"); initialized = true; name = _name; owner = msg.sender; } function setValue(uint256 _value) external { value = _value; } function _authorizeUpgrade(address _newImplementation) internal override { require(msg.sender == owner, "Not authorized"); } } // Upgraded version contract MyUUPSTokenV2 is UUPSUpgradeable { string public name; uint256 public value; uint256 public multiplier; // New address public owner; bool private initialized; function initialize(string memory _name) public { require(!initialized, "Already initialized"); initialized = true; name = _name; owner = msg.sender; multiplier = 1; // Initialize new variable } function setValue(uint256 _value) external { value = _value * multiplier; // New logic } function setMultiplier(uint256 _multiplier) external { require(msg.sender == owner, "Not owner"); multiplier = _multiplier; } function _authorizeUpgrade(address _newImplementation) internal override { require(msg.sender == owner, "Not authorized"); } }
5. Diamond Pattern (EIP-2535)
Supports multiple implementation contracts, suitable for large complex systems.
solidity// Diamond storage structure library LibDiamond { bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage"); struct FacetAddressAndPosition { address facetAddress; uint96 functionSelectorPosition; } struct FacetFunctionSelectors { bytes4[] functionSelectors; uint256 facetAddressPosition; } struct DiamondStorage { mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition; mapping(address => FacetFunctionSelectors) facetFunctionSelectors; address[] facetAddresses; address contractOwner; } function diamondStorage() internal pure returns (DiamondStorage storage ds) { bytes32 position = DIAMOND_STORAGE_POSITION; assembly { ds.slot := position } } event DiamondCut( address[] _facetAddresses, bytes4[][] _functionSelectors, address _initAddress, bytes _calldata ); } // Diamond contract contract Diamond { constructor(address _contractOwner, address _diamondCutFacet) { LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); ds.contractOwner = _contractOwner; // Add diamondCut function // ... } fallback() external payable { LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress; require(facet != address(0), "Function does not exist"); assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } receive() external payable {} } // Diamond facet example contract TokenFacet { struct TokenStorage { mapping(address => uint256) balances; uint256 totalSupply; } bytes32 constant TOKEN_STORAGE_POSITION = keccak256("token.facet.storage"); function tokenStorage() internal pure returns (TokenStorage storage ts) { bytes32 position = TOKEN_STORAGE_POSITION; assembly { ts.slot := position } } function balanceOf(address _account) external view returns (uint256) { return tokenStorage().balances[_account]; } function transfer(address _to, uint256 _amount) external { TokenStorage storage ts = tokenStorage(); require(ts.balances[msg.sender] >= _amount, "Insufficient balance"); ts.balances[msg.sender] -= _amount; ts.balances[_to] += _amount; } } // Governance facet contract GovernanceFacet { struct GovernanceStorage { mapping(address => uint256) votingPower; uint256 proposalCount; } bytes32 constant GOVERNANCE_STORAGE_POSITION = keccak256("governance.facet.storage"); function createProposal(string memory _description) external { // Create proposal logic } function vote(uint256 _proposalId, bool _support) external { // Voting logic } }
6. Upgrade Pattern Comparison
| Feature | Transparent Proxy | UUPS | Diamond Pattern |
|---|---|---|---|
| Gas Cost | Higher | Lower | Medium |
| Complexity | Medium | Low | High |
| Function Selector Collision | Auto-resolved | Manual handling | Supports multiple facets |
| Upgrade Permission | Proxy contract control | Implementation contract control | Flexible configuration |
| Use Cases | General scenarios | Simple upgrades | Large complex systems |
| Deployment Cost | Medium | Low | High |
7. Upgrade Best Practices
Storage Layout Management
solidity// Use storage slots to avoid conflicts contract StorageLayout { // Storage slot 0 uint256 public value1; // Storage slot 1 address public owner; // Storage slot 2 mapping(address => uint256) public balances; // When upgrading, can only append new variables, cannot modify existing variable order and types // V2 version uint256 public value1; // Slot 0 - unchanged address public owner; // Slot 1 - unchanged mapping(address => uint256) public balances; // Slot 2 - unchanged uint256 public newValue; // Slot 3 - new variable }
Using OpenZeppelin Upgrades
javascript// hardhat.config.js require('@openzeppelin/hardhat-upgrades'); module.exports = { solidity: '0.8.19', }; // Deployment script const { ethers, upgrades } = require('hardhat'); async function main() { const MyToken = await ethers.getContractFactory('MyTokenV1'); // Deploy proxy const proxy = await upgrades.deployProxy(MyToken, ['My Token'], { initializer: 'initialize', }); await proxy.deployed(); console.log('Proxy deployed to:', proxy.address); // Upgrade const MyTokenV2 = await ethers.getContractFactory('MyTokenV2'); const upgraded = await upgrades.upgradeProxy(proxy.address, MyTokenV2); console.log('Upgraded to:', upgraded.address); } main();
Security Considerations
solidity// 1. Use initialization lock to prevent repeated initialization contract Initializable { bool private _initialized; bool private _initializing; modifier initializer() { require( _initializing || !_initialized, "Initializable: contract is already initialized" ); bool isTopLevelCall = !_initializing; if (isTopLevelCall) { _initializing = true; _initialized = true; } _; if (isTopLevelCall) { _initializing = false; } } } // 2. Access control contract UpgradeableWithAuth is Initializable { address public admin; modifier onlyAdmin() { require(msg.sender == admin, "Not admin"); _; } function initialize() public initializer { admin = msg.sender; } function upgrade(address _newImplementation) external onlyAdmin { // Upgrade logic } } // 3. Test before upgrade // - Test thoroughly on testnet // - Verify storage layout compatibility // - Check security of new features
8. Summary
Contract upgrade is an important skill in smart contract development:
-
Transparent Proxy Pattern: OpenZeppelin recommended, suitable for most scenarios
-
UUPS Pattern: More lightweight, higher Gas efficiency
-
Diamond Pattern: Suitable for large complex systems, supports modular upgrades
-
Best Practices:
- Carefully manage storage layout
- Use initialization locks
- Test upgrade process thoroughly
- Consider using OpenZeppelin Upgrades plugin