| name | upgradeability |
| description | Auto-loaded by access-control-auditor agent during Phase 2. Provides detection patterns for: UUPS/Transparent/Beacon proxy vulnerabilities, storage collision, initialization issues, function selector clashing, upgrade authorization. Core artifact: Proxy Architecture Map.
|
| user-invocable | false |
Upgradeability & Proxy Pattern Vulnerabilities
2025-2026 Statistics: Proxy-related vulnerabilities caused $340M+ in losses, with initialization bugs and storage collision being the leading causes.
Why Upgradeability Fails (Root Causes)
Root Cause 1: Initialization vs Constructor
Proxies can't use constructors; initializers can be called multiple times.
// VULNERABLE: No initialization protection
contract ImplementationV1 {
address public owner;
function initialize(address _owner) external {
owner = _owner; // @audit Can be called by anyone, multiple times!
}
}
Attacker's view: "They forgot the initializer guard. I'll take ownership."
Root Cause 2: Storage Layout Mismatch
Upgrades must maintain exact storage slot ordering.
// V1 Storage
contract V1 {
address public owner; // slot 0
uint256 public balance; // slot 1
}
// V2 Storage - WRONG!
contract V2 {
uint256 public newVar; // slot 0 - COLLISION with owner!
address public owner; // slot 1
uint256 public balance; // slot 2
}
Root Cause 3: delegatecall Context
Implementation code runs with proxy's storage and context.
// VULNERABLE: selfdestruct in implementation
contract Implementation {
function destroy() external onlyOwner {
selfdestruct(payable(owner)); // @audit Destroys PROXY, not implementation!
}
}
Root Cause 4: Unprotected Upgrade Function
Anyone can upgrade to malicious implementation.
// VULNERABLE: Missing access control on upgrade
contract UUPSVulnerable is UUPSUpgradeable {
function _authorizeUpgrade(address) internal override {
// @audit No onlyOwner check!
}
}
Proxy Patterns Overview
Transparent Proxy Pattern
User → Proxy (storage + delegatecall) → Implementation (logic)
↓
Admin → ProxyAdmin (upgrade logic)
Key Features:
- Admin calls go to ProxyAdmin, not implementation
- Users can't accidentally call admin functions
- Requires separate ProxyAdmin contract
UUPS Pattern (EIP-1822)
User → Proxy (storage + delegatecall) → Implementation (logic + upgrade)
Key Features:
- Upgrade logic in implementation
- Smaller proxy bytecode
- Risk: forgetting upgrade function in new implementation
Beacon Pattern
User → Proxy → Beacon → Implementation
↑
Multiple proxies share beacon
Key Features:
- Single upgrade updates all proxies
- Good for factory patterns
- More complex architecture
The Proxy Architecture Map (Core Artifact)
For each proxy system, document:
| Component | Address | Type | Storage | Risk |
|---|
| Proxy | 0x1234... | TransparentProxy | User data | Storage collision |
| Implementation | 0x5678... | Logic V1 | None | selfdestruct |
| ProxyAdmin | 0x9abc... | Admin | None | Access control |
| Beacon | N/A | N/A | N/A | N/A |
Detection Patterns
Pattern 1: Unprotected Initializer
Root Cause: Initialization vs Constructor
// VULNERABLE: Can be re-initialized
contract VaultV1 {
address public owner;
bool private _initialized; // @audit Wrong pattern
function initialize(address _owner) external {
require(!_initialized); // @audit Can be front-run!
owner = _owner;
_initialized = true;
}
}
Attack Flow:
- Protocol deploys implementation
- Protocol deploys proxy
- Attacker front-runs initialize() call
- Attacker becomes owner
- Attacker drains protocol
Search Queries:
Grep("function initialize|initializer", glob="**/*.sol")
Grep("Initializable|_initialized|initializing", glob="**/*.sol")
Secure Pattern:
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract VaultV1 is Initializable {
address public owner;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // Prevent implementation initialization
}
function initialize(address _owner) external initializer {
owner = _owner;
}
}
Pattern 2: Storage Collision
Root Cause: Storage Layout Mismatch
// V1
contract VaultV1 {
address public owner; // slot 0
uint256 public balance; // slot 1
}
// V2 - VULNERABLE
contract VaultV2 {
address public newAdmin; // slot 0 - OVERWRITES owner!
address public owner; // slot 1 - READS balance as address!
uint256 public balance; // slot 2
}
Attack Flow:
- V1 deployed with owner = 0x1234...
- Upgrade to V2
- newAdmin (slot 0) now contains old owner value
- owner (slot 1) now reads balance (corrupted)
- Access control broken
Search Queries:
Grep("pragma solidity|contract.*is.*Upgradeable", glob="**/*.sol")
Detection:
- Use
forge inspect Contract storage-layout
- Compare layouts between versions
- Check inheritance order
Secure Pattern:
// V2 - CORRECT: Append only
contract VaultV2 {
address public owner; // slot 0 - SAME
uint256 public balance; // slot 1 - SAME
address public newAdmin; // slot 2 - NEW at end
}
Pattern 3: UUPS Missing Upgrade Authorization
Root Cause: Unprotected Upgrade Function
// VULNERABLE: No access control
contract MyUUPS is UUPSUpgradeable {
function _authorizeUpgrade(address newImplementation) internal override {
// @audit Empty = anyone can upgrade!
}
}
// VULNERABLE: Wrong check
contract MyUUPS is UUPSUpgradeable {
function _authorizeUpgrade(address newImplementation) internal override {
require(msg.sender == owner); // @audit What if owner is uninitialized?
}
}
Attack Flow:
- Attacker deploys malicious implementation
- Calls upgradeTo(maliciousImpl)
- No authorization check passes
- Proxy now points to malicious code
- Attacker drains all funds
Search Queries:
Grep("_authorizeUpgrade|UUPSUpgradeable", glob="**/*.sol")
Grep("upgradeTo|upgradeToAndCall", glob="**/*.sol")
Secure Pattern:
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
// Additional checks possible
require(newImplementation != address(0), "Zero address");
}
Pattern 4: UUPS Implementation Without Upgrade Function
Root Cause: Upgrade Logic Removed
// V1 - Has upgrade
contract VaultV1 is UUPSUpgradeable {
function _authorizeUpgrade(address) internal override onlyOwner {}
}
// V2 - VULNERABLE: No longer upgradeable!
contract VaultV2 {
// Forgot to inherit UUPSUpgradeable!
// Contract is now BRICKED - can never upgrade again
}
Search Queries:
Grep("is UUPSUpgradeable|is.*Upgradeable", glob="**/*.sol")
Pattern 5: selfdestruct in Implementation
Root Cause: delegatecall Context
// VULNERABLE: selfdestruct destroys proxy's storage
contract Implementation {
function emergencyDestroy() external onlyOwner {
selfdestruct(payable(msg.sender));
// @audit This destroys the PROXY, not the implementation!
}
}
Attack Flow:
- Attacker gains owner access (via other vuln)
- Calls emergencyDestroy()
- Proxy contract is destroyed
- All funds in proxy lost forever
Note: selfdestruct is deprecated in Solidity 0.8.20+ but may still exist in older code.
Search Queries:
Grep("selfdestruct|SELFDESTRUCT", glob="**/*.sol")
Grep("delegatecall.*selfdestruct", glob="**/*.sol")
Pattern 6: Function Selector Clashing
Root Cause: Proxy and Implementation Share Function Namespace
// Proxy has admin() at selector 0xf851a440
// If implementation has function with same selector, collision occurs
// DANGEROUS: Implementation function clashes with proxy admin function
contract Implementation {
// This selector might clash with proxy's admin()
function admin_() external returns (address) { // @audit Check selector!
return address(this);
}
}
Detection:
cast sig "admin()"
cast sig "functionName()"
Search Queries:
Grep("function admin|function upgrade|function implementation", glob="**/*.sol")
Pattern 7: Transparent Proxy Admin Exposure
Root Cause: ProxyAdmin Access Control
// VULNERABLE: ProxyAdmin owner can be changed
contract MyProxyAdmin is ProxyAdmin {
// If ownership transferred to attacker...
// Attacker can upgrade to malicious implementation
}
Search Queries:
Grep("ProxyAdmin|TransparentUpgradeableProxy", glob="**/*.sol")
Grep("changeProxyAdmin|transferOwnership", glob="**/*.sol")
Pattern 8: Storage Collision Prevention (ERC-7201)
Root Cause: Unnamespaced Storage in Upgradeable Contracts
ERC-7201 introduces namespaced storage to prevent collisions when inheriting from multiple upgradeable contracts. Without it, storage slots can collide across inheritance chains.
// VULNERABLE: Sequential storage without namespacing
contract VaultV1 {
address public owner; // slot 0
uint256 public balance; // slot 1
}
// When inherited by child contract, slots collide
contract ChildVault is VaultV1 {
address public admin; // slot 0 - COLLISION with owner!
uint256 public fee; // slot 1 - COLLISION with balance!
}
Attack Flow:
- Parent contract uses slots 0-1
- Child contract adds variables
- Child's variables overwrite parent's storage
- State corruption and access control bypass
Secure Pattern (ERC-7201):
// @custom:storage-location erc7201:myprotocol.vault
contract VaultV1 is Initializable {
struct VaultStorage {
address owner;
uint256 balance;
}
bytes32 private constant VAULT_STORAGE_LOCATION =
keccak256(abi.encode(uint256(keccak256("myprotocol.vault")) - 1)) & ~bytes32(uint256(0xff));
function _getVaultStorage() private pure returns (VaultStorage storage $) {
assembly {
$.slot := VAULT_STORAGE_LOCATION
}
}
function initialize(address _owner) external initializer {
VaultStorage storage $ = _getVaultStorage();
$.owner = _owner;
}
}
Search Queries:
Grep("@custom:storage-location|erc7201|erc-7201", glob="**/*.sol")
Grep("keccak256.*abi.encode.*keccak256", glob="**/*.sol")
Grep("bytes32.*STORAGE_LOCATION", glob="**/*.sol")
Detection:
- Check for
@custom:storage-location NatSpec annotations
- Verify storage location calculation uses ERC-7201 formula
- Confirm assembly block uses correct slot offset
- Validate namespace ID is unique per contract
Upgradeability Audit Checklist
Initialization
Storage
UUPS Specific
Transparent Proxy Specific
General
Storage Gap Pattern
// Reserve storage slots for future variables in base contracts
abstract contract BaseContractV1 is Initializable {
address public owner;
uint256[49] private __gap; // Reserve 49 slots
}
// When adding new variables, reduce gap
abstract contract BaseContractV2 is Initializable {
address public owner;
address public newAdmin; // Uses one gap slot
uint256[48] private __gap; // Now 48 slots
}
Search Query Reference
# Find proxy patterns
Grep("delegatecall|Proxy|proxy", glob="**/*.sol")
Grep("ERC1967|TransparentUpgradeable|UUPSUpgradeable", glob="**/*.sol")
# Find initialization
Grep("initialize|initializer|Initializable", glob="**/*.sol")
Grep("_disableInitializers|reinitializer", glob="**/*.sol")
# Find dangerous operations
Grep("selfdestruct|SELFDESTRUCT", glob="**/*.sol")
Grep("delegatecall\\(|staticcall\\(", glob="**/*.sol")
# Find upgrade functions
Grep("upgradeTo|upgradeToAndCall|_authorizeUpgrade", glob="**/*.sol")
Grep("changeProxyAdmin|setImplementation", glob="**/*.sol")
# Find storage patterns
Grep("__gap|storage.*gap", glob="**/*.sol")
Severity Classification
Critical
- Unprotected initializer (ownership takeover)
- UUPS without authorization (upgrade takeover)
- selfdestruct in implementation (permanent fund loss)
High
- Storage collision in upgrade (data corruption)
- Missing upgrade function in new version (brick)
- ProxyAdmin ownership vulnerability
Medium
- Function selector clashing
- Missing storage gaps
- Incomplete initialization
Rationalization Table (Reject These Excuses)
| Excuse | Reality |
|---|
| "We'll initialize right away" | Attackers monitor mempool. They WILL front-run. |
| "Storage order is obvious" | Inheritance chains make it complex. Verify with tools. |
| "Nobody uses selfdestruct" | Legacy code exists. Check every implementation. |
| "Upgrades are rare" | One bad upgrade = total loss. Every upgrade matters. |
| "ProxyAdmin is secure" | ProxyAdmin controls millions. It's THE target. |
| "UUPS is simpler" | UUPS has unique risks. Don't assume safety. |