๐ 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
delegatecallto 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
| Pattern | Description |
|---|---|
| Transparent Proxy | Proxy 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ย |
| Beacon | Many proxies point to one โbeaconโ; upgrading the beacon upgrades all. OpenZeppelinย |
๐ง OpenZeppelin Upgrades (Hardhat)
Install:
npm install --save-dev @openzeppelin/hardhat-upgradesConfig:
// 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
initializerinstead ofconstructorfor upgradeable contracts so the implementation isnโt constructed twice. - Access control โ Restrict who can upgrade (owner, multisig, timelock).
๐ Resources
Last updated on