Skip to Content
๐ŸŒ Web3๐Ÿ“– EVM๐Ÿ“œ Proxy Contracts

๐Ÿ“œ Proxy Contracts

Proxy contracts let you keep the same contract address while changing the code that runs behind it. Users and integrations always call the same address; only the implementation (logic) is swapped. This is the standard way to build upgradeable smart contracts on the EVM.


๐ŸŽฏ Why Proxies

  • Bug fixes โ€” Deploy a new implementation and point the proxy to it
  • New features โ€” Add functions or change logic without migrating state
  • Same address โ€” Integrations, frontends, and users keep using the original contract address

๐Ÿ“š Core Idea

User โ†’ Proxy (fixed address) โ†’ Implementation (replaceable) โ”‚ โ””โ”€โ”€ holds state (storage) โ””โ”€โ”€ delegatecall() to implementation
  • Proxy โ€” Holds state (storage layout) and uses delegatecall to run the implementationโ€™s code in its own context
  • Implementation โ€” Contains the logic; has no state of its own (storage lives in the proxy)
  • Critical โ€” Implementation and upgrades must preserve storage layoutย  to avoid corruption

๐Ÿ“ Common Patterns

PatternDescription
Transparent ProxyProxy has an admin; admin calls go to proxy, others are delegated. OpenZeppelinย 
UUPS (EIP-1822)Upgrade logic is in the implementation; implementation can be replaced. OpenZeppelinย 
BeaconMany proxies point to one โ€œbeaconโ€; upgrading the beacon upgrades all. OpenZeppelinย 

๐Ÿ”ง OpenZeppelin Upgrades (Hardhat)

Install:

npm install --save-dev @openzeppelin/hardhat-upgrades

Config:

// hardhat.config.js require("@openzeppelin/hardhat-upgrades");

Deploy as upgradeable:

// scripts/deploy.js const { ethers, upgrades } = require("hardhat"); async function main() { const MyContract = await ethers.getContractFactory("MyContract"); const proxy = await upgrades.deployProxy(MyContract, [/* constructor args */], { kind: "uups", // or "transparent" }); await proxy.waitForDeployment(); console.log("Proxy at", await proxy.getAddress()); } main().catch(console.error);

Upgrade later:

// scripts/upgrade.js const { ethers, upgrades } = require("hardhat"); async function main() { const proxyAddress = "0x..."; const MyContractV2 = await ethers.getContractFactory("MyContractV2"); const upgraded = await upgrades.upgradeProxy(proxyAddress, MyContractV2); console.log("Upgraded to", await upgraded.getAddress()); } main().catch(console.error);

โš ๏ธ Security Notes

  • Storage layout โ€” Never remove or reorder existing storage variables; only append. See OpenZeppelin upgrades docsย .
  • Initializers โ€” Use initializer instead of constructor for upgradeable contracts so the implementation isnโ€™t constructed twice.
  • Access control โ€” Restrict who can upgrade (owner, multisig, timelock).

๐Ÿ”— Resources

Last updated on