- 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.
```solidity
// 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.
```solidity
// 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.
```solidity
// 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.
```solidity
// 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
```solidity
// 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**:
1. Protocol deploys implementation
2. Protocol deploys proxy
3. Attacker front-runs initialize() call
4. Attacker becomes owner
5. Attacker drains protocol
**Search Queries**:
```
Grep("function initialize|initializer", glob="**/*.sol")
Grep("Initializable|_initialized|initializing", glob="**/*.sol")
```
**Secure Pattern**:
```solidity
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
```solidity
// 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**:
1. V1 deployed with owner = 0x1234...
2. Upgrade to V2
3. newAdmin (slot 0) now contains old owner value
4. owner (slot 1) now reads balance (corrupted)
5. 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**:
```solidity
// 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
```solidity
// 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**:
1. Attacker deploys malicious implementation
2. Calls upgradeTo(maliciousImpl)
3. No authorization check passes
4. Proxy now points to malicious code
5. Attacker drains all funds
**Search Queries**:
```
Grep("_authorizeUpgrade|UUPSUpgradeable", glob="**/*.sol")
Grep("upgradeTo|upgradeToAndCall", glob="**/*.sol")
```
**Secure Pattern**:
```solidity
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
```solidity
// 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
```solidity
// 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**:
1. Attacker gains owner access (via other vuln)
2. Calls emergencyDestroy()
3. Proxy contract is destroyed
4. 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
```solidity
// 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**:
```bash
# Check function selectors
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
```solidity
// 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.
```solidity
// 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**:
1. Parent contract uses slots 0-1
2. Child contract adds variables
3. Child's variables overwrite parent's storage
4. State corruption and access control bypass
**Secure Pattern (ERC-7201)**:
```solidity
// @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
- [ ] Uses OpenZeppelin Initializable pattern
- [ ] Constructor calls `_disableInitializers()`
- [ ] `initialize()` has `initializer` modifier
- [ ] Cannot be re-initialized
### Storage
- [ ] Storage layout documented
- [ ] No variables reordered in upgrades
- [ ] New variables only appended
- [ ] Inheritance order preserved
- [ ] Storage gaps for future inheritance
- [ ] ERC-7201 namespaced storage used (if multiple inheritance)
- [ ] `@custom:storage-location` annotations present
- [ ] Storage location calculation correct (keccak256 formula)
### UUPS Specific
- [ ] `_authorizeUpgrade` has proper access control
- [ ] All upgrade versions maintain UUPSUpgradeable
- [ ] `upgradeTo` is properly protected
### Transparent Proxy Specific
- [ ] ProxyAdmin properly secured
- [ ] No selector clashing
- [ ] Admin functions not callable by users
### General
- [ ] No `selfdestruct` in implementation
- [ ] No `delegatecall` to untrusted contracts
- [ ] Upgrade timelock exists
- [ ] Emergency pause available
---
## Storage Gap Pattern
```solidity
// 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")
Auf GitHub ansehen