All articles
Web3 FoundationsSeptember 14, 20269 min read

What Is Actually in a .sol File: SPDX, pragma, contract

The first three lines of every Solidity file explained: what the licence comment does, what the pragma version actually pins, and where the contract really begins.

By Carlos (Bloqarl)

TL;DR

  • A .sol file opens with paperwork, not logic. The first two lines do nothing once the contract is live.
  • // SPDX-License-Identifier: MIT is a comment. It names the licence for humans and tooling, and the machine ignores it completely.
  • pragma solidity ^0.8.20; tells the compiler which versions of the language this text is written for. It is an instruction to the compiler, never to the blockchain.
  • The real machine starts at the word contract and ends at its closing brace. Everything inside is either memory or behaviour.
  • Delete the pragma line and the file will not compile. Delete it after deployment and nothing happens, because it was never deployed in the first place.

What is in a .sol file, from the top?

Every Solidity file starts with two lines of paperwork, a licence comment and a compiler version, and then opens a contract block that contains everything the deployed machine can remember and do.

Here is a complete one. Twenty four lines, nothing hidden, nothing abbreviated.

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

contract PiggyBank {
    address public owner;
    uint256 public goal;

    constructor(uint256 _goal) {
        owner = msg.sender;
        goal = _goal;
    }

    receive() external payable {}

    function progress() external view returns (uint256) {
        return address(this).balance;
    }

    function smash() external {
        require(msg.sender == owner, "only the owner");
        require(address(this).balance >= goal, "goal not reached");
        payable(owner).transfer(address(this).balance);
    }
}

Three regions: lines 1 and 2 are paperwork, line 4 opens the box, line 24 closes it. If you can reliably find those three landmarks in an unfamiliar file, you can read the file. The method for the rest is in how to read a smart contract.

What does the SPDX licence line do?

Nothing, mechanically. It is a comment.

// SPDX-License-Identifier: MIT

Two forward slashes start a comment in Solidity, and everything after them on that line is words for readers, never instructions. The compiler skips it. The chain never sees it.

What it is for is machine-readable licensing. SPDX is a standard list of licence identifiers, and putting one at the top of the file states the terms the source is published under in a form tooling can parse. MIT is the permissive default most of the ecosystem uses.

You will sometimes see the Solidity compiler warn when this line is missing. That is a warning about metadata hygiene, not about your code. A file without it compiles and deploys exactly the same.

For a reader, the useful takeaway is narrower than the licence itself: this is the first example in the file of a line that exists for humans. Contracts are full of them. Learning to recognise comments instantly, and not spend attention on them, is a real reading skill.

What does pragma solidity ^0.8.20 actually mean?

pragma solidity ^0.8.20;

This line names which versions of the Solidity language the file is written for. pragma is the keyword, solidity says which pragma, and ^0.8.20 is the version range.

The caret is the part worth understanding. ^0.8.20 means "0.8.20 or later, but not 0.9.0". It accepts patch and minor bumps within the 0.8 line and refuses the next breaking series. Without the caret, pragma solidity 0.8.20; would mean exactly that version and nothing else.

Two things this line is not:

  • It is not a runtime instruction. The pragma is consumed by the compiler when the source is turned into bytecode. The deployed contract contains no pragma. Nothing checks it ever again.
  • It does not pin the compiler. It states a range the source claims to be compatible with. Choosing which compiler actually runs is a build configuration decision made elsewhere.

Why should a reader care about a compiler directive at all? Because the version tells you which language rules are in force. The 0.8 series made arithmetic overflow revert by default, where earlier versions wrapped around silently. A contract whose pragma starts with 0.8 is therefore telling you something real about how its maths behaves. That is the kind of fact you can read straight off line 2, before you understand a single function.

Where does the contract actually begin?

contract PiggyBank {

Line 4 opens the box. contract is the keyword, PiggyBank is the name, and the brace starts a region that ends at the matching brace on line 24.

Function before name, which is how we teach every term: a program that lives at its own address on the chain and follows its own text, nothing more and nothing less. The name Solidity gives that is a contract.

Inside the braces there are exactly two kinds of contents:

  • Things it remembers. Values written into chain state that survive between calls. In PiggyBank, owner and goal. Covered in state variables in Solidity.
  • Things it can do. Functions, plus the special ones like constructor and receive.

That is the whole taxonomy. Every line inside a contract block sorts into one of those two piles, and a reader who sorts as they go never loses the thread.

Why does the brace matter more than it looks?

In a 24-line file, matching braces is trivial. In a real file it is the single most common way readers get lost, because real files contain more than one contract.

A production .sol file routinely holds an interface, an abstract base, a library, and the main contract, stacked in one document. Words that look like they belong to the contract you care about often belong to the one above it. Before you conclude "this contract has a function called X", check that X is actually inside the braces you think it is.

This is also where import shows up, on the lines between the pragma and the first contract:

import "@openzeppelin/contracts/access/Ownable.sol";

An import pulls in another file so this one can use what it defines. For a reader, an import is a signpost, not an obstacle: part of this contract's behaviour is written in another file, and that file reads by exactly the same method.

What about all the comment blocks in real files?

Open a production contract and the paperwork does not stop at line 1. Real files are full of comments, and a reader who cannot triage them quickly spends most of their attention on text the machine never runs.

There are three kinds, and the difference matters:

// a single line comment, everything after the slashes is ignored

/* a block comment,
   which can run over several lines */

/// @notice Withdraws the full balance to the owner
/// @dev Reverts unless the goal has been reached
function smash() external {

The third kind is NatSpec, Solidity's documentation convention. The triple slash and the @notice and @dev tags are still comments, still ignored by the machine, but they follow a format that tooling reads. @notice is written for the end user and wallets can surface it at signing time. @dev is written for other developers. @param and @return document inputs and outputs.

For a reader, NatSpec is the single most valuable text in a file that you get for free, with one large caveat: comments are claims, not guarantees. The code below a comment does whatever it does, regardless of what the comment says it does. A comment that has drifted out of date with the code beneath it is one of the most common things you will find in real contracts, and noticing the gap is a genuinely useful reading skill.

So read the NatSpec first to get the author's intent, then read the code to find out what actually happens, and treat any mismatch as a question rather than a typo.

Delete the line and ask what breaks

The fastest way to check whether you actually understand these three regions is to remove one piece at a time and predict the consequence.

  • Delete the SPDX comment and the compiler emits a warning. The contract compiles, deploys, and behaves identically.
  • Delete the pragma line and compilation fails outright, because the compiler will not guess which language version you meant.
  • Delete the contract PiggyBank { line and keep the rest and nothing is left to deploy. The functions are floating in a file with no machine to belong to.
  • Delete the closing brace on line 24 and the file does not parse, because the box never closes.

Notice the asymmetry. The two paperwork lines have zero and total impact respectively, and neither of them affects the running contract at all. That is the shape of the top of a Solidity file: a small amount of text that matters enormously at build time and not at all afterwards.

Related questions

Is the SPDX line required in Solidity? No. Omitting it produces a compiler warning, not an error, and the resulting contract is identical. It is a metadata convention for machine-readable licensing rather than a language requirement.

What does the caret in ^0.8.20 mean? It accepts that version or any later version within the same minor series, stopping before the next breaking one. So ^0.8.20 allows 0.8.20 through 0.8.x but excludes 0.9.0. Without the caret, only the exact version is accepted.

Does the pragma version affect the deployed contract? Only indirectly. The pragma is a compile-time constraint and does not exist in the deployed bytecode. It matters because the compiler version it selects determines the language rules the code was built under, such as whether arithmetic overflow reverts.

Can one .sol file contain more than one contract? Yes, and production files often do, mixing interfaces, libraries, abstract bases, and the main contract in a single document. Each gets its own contract, interface, or library block, so matching braces carefully is essential when reading.

What is the difference between import and inheritance? import makes another file's definitions available in this one. Inheritance, written with is, makes this contract actually take on another contract's state and functions. You can import a file without inheriting from anything in it.

Where does the contract's code live after deployment? The compiled bytecode is stored at the contract's address on chain. The .sol source is not deployed. Block explorers can display source only when someone uploads it and it is verified to match the deployed bytecode, which is what reading a contract on Etherscan relies on.

Where to go next

The top of the file is the easiest part to read and the easiest part to skip badly. Two lines of paperwork, then a named box with a brace at each end. Once those landmarks are automatic, you always know where you are in a file, which is most of what stops people reading real code.

Next, go inside the box and start with what the contract remembers: state variables in Solidity covers the lines directly under the opening brace, and why reading the memory before the functions answers more questions than anything else you can do.

Tagged

SoliditySmart ContractsLearn to Code