Proxy contract development (UUPS, Transparent Proxy)
Contract deployed on mainnet, critical vulnerability found. Without proxy — migration gone, convince users to move to new address, bury old TVL. With properly configured proxy — upgrade through multisig, 15 minutes, contract address unchanged. But "properly configured" is key phrase. Proxy patterns add entire class of vulnerabilities absent in immutable contracts.
Two patterns and their real differences
Transparent Proxy
Classic OpenZeppelin implementation (EIP-1967). Proxy contract contains routing logic: if admin calls — manages proxy directly (upgrade, changeAdmin). If anyone else calls — call delegated to implementation.
Problem: each contract call requires additional SLOAD to read admin address (100 gas per EIP-2929) and comparison with msg.sender. On hot paths — constant overhead. For protocol with millions calls daily — noticeable.
Second point: ProxyAdmin is separate contract owning upgrade rights. Adds another contract to system, another responsibility point for keys.
UUPS (EIP-1822 / ERC-1967)
Upgrade logic moved from proxy to implementation. Proxy itself — "dumb" delegator without any caller routing logic. No extra SLOAD per call — cheaper to operate.
But critical risk: if deploying new implementation without upgradeTo function (forget to inherit UUPSUpgradeable, or intentionally remove for gas economy) — proxy loses upgrade capability forever. Contract frozen on current version without fix.
Real case: in 2022 several UUPS protocols faced "uninitialized implementation" issue. Implementation deployed without initialize() call, and attacker called initialize() first, becoming owner, then upgradeTo() substituted implementation with self-destructing contract. All proxies pointing to this implementation became non-functional. Solution: _disableInitializers() in implementation constructor — mandatory pattern in OpenZeppelin 4.3+.
Storage collision — most dangerous problem
Problem essence: proxy and implementation share one storage. If proxy has variable in slot 0 and implementation also has variable in slot 0 — they overwrite each other.
EIP-1967 solves this for proxy service variables (implementation address, admin address) — stored in pseudorandom slots based on keccak256 hash of string, practically eliminating collision with user storage.
But implementation storage during upgrades — developer responsibility. If V1 had:
uint256 public totalSupply; // slot 0
address public owner; // slot 1
And V2 added variable before existing:
bool public paused; // slot 0 — COLLISION with totalSupply
uint256 public totalSupply; // slot 1 — COLLISION with owner
address public owner; // slot 2
totalSupply now reads what was owner (address interpreted as number). Silent data corruption, no reverting transactions, no compiler errors.
ERC-7201 (Namespaced Storage Layout) solves this radically. All implementation variables gathered in one struct, stored in precomputed slot:
bytes32 private constant STORAGE_LOCATION =
keccak256(abi.encode(uint256(keccak256("myprotocol.storage.v1")) - 1)) & ~bytes32(uint256(0xff));
New variables added to struct end. No collisions with proxy service slots, no problems during upgrades. Current best practice for production UUPS contracts.
Initialization instead of constructors
In proxy architecture, implementation constructor doesn't execute in proxy context — only when deploying implementation itself. So all initializing actions (set owner, initial parameters) moved to initialize() function, protected by initializer modifier.
Common bug: forgot to call initialize() after proxy deployment. Contract works, but owner not set — first one calling initialize() becomes owner. In 2021 this is how contract Parity was attacked — uninitialized WalletLibrary was captured, then attacker called kill(), freezing 587 ETH forever.
Solution: deployment script atomically deploys proxy and calls initialize() in one script. Never deploy proxy without immediate initialization.
How we implement proxy contracts
Base library — OpenZeppelin Upgrades (Hardhat plugin or Foundry-compatible variant). Plugin automatically checks storage layout compatibility between versions during each upgrade — mandatory tool, not optional.
For UUPS choose UUPSUpgradeable from OpenZeppelin 5.x. For systems where upgrade managed by DAO or multisig — AccessControlUpgradeable with UPGRADER_ROLE granted to Gnosis Safe address.
Test upgrades via Foundry fork tests: fork mainnet, simulate upgrade, check all storage variables preserved values, functions work correctly, new variables initialized properly.
| Selection criterion | Transparent Proxy | UUPS |
|---|---|---|
| Gas per call | +100-200 gas (SLOAD admin) | No overhead |
| Upgrade loss risk | No | Yes (forgotten upgradeTo) |
| Code complexity | Lower | Slightly higher |
| OZ 5.x recommendation | Outdated for new | Preferred |
| Separate ProxyAdmin | Yes | No |
When proxy isn't needed
Immutable contract simpler, cheaper to audit, generates more user trust (no "rug via upgrade" risk). If logic stable and critical error risk minimal — proxy adds complexity unnecessarily.
For DeFi protocols with large TVL usually better immutable + timelock on parameters, than upgradeability without formal governance.
Process and timeline
Proxy system development: 2-3 days for basic implementation, another 1-2 days for upgrade tests and storage layout validation. Complex systems with multiple proxies and custom governance — from week.
Cost depends on contract count and governance requirements.







