$npx -y skills add quillai-network/quillshield_skills --skill proxy-upgrade-safetyDetects vulnerabilities in upgradeable proxy smart contracts including storage layout collisions, uninitialized implementations, function selector clashing, delegatecall context issues, and upgrade path safety. Covers Transparent Proxy, UUPS (EIP-1822), Beacon, Diamond (EIP-2535)
| 1 | # Proxy & Upgrade Safety |
| 2 | |
| 3 | Detect vulnerabilities specific to **upgradeable proxy architectures** — the most widely deployed contract pattern on Ethereum (54.2% of contracts). Proxy bugs cause storage corruption, unauthorized upgrades, and complete contract takeover. |
| 4 | |
| 5 | ## When to Use |
| 6 | |
| 7 | - Auditing any contract using proxy/implementation pattern (Transparent, UUPS, Beacon, Diamond) |
| 8 | - Reviewing implementation contract upgrades for storage layout compatibility |
| 9 | - Analyzing `delegatecall`-based architectures and library usage |
| 10 | - Verifying initialization safety (can `initialize()` be front-run?) |
| 11 | - Checking Diamond (EIP-2535) facet management for selector collisions |
| 12 | |
| 13 | ## When NOT to Use |
| 14 | |
| 15 | - Non-upgradeable contracts without proxy patterns |
| 16 | - Pure logic audits without proxy architecture (use behavioral-state-analysis) |
| 17 | - Token standard compliance (use external-call-safety) |
| 18 | |
| 19 | ## Core Concept: The Delegatecall Storage Model |
| 20 | |
| 21 | When Proxy calls Implementation via `delegatecall`: |
| 22 | |
| 23 | ``` |
| 24 | ┌─────────────────────┐ delegatecall ┌─────────────────────┐ |
| 25 | │ PROXY │ ──────────────────→ │ IMPLEMENTATION │ |
| 26 | │ │ │ │ |
| 27 | │ Storage: │ Implementation code │ Code only: │ |
| 28 | │ slot 0: admin │ executes in proxy's │ No persistent │ |
| 29 | │ slot 1: impl addr │ storage context │ storage │ |
| 30 | │ slot 2: user data │ │ │ |
| 31 | │ slot 3: user data │ │ │ |
| 32 | └─────────────────────┘ └─────────────────────┘ |
| 33 | ``` |
| 34 | |
| 35 | **Key Rule:** The implementation's code reads/writes the PROXY's storage slots. If storage layouts don't match, data corruption occurs. |
| 36 | |
| 37 | ## Five Vulnerability Classes |
| 38 | |
| 39 | ### Class 1: Storage Layout Collision |
| 40 | |
| 41 | **Between Proxy and Implementation:** |
| 42 | |
| 43 | ```solidity |
| 44 | // Proxy contract |
| 45 | contract Proxy { |
| 46 | address public admin; // slot 0 |
| 47 | address public implementation; // slot 1 |
| 48 | |
| 49 | fallback() external payable { |
| 50 | delegatecall(implementation); |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // Implementation contract |
| 55 | contract ImplementationV1 { |
| 56 | uint256 public totalSupply; // slot 0 — COLLIDES with admin! |
| 57 | mapping(address => uint256) public balances; // slot 1 — COLLIDES with implementation! |
| 58 | } |
| 59 | ``` |
| 60 | |
| 61 | **Detection:** Compare storage slot assignments between proxy and implementation. Any overlap = CRITICAL vulnerability. |
| 62 | |
| 63 | **Between Implementation Versions:** |
| 64 | |
| 65 | ```solidity |
| 66 | // V1 |
| 67 | contract ImplementationV1 { |
| 68 | uint256 public totalSupply; // slot 0 |
| 69 | address public owner; // slot 1 |
| 70 | mapping(address => uint256) balances; // slot 2 |
| 71 | } |
| 72 | |
| 73 | // V2 — DANGEROUS: inserted variable before existing ones |
| 74 | contract ImplementationV2 { |
| 75 | bool public paused; // slot 0 — COLLIDES with totalSupply! |
| 76 | uint256 public totalSupply; // slot 1 — COLLIDES with owner! |
| 77 | address public owner; // slot 2 — COLLIDES with balances! |
| 78 | mapping(address => uint256) balances; // slot 3 |
| 79 | } |
| 80 | ``` |
| 81 | |
| 82 | **Safe V2:** |
| 83 | |
| 84 | ```solidity |
| 85 | contract ImplementationV2 { |
| 86 | uint256 public totalSupply; // slot 0 — same |
| 87 | address public owner; // slot 1 — same |
| 88 | mapping(address => uint256) balances; // slot 2 — same |
| 89 | bool public paused; // slot 3 — NEW, appended at end |
| 90 | } |
| 91 | ``` |
| 92 | |
| 93 | ### Class 2: Uninitialized Implementation |
| 94 | |
| 95 | Proxy pattern uses `initialize()` instead of `constructor()`. If the implementation contract itself is not initialized, an attacker can call `initialize()` directly on it. |
| 96 | |
| 97 | ```solidity |
| 98 | contract ImplementationV1 is Initializable { |
| 99 | address public owner; |
| 100 | |
| 101 | function initialize(address _owner) external initializer { |
| 102 | owner = _owner; |
| 103 | } |
| 104 | |
| 105 | function selfDestruct() external { |
| 106 | require(msg.sender == owner); |
| 107 | selfdestruct(payable(msg.sender)); |
| 108 | } |
| 109 | } |
| 110 | ``` |
| 111 | |
| 112 | **Attack:** |
| 113 | |
| 114 | ``` |
| 115 | 1. Implementation deployed but initialize() not called on impl itself |
| 116 | 2. Attacker calls implementation.initialize(attacker_address) |
| 117 | 3. Attacker is now owner of the IMPLEMENTATION contract |
| 118 | 4. Attacker calls selfDestruct() on implementation |
| 119 | 5. Proxy now delegatecalls to destroyed contract |
| 120 | 6. ALL proxy calls return empty data — contract bricked |
| 121 | ``` |
| 122 | |
| 123 | **Detection:** |
| 124 | |
| 125 | ``` |
| 126 | For each implementation contract: |
| 127 | 1. Does it have initialize() or any initializer function? |
| 128 | 2. Was initialize() called on the implementation address (not just the proxy)? |
| 129 | 3. Does the constructor call _disableInitializers()? |
| 130 | 4. If no → UNINITIALIZED |