Education › Blockchain › Stage 3: Building dApps

Smart-contract security

Reentrancy, access control, integer bugs — the mindset of writing contracts that hold real money.

Intermediate ~35 min read Module 11 of 16

Smart contracts are the rare kind of software where a single bug can drain millions in seconds, with no undo. Contracts are public, immutable, and hold value directly, which means attackers can read your code, and every flaw is a standing invitation. This lesson covers the security mindset and the classic vulnerabilities every developer must know — reentrancy, broken access control, unchecked inputs — plus the defensive patterns and habits that prevent them. You don't need to become an auditor, but you must know these, because on-chain, security isn't a feature; it's the whole job.

After this module you can
  • Adopt the mindset that contract code is public, immutable, and adversarial
  • Explain the reentrancy attack and the checks-effects-interactions defence
  • Recognise access-control and input-validation failures
  • Apply defensive habits: audited libraries, testing, and audits before mainnet

The security mindset

Three facts make smart-contract security unlike ordinary software. First, your code is public — attackers read the exact source and bytecode looking for flaws. Second, a deployed contract is usually immutable — you can't quietly patch a bug once it's live. Third, contracts hold value directly, so a flaw isn't a crash, it's a theft. Together these mean you must assume every function will be called by a hostile party, in any order, with any inputs, possibly many times in one transaction. Security here is adversarial by default. The good news is that most real losses come from a small set of well-understood mistakes — learn them and you avoid the majority of danger.

Reentrancy: the classic attack

Reentrancy is the most famous smart-contract vulnerability, and it caused one of the largest early hacks. It happens when your contract sends ether to an external address *before* it updates its own bookkeeping. Sending ether can hand control to the recipient's code, and a malicious recipient can call back *into your function again* before your first call finished — re-entering it while your state still says they're owed money. Done repeatedly in one transaction, they withdraw far more than they had. The vulnerable shape is: check balance, send money, then reduce balance. The attacker strikes in the gap between sending and reducing.

solidity
// VULNERABLE: sends ether before updating state
function withdraw() public {
    uint256 amount = balanceOf[msg.sender];
    (bool ok, ) = msg.sender.call{value: amount}("");  // hands over control!
    require(ok, "send failed");
    balanceOf[msg.sender] = 0;   // too late: attacker re-entered above
}

The checks-effects-interactions defence

The fix is a simple, universal ordering rule: checks, then effects, then interactions. First do all your checks (require statements). Then apply all your effects — update your own state, including zeroing the balance. *Only last* do you interact with external addresses (send ether, call another contract). Because your state is already updated before you hand over control, a re-entering call sees the correct, updated state and gets nothing. This one ordering discipline prevents reentrancy. A reusable reentrancy guard modifier (OpenZeppelin's nonReentrant) adds belt-and-braces protection by blocking any nested call into the same function.

solidity
// SAFE: update state BEFORE sending (effects before interactions)
function withdraw() public {
    uint256 amount = balanceOf[msg.sender];
    require(amount > 0, "nothing to withdraw");   // checks
    balanceOf[msg.sender] = 0;                     // effects (state first!)
    (bool ok, ) = msg.sender.call{value: amount}("");  // interactions last
    require(ok, "send failed");
}
Tip

Memorise the order: checks → effects → interactions. Update your own bookkeeping before you ever call out to another address. This single habit closes the door on reentrancy, the vulnerability behind some of the biggest losses in the space's history.

Access control and input validation

Two more failure classes cause a huge share of real incidents. Broken access control means a sensitive function isn't properly restricted — a mint function, a withdraw-everything function, or an upgrade function left callable by anyone, or a missing onlyOwner. Because the code is public, attackers specifically hunt for powerful functions lacking a guard. Always ask, for every state-changing function: *who should be allowed to call this, and is that enforced?* Missing input validation is the other: not checking amounts, addresses (the zero address is a classic trap), or conditions, so the contract does something nonsensical or exploitable. Every external input is attacker-controlled and must be validated.

  • Restrict privileged functions (mint, withdraw, upgrade) with a modifier like onlyOwner.
  • Reject the zero address where an address must be real.
  • Validate amounts and ranges — don't trust that inputs are sensible.
  • Assume every external caller is hostile and every input is chosen to break you.

Other traps and defensive habits

A few more to keep on your radar. Integer overflow — arithmetic wrapping around — was a common bug, but Solidity 0.8+ reverts on it automatically, so keep your compiler current and be cautious with any unchecked blocks. Oracle and price manipulation: never trust a price you can cheaply move (like a single DEX's spot price) for anything valuable; use robust oracles (a DeFi topic). Relying on secrecy: nothing on-chain is hidden — private variables are still readable, so never store secrets in a contract. The defensive habits that tie it together are steady: prefer audited libraries (OpenZeppelin) over hand-rolled code, test adversarially including the failure and attack cases, keep functions small and permissioned, and — for anything holding real value — get a professional audit before mainnet.

Watch out

Nothing on a public blockchain is secret. Marking a variable private only stops other contracts from reading it in Solidity — anyone can still read its value directly from the chain's storage. Never store passwords, keys, or answers you need to keep hidden in a contract.

Hands-on practice

Find and fix the vulnerability

  1. Explain the three facts (public, immutable, holds value) that make contract security adversarial.
  2. Given the vulnerable withdraw function, explain step by step how a reentrancy attack drains it.
  3. Rewrite it using checks-effects-interactions and explain why the reordering defeats the attack.
  4. Review a contract with a mint function and add the access control it needs.
  5. List three defensive habits you'll apply to any contract that will hold real value.
Cheat sheet

Smart-contract security — at a glance

Main things to focus on

  • Contract code is public, immutable, and holds value — assume every caller is hostile.
  • Reentrancy: sending ether before updating state lets an attacker re-enter and over-withdraw.
  • Defence: checks → effects → interactions; update state before any external call.
  • Access control: guard privileged functions; attackers hunt for unguarded powerful ones.
  • Validate every input — amounts, ranges, and the zero address.
  • Prefer audited libraries, test the attack cases, and audit before mainnet.

Mindset

public codeattackers read your source
immutableusually can't patch after deploy
holds valuea bug is a theft, not a crash
assume hostileany caller, order, and input

Reentrancy

send before updatethe vulnerable ordering
attacker re-enterscalls back before state updates
checks-effects-interactionsthe ordering that fixes it
nonReentranta guard modifier for extra safety

Access & inputs

onlyOwnerrestrict privileged functions
address(0) checkreject the zero address
validate amountsdon't trust inputs are sane
hunt for gapsunguarded powerful functions

Habits

audited librariesOpenZeppelin over hand-rolled
test attackscover failure and exploit cases
no on-chain secretsprivate isn't hidden
audit before mainnetfor anything holding value

Common pitfalls

  • Sending ether before updating state, opening a reentrancy hole.
  • Leaving a powerful function (mint, withdraw, upgrade) without an access-control guard.
  • Trusting external inputs without validating amounts, ranges, and the zero address.
  • Storing secrets in a contract, forgetting that private variables are still readable on-chain.
  • Shipping to mainnet without adversarial tests or an audit for value-holding code.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →