DEV Community

Cover image for What a Confusing Line of Solidity Taught Me About Defaults and Security
Bijan
Bijan

Posted on

What a Confusing Line of Solidity Taught Me About Defaults and Security

Last night I spent an hour confused about something that should've taken two minutes.

I was writing my first Solidity storage contract — a simple one, SimpleStorage, that saves entries by ID. Coming from years of web app pentesting, SOC work, and red/purple team ops, I'm used to thinking about how systems get broken, not how they're built. So when I declared:

I expected I'd need to explicitly tell Solidity this was permanent — the same way I had to write string memory _StringData for the temporary parameter in my function. I didn't. State variables like that mapping are storage by default, no keyword required. It took a tutorial, a web search, and finally asking AI directly before that clicked.

Small thing. But it's the kind of small thing that matters more here than in most languages I've worked in — because in Solidity, a misunderstanding like that isn't just a bug. It's the difference between "works in testing" and "exploitable in production."

The problem: two things that look similar, aren't

In most languages I've touched, "where does this variable live" is either irrelevant to the syntax or explicit everywhere. Solidity splits the difference in a way that isn't obvious until it bites you:

  • Inside a function, if you're working with a string, struct, array, or mapping, you have to say whether it's memory (temporary, wiped after the function call) or storage (permanent, written to the blockchain).
  • At the contract level — where you declare state variables — you don't say anything, because there's only one option. State variables are always storage.

The inconsistency isn't a language flaw. It's that the "choice" only exists in one context. Outside a function, there's nothing to choose between — a state variable's whole purpose is to persist, so Solidity doesn't ask.

Why the default exists

This maps directly to how the Ethereum Virtual Machine actually works. A smart contract has three places data can live:

Location Lifespan Cost Where it's used
storage Permanent, written to the blockchain Expensive (gas cost scales with writes) Contract state variables
memory Temporary, exists only during function execution Cheap Local variables inside functions
calldata Temporary, read-only Cheapest Function input parameters (external calls)

State variables exist specifically so a contract can remember something between transactions — a balance, an owner address, an entry in a mapping. There's no scenario where a state variable would need to be memory, because a memory variable disappears the moment the function returns. So Solidity doesn't make you spell out the obvious. Everything declared at the contract level, outside a function, is storage.

Inside a function, though, the compiler can't guess your intent. You might want a string that's just scratch space for a calculation — memory — or you might be about to write it back into a state variable — storage. That ambiguity is why the keyword is required there and only there.

How it plays out in code

Here's the shape of the confusion, side by side:

// State variable — always storage, no keyword needed
mapping(uint256 => string) public DataIdToData;

// Function parameter — location must be explicit
function addData(uint256 _id, string memory _StringData) public {
    DataIdToData[_id] = _StringData; // copied from memory into storage
}
Enter fullscreen mode Exit fullscreen mode

_StringData lives in memory for the duration of the function call. The moment you assign it into DataIdToData[_id], it's copied into permanent contract storage. Nothing about the syntax announces that copy is happening — you have to already know the mapping is storage and the parameter is memory to see it.

Why this is a security issue, not just a syntax quirk

This is where the pentester instincts kicked back in. Getting storage vs memory wrong isn't cosmetic — it's a documented vulnerability class in Solidity, and it gets worse with structs and arrays.

If you declare a local variable as storage when you meant memory (or the reverse), you're not creating a copy — you're creating a reference to existing storage. Assign a storage pointer to the wrong slot, or leave one uninitialized, and you can end up writing to state you never meant to touch. This general class of bug — storage layout confusion, particularly around delegatecall and library contracts — is widely cited in postmortems of real incidents, including the 2017 Parity multisig wallet freeze, where a storage layout issue tied to a library contract left funds permanently inaccessible. I'm citing that as a well-known industry example, not something from my own contract — but it's exactly the category of mistake that starts as "wait, which one is this again?"

That's the pattern I keep running into three weeks in: things that would be a minor bug in a web app are a fund-draining or fund-freezing bug on-chain, because there's no "restart the server and it's fine" — the mistake is written to the chain.

What I'm taking from this

I've been studying Solidity for about three weeks now, doing it in the opposite order from how I originally learned security: build first, then go back and audit. It's blockchain, smart contracts, tokens, and a syntax that reads like a mix of Python, C++, and PHP — simpler than I expected, but with sharper edges than I'm used to.

The practical habit I'm building from this: whenever Solidity doesn't make me declare something, I now ask why not, instead of assuming it's not important. The implicit defaults are often exactly where the design decisions — and the risk — are concentrated.

If you've made the same shift — from finding vulnerabilities to writing the code that creates them — what tripped you up first? What resources actually moved the needle for you when you were starting out?

Top comments (0)