Loading...
Contract code is immutable once deployed. But if you MUST ship a bugfix, the proxy pattern gives you a way.
You deploy V1. Users put $100M in. You find a bug. You can't edit V1. Options:
1. Live with the bug (bad) 2. Migrate all $100M to a fresh V2 (expensive, trust-breaking) 3. Use a proxy so you can point all the money at new code
Two contracts:
User calls proxy.doThing(). Proxy's fallback() uses delegatecall to invoke V1's code — but the storage reads/writes happen in Proxy's slots. When you upgrade, you deploy V2 and point the proxy at it. Storage stays intact.
solidity// simplified proxy contract Proxy { address public implementation; address public admin; fallback() external payable { assembly { let impl := sload(0) // implementation slot 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 upgradeTo(address newImpl) public { require(msg.sender == admin); implementation = newImpl; } }
initialize() function that can only be called once (OZ's Initializable modifier).bashnpm install --save-dev @openzeppelin/hardhat-upgrades npm install @openzeppelin/contracts-upgradeable
solidityimport "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract MyTokenV1 is Initializable, ERC20Upgradeable, OwnableUpgradeable { function initialize(address owner_) public initializer { __ERC20_init("MyToken", "MTK"); __Ownable_init(owner_); _mint(owner_, 1_000_000 * 10**18); } }
Deploy with OZ's upgrades plugin:
typescriptimport { upgrades, ethers } from "hardhat"; const Factory = await ethers.getContractFactory("MyTokenV1"); const instance = await upgrades.deployProxy(Factory, [deployer.address]);
Answer on upgrade safety.