How to Deploy and Upgrade Smart Contracts Using Proxy Patterns

·

Smart contracts are the backbone of decentralized applications (dApps) on blockchains like Ethereum. However, their immutability—while ensuring trust and security—presents a challenge when updates are needed. This is where upgradable smart contracts come into play.

In this comprehensive guide, you’ll learn the core principles behind upgradable smart contracts, including why they’re necessary, how to implement them using modern proxy patterns, and what pitfalls to avoid during deployment and upgrades. We’ll focus on Ethereum and EVM-compatible blockchains, using Solidity and Hardhat for hands-on implementation.

Whether you're fixing bugs, improving functionality, or adapting to market changes, upgradable contracts allow seamless evolution without disrupting user experience.

👉 Discover how blockchain developers deploy scalable dApps with advanced contract architecture.


Why Upgrade Smart Contracts?

Blockchain data is immutable by design—once deployed, a smart contract cannot be altered. But real-world applications require maintenance and improvement. Here’s why upgrades matter:

Since we can’t change existing code, the solution lies in design patterns that decouple contract logic from state. This enables replacing the "logic" while preserving data and maintaining a consistent user-facing address.


Understanding Upgradable Contract Architecture

Upgradable smart contracts rely on the proxy pattern, a design that separates execution logic from storage. The core components are:

  1. Proxy Contract – Holds the application’s state and serves as the permanent entry point.
  2. Logic (Implementation) Contract – Contains executable functions; can be swapped out.

The proxy forwards calls to the logic contract using delegatecall, which executes the logic in the proxy’s context—meaning state changes affect the proxy’s storage.

This setup allows developers to deploy a new logic contract and redirect the proxy to it—effectively upgrading the system without changing its address or losing data.


Common Proxy Patterns

There are several approaches to implementing upgradable contracts. Each has trade-offs in security, gas cost, and complexity.

1. Simple Proxy Pattern

In this basic model:

Limitation: No protection against function selector clashes—when two functions share the same 4-byte signature, leading to unexpected behavior or exploits.

2. Transparent Proxy Pattern

Popularized by OpenZeppelin, this pattern adds access control:

This prevents accidental conflicts but introduces higher gas costs due to additional checks in fallback.

A key enhancement: use of a ProxyAdmin contract to manage upgrades securely across multiple proxies.

3. UUPS (Universal Upgradable Proxy Standard)

Defined in EIP-1822, UUPS moves upgrade logic into the logic contract itself:

However, if a future version doesn’t inherit the upgradeable base, the contract becomes permanently un-upgradable—a trade-off for flexibility.

👉 Learn how top Web3 projects streamline contract deployment with secure upgrade paths.


Hands-On: Deploying an Upgradable Contract with Hardhat

Let’s walk through deploying and upgrading a price-tracking contract using Chainlink oracles and OpenZeppelin’s upgrade plugin.

Prerequisites

Ensure you have:


Step 1: Project Setup

Initialize your project and install dependencies:

yarn add -D hardhat @openzeppelin/hardhat-upgrades @nomiclabs/hardhat-ethers ethers
yarn add @chainlink/contracts @openzeppelin/contracts-upgradeable

Create hardhat.config.js:

require("@nomiclabs/hardhat-ethers");
require("@openzeppelin/hardhat-upgrades");

const GOERLI_RPC_URL = process.env.GOERLI_RPC_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const ETHERSCAN_KEY = process.env.ETHERSCAN_API_KEY;

module.exports = {
  solidity: "0.8.17",
  defaultNetwork: "hardhat",
  networks: {
    goerli: {
      url: GOERLI_RPC_URL,
      accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
      chainId: 5,
    },
  },
  etherscan: {
    apiKey: ETHERSCAN_KEY,
  },
};

Step 2: Write the Initial Contract (V1)

Create contracts/PriceFeedTracker.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract PriceFeedTracker is Initializable {
    address private admin;

    function initialize(address _admin) public initializer {
        admin = _admin;
    }

    function getAdmin() public view returns (address) {
        return admin;
    }

    function retrievePrice() public view returns (int) {
        AggregatorV3Interface aggregator = AggregatorV3Interface(
            0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e // Goerli ETH/USD feed
        );
        (, int price, , , ) = aggregator.latestRoundData();
        return price;
    }
}

Note the use of Initializable instead of a constructor—required for upgradable contracts.


Step 3: Deploy Script

Create scripts/deploy_upgradeable_pricefeedtracker.js:

const { ethers, upgrades } = require("hardhat");

async function main() {
  const PriceFeedTracker = await ethers.getContractFactory("PriceFeedTracker");
  const [deployer] = await ethers.getSigners();

  console.log("Deploying PriceFeedTracker...");
  const proxy = await upgrades.deployProxy(PriceFeedTracker, [deployer.address], { initializer: "initialize" });

  await proxy.deployed();
  console.log("PriceFeedTracker deployed to:", proxy.address);
}

main().catch(console.error);

Deploy with:

yarn hardhat run --network goerli scripts/deploy_upgradeable_pricefeedtracker.js

This deploys:

Record the proxy address—it remains constant across upgrades.


Step 4: Upgrade to V2

Update functionality to support dynamic price feeds.

Create PriceFeedTrackerV2.sol:

contract PriceFeedTrackerV2 is Initializable {
    address private admin;
    int public price;
    event PriceRetrievedFrom(address feed, int price);

    function initialize(address _admin) public initializer {
        admin = _admin;
    }

    function getAdmin() public view returns (address) {
        return admin;
    }

    function retrievePrice(address feed) public returns (int) {
        require(feed != address(0), "Invalid feed address");
        AggregatorV3Interface aggregator = AggregatorV3Interface(feed);
        (, int _price, , , ) = aggregator.latestRoundData();
        price = _price;
        emit PriceRetrievedFrom(feed, _price);
        return price;
    }
}

Ensure storage layout remains unchanged—new variables must be appended, not inserted.

Upgrade script (scripts/upgrade_pricefeedtracker.js):

const { upgrades } = require("hardhat");

async function main() {
  const proxyAddress = "YOUR_PROXY_ADDRESS";
  const PriceFeedTrackerV2 = await ethers.getContractFactory("PriceFeedTrackerV2");
  console.log("Upgrading to V2...");
  await upgrades.upgradeProxy(proxyAddress, PriceFeedTrackerV2);
  console.log("Upgrade complete.");
}

main().catch(console.error);

Run:

yarn hardhat run --network goerli scripts/upgrade_pricefeedtracker.js

The proxy address stays the same—users continue interacting seamlessly.


Best Practices & Pitfalls

✅ Do:

❌ Don’t:


Frequently Asked Questions

Q: Can any smart contract be made upgradable?
A: Technically yes, but it adds complexity. Only use upgradability when necessary—critical for dApps needing long-term maintenance.

Q: Is upgradability secure?
A: When implemented correctly (e.g., with access control), yes. However, centralizes trust in admins. Consider timelocks or DAO governance for production systems.

Q: What happens if I change storage layout during upgrade?
A: It causes storage clashes, corrupting data. Always append new variables at the end.

Q: How do I verify an upgradable contract on Etherscan?
A: Verify both logic and proxy separately. Use OpenZeppelin’s CLI tools to assist with verification of proxy contracts.

Q: Can I disable upgradability permanently?
A: Yes—with UUPS, simply deploy a version that doesn’t inherit upgrade logic. With transparent proxies, renounce ownership of ProxyAdmin.

Q: Are there alternatives to proxy patterns?
A: Yes—some projects use diamond patterns (EIP-2535) for modular logic, but they’re more complex. Proxies remain the standard for most use cases.


Conclusion

Upgradable smart contracts empower developers to build resilient, evolving dApps while preserving user trust and continuity. By leveraging proxy patterns like Transparent or UUPS—and tools like Hardhat and OpenZeppelin—you can deploy secure, maintainable systems on Ethereum and EVM chains.

Always prioritize security audits, test thoroughly in staging environments, and design with long-term governance in mind.

👉 Explore how leading DeFi platforms manage smart contract lifecycle securely and efficiently.