Not your Type! Detecting Storage Collision Vulnerabilities in Ethereum Smart Contracts

Nicola Ruaro

Network and Distributed System Security (NDSS) Symposium 2024 · Day 2 · Blockchain & Smart Contracts · Blockchain & Smart Contracts

Overview

In the rapidly evolving landscape of decentralized finance (DeFi) and Ethereum smart contracts, the flexibility offered by features like contract upgradability comes with inherent security complexities. This talk, "Not your Type! Detecting Storage Collision Vulnerabilities in Ethereum Smart Contracts," presented by Nicola Ruaro, addresses a critical and often overlooked class of vulnerabilities: storage collisions. These issues arise when two smart contracts, particularly in the context of the widely used proxy pattern, share the same underlying storage but interpret the data within specific storage slots differently, either due to type mismatches or semantic misunderstandings. Such discrepancies can lead to severe consequences, including denial of service, privilege escalation, and direct theft of digital assets, as tragically exemplified by the $6 million AUDIUS platform attack in July 2022.

Slides

Visual summary for Not your Type! Detecting Storage Collision Vulnerabilities in Ethereum Smart Contracts by Nicola Ruaro
Visual summary for Not your Type! Detecting Storage Collision Vulnerabilities in Ethereum Smart Contracts by Nicola Ruaro

Key moments

  1. 0:00 Introduction: Storage collision vulnerabilities and DELEGATECALL
  2. 0:40 AUDIUS attack: $6M loss from type misinterpretation
  3. 1:10 CRUSH: Novel system for detecting and exploiting collisions
  4. 2:10 DELEGATECALL and shared storage mechanisms explained
  5. 2:50 Contract storage layout: Slots, packing, dynamic types
  6. 4:00 Proxy pattern: Upgradability and collision vulnerability origin
  7. 4:30 Detailed example: NFT marketplace storage collision exploit

Not your Type! Detecting Storage Collision Vulnerabilities in Ethereum Smart Contracts

Speakers: Nicola Ruaro

Conference: NDSS Symposium

YouTube: (no public video)

Overview

In the rapidly evolving landscape of decentralized finance (DeFi) and Ethereum smart contracts, the flexibility offered by features like contract upgradability comes with inherent security complexities. This talk, "Not your Type! Detecting Storage Collision Vulnerabilities in Ethereum Smart Contracts," presented by Nicola Ruaro, addresses a critical and often overlooked class of vulnerabilities: storage collisions. These issues arise when two smart contracts, particularly in the context of the widely used proxy pattern, share the same underlying storage but interpret the data within specific storage slots differently, either due to type mismatches or semantic misunderstandings. Such discrepancies can lead to severe consequences, including denial of service, privilege escalation, and direct theft of digital assets, as tragically exemplified by the $6 million AUDIUS platform attack in July 2022.

The presentation introduces CRUSH, a novel analysis system designed to automatically detect and exploit storage collision vulnerabilities at an unprecedented scale. Traditional detection methods, primarily compiler warnings, are often insufficient because they rely on specific development frameworks and, crucially, require the source code of all interacting contracts—a condition rarely met in the real-world Ethereum ecosystem. CRUSH overcomes these limitations by performing bytecode-based analysis, enabling it to scrutinize millions of deployed contracts without requiring source code.

CRUSH's contributions are multifaceted: it automates the identification of contract groups sharing storage through on-chain interaction analysis, leverages symbolic execution and program slicing for precise collision detection, and automatically synthesizes proof-of-concept exploits. The system's evaluation across over 14 million Ethereum smart contracts revealed 14,891 potentially vulnerable contracts and successfully generated end-to-end exploits for 956 of them. This groundbreaking research not only quantifies the significant threat posed by storage collisions but also uncovered over $6 million in novel, previously unreported potential financial damage, underscoring the urgent need for robust security analysis in the DeFi space.

Background

[▶ Watch: Introduction: Storage collision vulnerabilities and DELEGATECALL (0:00)]()

Smart contracts, typically authored in high-level languages like Solidity or Vyper, are compiled into EVM bytecode and deployed on the Ethereum blockchain. These programs execute on demand, managing persistent storage, transferring funds, and interacting with other contracts. Contract interactions can be initiated externally by an externally-owned account (EOA) or internally by another smart contract. Public functions serve as the primary entry points, with calls encoded in calldata, comprising a 4-byte function selector and arguments.

The Ethereum Virtual Machine (EVM) provides several opcodes for inter-contract communication. The CALL opcode is fundamental, allowing function invocation and ETH transfers. STATICCALL provides a view-only invocation, preventing any storage modifications. Central to storage collision vulnerabilities is the DELEGATECALL opcode. When a source contract (often termed a proxy) uses DELEGATECALL to invoke a destination contract (the logic contract), the destination contract executes its code in the context of the caller. This means the logic contract gains direct read and write access to the proxy contract's persistent storage and utilizes the proxy's ETH balance. This shared storage mechanism is the fundamental enabler for the proxy pattern and, concurrently, the root cause of storage collision vulnerabilities.

Contract storage itself is a critical component. All persistent variables of a smart contract reside in its storage, which is conceptualized as a vast array of 32-byte (256-bit) slots, each uniquely identified by an index. The storage layout is deterministically computed by the compiler based on the variable's declaration order.

  • Fixed-size variable types (e.g., uint, address, bool) are stored contiguously. To optimize space, multiple variables smaller than 32 bytes can be packed into a single storage slot. For instance, a bool and an address might occupy the same slot, with the bool aligned to the least significant bytes. If a variable doesn't fit, it spills into the next slot. The EVM instructions SLOAD and SSTORE always operate on a full 32-byte slot. When variables are packed, the compiler generates specific bit-masking procedures to extract or update the relevant portion of the slot.
  • Dynamic-size variable types (e.g., arrays, mappings) are always allocated a new BASE slot. For arrays, this BASE slot stores the array's length, and individual elements are stored at keccak256(BASE) + INDEX. For mappings, the BASE slot is typically unused, serving primarily to calculate the starting point for elements, which are stored at keccak256(KEY.BASE).

The proxy pattern is a widely adopted design paradigm for smart contract upgradability. It decouples the application's logic from its persistent state. Users interact with a proxy contract, which holds the contract's storage and then forwards all function calls to an immutable logic contract using DELEGATECALL. This allows developers to deploy new logic contracts to fix bugs or add features without migrating the existing state, as the new logic contract will seamlessly operate on the proxy's storage. However, this shared storage mechanism introduces storage collision vulnerabilities when the proxy and logic contracts, or different versions of logic contracts, interpret the variables within the same storage slot differently. If contract A writes to a slot expecting one type or semantic, and contract B subsequently reads that same slot expecting another, it can lead to corrupted state and critical security flaws.

Key Findings

[▶ Watch: CRUSH: Novel system for detecting and exploiting collisions (1:10)]()

CRUSH's large-scale analysis of over 14 million smart contracts on Ethereum yielded significant findings:

  • Prevalence of DELEGATECALL Interactions: Out of 53,580,899 deployed smart contracts, 14,237,696 actively participated in DELEGATECALL interactions. This highlights the widespread adoption of proxy patterns and similar shared-storage mechanisms.
  • Identification of Proxy/Logic Components: CRUSH identified 14,134,133 proxy contracts and 103,833 logic contracts, with some contracts serving dual roles.
  • Source Code Scarcity: A crucial justification for CRUSH's bytecode-based approach is that while 54% of the 14 million contracts have available source code on Etherscan, over 90% of proxy contracts with source code interact with logic contracts without available source. This demonstrates the limitations of source-code-only analysis tools.
  • Contract Characteristics: Proxy contracts are typically small (median of 3 basic blocks), primarily serving as storage holders. Logic contracts are significantly larger (median of 147 basic blocks), containing the application's functionality.
  • Logic Target Diversity: The majority (88%) of deployed proxy contracts use at least one constant target logic. A substantial portion also uses external source targets (39%) or storage slot targets (14%), demonstrating CRUSH's robustness across various targeting mechanisms.
  • Improved Type Inference: CRUSH's symbolic execution-based type inference module correctly identified 87.3% (484,465) of variable types, outperforming Gigahorse's 76.9% (425,740) by accurately tracking casting operations and packed variables.
  • Widespread Collisions: CRUSH uncovered 15,092 distinct contracts with potential storage collisions, leading to 46,403 collision candidates. Of these, 11% (4,877) were attributed to cascading collisions, where the introduction of a new variable shifts the layout of subsequent variables.
  • Security-Relevant Vulnerabilities: After impact analysis, 39,895 collisions across 14,891 contracts were deemed security-relevant, with 98% affecting sensitive slots (e.g., access control, financial balances) and 2% affecting guarding slots (e.g., initialization flags).
  • Successful Exploit Generation: CRUSH successfully generated end-to-end proof-of-concept exploits for 956 contracts. This included 7,759 sensitive collisions in 878 contracts and 424 guarding collisions in 143 contracts.
  • Substantial Financial Impact: The system uncovered over $6 million in novel, previously unreported potential financial damage. Notable examples included attacks like cfa7 ($4.6M) and 295b ($1.1M). While the AUDIUS attack was previously reported, CRUSH confirmed its nature.
  • Historical Vulnerabilities: 132 of the 956 exploits are still active, while 768 were exploitable in the past, indicating a persistent threat that has grown linearly since 2018, coinciding with the popularization of OpenZeppelin's proxy pattern.
  • Superiority over Existing Tools: CRUSH significantly outperformed USCHUNT, a state-of-the-art collision detection tool. On a benchmark dataset, CRUSH achieved 98 true positives, 40 false positives, and 4 false negatives, compared to USCHUNT's 21 true positives, 22 false positives, and 81 false negatives. This highlights the advantage of CRUSH's bytecode-based, precise analysis over source-code-only, rule-based approaches.

Technical Deep Dive

[▶ Watch: DELEGATECALL and shared storage mechanisms explained (2:10)]()

CRUSH's architecture is a sophisticated, multi-stage analysis system built upon the Gigahorse framework, which provides a register-based Intermediate Representation (IR) from EVM bytecode, enabling powerful static analysis without source code. The system operates in three primary stages: Component Discovery, Collision Discovery, and Vulnerability Discovery.

1. Component Discovery

This initial stage focuses on identifying groups of contracts that interact via DELEGATECALL and share storage.

  • Proxy Detection: CRUSH first analyzes on-chain transactions to identify DELEGATECALL operations. It extracts (source, destination) pairs from these calls and constructs a directed graph where nodes represent contracts and edges represent DELEGATECALL invocations. From this graph, CRUSH identifies COMPONENTS, which are groups of contracts sharing a common root node (the proxy) and its associated logic contracts. This process revealed 14,134,133 proxy contracts and 103,833 logic contracts among 14,237,696 DELEGATECALL-participating contracts. A significant finding was that over 90% of proxy contracts with available source code interact with logic contracts without source code, validating the necessity of a bytecode-based approach. Proxy contracts were found to be typically small (median 3 basic blocks), serving primarily as storage containers, while logic contracts were much larger (median 147 basic blocks), containing the application logic.
  • Lifespan Analysis: CRUSH determines the active lifespan for each proxy and its associated logic contracts. A proxy's lifespan is straightforward (creation to a reference block). For logic contracts, whose activity depends on how the proxy targets them, CRUSH uses lightweight static analysis and backward slicing to determine how the DELEGATECALL target address is set. Logic targets are classified into three categories:
  • Constant: The target address is hardcoded, implying the logic contract is active for the proxy's entire lifespan.
  • Read from storage slot: The target address is stored in a storage slot. CRUSH analyzes how this slot's value changes over time to infer the logic's active period.
  • External source: The target address originates from external input (e.g., calldata). In this conservative case, the logic is considered active for the proxy's entire lifespan.

Approximately 88% of proxies utilize at least one constant target logic, demonstrating the prevalence of this pattern. Median lifespans were 5 months for proxies and 4 months for logic contracts, with upgraded contracts showing significantly longer proxy lifespans (21 months).

2. Collision Discovery

This stage identifies actual storage collisions within the discovered COMPONENTS.

  • Type Inference: The core of collision detection is understanding the types associated with each storage slot accessed by SLOAD and SSTORE instructions. This involves two sub-steps:
  1. Identifying the target storage slot: Backward slicing is used to retrieve instructions involved in computing the slot address. Symbolic execution then classifies the access pattern:
  • Constant: Indicates fixed-size types (e.g., uint, address, bool).
  • keccak256(slot)+index: Indicates a dynamic array type.
  • keccak256(key.slot): Indicates a mapping type.
  • Unknown: If the pattern cannot be determined.
  1. Identifying variable types and access masks within the target slot: For dynamic types, they occupy a whole 32-byte slot. For fixed-size types, where multiple variables can be packed, CRUSH determines their precise access masks. For SLOAD instructions, a forward slice of instructions operating on the SLOAD result is computed. By symbolizing the 32 bytes of the slot and executing the forward slice, CRUSH infers the exact bit-mask used to extract the relevant variable. For example, if a 32-byte value 0xAABBCCDD is masked to extract 0x0000BBCC via RSHIFT and AND operations, CRUSH infers an access mask of 0x00ffff00. These masks are expressed as bytes_offset#nbytes. CRUSH's type inference significantly outperforms Gigahorse (87.3% vs. 76.9% accuracy) by precisely tracking casting operations missed by Gigahorse's rule-based approach.
  • Collision Detection: After inferring storage layouts and access masks for all contracts within a COMPONENT, CRUSH performs a pairwise comparison. A storage collision candidate is reported as a 3-tuple (A, B, S) if contracts A and B use different access masks for the same byte within a shared storage slot S. While different data types are sufficient, different semantics for the same data type can also trigger issues. A common cause is cascading collisions, where adding a new state variable in an upgraded logic contract shifts the layout of all subsequent variables, creating multiple collisions with the original logic contract. CRUSH identified 46,403 collision candidates across 15,092 contracts, with 11% stemming from cascading collisions. Simple collisions on mapping BASE slots, which typically have no security impact, were ignored, but 428 more subtle mapping collisions due to displacement of the BASE slot were considered.

3. Vulnerability Discovery and Exploit Generation

This final stage assesses the security impact of detected collisions and synthesizes proof-of-concept exploits.

  • Impact Analysis: CRUSH identifies collisions that are security-relevant. A contract B is vulnerable if a WRITE operation in contract A to slot S can be followed by a USE operation in contract B on the same slot S, with conflicting interpretations. CRUSH leverages existing techniques to identify sensitive slots (involved in access control, restricted writes, read-only values like creator address, dynamic array sizes) and guarding slots (protecting writes to sensitive slots, like an initialized flag). Collisions not impacting these critical slots are discarded. This filtering reduced the 46,403 candidates to 39,895 security-relevant collisions across 14,891 contracts, with 98% affecting sensitive slots.
  • Exploit Generation: CRUSH attempts to confirm if a WRITE-USE pair can trigger a vulnerable state and then synthesize an exploit.
  • Attack Preparation: Since all interactions happen through the proxy, CRUSH generates transactions that reach the relevant code in the logic contract via the proxy's DELEGATECALL.
  • WRITE Operation: Symbolic execution is used to find a feasible path to an SSTORE instruction in contract A that writes to the colliding slot S. If successful, a concrete transaction is crafted. If not, historical on-chain writes are inspected.
  • USE Operation: If S is a sensitive slot, symbolic execution finds an SLOAD instruction in contract B that uses S. If S is a guarding slot, CRUSH attempts to write any sensitive slot that S guards.
  • Exploit Synthesis & Verification: The discovered WRITE and USE operations are combined. Path constraints from symbolic execution are solved to obtain concrete calldata inputs. Finally, an EVM implementation is used to verify that the generated exploits are reproducible on-chain within the contract's lifespan. If all steps succeed, a verified storage collision vulnerability is reported. CRUSH successfully generated 956 end-to-end exploits, uncovering over $6 million in novel financial damage.

Demo / Proof of Concept

[▶ Watch: Proxy pattern: Upgradability and collision vulnerability origin (4:00)]()

The talk illustrated the criticality of storage collision vulnerabilities through several compelling examples, starting with a simplified version of the AUDIUS attack and further detailing two examples from the appendix: Denial of Service and Theft of Funds.

AUDIUS-Inspired Example (Privilege Escalation)

This example highlights how a type misinterpretation (uint to bool) can lead to privilege escalation.

  1. Setup: Alice deploys a Proxy contract (holding visits as uint) and a Logic contract (holding initialized as bool and admin as address). Critically, visits, initialized, and admin all reside in storage slot 0x0.
  2. Deployment & Initialization: The Proxy's constructor calls Logic.initialize() via DELEGATECALL. This sets initialized = 1 and admin = ALICE in slot 0x0. Immediately after, the Proxy constructor sets visits = 0, overwriting the entire slot 0x0 and corrupting the values set by Logic.initialize().
  3. Attacker Action: An attacker (EVIL) then calls initialize() on the Proxy contract. This call is DELEGATECALLed to the Logic contract.
  4. Collision Trigger: The Logic contract reads slot 0x0 to check initialized. Because visits was set to 0 by the Proxy, the Logic contract interprets the value in slot 0x0 as initialized = false (0), even though it was previously set to true by its own initialize function.
  5. Exploit: The require(!initialized) check passes. Logic.initialize() proceeds, setting initialized = 1 and, crucially, admin = EVIL (the attacker's address). The attacker is now the administrator and can invoke functions like withdraw() to steal funds.

Denial of Service (Appendix A)

This example, involving ChiProxy and ChiToken contracts, demonstrates how a storage collision can render a contract inoperable.

  1. Setup: ChiProxy declares logic, wallet, and owner variables, with owner at storage slot 0. ChiToken declares _balances, _allowances, and totalMinted, with _balances also at slot 0. The developers deploy ChiProxy and point its logic to ChiToken.
  2. Collision: A collision occurs because ChiProxy.owner (slot 0) collides with ChiToken._balances (also slot 0). The totalMinted variable in ChiToken is positioned such that its update overlaps with the owner address in the proxy.
  3. Attack: An attacker calls the mint function in ChiToken (via ChiProxy). This function increments totalMinted. Due to the collision, this write operation corrupts the ChiProxy.owner address by overwriting the relevant part of slot 0.
  4. Impact: Subsequent calls to functions like withdrawETH or withdrawToken in ChiProxy (which are protected by an onlyOwner modifier) will fail because the owner address has been corrupted to an invalid or zero value. This leads to a denial of service, preventing legitimate owners from withdrawing funds or upgrading the contract.

Theft of Funds (Appendix B)

This example, involving NFTParentProxy and NFTParentImpl contracts, illustrates a direct theft of funds.

  1. Setup: NFTParentProxy inherits Ownable, which declares _owner at storage slot 0. NFTParentImpl inherits Initializable, which declares _initialized and _initializing (both Booleans) at storage slot 0.
  2. Deployment & Collision: During deployment, NFTParentProxy's constructor calls NFTParentImpl.initialize() via DELEGATECALL. This sets _initialized = 1 and _initializing = 1 in slot 0. Immediately after, NFTParentProxy's constructor sets owner = msg.sender (the deployer's address) in slot 0, overwriting the boolean values. A collision exists between _initialized/_initializing (Logic) and owner (Proxy) in slot 0.
  3. Attack: An attacker calls NFTParentImpl.initialize() (via NFTParentProxy). NFTParentImpl, operating on the proxy's storage, reads slot 0 to check _initialized. Due to the type collision, the owner address (a non-zero 32-byte value) is misinterpreted by the Logic contract's specific packing and masking as _initialized = false (0).
  4. Exploit: The require(!initialized) check passes. The initialize() function proceeds, setting admin = msg.sender, which is now the attacker's address. The attacker gains administrative privileges and can call restricted methods like mintNFT to mint arbitrary NFTs, effectively stealing funds. The fix would involve modifying the implementation contract to move the boolean variables after a storage padding, but once exploited, the upgrade functionality is compromised, preventing remediation.

These examples vividly demonstrate the severe and diverse consequences of storage collision vulnerabilities and underscore the critical importance of tools like CRUSH for their detection and exploitation analysis.

Defensive Implications

[▶ Watch: Detailed example: NFT marketplace storage collision exploit (4:30)]()

The findings presented by CRUSH highlight a critical and often underestimated vulnerability class in the Ethereum ecosystem, particularly for contracts employing the proxy pattern and other DELEGATECALL-based shared storage mechanisms. Defenders—including smart contract developers, auditors, and platform operators—must adopt proactive strategies to mitigate these risks.

  1. Careful Storage Layout Design: The primary defense lies in meticulous design and management of storage layouts. Developers must ensure that proxy and logic contracts, as well as different versions of logic contracts, have compatible storage layouts. This means:
  • Never reuse storage slots for different purposes. If a slot is used for an address in the proxy, it should not be used for a bool in the logic contract, even if packing seems to allow it.
  • Utilize storage padding. Explicitly reserve storage slots to prevent cascading collisions when new variables are introduced in upgraded logic contracts. For example, OpenZeppelin's UUPS (Universal Upgradeable Proxy Standard) and Transparent Proxy patterns are designed to manage storage layout robustly, often by placing proxy-specific variables at the beginning of storage and implementation-specific variables at higher, potentially padded slots. However, even with these patterns, developers must be diligent.
  • Always append new state variables. When upgrading logic contracts, new state variables should only be appended to the existing storage layout to avoid shifting the positions of previously declared variables. This is crucial to prevent cascading collisions.
  1. Advanced Static Analysis and Auditing: Relying solely on compiler warnings is insufficient, as CRUSH's findings demonstrate (they often require full source code and specific frameworks). Developers and auditors should integrate advanced static analysis tools like CRUSH into their development and auditing pipelines. These tools, capable of bytecode-level analysis, can detect subtle type and semantic mismatches across shared storage without requiring source code for all components. Regular security audits must specifically look for storage collision vulnerabilities, paying close attention to DELEGATECALL usage, storage packing, and potential layout discrepancies between interacting contracts.
  1. Post-Deployment Monitoring and Incident Response: Even with robust pre-deployment checks, vulnerabilities can emerge or be discovered. Continuous monitoring of contract interactions and on-chain state changes can help identify suspicious activity that might indicate a storage collision exploit. A well-defined incident response plan is crucial to react swiftly if a vulnerability is exploited, potentially involving emergency upgrades (if the upgrade mechanism itself is not compromised) or coordinated communication with users.
  1. Responsible Disclosure: As CRUSH demonstrates, many vulnerabilities may be previously unreported. Security researchers should follow responsible disclosure guidelines, reporting findings to affected developers and relevant cybersecurity agencies (like CISA) to enable remediation before public exploitation.
  1. Education and Best Practices: Emphasizing the risks of DELEGATECALL and the proxy pattern's storage implications through developer education is vital. Promoting established, secure upgradeable contract patterns and discouraging custom, untested implementations can significantly reduce the attack surface. Developers should be aware of how the EVM handles storage packing and bit-masking, as well as the deterministic nature of storage slot allocation.

By integrating these defensive strategies, the DeFi ecosystem can enhance its resilience against storage collision vulnerabilities, protecting billions of dollars in digital assets and fostering greater trust in decentralized applications.

Key Takeaways

  • Storage collisions are a critical, under-investigated vulnerability class in Ethereum smart contracts, particularly prevalent in the context of the DELEGATECALL opcode and the proxy pattern.
  • CRUSH is a novel, bytecode-based analysis system capable of automatically detecting storage collision vulnerabilities at scale and synthesizing proof-of-concept exploits, overcoming limitations of source-code-only tools.
  • The financial impact of these vulnerabilities is substantial, with CRUSH uncovering over $6 million in novel, previously unreported potential financial damage across 956 exploitable contracts.
  • Type inference and precise storage layout analysis are crucial for identifying collisions, with CRUSH's symbolic execution-based approach significantly outperforming existing state-of-the-art tools like USCHUNT.
  • Defenders must prioritize careful storage layout design in upgradeable contracts, utilize advanced static analysis tools, and implement robust auditing processes to prevent and mitigate storage collision risks.
  • Cascading collisions and subtle mapping-related issues represent complex challenges that require sophisticated analysis to fully understand their security implications.

About the Speaker(s)

Nicola Ruaro is a researcher who presented the work "Not your Type! Detecting Storage Collision Vulnerabilities in Ethereum Smart Contracts" at the NDSS Symposium. The talk highlights their expertise in smart contract security, static analysis, symbolic execution, and the detection of complex vulnerabilities within the Ethereum ecosystem. Their work, focusing on bytecode analysis and large-scale vulnerability discovery, contributes significantly to understanding and mitigating risks in decentralized finance.

All talks from Network and Distributed System Security (NDSS) Symposium 2024