Skip to content

09. Web2 to Web3: Solidity & Hardhat Testing

This is the longest lesson in the module and the real starting line for writing contracts. It covers Solidity — the language the EVM’s bytecode is compiled from — and, just as importantly, how to test what you write. Because a deployed contract is immutable and can hold real money, testing is not an afterthought here; it is the discipline that makes deploying safe. Watch actively with an editor open and pause to try things.

  • Contract structure — a Solidity file starts with a license identifier and a pragma solidity version, then declares a contract that holds state and functions. The contract is the unit that gets deployed to an address.
  • State variables — data stored permanently in the contract’s onchain storage. Writing to storage costs gas; reading it in a transaction does too. Types you will use constantly: uint256, address, bool, string, and mapping.
  • mapping — a key→value store (e.g. mapping(address => uint256) for balances). It is the workhorse data structure of Solidity; there is no iteration over it, so you track keys yourself if you need to.
  • Functions and visibilitypublic, external, internal, private control who can call a function; view/pure mark functions that don’t change state (and are free to call off-chain). A function that changes state requires a transaction and gas.
  • event — a log the contract emits (e.g. Transfer). Events are how off-chain apps and indexers learn what happened without reading storage directly; you will rely on them in Module 3 and 4.
  • modifier — reusable pre-conditions attached to functions (e.g. onlyOwner) that require something before the body runs. This is the standard pattern for access control.
  • require / revert — validate inputs and permissions; a failed require reverts the whole transaction (rolling back state, per Module 1’s gas lesson).
  • Testing with Hardhat — you write tests in JavaScript/TypeScript that deploy the contract to a local network, call its functions, and assert the results. A good test names the behavior, exercises both success and failure paths, and runs in seconds — a fast loop that lets you refactor without fear.
  • Forgetting that every storage write costs gas. Beginners store things onchain that could be derived or kept off-chain.
  • Getting visibility wrong — a function meant to be internal left public is a classic vulnerability. Default to the least access that works.
  • Skipping the failure-path tests. The bugs that matter are usually “this should have reverted but didn’t,” which a happy-path-only suite never catches.
  • Treating tests as busywork. On an immutable platform, the test suite is your safety net.
  • What is the difference between a view function and one that writes state, in terms of gas and how it’s called?
  • Why are events important if the data is already in storage?
  • Name one thing a good test suite checks that a “does it work once” manual click does not.

The notes above are enough to follow the lesson, but if you want a minimal, runnable contract for each Solidity building block, Solidity by Example (maintained by Cyfrin) is the best companion reference — every page is one small contract you can read in a minute.

Language building blocks

Testing in Solidity

Solidity by Example also has a set of Foundry test examples — basic tests, expected reverts, emitted events, vm.store, and time control. Even while you write your Hardhat tests in JavaScript, skimming these shows the same testing ideas (arrange, act, assert; expect a revert; check an event) expressed in Solidity — which you’ll meet again with Foundry in Module 3.

Source: Solidity by Example, an open-source reference maintained by Cyfrin. Code belongs to its authors.

Module 2: Solidity & Token Standards.