Upgradable Smart Contracts (Proxy patterns)

Loading

Smart contracts, by default, are immutable once deployed to a blockchain. This means their code cannot be changed, upgraded, or patched — a feature that ensures trust and transparency. However, in practice, the need often arises to fix bugs, improve logic, or enhance functionality in deployed contracts, especially in long-term projects like DeFi platforms or DAOs. To address this, developers use Upgradable Smart Contracts through Proxy Patterns.

This approach separates the logic of the contract from the data (storage) so that the logic can be updated without losing the existing state or user balances. Proxy patterns enable contracts to be upgradeable while maintaining the advantages of decentralization and security.


1. Why Do Smart Contracts Need to Be Upgradable?

  1. Bug Fixes – A deployed contract with bugs cannot be changed unless upgradable.
  2. Security Patches – Emerging threats or discovered vulnerabilities may require urgent changes.
  3. New Features – Applications often need to evolve over time.
  4. Regulatory Compliance – Updates may be required to meet legal or governance standards.

2. Basic Architecture of Proxy-Based Upgradable Contracts

The upgradable pattern typically involves three core components:

  1. Proxy Contract
    This is the only contract directly interacted with by users. It stores all data (state variables) and delegates all logic execution to the implementation contract using delegatecall.
  2. Implementation Contract (Logic Contract)
    This contains the business logic (functions) but no data. The proxy forwards calls to this contract.
  3. Admin/Controller Contract (optional)
    Controls the upgradeability (who can change the logic contract) and restricts unauthorized upgrades.

3. How delegatecall Works

The delegatecall opcode in Solidity is a low-level function that lets a contract execute code from another contract but use the calling contract’s storage context.

So, when the proxy uses delegatecall to call the logic contract:

  • The logic runs from the proxy’s address.
  • State changes affect the proxy’s storage.
  • Users see only the proxy, not the implementation contract.

This makes it seem like the smart contract is one unified, upgradeable unit.


4. Types of Proxy Patterns

There are several recognized proxy patterns in smart contract development:

a. Transparent Proxy Pattern

  • Introduced by OpenZeppelin.
  • Separates admin functions (like upgrading) from user functions.
  • Only the admin can upgrade; users cannot access the implementation contract directly.
  • Prevents accidental conflicts between admin and implementation functions.

Pros:

  • Widely used and secure.
  • Easy to understand.
  • Protected by clear access control.

Cons:

  • Slight overhead due to admin separation.
  • More complex routing logic.

b. Universal Upgradeable Proxy Standard (UUPS)

  • Also developed by OpenZeppelin.
  • The logic contract contains the upgrade code.
  • Proxy is slim and only handles storage and delegation.

Pros:

  • More gas-efficient (smaller proxy).
  • Modular and upgradable itself.

Cons:

  • The logic contract must be coded securely.
  • More complexity on the logic side.

c. Beacon Proxy Pattern

  • Suitable for deploying many instances of the same logic.
  • A beacon contract holds the implementation address.
  • All proxies point to the beacon.

Pros:

  • Efficient for deploying many upgradable contracts (like DeFi vaults or NFT collections).
  • Centralized upgrade management.

Cons:

  • Slightly more complex setup.
  • A compromised beacon affects all proxies.

5. Example Walkthrough of Transparent Proxy

Here’s a simplified structure of how Transparent Proxy works:

Step 1: Deploy Logic Contract

contract LogicContract {
uint256 public value;

function setValue(uint256 _val) public {
value = _val;
}
}

Step 2: Deploy Proxy Contract

contract Proxy {
address public implementation;
address public admin;

constructor(address _impl) {
implementation = _impl;
admin = msg.sender;
}

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()) }
}
}

function upgrade(address _newImpl) external {
require(msg.sender == admin, "Only admin");
implementation = _newImpl;
}
}

Step 3: Interacting

Users interact only with the Proxy. The proxy delegates execution to the implementation, and the data is stored in the proxy.


6. Managing Storage Layouts

A key challenge in upgradable contracts is storage collision. Since the proxy and implementation share the same storage, any mismatch in variable order or layout can corrupt data.

Best Practices:

  • Avoid deleting or reordering variables.
  • Use OpenZeppelin’s Initializable contract to replace constructors.
  • Use Storage Gaps: reserved empty slots that allow for future expansion.

7. Security Considerations

  • Access Control: Only trusted entities should control upgrades.
  • Timelocks and Multisigs: Add governance layers to prevent abuse.
  • Testing Upgrade Paths: Simulate upgrades thoroughly in testnets.
  • Audit: Proxy systems must be independently audited due to complexity.

8. Tooling Support

  • OpenZeppelin Upgrades Plugin (for Hardhat/Truffle): Simplifies deployment and upgrades.
  • Tenderly: Visualizes proxy contracts and their logic mappings.
  • Etherscan: Now supports proxy verification and upgrade tracking.

9. Use Cases in Real Projects

  • Compound: Uses proxies to upgrade markets and controllers.
  • Aave: Implements proxy contracts for lending pools.
  • Uniswap v3: Integrates proxy-like patterns for gas efficiency and configurability.
  • Gnosis Safe: Relies on proxies to create multisig vaults.

Leave a Reply

Your email address will not be published. Required fields are marked *