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