Track 4 · Solidity foundations · lesson 15

Milestone: the piggy bank

25 min


No new concepts. Everything below is something you have already done — this time all at once, with no worked example in front of you.

The brief

A contract that holds ETH for people:

Twelve tests. The state variables, events and errors are already declared — you are writing two function bodies.

PiggyBank.solComplete
Loading editor…
Build it so all twelve tests pass.
Run the code to see what happens.
Nudge
Two functions, two bodies. `deposit` is three lines shorter than `withdraw`.
Show me the approach
deposit: credit `msg.value` to `balances[msg.sender]`, then emit. withdraw: check the caller, check the balance, subtract, emit, then send.
Show me the code
In `withdraw`, revert with `NotOwner()` if the caller is not `owner`, revert with `InsufficientBalance()` if `balances[msg.sender] < amount`, then `balances[msg.sender] -= amount;`, emit, and finally `(bool ok, ) = payable(msg.sender).call{value: amount}(""); require(ok);`.
Explain the solution
The ordering in `withdraw` is the whole lesson: checks, then the state change, then the external call last. That is checks-effects-interactions. Send the ETH before subtracting the balance and the contract can be drained — which is precisely the vault you will empty in track 7.

One thing worth getting right

The order of operations inside withdraw matters more than anything else on this page.

Checks, effects, interactions.

  1. Check the caller is allowed and the balance is sufficient.
  2. Update the balance.
  3. Send the ETH.

Sending before subtracting leaves a window where the recipient can call withdraw again while their balance still reads full. That is reentrancy, and it took $60 million out of The DAO.

Track 7 opens with a vault that gets this backwards, and asks you to empty it.

Check

Why send the ETH last rather than first?

Choose one answer
What you have actually learnedOptional

This contract is a small version of something real. Strip the owner check and you have the deposit half of a lending protocol. Add interest and you have a vault. Swap ETH for a token and you have a staking contract.

The patterns are the same at every scale:

  • a mapping of address to amount
  • events so the outside world can follow along
  • custom errors for cheap, specific failures
  • checks-effects-interactions around anything that leaves the contract

Track 5 takes the same shape and turns it into a token that anyone can hold.

Worth remembering

  • State, events and errors are declared at contract level; logic goes in the functions.
  • Guard first, mutate second, call out last — checks-effects-interactions.
  • `payable(addr).call{value: x}("")` is the way to send ETH, and its result must be checked.
  • Custom errors give callers a specific reason for far less gas than a string.
  • This contract's shape underlies vaults, staking and lending protocols.