Anatomy of a Solidity file
Solidity is a statically-typed programming language designed for developing smart contracts that run on the Ethereum Virtual Machine (EVM).
File extensions: .sol
Every part of the example below is labelled and explained. This page is one of 55 annotated tours on AnatomyOf, a free, open-source project by LunarWerx Studios.
What is inside a Solidity file
SPDX license identifier
A comment declaring the source file's license.
The compiler emits a warning if a source file is missing a machine-readable license comment, so // SPDX-License-Identifier: MIT is effectively mandatory in modern Solidity. It has no runtime effect at all -- it is metadata for tooling, block explorers, and anyone auditing the bytecode later. Multiple identifiers can be combined with AND/OR (e.g. MIT OR Apache-2.0) when a file is dual-licensed. Leaving it out does not stop compilation, it just nags you every single time, forever.
Pragma directive
Specifies the compiler version compatibility.
pragma solidity ^0.8.0; tells the compiler which versions are allowed to compile this file. The caret pins a floor and allows patch/minor upgrades below the next breaking change, while an explicit range like >=0.8.0 <0.9.0 spells the same intent out longhand. Pinning matters because Solidity has changed semantics across major versions -- most famously, 0.8.x added default overflow/underflow checks on arithmetic that earlier versions silently wrapped. Deploying with an unexpected compiler version is a classic source of "it worked in Remix" surprises.
Comments
Single-line (//), block (/*...*/), or NatSpec (///) for documentation.
Ordinary // and /* */ comments are stripped before compilation like in any C-family language. NatSpec comments (/// or /** */) are special: tags like @title, @notice, @param, and @dev are parsed by the compiler and can be surfaced in generated documentation, Etherscan's "Read/Write Contract" UI, and wallet confirmation prompts. @notice is meant for end users ("what does this do"), while @dev is for other developers ("how does this work internally"). Because a deployed contract's logic can't be casually patched, comments explaining *why* a check exists are disproportionately valuable here compa
Contract definition
The fundamental building block for smart contracts.
A contract is Solidity's closest analogue to a class: it bundles state variables, functions, events, and modifiers, and it compiles down to EVM bytecode deployed at its own address. Every contract implicitly has an address, a balance, and persistent storage that survives between transactions. Contracts can inherit from other contracts (contract Token is ERC20, Ownable), and Solidity supports multiple inheritance with a C3-linearization-based resolution order. Unlike most classes, a contract's deployed code is immutable by default -- there is no git push to fix a bug after mainnet deployment, o
State variable
Data stored permanently on the blockchain.
State variables declared at contract scope live in persistent storage, meaning every write is a SSTORE opcode that costs real gas -- often the single largest cost in a transaction. Their visibility (public, private, internal) controls Solidity-level access, not blockchain-level secrecy: all storage is publicly readable off-chain regardless of the keyword. private only stops other *contracts* from reading the variable through Solidity; anyone can still inspect it directly via the storage slot with a node RPC call. This trips up newcomers who expect private to mean what it means in Java.
Event definition
Defines a structure for logging and external notifications.
event declares a log entry shape that emit writes into the transaction receipt rather than into storage, making events far cheaper than an equivalent state write. Off-chain applications (frontends, indexers like The Graph) subscribe to these logs instead of polling contract state. Parameters marked indexed (up to three per event) are stored in a searchable topic, letting clients filter logs by that value efficiently; non-indexed parameters are ABI-encoded into the log data and must be decoded client-side.
Constructor
An optional function executed once upon deployment.
constructor runs exactly once, at deployment, and is not part of the deployed runtime bytecode -- it only exists in the creation transaction. It is commonly used to set immutable configuration, assign an initial owner, or seed state variables from constructor arguments. Parameters marked immutable can only be assigned in the constructor (or at declaration) and are then baked directly into the bytecode rather than read from storage, saving a SLOAD on every later access.
Function modifier
Reusable code that can change the behavior of functions.
A modifier wraps a function body, typically to run a precondition check before letting execution continue. The special symbol _; marks where the wrapped function's body gets inlined; code before it runs first, code after it runs after the function returns. Modifiers can take parameters, be stacked (function f() onlyOwner whenNotPaused), and are the idiomatic place for access control and reentrancy guards -- OpenZeppelin's nonReentrant and onlyOwner are both just modifiers.
Error handling
Validates conditions and reverts state changes on failure.
require(condition, "message") reverts the entire transaction and refunds unused gas if the condition is false, undoing every state change made so far -- there is no partial success. revert and custom errors (error InsufficientBalance(uint256 available);) do the same but are more gas-efficient than string messages since 0.8.4, as the error data is ABI-encoded rather than stored as a string. assert is reserved for conditions that should be mathematically impossible (internal invariant violations); tripping one used to consume all remaining gas and still signals a bug in the contract rather than
Function definition
Executable units of code within the contract.
Functions declare visibility (public, external, internal, private) and optional state-mutability (view, pure, payable). external functions can only be called from outside the contract and are slightly cheaper to call than public because arguments are read directly from calldata instead of copied to memory. payable allows a function to receive Ether alongside the call; omitting it makes the compiler reject any transaction that tries to send value to that function, which is a deliberate safety default rather than an oversight.
Visibility & mutability
Controls access and state-change behavior (public, private, view, pure).
view promises the function reads state but never writes it, and pure promises it touches neither state nor blockchain context (msg.sender, block.timestamp, etc.) -- both are enforced by the compiler, not just documentation. Calling a view/pure function from off-chain (e.g. via eth_call) costs no gas since no transaction or state change is involved. If a view function is called from within a state-changing transaction, though, it still costs gas as part of that transaction's execution -- the free lunch only applies to standalone read calls. Mixing this up is a frequent source of confused gas es
Official Solidity site · All languages on AnatomyOf
The interactive tour needs JavaScript. Enable it to hover a callout and trace it into the code.