Smart contracts are supposed to be immutable. But protocols need to fix bugs and add features. The solution — proxy patterns — introduces a new class of vulnerabilities. The Parity Wallet hack locked $150M in a self-destructed implementation contract. Wormhole's $325M exploit involved an uninitialized proxy. Storage collisions have silently corrupted state in dozens of protocols.
How Proxy Patterns Work
A proxy contract holds state and delegates all logic to a separate implementation contract via delegatecall. To upgrade, you point the proxy to a new implementation. The critical invariant: proxy storage and implementation storage must never collide.
// Simplified proxy
contract Proxy {
address implementation;
fallback() external payable {
address impl = implementation;
assembly {
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()) }
}
}
}
Common Proxy Vulnerabilities
1. Storage Collision
// Proxy storage slot 0: implementation address
// Implementation storage slot 0: owner address
// COLLISION! Writing owner overwrites implementation address
2. Uninitialized Implementation
The implementation contract itself isn't initialized, allowing an attacker to call initialize() directly on it and take ownership — then selfdestruct it, breaking the proxy.
3. Function Selector Clashing
If the proxy and implementation have functions with the same selector, the proxy's function executes instead of delegating to the implementation. OpenZeppelin's TransparentProxy solves this by restricting admin calls.
4. Missing Upgrade Authorization
UUPS proxies put the upgrade logic in the implementation. If the implementation forgets to include upgrade authorization, anyone can upgrade to a malicious contract.
Secure Upgrade Patterns
// UUPS with proper initialization and authorization
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract SecureVault is UUPSUpgradeable, OwnableUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // Prevent init on implementation
}
function initialize(address owner) public initializer {
__Ownable_init(owner);
__UUPSUpgradeable_init();
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
- ✅ Use OpenZeppelin's TransparentProxy or UUPS patterns
- ✅ Call
_disableInitializers()in the implementation constructor - ✅ Never use
selfdestructin implementation contracts - ✅ Use EIP-1967 storage slots to avoid collisions
- ✅ Test upgrades with OpenZeppelin's upgrade plugin
- ✅ Add timelock to upgrade operations
How Vultbase Detects Proxy Issues
- Pattern DB — 13 proxy/upgrade patterns including storage collision, uninitialized proxy, and missing authorization
- Slither — Detects unprotected upgrade functions and storage layout conflicts
- Challenge Execution — Tests upgrade authorization and initialization paths
Upgradability is necessary but dangerous. Audit your proxy implementation before it becomes a backdoor.