Education › Blockchain › Stage 2: Ethereum and smart contracts

Solidity, the contract language

Contract structure, types, state variables and functions — writing and understanding your first contract.

Beginner→Intermediate ~34 min read Module 6 of 16

Solidity is the language most smart contracts are written in — the code that lives at a contract account and runs on the EVM. It looks a little like JavaScript, but it runs in a very unusual environment: every line costs gas, storage is permanent and public, and a deployed contract usually can't be changed. This lesson walks through the anatomy of a Solidity contract — its structure, its types, state variables, and functions — by building up a small, real contract you could actually deploy. By the end you can read most simple contracts and write one yourself.

After this module you can
  • Lay out a Solidity file: license, pragma, and a contract block
  • Use the core value types and understand state vs local variables
  • Write functions with visibility and state-mutability modifiers
  • Read and reason about a small, complete contract

The skeleton of a contract

Every Solidity file starts with two lines and a contract block. The first is an SPDX licence identifier, a machine-readable note of the code's licence (the compiler warns if it's missing). The second is the pragma, which pins the compiler version so your code isn't compiled by an incompatible future version — ^0.8.20 means 0.8.20 or any compatible 0.8.x. Then comes the contract keyword and a name; everything inside the braces — the state variables and functions — is what gets deployed to a contract account. This skeleton never changes; internalise it and every contract you read starts to look familiar.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
    // state variables and functions go here
    uint256 public count;

    function increment() public {
        count += 1;
    }
}

That is a complete, deployable contract. It stores a number count and offers a function to increase it. Notice public on the state variable — Solidity automatically creates a getter function for it, so anyone can read count for free. This tiny contract already demonstrates the two things a contract is made of: persistent state (the number, stored on-chain forever) and functions that change it.

Types you'll use constantly

Solidity is statically typed — every variable declares its type. A handful cover most needs. uint256 is an unsigned (non-negative) integer, the workhorse for amounts, counts and balances; you'll see uint as shorthand for it. bool is true/false. address holds a 20-byte account address and has members like .balance. string holds text and bytes holds raw bytes. For collections, mapping(address => uint256) is a key-value store — think of it as a dictionary from addresses to numbers, ideal for tracking balances — and arrays hold ordered lists. Choosing the right type matters because storage costs gas and types can't silently overflow (since Solidity 0.8, arithmetic that would overflow reverts automatically).

solidity
uint256 public totalSupply;              // a non-negative integer
bool public paused;                      // true / false
address public owner;                    // an account address
string public name;                      // text, e.g. a token name
mapping(address => uint256) public balanceOf;  // address -> amount

State variables vs local variables

This distinction is central to writing correct, gas-aware contracts. A state variable is declared at the contract level and lives in the contract's permanent, on-chain storage — it persists between transactions and is part of the world state, so writing to it costs significant gas. A local variable is declared inside a function and lives only for that call in cheap, temporary memory; it vanishes when the function returns. The rule of thumb: state variables are the contract's long-term memory (a balance, an owner), and local variables are scratch space for a single computation. Because storage writes are expensive, well-written contracts minimise how often they touch state.

Note

Storage is permanent, public, and expensive; memory is temporary, private to the call, and cheap. Every state variable you add and every time you write to one costs real gas forever. Reaching for a local variable when you only need a value during one call is both cheaper and clearer.

Functions, visibility and mutability

Functions carry two important labels. Visibility says who can call them: public (anyone, and other contract code), external (only from outside the contract), internal (only this contract and ones that inherit it), and private (only this contract). State mutability says what they do to state: a plain function can change state and costs gas; a view function only reads state and is free to call from outside; a pure function uses neither state nor even reads it. Marking a read-only function view signals intent and lets others query it without paying. Getting these labels right is part of writing safe contracts — a function that should be internal but is left public is a common vulnerability.

solidity
// changes state -> costs gas when called in a transaction
function setOwner(address newOwner) public {
    owner = newOwner;
}

// only reads state -> free to call from outside
function getOwner() public view returns (address) {
    return owner;
}

// uses no state at all -> pure
function add(uint256 a, uint256 b) public pure returns (uint256) {
    return a + b;
}

Reading a whole small contract

Put the pieces together and you can read a real contract. Below is a minimal 'piggy bank': anyone can deposit ether, and a mapping records each depositor's balance. payable marks a function that can receive ether, and msg.sender and msg.value are built-in values giving the caller's address and the ether they sent — you'll meet these fully in the next lesson. Trace it: a deposit adds msg.value to the caller's stored balance; a withdrawal reduces it and sends ether back. Everything here is state, functions, types, and visibility — exactly the pieces from this lesson.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract PiggyBank {
    mapping(address => uint256) public balanceOf;   // state

    function deposit() public payable {             // can receive ether
        balanceOf[msg.sender] += msg.value;         // credit the sender
    }

    function withdraw(uint256 amount) public {
        require(balanceOf[msg.sender] >= amount, "insufficient");
        balanceOf[msg.sender] -= amount;            // update state first
        payable(msg.sender).transfer(amount);       // then send ether
    }
}

If you can follow that contract, you have the foundation. The next lesson adds the patterns that make contracts safe and expressive — events, modifiers, require, and the meaning of msg.sender — turning these building blocks into contracts you'd actually deploy.

Hands-on practice

Read and modify a contract

  1. Write the two mandatory top lines of any Solidity file (the SPDX identifier and the pragma) from memory.
  2. In the Counter contract, add a decrement function and explain why count can't go below zero without a check (0.8 reverts on underflow).
  3. For three variables — a token name, an owner address, and a balances table — write the correct Solidity type for each.
  4. Take the getOwner function and explain why marking it view lets others read the owner without paying gas.
  5. Read the PiggyBank contract and trace, line by line, what happens when someone deposits 1 ether.
Cheat sheet

Solidity, the contract language — at a glance

Main things to focus on

  • Every file starts with an SPDX licence line and a pragma pinning the compiler version.
  • Core types: uint256, bool, address, string, and mapping(key => value).
  • State variables live in permanent, costly on-chain storage; locals live in cheap memory.
  • Visibility (public/external/internal/private) says who can call a function.
  • Mutability: plain functions change state and cost gas; view reads only; pure uses nothing.
  • Since Solidity 0.8, overflowing arithmetic reverts automatically.

File skeleton

// SPDX-License-Identifiermachine-readable licence line
pragma solidity ^0.8.20;pins a compatible compiler
contract Name { }the block that gets deployed
public state varauto-creates a free getter

Types

uint256non-negative integer; amounts and counts
booltrue / false
addressa 20-byte account address
mapping(k => v)key-value store, e.g. balances

Storage vs memory

state variablepermanent on-chain storage; costly
local variabletemporary memory; cheap
storage writesthe expensive operation to minimise
public statereadable by anyone, forever

Functions

public / externalcallable from outside
internal / privaterestricted to the contract
viewreads state; free from outside
puretouches no state at all

Common pitfalls

  • Omitting the SPDX line or pragma, drawing a compiler warning or a version mismatch.
  • Storing in a state variable what only needs to live for one call, wasting gas.
  • Leaving a function public when it should be internal — a common vulnerability.
  • Forgetting that public state variables are readable by everyone, so nothing on-chain is secret.
  • Assuming pre-0.8 overflow behaviour; modern Solidity reverts instead of wrapping around.
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 →