Opcode Summary

PropertyValue
Opcode0x55
MnemonicSSTORE
GasExtremely complex: 100 (no-op / dirty slot) to 20,000+ (cold access + zero-to-nonzero). See EIP-2200, EIP-2929, EIP-3529.
Stack Inputkey (32-byte storage slot), val (32-byte value)
Stack Output(none)
BehaviorWrites a 32-byte word to the executing contract’s persistent storage at the given 32-byte key. The write persists on-chain indefinitely (or until overwritten). Gas cost depends on the slot’s current value, original value (at transaction start), whether the slot is warm or cold, and whether the write is a no-op, allocation, modification, or clearing. SSTORE is prohibited in a STATICCALL context and requires a minimum of 2,300 gas remaining (EIP-2200 stipend guard).

Threat Surface

SSTORE is the most gas-expensive and security-critical opcode in the EVM. It is the only opcode that permanently modifies on-chain state (aside from contract creation and the deprecated SELFDESTRUCT), making it the focal point for the most devastating vulnerability classes in smart contract history.

The threat surface centers on five properties:

  1. SSTORE ordering relative to external calls enables reentrancy. The most exploited vulnerability pattern in Ethereum’s history — reentrancy — exists because developers place SSTORE (state updates) after external calls (CALL, DELEGATECALL). When a contract sends ETH or calls an external contract before updating its own storage, the callee can re-enter the calling contract and find stale state. The DAO hack ($60M, 2016) is the canonical example: balances[msg.sender] was set to zero via SSTORE only after ETH was sent to msg.sender, allowing recursive withdrawals against the unchanged balance.

  2. SSTORE in DELEGATECALL writes to the caller’s storage, not the callee’s. When contract A delegatecalls contract B, B’s SSTORE instructions write to A’s storage using B’s slot layout. If A and B disagree on storage layout (different variable ordering, different inheritance chains, or intermediate upgrade versions), SSTORE silently overwrites critical state — ownership addresses, initialization flags, balances — with garbage values. This is the storage collision vulnerability that has destroyed proxy contracts holding millions in TVL.

  3. SSTORE has the most complex gas schedule in the EVM. The gas cost depends on six variables: current value, new value, original value (at transaction start), warm/cold access, whether the slot is “dirty” (already modified this transaction), and the 2,300-gas stipend floor. This complexity created the Constantinople reentrancy vulnerability (EIP-1283 made dirty SSTOREs cheap enough to fit in the 2300 gas stipend), the GasToken exploit (storing gas refunds as tokens), and ongoing developer confusion about when SSTORE is safe within low-gas contexts.

  4. SSTORE is the only opcode that triggers gas refunds. Clearing a storage slot (nonzero to zero) refunds 4,800 gas (post-EIP-3529). Before London, the refund was 15,000 gas with a 50% cap, which enabled GasToken — a scheme that bloated Ethereum’s state trie by millions of entries to “bank” gas refunds for later sale. EIP-3529 reduced refunds to 1/5 of gas used, killing GasToken but not eliminating all refund gaming.

  5. Every SSTORE allocating a new storage slot permanently grows the state trie. Writing a nonzero value to a previously-zero slot costs 20,000 gas and creates a new leaf in Ethereum’s Merkle Patricia Trie. Unlike memory, storage is never garbage-collected. Contracts with unbounded storage growth (e.g., appending to an array for every user action) permanently increase the cost of running a full node, contributing to state bloat that threatens Ethereum’s long-term decentralization.


Smart Contract Threats

T1: Reentrancy via State Update Ordering — The DAO Pattern (Critical)

The single most exploited vulnerability pattern in Ethereum history arises from placing SSTORE after an external call. When a contract checks a balance (SLOAD), sends funds (CALL), and only then updates the balance (SSTORE), the recipient can re-enter the contract during the CALL and find the balance unchanged.

  • Classic single-function reentrancy. Contract checks balances[msg.sender] > 0, sends ETH via msg.sender.call{value: amount}(""), then sets balances[msg.sender] = 0. The attacker’s receive() function calls withdraw() again. Since SSTORE hasn’t executed yet, balances[msg.sender] still holds the original value, and the contract sends funds again. This repeats until the contract is drained or gas runs out.

  • Cross-function reentrancy. Contract A has withdraw() that sends ETH before updating balances, and transfer(to, amount) that reads balances. During the re-entrant callback from withdraw(), the attacker calls transfer() to move their (stale) balance to another address, then allows withdraw() to complete and zero out the original slot. The attacker now has both the withdrawn ETH and the transferred balance.

  • Cross-contract reentrancy. Contract A calls contract B, which callbacks into contract C, which reads stale state from contract A. The SSTORE that would have made A’s state consistent hasn’t executed yet because A’s execution is paused mid-call. Curve Finance’s $70M+ exploit (July 2023) used this pattern via a broken Vyper reentrancy guard.

  • Read-only reentrancy. A view function on contract A is called during a re-entrant callback while A’s storage is in an inconsistent state. External protocols that price assets based on A’s state (e.g., LP token valuations, oracle feeds) get incorrect values. No SSTORE occurs in the re-entrant call itself, but the missing SSTORE from the paused execution is what makes the state inconsistent.

Why it matters: Reentrancy accounts for the largest single exploit in Ethereum history (The DAO, 70M+; Lendf.Me, $25M). The root cause is always SSTORE ordering relative to external calls.

T2: Storage Collision in Proxy Upgrades (Critical)

When a proxy contract uses DELEGATECALL to forward execution to an implementation contract, every SSTORE in the implementation writes to the proxy’s storage using the implementation’s slot layout. If the proxy’s storage layout disagrees with the implementation’s, SSTORE silently overwrites critical state:

  • Variable reordering across upgrades. An upgraded implementation inserts a new variable before existing ones, shifting all subsequent variables by one slot. The SSTORE that writes to what the new implementation thinks is balances actually overwrites what was owner in the proxy’s storage. No error, no revert — just silent corruption.

  • Inheritance chain changes. Adding a new base contract to the implementation’s inheritance chain shifts storage slots for all derived variables. If the proxy was deployed with the old inheritance order, every SSTORE in the upgraded implementation writes to the wrong slot.

  • Initialization flag collision. OpenZeppelin’s Initializable contract stores initialized and initializing booleans in storage slot 0. If the proxy also stores state (e.g., proxyAdmin) in slot 0, the SSTORE that sets initialized = true during initialization writes to a different byte of the slot than expected, or the proxy admin address’s bytes are misinterpreted as initialization flags. The Audius exploit ($6M, July 2022) used exactly this: the proxy admin address’s last two bytes coincidentally satisfied the initializing check, allowing repeated re-initialization.

  • Unstructured storage not using EIP-1967. Proxy patterns that don’t use the EIP-1967 standard storage slots (bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)) risk colliding with the implementation’s sequential storage layout.

Why it matters: Storage collision produces no errors or reverts — state is silently corrupted. Audits routinely miss these bugs because they require reasoning about storage layouts across two contracts simultaneously. The Audius hack bypassed two professional audits (OpenZeppelin, Kudelski).

T3: Gas Refund Manipulation and GasToken Abuse (High)

SSTORE is the only opcode that generates gas refunds, and this mechanism has been extensively exploited:

  • GasToken / CHI token. Before EIP-3529 (London, August 2021), clearing a storage slot refunded 15,000 gas, and the refund cap was 50% of gas used. GasToken exploited this by writing arbitrary data to storage slots during low-gas-price periods (creating entries via 20,000-gas SSTOREs), then clearing those slots during high-gas-price periods to claim refunds. This effectively “banked” gas, creating a secondary gas futures market. The result was millions of dead storage entries permanently bloating the state trie — an estimated 18% of all state entries at peak.

  • Artificial gas consumption to maximize refunds. Attackers could pad transactions with useless computation to increase gas_used, thereby increasing the maximum refund (which was capped as a fraction of total gas). This made refund-heavy transactions cheaper at the expense of block space.

  • Post-EIP-3529 residual gaming. While EIP-3529 reduced the refund cap to 1/5 and lowered the clearing refund to 4,800 gas, the mechanism still exists. Contracts that clear large numbers of storage slots in a single transaction still receive meaningful refunds, creating minor economic incentives to batch state cleanup with expensive operations.

Why it matters: GasToken and its variants contributed significantly to Ethereum’s state bloat problem, directly threatening the network’s ability to maintain decentralized full nodes. EIP-3529 fixed the worst abuse but the refund mechanism remains a source of gas-schedule complexity.

T4: 2300 Gas Stipend and SSTORE Compatibility (High)

When Solidity’s .transfer() or .send() sends ETH, the recipient receives exactly 2,300 gas — originally designed to be enough for a LOG event but not enough for an SSTORE (which costs at minimum 5,000 gas pre-Istanbul). This created a widely relied-upon “reentrancy safety” property that has been broken and restored multiple times:

  • EIP-1283 broke the stipend safety (Constantinople, January 2019). EIP-1283 reduced dirty-slot SSTORE cost to 200 gas, well within the 2,300 stipend. ChainSecurity discovered that this enabled reentrancy attacks through .transfer() — a function the entire Solidity ecosystem considered safe. The Constantinople hard fork was delayed at the last minute to address this, demonstrating that SSTORE gas cost changes have cascading security implications across all deployed contracts.

  • EIP-2200 restored the safety with a stipend guard. EIP-2200 (Istanbul, December 2019) reintroduced net gas metering but added a critical check: SSTORE reverts if the remaining gas is less than or equal to 2,300. This explicitly prevents SSTORE execution within the .transfer() stipend, restoring the reentrancy safety property.

  • TSTORE (EIP-1153, Cancun) breaks the assumption again. TSTORE costs only 100 gas and has no stipend guard. Contracts that rely on the 2,300 stipend being “too low for state changes” are vulnerable post-Cancun: an attacker can use TSTORE within the stipend to coordinate reentrancy via transient storage. While TSTORE changes are cleared after the transaction, they persist across re-entrant calls within the same transaction.

  • Contracts receiving ETH via .transfer() may break on gas repricing. Any future EIP that changes SSTORE gas costs risks breaking the stipend safety assumption again. Contracts that depend on .transfer() for reentrancy protection are fragile by design.

Why it matters: The 2,300 stipend was the Solidity ecosystem’s primary reentrancy defense for years. SSTORE gas changes nearly broke it once (Constantinople) and TSTORE has created a new vector. Relying on gas costs for security is inherently fragile.

T5: State Bloat via Unbounded Storage Writes (Medium)

Every SSTORE that allocates a new storage slot (zero to nonzero) creates permanent state. Contracts that allow unbounded storage growth impose externalities on the entire network:

  • Unbounded arrays and mappings. Contracts that push to a storage array on every user interaction (e.g., storing every trade, every vote, every message) grow storage without bound. Each new entry costs 20,000 gas to the caller but imposes permanent storage costs on every full node operator.

  • Mapping-based denial of service. An attacker who can cause SSTORE to write to new mapping entries at low cost (e.g., creating fake accounts, dust transfers that create new balance entries) can bloat a contract’s storage footprint, increasing SLOAD costs for legitimate operations and increasing the node resources required to serve the contract.

  • No garbage collection. The EVM has no mechanism to reclaim storage. Even if a contract’s logic no longer references a slot, the data persists in the state trie forever. The 4,800-gas refund for clearing slots provides a minor incentive, but contracts must actively clear storage — no automatic cleanup occurs.

Why it matters: Ethereum’s state trie has grown to hundreds of gigabytes, with a significant portion being dead storage from GasToken, abandoned contracts, and poorly designed storage patterns. State bloat increases sync times, disk requirements, and the barrier to running a full node, threatening decentralization.


Protocol-Level Threats

P1: SSTORE Gas Schedule Complexity and Consensus Risk (Medium)

SSTORE has the most complex gas calculation of any EVM opcode, depending on six variables (current value, new value, original value, warm/cold, dirty/clean, stipend floor). This complexity increases the risk of consensus bugs between client implementations:

  • Client implementation divergence. Go-Ethereum, Nethermind, Besu, and Erigon must all compute identical SSTORE gas costs for every execution. The six-variable gas schedule, combined with the refund mechanism and EIP-2929 access sets, creates a large surface for subtle implementation differences. A gas miscalculation in one client produces a different state root, causing a chain split.

  • EIP interaction complexity. SSTORE’s gas schedule has been modified by EIP-1283, EIP-2200, EIP-2929, and EIP-3529 across four hard forks. Each EIP interacts with the others, and the combined behavior requires tracking transaction-wide state (access sets, original values, dirty flags) that didn’t exist in earlier versions.

P2: State Trie Growth and Full Node Sustainability (High)

Every zero-to-nonzero SSTORE permanently adds a leaf to Ethereum’s Merkle Patricia Trie:

  • State size compounds. As of 2025, Ethereum’s state exceeds 300GB and continues growing. Each new storage entry adds ~100 bytes to the trie (key, value, hash path). The state cannot shrink — clearing a slot marks it as zero but doesn’t remove the trie path.

  • State expiry and Verkle trees. Proposed solutions (EIP-4444 history expiry, Verkle trees) may eventually address state growth, but current Ethereum has no mechanism to bound state size. SSTORE is the primary contributor to state growth, and any gas repricing that makes storage cheaper accelerates the problem.

  • Shanghai DoS attacks (2016). Attackers exploited underpriced SLOAD/SSTORE operations to create IO-heavy transactions that took 20-80 seconds to process, compared to milliseconds for normal transactions. EIP-2929 (Berlin, 2021) addressed this by introducing cold/warm access pricing, increasing the first access to 2,100 gas (SLOAD) and adding a 2,100-gas cold surcharge to SSTORE.

P3: Gas Refund Economics Post-EIP-3529 (Low)

EIP-3529 (London, August 2021) reduced the maximum gas refund from gas_used / 2 to gas_used / 5 and reduced the SSTORE clearing refund from 15,000 to 4,800 gas. These changes:

  • Killed GasToken economics. The reduced refund cap makes gas tokenization unprofitable, eliminating the incentive to bloat state for future gas savings.

  • Preserved reentrancy lock incentives. The reduced but non-zero refund still incentivizes clearing reentrancy guard flags (1 → 0), which is the primary legitimate use of SSTORE refunds.

  • May affect future tokenomics. Protocols that rely on state cleanup incentives (e.g., clearing expired orders, removing stale entries) see reduced economic benefit from the refund reduction. This is a minor but real change in the cost structure of storage-heavy protocols.

P4: SSTORE in STATICCALL Context (Low)

SSTORE is prohibited during STATICCALL execution. Any attempt to execute SSTORE in a static context causes an immediate revert. This is enforced at the EVM level and prevents read-only calls from modifying state. No consensus bugs or exploits have been attributed to this check, but contracts that incorrectly assume they can write storage during a callback from a STATICCALL will fail silently.


Edge Cases

Edge CaseBehaviorSecurity Implication
Writing the same value (no-op)Costs 100 gas (warm) or 2,200 gas (cold). No state change, no refund.Developers may assume no-op writes are free; they still cost gas. Can be used for gas griefing if an attacker can trigger no-op SSTOREs in a loop.
Zero to nonzero (slot allocation)Costs 20,000 gas (+ 2,100 cold surcharge if applicable). Creates a new state trie entry.Most expensive SSTORE case. Unbounded allocation enables state bloat attacks.
Nonzero to zero (slot clearing)Costs 2,900 gas (warm, clean slot) and refunds 4,800 gas.Refund incentivizes state cleanup but is capped at 1/5 of total gas used. GasToken exploited this before EIP-3529.
Nonzero to different nonzeroCosts 2,900 gas (warm, clean slot). No refund.Moderate cost, no refund. Common for balance updates, counter increments.
Cold access surchargeFirst SSTORE to a slot in a transaction adds 2,100 gas. Slot becomes warm afterward.EIP-2930 access lists can pre-warm slots for 1,900 gas, saving 200 gas per cold SSTORE.
Dirty slot (already written this tx)Costs 100 gas for any subsequent write within the same transaction.Extremely cheap follow-up writes. Enables efficient reentrancy guards (set on entry, clear on exit) but also means an attacker who can trigger multiple SSTOREs to the same slot pays very little.
Dirty slot reset to original valueCosts 100 gas and refunds the difference (2,800 if original was nonzero, 19,900 if original was zero).Encourages patterns that temporarily modify and then restore storage (e.g., reentrancy locks), rewarding clean transaction behavior.
Remaining gas 2,300SSTORE reverts (EIP-2200 stipend guard).Prevents SSTORE within .transfer() / .send() 2300-gas callbacks. Restores reentrancy safety that EIP-1283 would have broken. Does NOT apply to TSTORE.
SSTORE in DELEGATECALLWrites to the calling contract’s storage, not the implementation’s.Storage collision risk: the implementation’s slot layout may not match the proxy’s. Silent state corruption.
SSTORE in STATICCALLImmediately reverts.Hard enforcement of read-only semantics. No bypass possible.
SSTORE with key = keccak256(…)Solidity mappings and dynamic arrays hash keys to distribute storage across the 2^256 slot space.Collision probability is negligible (birthday bound ~2^128), but contracts that allow user-controlled keys to be hashed may enable targeted slot writes if the hash function is misused.

Real-World Exploits

Exploit 1: The DAO Hack — $60M Reentrancy via SSTORE Ordering (June 2016)

Root cause: The splitDAO function transferred ETH to the caller before executing the SSTORE that zeroed the caller’s balance.

Details: The DAO was a decentralized investment fund holding ~15% of all ETH in existence at the time. Its splitDAO function allowed members to withdraw their proportional share of funds. The function followed a dangerous pattern:

  1. Calculate the member’s share based on balances[msg.sender] (SLOAD)
  2. Transfer ETH to the member via withdrawRewardFor, which called payOut — an external call to msg.sender
  3. Only after the external call returned: balances[msg.sender] = 0 (SSTORE)

The attacker deployed a contract whose fallback() function called splitDAO recursively. Each re-entrant call found balances[msg.sender] unchanged (the SSTORE hadn’t executed yet), so the DAO transferred funds again. The attacker recursed approximately 20 times per transaction, repeating the attack 250 times across two addresses, draining 3.6 million ETH ($60M).

SSTORE’s role: The vulnerability existed solely because SSTORE (the balance update) was positioned after the external CALL (the ETH transfer). Had the code executed balances[msg.sender] = 0 before the external call, the re-entrant call would have found a zero balance and transferred nothing.

Impact: 3.6 million ETH stolen (~$60M). The attack led to the Ethereum hard fork that created Ethereum Classic (ETC), permanently splitting the community and the chain. Reentrancy became the most studied vulnerability class in smart contract security.

References:


Exploit 2: Constantinople Reentrancy Discovery — Hard Fork Delayed (January 2019)

Root cause: EIP-1283 reduced the gas cost of dirty-slot SSTORE to 200 gas, making it executable within the 2,300 gas stipend provided by .transfer() and .send().

Details: ChainSecurity discovered that EIP-1283, scheduled for the Constantinople hard fork, would break a fundamental security assumption: that .transfer() and .send() were reentrancy-safe because their 2,300 gas stipend was insufficient for an SSTORE. With EIP-1283’s reduced cost for dirty-slot writes (200 gas), an attacker could:

  1. Call a victim contract that sends ETH via .transfer()
  2. In the attacker’s receive() function (executing with 2,300 gas), perform a dirty-slot SSTORE to modify the victim’s state
  3. Re-enter the victim via a second entry point that reads the modified state

This affected every deployed Solidity contract that relied on .transfer() for reentrancy protection — potentially thousands of contracts holding significant value. The Constantinople hard fork was postponed less than 24 hours before activation.

SSTORE’s role: The vulnerability was directly caused by SSTORE gas repricing. The fix (EIP-2200) added an explicit check: SSTORE reverts if remaining gas is 2,300, making the stipend guard a protocol-level invariant rather than an implicit gas-cost side effect.

Impact: No funds were lost (the vulnerability was caught pre-deployment), but the hard fork delay demonstrated that SSTORE gas changes have global security implications across all deployed contracts. EIP-2200 formalized the stipend guard.

References:


Exploit 3: Audius Governance Takeover — $6M via Storage Collision (July 2022)

Root cause: Storage slot collision between the proxy contract’s admin address and OpenZeppelin’s Initializable contract flags, allowing repeated re-initialization.

Details: Audius used an AudiusAdminUpgradeabilityProxy that stored the proxyAdmin address in storage slot 0. OpenZeppelin’s Initializable contract stored its initialized (bool) and initializing (bool) flags in the first two bytes of the same slot 0. Because the proxy admin address ended in 0xac (nonzero), the EVM interpreted the initialized flag as true. The second byte (0xab) made initializing also true.

This coincidence caused the initializer() modifier to always pass its guard check, allowing anyone to call initialize() on the governance, staking, and delegation contracts as many times as they wanted. The attacker:

  1. Called initialize() on the Governance contract to change voting parameters
  2. Called initialize() on the Staking contract to give themselves arbitrary staked token amounts
  3. Created a fraudulent governance proposal to transfer 18 million AUDIO tokens from the community treasury
  4. Used their manipulated stake to pass the proposal

Every initialize() call wrote to storage via SSTORE, but because the initialization guard was broken by the slot 0 collision, these SSTOREs rewrote critical governance parameters that should have been immutable after the first initialization.

SSTORE’s role: SSTORE in a DELEGATECALL context writes to the proxy’s storage. The collision between the proxy’s admin address (written by SSTORE during proxy deployment) and the implementation’s initialization flags (read/written by SSTORE during initialization) was the root cause. Both SSTOREs targeted slot 0, and the proxy’s value coincidentally satisfied the implementation’s boolean checks.

Impact: 18 million AUDIO tokens stolen (~$6M). The vulnerability bypassed audits by OpenZeppelin (2020) and Kudelski (2021), demonstrating that storage collision bugs are exceptionally difficult to detect through manual review.

References:


Exploit 4: GasToken State Bloat — Millions of Dead Storage Entries (2018-2021)

Root cause: The pre-EIP-3529 gas refund mechanism (15,000 gas refund for clearing a slot, 50% refund cap) made it profitable to “bank” gas by writing to storage during low-fee periods and clearing during high-fee periods.

Details: GasToken (GST2) and 1inch’s CHI token exploited SSTORE’s refund mechanism as a gas futures market:

  1. During low-gas-price periods, GasToken contracts executed SSTORE to write nonzero values to thousands of storage slots, paying 20,000 gas per slot
  2. Each slot creation minted a GasToken (an ERC-20 token representing the stored gas)
  3. During high-gas-price periods, holders burned their GasTokens, which triggered SSTOREs clearing the slots (nonzero to zero), claiming 15,000 gas refunds per slot
  4. With the 50% refund cap, users could effectively halve their transaction gas costs by burning GasTokens alongside their actual transactions

At peak usage, GasToken accounted for an estimated 18% of Ethereum state entries. The contracts created millions of storage slots containing arbitrary nonzero values, permanently bloating the state trie. Even after GasToken became economically unviable (EIP-3529), the dead storage entries remain in the trie forever.

SSTORE’s role: The entire GasToken economy was built on SSTORE’s refund mechanism. SSTORE was both the “deposit” action (zero-to-nonzero write, 20,000 gas) and the “withdrawal” action (nonzero-to-zero write, 15,000 gas refund). No other opcode produces refunds.

Impact: Millions of permanent state trie entries, contributing significantly to Ethereum’s state growth problem. EIP-3529 (London, 2021) reduced the clearing refund to 4,800 gas and capped refunds at 1/5 of gas used, making GasToken unprofitable.

References:


Attack Scenarios

Scenario A: Classic Reentrancy — SSTORE After External Call (The DAO Pattern)

contract VulnerableVault {
    mapping(address => uint256) public balances;
 
    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }
 
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "no balance");
 
        // VULNERABLE: External call BEFORE SSTORE
        // The recipient's receive() can re-enter withdraw()
        (bool success,) = msg.sender.call{value: amount}("");
        require(success, "transfer failed");
 
        // SSTORE happens here -- too late.
        // During reentrancy, this line hasn't executed yet,
        // so balances[msg.sender] still holds the original value.
        balances[msg.sender] = 0;
    }
}
 
contract ReentrancyAttacker {
    VulnerableVault immutable vault;
    uint256 attackCount;
 
    constructor(VulnerableVault _vault) { vault = _vault; }
 
    function attack() external payable {
        vault.deposit{value: msg.value}();
        vault.withdraw();
    }
 
    receive() external payable {
        // Re-enter: balances[address(this)] hasn't been zeroed yet
        if (attackCount < 10 && address(vault).balance >= vault.balances(address(this))) {
            attackCount++;
            vault.withdraw();
        }
    }
}

Scenario B: Storage Collision in Proxy Upgrade

// Original implementation (V1): storage layout starts at slot 0
contract ImplementationV1 {
    address public owner;      // slot 0
    uint256 public totalSupply; // slot 1
    mapping(address => uint256) public balances; // slot 2
 
    function initialize(address _owner) external {
        require(owner == address(0), "already initialized");
        owner = _owner;
    }
}
 
// Upgraded implementation (V2): developer adds a variable BEFORE owner
contract ImplementationV2 {
    bool public paused;         // slot 0 -- NEW VARIABLE inserted here
    address public owner;       // slot 1 -- SHIFTED from slot 0 to slot 1
    uint256 public totalSupply; // slot 2 -- SHIFTED from slot 1 to slot 2
    mapping(address => uint256) public balances; // slot 3 -- SHIFTED
 
    function setPaused(bool _paused) external {
        require(msg.sender == owner, "not owner");
        // Reads slot 1 as "owner", but proxy's slot 1 contains totalSupply.
        // If totalSupply happens to match the caller's address (unlikely but
        // illustrative), this check passes. More commonly, it always fails,
        // locking out the real owner permanently.
        paused = _paused;
    }
 
    // Every SSTORE in V2 writes to the WRONG slot in the proxy's storage.
    // balances[user] now targets a different slot range than V1's balances.
    // Users' funds are effectively lost -- reads return zero.
}

Scenario C: Gas Stipend Reentrancy (Pre-EIP-2200)

// This scenario was possible under EIP-1283 (never deployed)
// and is again possible via TSTORE post-Cancun
 
contract VulnerableStore {
    mapping(address => uint256) public credits;
 
    function pay(address to) external {
        uint256 amount = credits[msg.sender];
        require(amount > 0);
        credits[msg.sender] = 0;
 
        // .transfer() sends only 2300 gas.
        // Pre-EIP-2200: with EIP-1283, the recipient could SSTORE
        // within 2300 gas (dirty write = 200 gas).
        // Post-Cancun: the recipient can TSTORE within 2300 gas
        // (TSTORE = 100 gas), coordinating reentrancy via transient storage.
        payable(to).transfer(amount);
    }
}
 
contract StipendAttacker {
    // Under EIP-1283 (never deployed): could SSTORE within 2300 gas
    // Under Cancun: can TSTORE within 2300 gas to set a flag
    mapping(uint256 => uint256) public flag;
 
    receive() external payable {
        // This TSTORE costs 100 gas, fits in 2300 stipend
        assembly {
            tstore(0, 1)
        }
    }
}

Scenario D: Unbounded Storage Growth DoS

contract VulnerableRegistry {
    struct Entry {
        address creator;
        bytes32 data;
        uint256 timestamp;
    }
 
    Entry[] public entries;
 
    // Anyone can add entries with no cost limit beyond gas
    function register(bytes32 data) external {
        // Each push allocates new storage slots (20,000 gas per slot).
        // Over time, entries.length grows without bound.
        entries.push(Entry({
            creator: msg.sender,
            data: data,
            timestamp: block.timestamp
        }));
    }
 
    // Iterating over entries becomes increasingly expensive
    function findByCreator(address creator) external view returns (uint256) {
        for (uint256 i = 0; i < entries.length; i++) {
            if (entries[i].creator == creator) return i;
        }
        revert("not found");
    }
 
    // An attacker can call register() thousands of times with dust data,
    // making findByCreator() exceed the block gas limit for all users.
    // The storage slots persist forever -- no cleanup mechanism exists.
}

Mitigations

ThreatMitigationImplementation
T1: Reentrancy via SSTORE orderingChecks-Effects-Interactions pattern: perform all SSTOREs before any external callsMove balances[msg.sender] = 0 before msg.sender.call{value: amount}(""). All state changes must precede all external interactions.
T1: Reentrancy guardUse a mutex that prevents re-entrant callsOpenZeppelin ReentrancyGuard (nonReentrant modifier). Post-Cancun, use EIP-1153 transient storage for gas-efficient cross-contract locks.
T1: Cross-contract reentrancyGlobal reentrancy locks shared across related contractsDeploy a shared lock contract; all participating contracts check/set the lock via TSTORE before external calls.
T2: Storage collisionUse EIP-1967 standard storage slots for proxy stateAll proxy variables (implementation address, admin, beacon) stored at keccak256("eip1967.proxy.X") - 1 slots, avoiding collision with sequential layout.
T2: Layout verificationAutomated storage layout comparison between upgradesOpenZeppelin Upgrades plugin for Hardhat/Foundry validates storage layout compatibility at deploy time.
T2: Initialization safetyDisable initializers on implementation contractsCall _disableInitializers() in the implementation’s constructor to prevent direct initialization.
T3: Gas refund abuseNo action needed post-EIP-3529EIP-3529 reduced refunds to 4,800 gas (clearing) and capped total refunds at 1/5 of gas used, making GasToken unviable.
T4: Stipend-based reentrancyDon’t rely on gas costs for security; use explicit guardsReplace .transfer() / .send() with .call{value: amount}("") plus a ReentrancyGuard. Gas costs change across hard forks; reentrancy guards don’t.
T5: Unbounded storage growthCap storage growth; use off-chain storage for unbounded dataUse a maximum array length; emit events (LOG opcodes) for historical data instead of storing on-chain; use IPFS/Arweave for large datasets.
T5: Storage cleanup incentivesEncourage users to clear their own stateProvide gas refund benefits (via SSTORE clearing) to users who clean up expired positions, completed orders, etc.
GeneralStatic analysis for SSTORE orderingSlither’s reentrancy detectors; Mythril’s symbolic execution. Both flag SSTOREs that occur after external calls.

Compiler/EIP-Based Protections

  • Solidity >= 0.8.0: Overflow/underflow checks prevent balance manipulation that could amplify reentrancy profits. Does not prevent reentrancy itself.
  • EIP-2200 (Istanbul, 2019): SSTORE stipend guard — reverts if remaining gas 2,300. Prevents SSTORE within .transfer() callbacks.
  • EIP-2929 (Berlin, 2021): Cold/warm access pricing. First SSTORE to a slot costs +2,100 gas (cold surcharge), reducing DoS potential from cheap storage writes.
  • EIP-3529 (London, 2021): Refund cap reduced to 1/5 of gas used; clearing refund reduced to 4,800 gas. Kills GasToken economics.
  • EIP-1153 (Cancun, 2024): TSTORE/TLOAD provide transaction-scoped storage at 100 gas, ideal for reentrancy locks that don’t need persistence. Cheaper and cleaner than using SSTORE for mutex patterns.
  • OpenZeppelin Upgrades Plugin: Validates storage layout compatibility between proxy and implementation at deploy time, preventing storage collision in upgradeable contracts.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalHighThe DAO (70M+, 2023), Lendf.Me ($25M, 2020)
T2Smart ContractCriticalMediumAudius (150M frozen, 2017)
T3Smart ContractHighLow (post-EIP-3529)GasToken state bloat (2018-2021, millions of dead entries)
T4Smart ContractHighMediumConstantinople delay (2019), TSTORE reentrancy vector (post-Cancun)
T5Smart ContractMediumMediumOngoing state bloat; no single large exploit
P1ProtocolMediumLowNo known consensus split, but high complexity increases risk
P2ProtocolHighHighShanghai DoS attacks (2016); ongoing state growth
P3ProtocolLowN/AEIP-3529 resolved GasToken; residual refund gaming is minor
P4ProtocolLowN/ANo known exploits; STATICCALL enforcement is robust

OpcodeRelationship
SLOAD (0x54)Reads from the same persistent storage that SSTORE writes to. SLOAD + SSTORE ordering is the root cause of reentrancy: SLOAD reads stale state when SSTORE hasn’t executed yet. SLOAD costs 100 gas (warm) / 2,100 gas (cold).
TSTORE (0x5D)Writes to transient storage (cleared after transaction). Costs 100 gas with no stipend guard. Preferred over SSTORE for reentrancy locks and temporary flags. Introduces new reentrancy risk within the 2,300 gas stipend.
DELEGATECALL (0xF4)Executes callee code in the caller’s storage context. Every SSTORE in delegatecalled code writes to the caller’s storage, creating the storage collision vulnerability in proxy patterns.
CALL (0xF1)External call that transfers execution control. When CALL precedes SSTORE, the callee can re-enter before state is updated. The CALL + SSTORE ordering is the reentrancy primitive.
STATICCALL (0xFA)Read-only call that prohibits SSTORE. Any SSTORE attempt in a STATICCALL context reverts immediately. Prevents state modification during view calls.
TLOAD (0x5C)Reads from transient storage. Paired with TSTORE as a lightweight alternative to SLOAD/SSTORE for intra-transaction state (e.g., reentrancy locks).