DEV Community

ohmygod
ohmygod

Posted on

Proxy Upgradeability Security Scanning: PROXION vs Slither vs OpenZeppelin Upgrades Plugin — Catching the $500M Bug Class

Proxy upgradeability bugs are responsible for over $500M in cumulative DeFi losses. The OWASP Smart Contract Top 10: 2026 added "Proxy & Upgradeability Vulnerabilities" (SC10) as a brand-new category — recognition that this bug class is no longer niche.

Yet most audit pipelines don't test for upgradeability-specific flaws. Your fuzzer won't catch a storage collision. Your static analyzer might miss a missing initializer. And nobody's checking if your UUPS proxy can be permanently bricked.

This article benchmarks four tools that specifically target proxy upgradeability bugs, shows you what each one catches, and gives you a CI pipeline that covers all the gaps.

Why Proxy Bugs Are Different

Traditional smart contract bugs (reentrancy, overflow, access control) live inside a single contract's logic. Proxy bugs live in the relationship between contracts:

  • Storage collisions: Proxy and implementation share the same storage address space. A layout change in the implementation can silently overwrite the proxy's admin slot.
  • Uninitialized implementations: UUPS contracts with initialize() on the implementation. If anyone can call it before you do, they own the contract.
  • Function selector clashes: A function in your implementation has the same 4-byte selector as a proxy admin function, creating ambiguous execution paths.
  • UUPS "silent death": Upgrading to an implementation that lacks upgradeTo() permanently bricks the proxy — no more upgrades, no recovery.

These are architectural bugs. They require tools that understand the proxy-implementation relationship, not just individual contract logic.

The Contenders

1. Slither Upgradeability Detectors

Slither ships built-in upgradeability checks since v0.9+:

# Install
pip install slither-analyzer

# Run upgradeability-specific checks
slither . --detect proxy-patterns,uninitialized-state \
  --print contract-summary

# Dedicated upgradeability comparison
slither-check-upgradeability \
  contracts/MyTokenV1.sol MyTokenV1 \
  contracts/MyTokenV2.sol MyTokenV2
Enter fullscreen mode Exit fullscreen mode

What it catches:

  • Storage layout changes between versions (order, type, deletion)
  • Missing initializer calls
  • Extra variables added before existing ones
  • Functions missing from upgraded implementation
  • selfdestruct in implementation (critical for UUPS)

Example output:

MyTokenV2 does not contain a call to MyTokenV1.initialize
Variable _gap was removed in MyTokenV2
Storage variable _totalSupply changed type: uint256 -> int256
Enter fullscreen mode Exit fullscreen mode

2. OpenZeppelin Upgrades Plugin

OpenZeppelin's Hardhat/Foundry plugin performs layout validation at deploy time:

// hardhat.config.js
require('@openzeppelin/hardhat-upgrades');

// deploy script
const { deployProxy, upgradeProxy } = require(
  '@openzeppelin/hardhat-upgrades'
);

// First deployment — validates initializer
const token = await deployProxy(TokenV1, [owner.address], {
  initializer: 'initialize',
  kind: 'uups'
});

// Upgrade — validates storage compatibility
const tokenV2 = await upgradeProxy(
  token.address,
  TokenV2,
  { kind: 'uups' }
);
Enter fullscreen mode Exit fullscreen mode

What it catches:

  • Storage layout incompatibilities (automatic comparison)
  • Missing __gap variables
  • Struct/enum changes that break ABI compatibility
  • Constructor usage (should use initializer)
  • Missing _disableInitializers() in constructor
  • UUPS missing _authorizeUpgrade()

Key feature: Stores .openzeppelin/ manifest files tracking deployed layouts, so it catches issues before the upgrade transaction hits the chain.

3. PROXION (Academic, Open-Source)

PROXION is a research tool from SecuraLab that analyzes bytecode directly — no source code required:

# Clone and setup
git clone https://github.com/Proxion-anonymous/Proxion
cd Proxion && pip install -r requirements.txt

# Analyze a deployed proxy
python proxion.py --address 0x1234...abcd \
  --rpc https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY

# Batch scan
python proxion.py --batch addresses.txt --output results.json
Enter fullscreen mode Exit fullscreen mode

What it catches:

  • Proxy detection from bytecode (even without source/txs)
  • Function selector collisions between proxy and implementation
  • Storage slot collisions via symbolic execution
  • Hidden proxy patterns (non-standard delegatecall wrappers)

Unique advantage: Works on closed-source contracts. PROXION disassembles bytecode, emulates EVM execution, and identifies delegatecall patterns without needing Etherscan-verified source.

4. Foundry Upgrade Safety (forge)

Foundry's native support via forge inspect:

# Compare storage layouts between versions
forge inspect src/TokenV1.sol:TokenV1 storage-layout > v1_layout.json
forge inspect src/TokenV2.sol:TokenV2 storage-layout > v2_layout.json

# Diff them
diff <(jq '.storage' v1_layout.json) \
     <(jq '.storage' v2_layout.json)
Enter fullscreen mode Exit fullscreen mode

For automated checks, use a Foundry test:

// test/UpgradeSafety.t.sol
pragma solidity ^0.8.20;

import \"forge-std/Test.sol\";
import \"@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol\";
import \"../src/TokenV1.sol\";
import \"../src/TokenV2.sol\";

contract UpgradeSafetyTest is Test {
    ERC1967Proxy proxy;
    TokenV1 v1;
    TokenV2 v2;

    function setUp() public {
        v1 = new TokenV1();
        v2 = new TokenV2();

        bytes memory initData = abi.encodeCall(
            TokenV1.initialize,
            (address(this))
        );
        proxy = new ERC1967Proxy(address(v1), initData);
    }

    function test_implementationCantBeInitialized() public {
        vm.expectRevert();
        v1.initialize(address(this));
    }

    function test_unauthorizedUpgradeReverts() public {
        vm.prank(address(0xdead));
        TokenV1 proxied = TokenV1(address(proxy));
        vm.expectRevert();
        proxied.upgradeToAndCall(address(v2), \"\");
    }

    function test_statePreservedAfterUpgrade() public {
        TokenV1 proxied = TokenV1(address(proxy));
        proxied.mint(address(this), 1000e18);
        uint256 balBefore = proxied.balanceOf(address(this));

        proxied.upgradeToAndCall(address(v2), \"\");

        TokenV2 proxiedV2 = TokenV2(address(proxy));
        assertEq(proxiedV2.balanceOf(address(this)), balBefore);
    }

    function test_v2HasUpgradeFunction() public {
        bytes4 selector = bytes4(keccak256(\"upgradeToAndCall(address,bytes)\"));
        (bool success,) = address(v2).staticcall(
            abi.encodeWithSelector(selector, address(0), \"\")
        );
        assertTrue(true, \"V2 must have upgrade function\");
    }
}
Enter fullscreen mode Exit fullscreen mode

Head-to-Head Benchmark

I tested all four tools against 8 common proxy vulnerability patterns:

Vulnerability Slither OZ Plugin PROXION Foundry
Storage slot collision ⚠️ manual
Missing initializer ⚠️ test
Function selector clash ⚠️ partial
UUPS silent death ⚠️ test
Unprotected selfdestruct ⚠️ test
Missing __gap ⚠️
_disableInitializers() ⚠️ test
Closed-source proxy scan

Verdict: No single tool catches everything. The OZ Plugin has the broadest coverage for development-time checks, PROXION is unmatched for post-deployment analysis of closed-source contracts, and Slither catches issues OZ might miss in complex inheritance chains.

The CI Pipeline That Catches Everything

Combine all four in a GitHub Actions workflow:

# .github/workflows/upgrade-safety.yml
name: Upgrade Safety Checks

on:
  pull_request:
    paths: ['contracts/**', 'src/**']

jobs:
  upgrade-safety:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1

      - name: Setup Node
        uses: actions/setup-node@v4
        with: { node-version: '20' }

      - name: Install Dependencies
        run: |
          npm ci
          pip install slither-analyzer

      - name: Slither Upgrade Check
        run: |
          slither-check-upgradeability \
            contracts/TokenV1.sol TokenV1 \
            contracts/TokenV2.sol TokenV2 \
            --json slither-upgrade-report.json
        continue-on-error: true

      - name: OZ Upgrade Validation
        run: npx hardhat test test/UpgradeSafety.ts

      - name: Foundry Layout Diff
        run: |
          forge build
          forge inspect TokenV1 storage-layout > v1.json
          forge inspect TokenV2 storage-layout > v2.json
          python3 scripts/compare-layouts.py v1.json v2.json

      - name: Forge Upgrade Tests
        run: forge test --match-contract UpgradeSafety -vvv

      - name: Upload Reports
        uses: actions/upload-artifact@v4
        with:
          name: upgrade-safety-reports
          path: |
            slither-upgrade-report.json
            v1.json
            v2.json
Enter fullscreen mode Exit fullscreen mode

The Layout Comparison Script

#!/usr/bin/env python3
# scripts/compare-layouts.py
import json, sys

def load_layout(path):
    with open(path) as f:
        data = json.load(f)
    return {v['label']: v for v in data.get('storage', [])}

def compare(old_path, new_path):
    old = load_layout(old_path)
    new = load_layout(new_path)
    errors = []

    for name in old:
        if name not in new:
            errors.append(f\"REMOVED: {name} (slot {old[name]['slot']})\")

    for name in old:
        if name in new:
            if old[name]['slot'] != new[name]['slot']:
                errors.append(
                    f\"SLOT SHIFT: {name} moved \"
                    f\"{old[name]['slot']} -> {new[name]['slot']}\"
                )
            if old[name]['type'] != new[name]['type']:
                errors.append(
                    f\"TYPE CHANGE: {name} changed \"
                    f\"{old[name]['type']} -> {new[name]['type']}\"
                )

    if errors:
        print(\"❌ Storage layout incompatibilities found:\")
        for e in errors:
            print(f\"  - {e}\")
        sys.exit(1)
    else:
        print(\"✅ Storage layouts are compatible\")

if __name__ == \"__main__\":
    compare(sys.argv[1], sys.argv[2])
Enter fullscreen mode Exit fullscreen mode

Real Exploit This Would Have Caught

The IoTeX ioTube bridge hack (February 2026, $4.4M) used a malicious upgrade via a compromised private key. An upgrade safety pipeline would have:

  1. Slither: Flagged the new implementation's different storage layout
  2. OZ Plugin: Blocked the upgrade due to incompatible state variables
  3. Foundry tests: Failed on state preservation checks
  4. PROXION: Detected the suspicious implementation at the bytecode level post-deployment

The attacker bypassed all of this because IoTeX had no automated upgrade validation — the compromised key could push any implementation directly.

Solana Perspective: Program Upgrade Safety

Solana's BPF programs have their own upgrade patterns. Here's a Rust safety check:

use anchor_lang::prelude::*;

#[program]
pub mod upgrade_guard {
    use super::*;

    pub fn validate_upgrade(
        ctx: Context<ValidateUpgrade>,
        expected_data_hash: [u8; 32],
    ) -> Result<()> {
        let program_info = &ctx.accounts.program_account;

        let (expected_authority, _) = Pubkey::find_program_address(
            &[b\"upgrade_authority\", program_info.key().as_ref()],
            ctx.program_id,
        );
        require!(
            ctx.accounts.authority.key() == expected_authority,
            ErrorCode::UnauthorizedUpgrade
        );

        let program_data = program_info.try_borrow_data()?;
        let actual_hash = anchor_lang::solana_program::hash::hash(
            &program_data
        );
        require!(
            actual_hash.to_bytes() == expected_data_hash,
            ErrorCode::ProgramDataMismatch
        );

        emit!(UpgradeValidated {
            program: program_info.key(),
            authority: ctx.accounts.authority.key(),
            data_hash: expected_data_hash,
            timestamp: Clock::get()?.unix_timestamp,
        });

        Ok(())
    }
}

#[error_code]
pub enum ErrorCode {
    #[msg(\"Unauthorized upgrade authority\")]
    UnauthorizedUpgrade,
    #[msg(\"Program data hash mismatch\")]
    ProgramDataMismatch,
}

#[event]
pub struct UpgradeValidated {
    pub program: Pubkey,
    pub authority: Pubkey,
    pub data_hash: [u8; 32],
    pub timestamp: i64,
}
Enter fullscreen mode Exit fullscreen mode

Upgrade Safety Audit Checklist

Before any proxy upgrade hits mainnet:

Storage Safety:

  • [ ] Run slither-check-upgradeability between old and new versions
  • [ ] Verify storage layout JSON diff shows no removals or type changes
  • [ ] Confirm __gap variables exist for future-proofing
  • [ ] Check that new variables are only appended, never inserted

Initialization Safety:

  • [ ] _disableInitializers() called in implementation constructor
  • [ ] initialize() protected by initializer modifier
  • [ ] Implementation contract cannot be initialized directly

UUPS Safety:

  • [ ] New implementation includes _authorizeUpgrade() (no \"silent death\")
  • [ ] _authorizeUpgrade() has proper access control
  • [ ] No selfdestruct or delegatecall to untrusted targets

Governance Safety:

  • [ ] Upgrade requires multisig approval
  • [ ] Timelock enforced (minimum 48h for mainnet)
  • [ ] Upgrade event emitted and monitored
  • [ ] Rollback plan documented and tested

Conclusion

Proxy upgradeability is now officially on OWASP's radar (SC10:2026), and for good reason — it's a $500M+ bug class that most CI pipelines completely ignore.

The fix isn't complex: layer Slither's upgrade checker, OpenZeppelin's plugin, Foundry layout diffs, and upgrade-specific tests into your pipeline. For monitoring deployed contracts you don't own, PROXION fills the gap with bytecode-level analysis.

No single tool catches everything. The pipeline does.


Part of the DeFi Security Research series. Follow for weekly deep-dives into smart contract security, audit tooling, and real exploit analysis.

Top comments (0)