Install in Claude Code
Copygit clone --depth 1 https://github.com/PurpleAILAB/Vigilo /tmp/upgradeability && cp -r /tmp/upgradeability/packages/opencode/skills/upgradeability ~/.claude/skills/upgradeabilityThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# 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