All articles
Web3 FoundationsSeptember 14, 20268 min read

State Variables: What a Contract Remembers Between Calls

State variables are a contract's entire world model. What gets written to chain storage, how storage differs from memory and calldata, and why reading the memory first answers most questions.

By Carlos (Bloqarl)

TL;DR

  • State variables are the values a contract keeps between calls, written into chain storage and still there when the next person calls, next week or next year.
  • They are declared directly inside the contract block, usually above the functions. That short list is the contract's entire world model.
  • If a fact is not in that list, the contract does not know it and cannot act on it. Reading the state first answers more questions than reading any function.
  • storage survives, memory lasts one call, calldata is the read-only input to one call. Most confusion about Solidity variables is really confusion about which of those three you are looking at.
  • Writing to storage is the expensive operation in Solidity. Reading it is cheap, and reading it from outside the chain is free.

What is a state variable in Solidity?

A state variable is a value declared at contract level whose contents live in chain storage, so it persists between separate calls and separate transactions, for as long as the contract exists.

In our worked example they are the two lines directly under the opening brace:

contract PiggyBank {
    address public owner;
    uint256 public goal;

owner holds an account's identity. goal holds a whole number. Both are written once by the constructor and then read by the functions below. When the transaction that set them ends, they do not disappear. They are part of the chain's state now.

Compare that with an ordinary variable declared inside a function, which exists only while that function runs and is gone the moment it returns. The difference is not stylistic. It is the difference between a contract that remembers and one that cannot.

Why read the state before the functions?

Because the state list tells you the boundaries of what the contract can possibly do.

Look again at PiggyBank's memory. It holds an owner and a goal. That is everything. Now ask a natural question: if someone deposits ETH and later wants it back before the goal is reached, can the contract refund them?

It cannot, and you can prove it without reading a single function. The contract has no record of who deposited what. There is no list of depositors, no mapping of addresses to amounts, nothing. Even if a function wanted to pay an individual back, the information required to do it was never stored.

That is the move. Read the state, then ask what the contract could not possibly know. In a real contract the state list is longer, but the logic is identical and it is the fastest way to find what a system is structurally incapable of, as opposed to what it merely chooses not to do.

The full reading method this fits into is in how to read a smart contract.

What does public do on a state variable?

    address public owner;

public here means anyone may read this value. That is all it means.

It is worth being precise, because this is one of the most commonly misread words in Solidity. Marking a state variable public does two things: it makes the value readable from outside, and it makes the compiler generate a small function for you, called a getter, that returns it. What it does not do is let anyone change the value. Nothing about public grants write access.

The mirror of that is also true and less obvious. Marking a state variable private does not make it secret. It means other contracts and the compiler will not give you a getter, but all chain state is publicly readable by anyone who queries the chain directly. A private variable is private from other Solidity code, not from people. If you read nothing else about visibility, read that sentence twice. The full picture is in Solidity visibility.

What is the difference between storage, memory, and calldata?

Three words appear again and again next to variables in real contracts, and the difference between them is where the value lives and how long it lasts.

LocationLifetimeTypical use
storagePermanent, until changedState variables, the contract's memory
memoryOne function callTemporary working copies inside a function
calldataOne function call, read onlyThe arguments a caller sent in

State variables are storage by definition, which is why you never see the word written on the lines above. Inside a function you do see it, because there the compiler needs to be told which you mean:

function example(string calldata name) external {
    string memory copy = name;   // a temporary copy, gone when this returns
}

The practical reading rule: if a value is in memory or calldata, nothing you do to it survives the call. Changing a memory copy of something does not change the stored original. A large share of the confusion beginners have with Solidity variables dissolves once that one fact is solid.

Why is writing storage the expensive part?

Every node that validates the chain has to store the result, permanently. The chain charges for that, in gas, and storage writes are among the most expensive operations a contract can perform.

Three consequences show up constantly in real code, and recognising them makes a lot of otherwise odd-looking Solidity make sense:

  • Contracts avoid writing when they can. Code that looks convoluted is often just avoiding a second storage write.
  • Values get packed together. Several small values can share one storage slot if their types allow it, which is why you sometimes see uint112 or uint32 where a plain uint256 would read more naturally. That is deliberate, and it is covered in EVM storage layout.
  • Reading is cheap, and reading from outside is free. A view function that only reads state costs the caller nothing when called from off chain, which is why contracts expose so many of them. See view vs pure.

Who can change a state variable?

Only the contract's own code. There is no external write path into storage that bypasses the functions the contract defines.

That gives a reader a powerful search. Pick a state variable, find every line in the file that assigns to it, and you have the complete list of ways that value can ever change. In PiggyBank:

    constructor(uint256 _goal) {
        owner = msg.sender;
        goal = _goal;
    }

Two assignments, both inside the constructor, which runs exactly once at deployment. There is no other line in the contract that writes owner or goal. So both values are fixed for the life of this pig. Nobody can transfer ownership, and nobody can move the goalposts, because no code exists to do it. See the Solidity constructor.

That is a complete, confident statement about a contract's behaviour, derived from grepping for two variable names. It is the single most productive habit in contract reading.

Delete the word and ask what breaks

  • Delete public from address public owner; and outsiders can no longer read the owner through the contract. The contract still knows, and the value is still visible to anyone querying chain state directly.
  • Move owner and goal inside a function and they stop being state entirely. They would be created fresh on every call and forgotten at the end, and the pig would have no memory at all.
  • Add a second assignment to owner somewhere outside the constructor and the contract's owner becomes changeable. One new line changes a permanent fact into a mutable one.
  • Change uint256 goal to a smaller type like uint32 and the biggest goal the pig can hold shrinks dramatically. The type is a ceiling, as covered in Solidity data types.

Related questions

Are private variables in Solidity actually private? No. private restricts access from other Solidity code, not from people. All contract storage is publicly readable by querying the chain directly, so anyone can read a private variable's contents without the contract's cooperation.

What is the difference between a state variable and a local variable? A state variable is declared at contract level and lives in storage, persisting between calls. A local variable is declared inside a function, lives in memory, and is discarded when that function returns.

Why do some contracts use uint112 instead of uint256? Usually to pack several values into a single storage slot and save gas on writes. It is a deliberate optimisation with a real cost: the smaller type has a lower maximum value, which the code must stay within.

Does a constant cost storage? No. A constant value is fixed at compile time and written into the contract's code rather than its storage, so it costs nothing to read and cannot be changed. immutable is similar but set once at deployment.

Can a contract read another contract's state variables? Only through functions the other contract exposes, such as the getters generated by public. There is no direct cross-contract storage read in Solidity, even though the data itself is publicly visible from outside the chain.

How do I find everything that changes a variable? Search the file for the variable name and look at every line that assigns to it. In a single-file contract that is the complete list. With inheritance you also have to search the parent contracts, which is why knowing which files a contract imports matters.

Where to go next

The memory of a contract is short, explicit, and sitting right at the top of the file. Read it first, ask what the contract therefore cannot know, and find every line that writes to it. Those three moves answer an unreasonable share of the questions people open a contract with.

Next, the types themselves. address and uint256 carry more meaning than they look like they do, and misreading them is how people misjudge what a contract can hold: the Solidity types you meet in the first ten lines.

Tagged

SoliditySmart ContractsLearn to Code