- name:
- proxy-vulnerabilities
- description:
- Fallback or receive function with delegatecall - risk of function selector clash between proxy admin functions and implementation functions
- category:
- vulnerability-pattern
- pattern_category:
- proxy
- - regex:
- fallback\(\)|receive\(\).*delegatecall
- severity:
- Medium
- confidence:
- Low
- swc:
- SWC-112
Proxy Vulnerability Patterns
Overview
Upgradeable proxy systems trade immutability for maintainability. That trade introduces a second security model: execution context and storage live in the proxy, while logic lives in implementations reached through delegatecall. Any mismatch between expected and actual layout, initialization state, or function routing can grant attackers full control.
Most proxy exploits are not complex cryptographic breaks. They are integration failures across deployment scripts, upgrade procedures, and ABI boundaries. A proxy setup can be technically standards-compliant and still vulnerable if governance, initialization, or selector design is weak.
Auditing proxies requires system-level reasoning across deployment transactions, upgrade authority, storage layout evolution, and fallback behavior. Reviewing only implementation code misses critical attack surface in proxy shell contracts and admin operations.
Key Attack Vectors
- Storage collision between proxy slots and implementation variables.
- Uninitialized implementation contracts that an attacker can initialize directly.
- Uninitialized proxies where initializer can be called by arbitrary users.
- Selector clashes between admin functions and delegated user functions.
- Unsafe fallback routing that forwards admin calls into implementation logic.
- Upgrade functions lacking role checks, timelocks, or upgrade validation.
- Incompatible storage layout changes during upgrades.
- Missing rollback testing for UUPS upgrades.
- Delegatecall to untrusted implementation addresses.
Typical Takeover Sequence (Uninitialized Proxy)
- Proxy is deployed without atomic initialization.
- Initializer remains externally callable.
- Attacker calls
initialize() first and becomes owner/admin.
- Attacker upgrades implementation or drains managed assets.
- Team loses control of proxy governance path.
Typical Storage Collision Sequence
- New implementation reorders or inserts state variables incorrectly.
- Critical admin/value fields map to unexpected slots.
- Routine function calls mutate sensitive proxy state.
- Access control breaks or funds accounting corrupts.
- Recovery requires emergency upgrade or migration.
Detection Heuristics
Proxy Primitive Identification
- Detect
delegatecall, fallback, receive, and implementation slot constants.
- Identify whether system is Transparent, UUPS, Beacon, or custom hybrid.
- Enumerate upgrade entry points and admin authority graph.
- Verify proxy and implementation compile with compatible storage assumptions.
Initialization Safety Checks
- Confirm implementation constructor calls
_disableInitializers().
- Ensure proxy initialization happens in deployment transaction.
- Verify initializer functions are single-use and role-gated where required.
- Check reinitializer versioning for upgrade modules.
Storage Layout Safety Checks
- Compare storage layout before and after upgrade.
- Ensure inherited contracts preserve variable ordering.
- Validate use of storage gaps (
uint256[50] private __gap) where applicable.
- Confirm EIP-1967 slots are used for implementation/admin/beacon pointers.
Selector Clash and Routing Checks
- Enumerate proxy admin selectors and implementation public selectors.
- Detect collisions where admin and user paths share selectors.
- Ensure Transparent proxy blocks admin from falling through to implementation.
- For UUPS, verify
proxiableUUID and upgrade authorization checks.
Concrete Code Smells
fallback() external payable {
(bool ok,) = implementation.delegatecall(msg.data);
require(ok);
}
function initialize(address owner_) external initializer {
owner = owner_; // callable by first caller if not atomically initialized
}
bytes32 internal constant IMPLEMENTATION_SLOT =
keccak256("implementation"); // non-standard slot risks conflicts
Audit Checklist
- Is every proxy deployed with initialization calldata in the same transaction?
- Are implementation contracts permanently non-initializable post-deploy?
- Are storage layout diffs reviewed and enforced in CI before upgrade?
- Are upgrade operations timelocked, multisig-gated, and event-rich?
- Are selector collisions tested against full ABI surface?
Prevention
Use Battle-Tested Standards
- Prefer OpenZeppelin TransparentUpgradeableProxy or ERC1967/UUPS implementations.
- Use EIP-1967 storage slots and audited upgrade libraries.
- Avoid custom proxy shells unless necessary for protocol-specific requirements.
- Keep proxy logic minimal and immutable where possible.
Initialization Hardening
- Call
_disableInitializers() in implementation constructor.
- Supply initializer calldata during proxy deployment.
- Restrict or remove external initializer exposure after setup.
- Document and test upgrade-time reinitializer sequences.
Upgrade Governance Controls
- Gate upgrades behind multisig + timelock.
- Require explicit implementation validation (
code.length > 0, interface checks).
- Emit events for proposed and executed upgrades.
- Maintain emergency pause/rollback procedures with clear authority boundaries.
Selector and Routing Safety
- For Transparent proxies, separate admin and user call paths strictly.
- For UUPS proxies, enforce
_authorizeUpgrade with robust roles.
- Run selector collision scans in CI against proxy and implementation ABIs.
- Avoid exposing overlapping administrative selectors in implementations.
Hardened Pattern Example
contract Impl is Initializable, UUPSUpgradeable, OwnableUpgradeable {
constructor() {
_disableInitializers();
}
function initialize(address owner_) external initializer {
__Ownable_init(owner_);
}
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner
{}
}
Operational Practices
- Store upgrade runbooks with preflight and postflight checks.
- Simulate upgrades on forked state before production execution.
- Track implementation bytecode hashes and signed release artifacts.
- Include automated storage-layout regression gates in release pipelines.
Real-World Examples
Proxy Initialization Incidents
- Pattern: implementation or proxy left uninitialized.
- Impact: attacker claims ownership role and controls upgrade path.
- Lesson: initialization must be atomic, single-use, and scripted.
Storage Layout Corruption Cases
- Pattern: variable order/type changes between implementation versions.
- Impact: admin slots and balances are overwritten unintentionally.
- Lesson: treat storage layout as immutable contract between versions.
Selector Clash Risk in Custom Proxies
- Pattern: fallback delegatecalls overlap with proxy admin selectors.
- Impact: privileged calls routed incorrectly or user calls blocked.
- Lesson: transparent separation and selector audits are mandatory.
Pattern-to-Impact Mapping
storage-collision -> critical state corruption and privilege compromise.
uninitialized-proxy -> hostile initialization and upgrade takeover.
selector-clash -> call-path confusion and admin bypass risk.
References
- SWC-112 (Delegatecall to untrusted callee): https://swcregistry.io/docs/SWC-112
- OpenZeppelin Upgrades docs: https://docs.openzeppelin.com/upgrades-plugins/1.x/
- OpenZeppelin proxy patterns: https://docs.openzeppelin.com/contracts/4.x/api/proxy
- EIP-1967 proxy storage slots: https://eips.ethereum.org/EIPS/eip-1967
- EIP-1822 (UUPS): https://eips.ethereum.org/EIPS/eip-1822
- ConsenSys best practices for upgradeability: https://consensys.github.io/smart-contract-best-practices/
- Trail of Bits proxy audit notes: https://blog.trailofbits.com/