Security

Storage vs Memory: The Pointer Bug

Solidity is unique in that it forces developers to explicitly define where data lives: in storage (persistent blockchain state) or memory (temporary RAM during execution).
A misunderstanding of these keywords leads to one of the most insidious bugs in smart contract development: the "Stale State" bug. This error allows an attacker to execute logic based on modified data that never actually gets saved to the blockchain.

The Mechanics of the Illusion

When you retrieve a struct or array from a mapping, you make a choice:
  1. storage: You create a pointer. Modifying the variable directly modifies the data on the blockchain.
  2. memory: You create a copy. Modifying this variable only changes the local copy in the RAM.
The bug occurs when a developer loads a critical structure into memory to save gas, modifies it (e.g., updates a balance or sets a flag), performs a payout based on that change, but fails to write the result back to storage.

Case Study: Cover Protocol Hack

In December 2020, the Cover Protocol was exploited due to this exact nuance.
The Blacksmith contract handled the minting of coverage tokens. In the function responsible for updating the pool state, the developers fetched the pool data into a memory struct. They updated the pool's cached fields (lowering the collateral requirement) within that memory struct. However, they never assigned that memory struct back to the global storagemapping.
The Result: The contract logic executed using the updated (valid) values in memory, allowing the attacker to mint tokens. But when the transaction finished, the memory was wiped. The global state remained unchanged, as if the minting never affected the pool's health. The attacker repeated this loop, printing infinite tokens.

The Code Pattern

This is what the vulnerability looks like:
// VULNERABLE
function withdraw() external {
    // 1. Create a COPY of the user's state
    User memory user = userMap[msg.sender]; 
    
    require(user.balance > 0, "No funds");
    
    // 2. Transfer funds
    token.transfer(msg.sender, user.balance);
    
    // 3. Update the COPY (Logic error!)
    user.balance = 0; 
    
    // Function ends. 'user.balance' in storage is STILL > 0.
    // The attacker can call withdraw() again.
}

The Solution

Always be intentional with your data keywords.
  • If you need to update the state, use User storage user = ....
  • If you use memory to save gas on reads, you must explicitly write it back: userMap[msg.sender] = user;.
Solidity will not warn you that your user.balance = 0 operation is useless. It assumes you know what you are doing.