From bytecode back up to source
Picture a vending machine. You feed it coins, press a button, and it dispenses a drink — no clerk, no trust, no negotiation. The rules are fixed in the metal, identical for everyone, running exactly the same way at 3am as at noon. A smart contract is that vending machine made of code: a program that lives at a fixed address on a blockchain, whose logic is public and runs precisely as written, with no one able to reach in and change it after deployment.
In the previous rung you watched the EVM grind through opcodes one at a time and saw how Solidity compiles down to raw bytecode. Now we work in the other direction: you write Solidity, the high-level language, and the compiler turns it into the bytecode the machine actually runs. This guide builds the smallest contract that is still real — a counter — and uses it to name the four parts every contract has.
Anatomy of your first contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
// state variable: lives permanently in contract storage
uint256 public count;
// constructor: runs exactly once, at deployment time
constructor(uint256 initialCount) {
count = initialCount;
}
// changes state -> must be sent as a transaction -> costs gas
function increment() public {
count += 1;
}
function decrement() public {
require(count > 0, "Counter: cannot go below zero");
count -= 1;
}
// reads state only -> free to call from outside the chain
function get() public view returns (uint256) {
return count;
}
// touches no state at all -> pure
function double(uint256 x) public pure returns (uint256) {
return x * 2;
}
}- The SPDX license line and the `pragma solidity ^0.8.24;` version directive sit at the top. The pragma tells the compiler which language version you wrote for; ^0.8 matters because since 0.8.0 arithmetic overflow reverts automatically instead of silently wrapping.
- `contract Counter { ... }` declares the contract — a bundle of state and code that will be deployed to a single address. Think of it like a class whose one instance lives on-chain.
- `uint256 public count;` is a state variable: a piece of data the contract remembers between calls.
- The `constructor` runs once, during the deployment transaction, to set the starting value — then it is gone and can never run again.
- The four `function`s are the contract's public surface: the messages the outside world is allowed to send it.
State variables: the contract's permanent memory
`count` is state. When `increment()` adds one, that new value is written into the contract's storage and stays there forever — across calls, across blocks, across years — as part of the blockchain's global world state. Every full node keeps a copy. This is what separates a smart contract from an ordinary program: its variables outlive any single execution.
Under the hood each state variable is laid out into 32-byte storage slots. `count` takes slot 0. Reading a slot is cheap, but writing one is among the most expensive things the EVM does — a cold storage write can cost ~20,000 gas, dwarfing arithmetic which costs single-digit gas. A useful mental rule: every byte you persist on-chain has a price, paid by whoever sends the transaction.
Reads are free, writes cost gas: view and pure
Here is the single most important distinction in this guide. A function that changes state — like `increment()` — can only take effect by being broadcast as a transaction, mined or attested into a block, and re-executed by every node. That work is metered and you pay gas for it. A function marked `view` promises only to read state, and one marked `pure` promises to touch no state at all (it just computes from its arguments).
Why does that matter for cost? Because a `view`/`pure` function changes nothing, a node can run it locally and just return the answer — no transaction, no block, no fee. When your app calls `get()` it sends a read request (an `eth_call`) and the node computes the result for free. But this freeness is only for off-chain reads: if a transaction calls a `view` function internally, that execution still runs on the EVM and still costs gas like any other opcode.
# READ — runs locally on a node, no transaction, no gas, costs you nothing $ cast call $ADDR "get()(uint256)" 7 # WRITE — broadcasts a transaction, included in a block, you pay gas $ cast send $ADDR "increment()" --private-key $PK blockHash 0x9f3c... status 1 (success) gasUsed 43124 # now the read reflects the new state $ cast call $ADDR "get()(uint256)" 8
Visibility: who is allowed to call this?
Every function carries a visibility keyword that decides who may call it. Solidity has four, and getting them right is your first line of defense — a function that should have been locked down but was left callable by anyone is one of the most common (and most expensive) bugs in the whole field.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Visibility {
uint256 private secret; // storage, only THIS contract's code names it
// public: callable from outside AND from inside this contract
function publicValue() public view returns (uint256) {
return _bump(); // can call internal helpers
}
// external: callable ONLY from outside (another account or contract)
function setSecret(uint256 v) external {
secret = v;
}
// internal: this contract and any contract that inherits it
function _bump() internal view returns (uint256) {
return secret + 1;
}
// private: only this exact contract, not even children
function _scratch() private pure returns (uint256) {
return 42;
}
}- `public` — the open door. Callable from outside the chain and from other functions inside the contract. State variables can be public too (that auto-getter from earlier).
- `external` — callable only from outside. You cannot call an external function from another function in the same contract without prefixing `this.`. It is the natural choice for entry points meant for users and other contracts.
- `internal` — usable by this contract and any contract that inherits from it; invisible to the outside world. Perfect for shared helper logic.
- `private` — usable by only this exact contract, not even by children that inherit it. The tightest scope.
The deployment lifecycle, and what comes next
- Write the Solidity source, then compile it. The compiler produces two artifacts: the EVM bytecode (what runs) and the ABI (the calling convention apps use to talk to it).
- Send a deployment transaction carrying that bytecode. The EVM runs your constructor exactly once, sets the initial state, and stores the runtime bytecode at a freshly minted contract address.
- From then on, users and other contracts interact by sending transactions (for state changes, paying gas) or eth_calls (for free reads) to that address.
You now have the skeleton every contract shares: state variables held in storage, a constructor that runs once, functions gated by visibility, and the `view`/`pure` markers that separate free reads from paid writes. From here the contract grows real muscles. The next guide adds richer data — types, modifiers come shortly after as guard rails, plus mappings and structs for modeling balances and records — and within a few guides you will assemble all of it into a working ERC-20 token.