Lesson 2 — Solidity primer: types, functions, modifiers, events
The minimum Solidity vocabulary you need to read DeFi contracts critically — without trying to make you a contract author. Today: the language constructs that show up everywhere and what each is hiding.
Solidity is the dominant smart-contract language by deployed value. The goal of this lesson is not to teach you to write Solidity from scratch but to give you reading literacy: enough vocabulary that an audit report or contract diff stops looking like noise. Every construct introduced here is a thing you'll see in dozens of audit findings later in this course.
**Value types vs reference types.** Solidity's value types are `uint{8..256}`, `int{8..256}`, `bool`, `address`, `bytes{1..32}`, and `enum`s. Reference types are dynamic arrays, `mapping`, `bytes` (dynamic), `string`, and `struct`. The distinction matters because function parameters and assignments behave differently: value-type assignment copies the value; reference-type assignment copies a *reference* (in storage) or *the data* (in memory), and the difference between `storage`-pointer assignment and `memory`-copy assignment in functions is a long-standing source of bugs in older code.
**The `mapping` is not a hash map.** A Solidity `mapping(K => V)` looks like a hash map but it's specified by EVM storage layout: `mapping(K => V) public foo` at storage slot `S` stores `foo[k]` at slot `keccak256(abi.encode(k, S))`. Mappings have no length, can't be iterated, can't be cleared in O(1), and don't distinguish 'never set' from 'set to default value' — accessing a non-existent key returns the type's zero value silently. Reading mapping declarations correctly is the foundation of understanding most DeFi contracts (every `balanceOf`, `allowance`, `_owners` is a mapping).
**Function visibility and mutability.** Four visibilities: `external` (callable only via transactions / from other contracts, not internal), `public` (both), `internal` (only from this contract + derived contracts), `private` (only this contract — but storage is still publicly readable; don't confuse 'private' with 'secret'). Three mutabilities: default (can modify state), `view` (reads state but doesn't modify it; can be called without a transaction), `pure` (neither reads nor writes state). Compile-time enforcement of these is decent but not airtight against tricky patterns like inline assembly.
**Modifiers are pre/post wrappers.** A modifier is a reusable pre/post-condition wrapper around a function body. The canonical example, `onlyOwner`, checks that `msg.sender == owner` before allowing the function to proceed. The body of the wrapped function appears at the position of `_;` inside the modifier. Modifiers stack: `function foo() onlyOwner whenNotPaused { ... }` runs both checks in order. The bug class to watch for: missing modifier on a sensitive function (lesson 6). Modifiers can also short-circuit silently if written carelessly.
**Events and logs.** An `event` declaration generates an `emit` statement that writes to the EVM's log buffer — a separate, cheap, append-only stream of indexed-topic data. Events are not readable from inside contracts (only from outside the chain); they're the primary channel by which dApps, indexers, and block explorers reconstruct contract state and history. Events with `indexed` parameters can be efficiently filtered. The pattern to remember: any state-changing action should emit a corresponding event, and the absence of an event when one was expected is a common audit finding (because off-chain consumers will miss the change).
**Inheritance, interfaces, libraries.** Solidity supports multiple inheritance with the C3 linearisation algorithm — the order of inheritance matters. An `interface` defines function signatures only (no implementation, no state); `abstract contract`s have implementation but can't be deployed; `library`s are like contracts but stateless and called via `delegatecall` from user contracts. The bug class to watch: linearisation surprises (a function override resolving to the wrong implementation), and library functions using `delegatecall` that accidentally write to the caller's storage layout in unexpected slots.
**Compiler version pragmas.** Every Solidity file starts with `pragma solidity ^0.8.x;` or similar. The minor version matters: 0.8.0 was when overflow checks became default, 0.8.20 introduced PUSH0 opcode (problems on chains that don't support it), etc. The pragma also gates behaviour for some EIPs. Audit findings often start with 'used unsafe compiler version' for old / unsupported versions; don't dismiss these — they catch real bugs.
Example
Read this contract fragment critically: `contract Vault { mapping(address => uint256) public balances; address public owner; modifier onlyOwner() { require(msg.sender == owner, "!owner"); _; } function deposit() external payable { balances[msg.sender] += msg.value; } function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount, "!bal"); (bool ok,) = msg.sender.call{value: amount}(""); require(ok, "!send"); balances[msg.sender] -= amount; } function emergencyDrain() external onlyOwner { payable(owner).transfer(address(this).balance); } }`. What jumps out: (a) `balances` is a public mapping — the auto-generated getter `balances(address)` lets anyone read any balance. (b) `withdraw` calls the external `msg.sender.call{value: amount}` *before* updating `balances[msg.sender]` — classic checks-effects-interactions violation, exploitable via reentrancy (lesson 4). (c) `emergencyDrain` is correctly modifier-gated but uses `transfer` which has a 2,300 gas stipend and would fail if `owner` is a smart-contract wallet with non-trivial fallback logic — a usability bug, not a security bug, but worth flagging. (d) The pragma isn't shown but matters: pre-0.8.0 the `balances[msg.sender] += msg.value` could overflow silently and is a bug class on its own. Reading every contract this way — line by line, identifying the surface area each line creates — is the core skill of audit work.
Common mistakes
- Treating `private` as 'secret.' Private functions can't be called from outside but the storage they touch is publicly readable; never store secrets on-chain.
- Assuming inheritance is single-parent like Java. Solidity does C3 multiple-inheritance linearisation; order matters and surprising overrides happen.
- Skipping the pragma when reading a contract. The compiler version gates safety behaviours and EIP support.
- Confusing `transfer` / `send` / `call` for ETH transfers. `transfer` has a 2,300 gas stipend (deprecated for portability); `call` is the modern recommendation but requires the checks-effects-interactions pattern.
- Confusing function-modifiers (preconditions) with state-mutability-modifiers (`view`, `pure`). They look similar in declaration syntax but do entirely different things.
Check your understanding
A contract exposes `function setOwner(address newOwner) public { owner = newOwner; }` — no modifier, no `require`. What does this mean?
Key terms covered
Sources & further reading
- Primary
- PrimaryOpenZeppelin Contracts source
Reference implementation of common patterns (Ownable, ERC20, ReentrancyGuard).
- Secondary
- Secondary
We prioritise primary sources. Where a topic moves quickly (regulation, security incidents), we re-check sources on the cadence shown by the page's "Next review" date.