Variables
Solidity has different variable kinds with strict semantics: storage (persistent, costly), memory (function-local, cheap), and calldata (read-only inputs, cheapest). Visibility and immutability are key gas levers.
Storage, memory, calldata, visibility
EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract VariablesShowcase {
// === 1. State variables (live in storage — persistent) ===
uint256 public count; // public auto-getter
address private owner; // private — internal use only
string internal name = "Showcase"; // internal — this contract + children
// 'constant' — compiled into bytecode, NEVER changes
uint256 constant MAX_SUPPLY = 1_000_000;
address constant ZERO_ADDRESS = address(0);
// 'immutable' — set once in constructor, then read-only (cheaper than storage)
address public immutable creator;
uint256 public immutable deployedAt;
constructor() {
creator = msg.sender;
deployedAt = block.timestamp;
}
// === 2. Visibility ===
// public : callable from anywhere, generates a getter for state vars
// external : callable only from OUTSIDE the contract (slightly cheaper than public for fns)
// internal : callable from this contract and inherited contracts
// private : only this contract (still visible on-chain — don't store secrets here)
function readCount() external view returns (uint256) {
return count;
}
function _internalHelper() internal pure returns (uint256) {
return 42;
}
// === 3. Data locations: storage / memory / calldata ===
struct User {
string name;
uint256 score;
}
mapping(address => User) public users;
function updateName(address userAddr, string calldata newName) external {
// 'storage' reference — modifying it changes contract state
User storage u = users[userAddr];
u.name = newName; // writes to storage; expensive (~5,000-20,000 gas)
}
function copyToMemory(address userAddr) external view returns (string memory) {
// 'memory' copy — function-local, cheap, discarded at return
User memory u = users[userAddr];
return u.name;
}
function readCalldata(uint256[] calldata nums) external pure returns (uint256) {
// 'calldata' — function arguments; read-only; cheapest of the three
uint256 sum = 0;
for (uint256 i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
// === 4. Gas comparison ===
function expensiveLoop(uint256 n) external {
// BAD: storage read in a loop costs ~2100 gas per iteration
for (uint256 i = 0; i < n; i++) {
count += 1;
}
}
function cheapLoop(uint256 n) external {
// GOOD: cache to memory; one storage write at the end
uint256 local = count;
for (uint256 i = 0; i < n; i++) {
local += 1;
}
count = local;
}
// === 5. Primitive types ===
function primitives() external pure returns (
bool b,
int256 i,
uint256 u,
bytes32 b32,
address a
) {
b = true;
i = -42;
u = 42;
b32 = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;
a = address(0xCAfe);
}
// uint8..uint256 (in steps of 8) — explicit size matters for packing
// int8..int256
// address payable — can receive ETH via .transfer / .send
// bytes1..bytes32 — fixed-size byte arrays
// === 6. Variable-size types ===
string publicMessage; // dynamic string
bytes data; // dynamic bytes
uint256[] nums; // dynamic array
uint256[5] fixedNums; // fixed-size array
mapping(address => uint256) balances; // mapping
mapping(address => mapping(uint256 => bool)) approved; // nested mapping
// Variable-size types in memory are pricier — copying is O(N).
// === 7. Default values ===
// No explicit init = the zero value.
// bool → false
// uint* → 0
// int* → 0
// address → 0x0000...0000
// bytes* → 0x00...00
// string → ""
// array → empty array
// mapping → empty (every key returns the zero value)
function isUninitialised(address a) external view returns (bool) {
return balances[a] == 0; // works because uninitialised mapping value = 0
}
// === 8. Storage layout (packing) ===
// Slots are 32 bytes. Variables PACKED together if they fit.
struct Packed {
uint128 a; // 16 bytes
uint128 b; // 16 bytes — packed with a in 1 slot
uint256 c; // 32 bytes — new slot
}
struct Unpacked {
uint128 a; // 16 bytes
uint256 c; // 32 bytes — new slot (doesn't fit with a)
uint128 b; // 16 bytes — new slot, wastes 16 bytes
}
// Order matters! Group small types together.
// === 9. Storage SLOT cost
// Setting from 0 → non-zero: SSTORE costs ~22,100 gas
// Changing non-zero → other: SSTORE costs ~5,000 gas
// Setting non-zero → 0 (refund): refunds ~15,000 gas (net write cheap)
// Reading any storage slot: SLOAD costs ~2,100 gas (cold) or 100 (warm)
// === 10. Read-only modifiers ===
// 'pure' — no read / write of state, no env access
function pureMath(uint256 a, uint256 b) external pure returns (uint256) {
return a + b;
}
// 'view' — read state, no writes
function readCount2() external view returns (uint256) {
return count;
}
// Functions without view/pure can MODIFY state and are payable-eligible.
// === 11. Local variables
function localDemo() external pure returns (uint256) {
uint256 x = 10; // memory; lives on stack
uint256 y = 20;
return x + y;
}
// === 12. Block / msg / tx globals (special vars)
function context() external view returns (address, uint256, address) {
return (msg.sender, block.number, tx.origin);
// msg.sender — direct caller (could be a contract)
// tx.origin — original EOA — RARELY use; phishing risk
// block.number, block.timestamp, block.chainid, block.basefee
// msg.value — wei sent with the call
}
// === 13. Events for off-chain reads
event CountUpdated(uint256 newCount, address by);
function setCount(uint256 newCount) external {
count = newCount;
emit CountUpdated(newCount, msg.sender);
}
// === 14. Common gas tips for variables ===
// • Pack struct fields by size
// • Use immutable / constant where possible
// • Cache storage variables to memory in loops
// • Prefer calldata over memory for read-only array args
// • Use uint256 unless you really need a smaller type (small types in storage cost the same; in calldata they save)
// • Set to 0 carefully — refund timing is complex post-Merge
}
// === 15. Naming conventions ===
// constants: SCREAMING_SNAKE_CASE (MAX_SUPPLY)
// state vars: camelCase (count, balances)
// privates: _underscorePrefix (_internalHelper)
// structs: PascalCase (User, Order)
// events: PascalCase (Transfer, Approval)
// functions: camelCase (transfer, balanceOf)
// === 16. Common bugs ===
// • Forgetting 'storage' vs 'memory' → silent copy when you wanted a reference
// • Public state vars that should be private (anyone can read)
// • tx.origin authentication → phishing vector; use msg.sender
// • Unpacked structs → wasted slots, extra gas
// • Modifying calldata → compile error
// • Reading from uninitialised local memory variable → unpredictable
Why it matters
storage for state, memory for function locals, calldata for read-only inputs — in increasing order of cheapness. Pack struct fields by size and prefer immutable + constant — gas costs collapse.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
uint256 public total; // unsigned 256-bit int256 public balance; bool public open = true; address public owner; string public name; bytes32 public hash;Try it Yourself »
Discussion
Loading…