← All 45 books Solidity smart contracts, one concept per page Get the full edition · £10
One concept per page

Solidity smart contracts, one concept per page

The twenty-four steps that take you from 'I understand blockchains' to a deployed contract you wrote yourself.


Steve Hodgkiss 8 concepts

A diagram of the mechanism, the classic mistake, and one thing to go try. That's a page.

Solidity smart contracts, one concept per page

The twenty-four steps that take you from 'I understand blockchains' to a deployed contract you wrote yourself.


Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).

Mechanisms checked against the Solidity documentation at docs.soliditylang.org (contracts, types, control structures, security considerations, units and global variables, current 0.8.x semantics), ethereum.org's developer documentation (token standards, smart contracts), and the OpenZeppelin Contracts 5.x documentation (Ownable, AccessControl, ERC20, ReentrancyGuard). All example code is written for Solidity 0.8.x, where arithmetic is checked by default and custom errors are available. This book builds on the canonical definitions in Blockchain basics, one concept per page.

Educational information only, not financial advice. No investment advice, no price talk, no token recommendations. This book explains mechanisms; it does not tell you to buy anything.

Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.

© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.

Edition 1.0 · stevehodgkiss.net

Contents

Contents


Part 1 · Your first contract4
Remix: start with zero setup5
State variables: the contract's memory6
Functions and visibility7
Part 2 · Data on chain
Mapping: keys to drawers8
Part 3 · Logic and safety9
require: the gate that reverts10
The reentrancy trap11
Part 4 · Money and tokens12
payable and receive: the ETH door13
ERC-20: the standard in plain words14
Part 1 of 4
From reading to typing
1

Your first contract

Zero installs, first deploy. Five pages that get you from 'I know what a contract is' to a compiled, deployed contract on a testnet, and explain what a contract and its variables actually ARE.


In this part
  1. 01Remix: start with zero setup
  2. 02State variables: the contract's memory
  3. 03Functions and visibility

Remix is the browser IDE the Solidity docs use in their own examples. Edit, compile and deploy a contract from one tab, with nothing installed.

Blockchain · No. 01
First contract

Remix: start with zero setup

A browser tab is the whole workshop

A BROWSER TAB IS THE WHOLE WORKSHOP: EDIT, COMPILE, DEPLOY, CALL.remix.ethereum.orgfilescontract Counter {uint public count;0.8.26Solidity CompilercompiledeploySepoliatestneta button press,not a toolchainFoundry comes later, when there's something worth testingThe docs themselves put an 'open in Remix' button on every example. That is the endorsement.

Let's say every tutorial opens with installing three tools. Open remix.ethereum.org instead: editor, compiler, deploy, one tab.

The misconception is that real developers install a toolchain first. Remix is not a toy.

The official docs put an "open in Remix" button under every example.

First deploy beats perfect setup.

TRY IT THIS WEEK

Open remix.ethereum.org, compile and deploy the default Storage contract to the Remix VM, then press retrieve.

A state variable is stored on the chain and keeps its value between calls. A local variable inside a function is rebuilt and thrown away on every call.

Blockchain · No. 02
First contract

State variables: the contract's memory

What the contract remembers

STORAGE REMEMBERS FOREVER. A LOCAL VARIABLE FORGETS AT THE END OF THE CALL.STORAGE (state variables)count = 0count = 1lasts between calls,survives a rebootwritten by a transaction, kept by every node, foreverLOCAL (in a function)tmp = 7gone at thecall's endrebuilt fresh on every callcheap: it lives in the call's scratchpad, not on the chainblue button = read for free · orange button = a transaction that writes storage and costs gas

Let's say your counter resets on every call. Values at the top live in storage: transaction-written, node-kept.

Here a storage write is a transaction: it costs gas and it sticks

Remix shows the split as button colour: blue reads free, orange writes cost gas

Want it remembered? Top of the contract.

TRY IT THIS WEEK

Read a public uint on Remix's blue button, set it on the orange one, then read it again.

Visibility decides who can call a function: public and external to anyone, internal to the contract and its children, private to the contract alone. None of them hide data from the world.

Blockchain · No. 03
First contract

Functions and visibility

Who can press the button

FOUR VISIBILITY WORDS: WHO IS ALLOWED TO PRESS THIS BUTTON.function tick()publicone function,one gateanyone, any contractcalls inis Basechild contractpublicanyone + insideexternalanyone, from outside onlyinternalthis contract + childrenprivatethis contract onlystate variables default to internalnone of these words hide data from the world:the docs warn that private state is still visible to everyone on chain

Let's say a function should only run for your contract's own code. Say it in the declaration: public, external, internal or private.

Private does not mean secret. Private state is still visible to the whole world

public also builds a free getter for state variables, so explorers can read them

Visibility is who may call, never what anyone may see.

TRY IT THIS WEEK

Declare a private uint in Remix, deploy, then read it anyway on an explorer with Get Storage at, slot 0. Private is not secret.

A mapping turns one key into one value slot: balances of this address, scores of that player. It cannot list or count its keys; it only answers lookups.

Blockchain · No. 04
Data on chain

Mapping: keys to drawers

One drawer per key, nothing else

A MAPPING IS A CHEST OF DRAWERS: HAND IT A KEY, ONE DRAWER OPENS.0xA1B2...c9d0the keykeccak256mapping(address => uint) balances0x1111...0xA1B2...0x77ee...=> 250only this drawer opensunread keys readas zero, not errorWhat a mapping never does: list its keys, count its entries, or iterate. It only answers one key at a time.

Let's say every player needs a score and if-chains feel wrong. Declare mapping(address => uint) scores: hand it an address, it hands you that address's drawer.

A mapping doesn't know its own contents. It cannot list keys or count itself, so pay-everyone loops don't work.

A key never written doesn't error; it reads as zero. Treat zero as no entry, or mark entries explicitly.

A mapping answers. It never narrates.

TRY IT THIS WEEK

Write mapping(address => uint) public balances in Remix, set two addresses, read each back. Two drawers, no others.

Part 3 of 4
Saying no, safely
3

Logic and safety

Contracts hold value, so the guardrails matter. Five pages on require's all-or-nothing transaction, the cheaper custom error, modifiers as reusable gates, the Ownable pattern, and the reentrancy trap every audit looks for first.


In this part
  1. 01require: the gate that reverts
  2. 02The reentrancy trap

require is a guard clause at the top of a function. If the condition fails, the whole transaction reverts: every state change in the call is rolled back as if it never ran.

Blockchain · No. 05
Logic and safety

require: the gate that reverts

All or nothing, every call

FAIL THE CHECK AND THE WHOLE TRANSACTION UNWINDS. THERE IS NO HALF-DONE.a callcomes inrequire(msg.sender ==owner)trueruns onfalserevert: the whole transactionunwinds to the state before it beganevery state change in this call is rolled backgas spent so far is still paidAll or nothing. The caller's ETH comes back; the gas already burned does not.

Let's say only the owner may change a number and someone else tries. require(msg.sender == owner) is the gate: fail it and the transaction reverts.

No partial success. Every change in the call unwinds.

Your state comes back; the gas burned does not.

One failed check, one clean undo.

TRY IT THIS WEEK

Add require(msg.sender == owner) to a setNumber function, then call it from a second account and read the revert reason.

Send ETH before updating the balance and the receiving contract can call back in, see the stale balance, and withdraw again and again. Checks-Effects-Interactions fixes the order.

Blockchain · No. 06
Logic and safety

The reentrancy trap

Money out, ledger late

THE CLASSIC THEFT: MONEY LEAVES BEFORE THE LEDGER UPDATES.contract Vaultbalances[you] = 11. sends ETH2. sets balance 0wrong ordercontract Thiefreceive() {vault.withdraw()}calls back mid-sendETH out, control handed overwithdraw() again: balance still 1the vault drains before line 2 ever runschecks->effects->interactionsupdate the balance first, then send: the second withdraw reads 0 and reverts

Let's say the DAO taught the ecosystem this in 2016 and audits still open with it. Any transfer hands control to the receiver's code, which can call you back before your next line runs.

The buggy order: send the ETH, then write the balance. The callback reads the stale balance and drains the vault.

The docs' fix is Checks-Effects-Interactions: check inputs, write all state, then interact. Zero the share before the send.

Checks, effects, interactions. Every time.

TRY IT THIS WEEK

Take the docs' buggy withdraw example and move shares[msg.sender] = 0 above the call. That's CEI.

Part 4 of 4
ETH in, tokens out
4

Money and tokens

Contracts can hold ETH and issue tokens, and both have well-worn rails. Four pages on payable and receive, the three ways to send ETH and which to use, what the ERC-20 standard actually defines, and why you import OpenZeppelin instead of writing your own.


In this part
  1. 01payable and receive: the ETH door
  2. 02ERC-20: the standard in plain words

A plain transfer to a contract without a payable path reverts. Functions marked payable accept ETH, and receive() external payable is the door for transfers with no message at all.

Blockchain · No. 07
Money and tokens

payable and receive: the ETH door

Marked doors, or the money bounces

A CONTRACT ONLY ACCEPTS ETH THROUGH A FUNCTION MARKED payable.plain transfera contractwith no payablepathreverts: the ETH returnsreceive() external payablethe door opens, ETH lands insidehow much came in is waiting in msg.value · how much is held is address(this).balance

Let's say someone sends ETH and it comes straight back. The door needs a sign: functions marked payable, or receive() for plain transfers.

Without either, the contract cannot receive ETH and the transfer throws.

Inside, the amount sits in msg.value, the total in address(this).balance.

No payable, no entry.

TRY IT THIS WEEK

Add receive() external payable {} to a Remix contract, deploy, then send it 0.1 Sepolia ETH from the low-level Transact panel.

ERC-20 defines the token interface: supply, balances, transfers, and the approve/transferFrom allowance flow, with Transfer and Approval events. Every wallet speaks it.

Blockchain · No. 08
Money and tokens

ERC-20: the standard in plain words

A shared list of function names

SIX FUNCTION SIGNATURES AND TWO EVENTS. THAT IS THE WHOLE CONTRACT.the ERC-20 interfacetotalSupply()balanceOf(who)transfer(to, amt)approve(spender, amt)transferFrom(from,to,amt)event Transferevent Approvalplus three labels:name() symbol()decimals()what the ledgeractually is:mapping(address=> uint)approve lets aspender spendfrom your rowthat's the wholeallowancemechanismKnown signatures mean any wallet holds any ERC-20. That is what a standard buys.

Let's say every app inventing its own token commands meant nothing works together. ERC-20 is the agreed list from 2015: these functions, these events, these meanings.

Your contract never holds the tokens. The token contract keeps the whole ledger, including your row in its mapping.

The clever part is approve: you let a spender use part of your row, and transferFrom is how they spend it.

A token is a contract with a shared address book.

TRY IT THIS WEEK

On any explorer, open a token's Contract tab and read down the ABI: transfer, approve, allowance. You're reading the standard.

Index

Index


ERC-20: the standard in plain words14
Functions and visibility7
Mapping: keys to drawers8
payable and receive: the ETH door13
Remix: start with zero setup5
require: the gate that reverts10
State variables: the contract's memory6
The reentrancy trap11