Section 01 · Definition
What is Solidity, exactly?
Solidity is the most widely used programming language in blockchain. It exists for one purpose: to write code that runs on the Ethereum Virtual Machine.
Quick answer
What is Solidity? Solidity is a statically typed, contract oriented programming language created in 2014 to compile to Ethereum Virtual Machine bytecode. You write programs called smart contracts in .sol files, the Solidity compiler turns them into EVM opcodes, and you deploy that bytecode to a public blockchain where it lives at a fixed address and runs forever.
The phrase "smart contract" trips a lot of beginners up. A smart contract is not a legal contract. It is a program. The program is deployed once, sits at an address on chain, and anyone in the world can call its functions by sending a transaction. The chain runs the code, updates the state, and records the outcome in a block. There is no central server, no deploy command after the first one, and no rollback if something goes wrong.
Solidity is the reason this is approachable. Before Solidity, writing for the EVM meant hand assembling opcodes. Solidity adds variables, functions, types, control flow, and inheritance, the same building blocks you find in any general purpose language. If you have written JavaScript, Python, or Java, the surface syntax of Solidity will feel familiar within an hour. The depth, the EVM specific quirks, the gas model, the security patterns, those take longer.
Solidity is open source. The compiler is called solc. It is maintained by the Ethereum Foundation and a community of contributors. New versions ship every few months. The current major line is 0.8, and pragmas at the top of every contract pin which compiler versions the file is allowed to compile against.
Solidity is just one piece of the stack
Writing Solidity is the first step. Around it sits a whole ecosystem of tools you will eventually touch. Hardhat and Foundry for compiling and testing, Remix for fast browser based experiments, OpenZeppelin for audited libraries, ethers.js or viem for client interaction, Slither and Mythril for static analysis. The language is small. The ecosystem is large.
Section 02 · Use Cases
Why Solidity exists and where it runs
Solidity solves one problem and reaches across many chains. Understanding both shapes how you decide what to build.
Solidity exists because the EVM by itself is too low level for everyday work. The EVM is a 256 bit stack machine that understands a few hundred opcodes. You can technically write contracts directly in EVM assembly using a language called Yul, and a small number of optimised libraries do exactly that. For everyone else, Solidity provides a sane abstraction. You declare a contract, write your business logic in something close to JavaScript, and let the compiler worry about which opcodes to emit and how to lay out storage slots.
Where does Solidity run? Anywhere the EVM runs. That list is long and growing. Ethereum mainnet is the original target. Layer 2 networks like Arbitrum, Optimism, Base, and zkSync are all EVM compatible. Sidechains and alternative L1s like Polygon, BNB Chain, Avalanche C Chain, Fantom, and Gnosis Chain run the same EVM. So do most testnets such as Sepolia and Holesky. A single Solidity contract, compiled once, can be deployed to any of them with no code changes. Only the gas costs and transaction speeds differ.
The use cases are equally broad. The vast majority of decentralised finance protocols (Uniswap, Aave, Compound, MakerDAO) are written in Solidity. Most NFT collections (the ERC721 and ERC1155 contracts) are Solidity. DAO governance modules, on chain games, tokenized real world assets, account abstraction wallets, prediction markets, identity systems, oracles, all Solidity. If something happens on an EVM chain and a regular user can interact with it, there is a Solidity contract behind it.
A simple way to picture it: you write .sol files, you run the compiler, you get bytecode and an ABI, you send that bytecode in a deployment transaction. The chain assigns the contract an address. From that moment on, your contract is part of the public state and anyone can interact with it.
Section 03 · Hello World
Your first Solidity contract
The fastest way to feel the language is to write a tiny one. This is the smallest contract that does something useful.
Open Remix in your browser at remix.ethereum.org. You do not need to install anything. Create a new file called Counter.sol and paste this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Counter {
uint256 public count;
function increment() public {
count += 1;
}
function reset() public {
count = 0;
}
}Five things happen here. The first line declares an SPDX license identifier so the compiler does not warn about missing licensing. The pragma tells the compiler to use any 0.8 series version. The contract Counter block is the contract itself. uint256 public count declares a state variable that automatically gets a getter. The two functions read and write that state.
Hit the green compile button in Remix. If there are no errors, switch to the Deploy tab, pick Remix VM (a sandbox EVM), and click Deploy. Your contract appears under Deployed Contracts. Click increment. Click count. You will see the number go up. That is a working smart contract. Storage is being read and written, transactions are being signed, gas is being measured. All of it runs locally in your browser.
You will spend the rest of your time in Solidity expanding on these primitives. State variables hold things, functions change them, and modifiers and events add layers of logic and observability around the changes. Every contract on Ethereum, no matter how complex, is built from the same shapes you just used.
Section 04 · Variables
Variables and data types
Solidity is statically typed. Every variable has a fixed type that the compiler enforces. Here are the types you will use the most.
When you declare a variable in Solidity, you state its type before its name. There is no type inference at the contract scope. A variable can live in three places: storage (persists on chain across transactions), memory (lives only during a function call), or calldata (read only inputs to external calls). Storage is expensive, memory is cheap, calldata is cheapest. Choosing where data lives is one of the first gas optimization habits to develop.
The numeric types are the workhorses. uint256 holds a non negative integer up to roughly 1.16 times 10 to the 77. There are also uint8, uint16, all the way up in steps of 8 bits. Smaller types do not save gas on their own, but they save storage when you can pack several into one 32 byte slot. The signed counterpart is int256 and friends. For booleans, see the bool type in Solidity.
The address type stores a 20 byte Ethereum address. It comes in two flavours: address and address payable. The payable variant can receive ether through .transfer(), .send(), or .call{value:...}(). The non payable form cannot, which is a safety feature. For text and binary, see the string type and the bytes type.
Beyond scalars, you compose state with reference types. Arrays come in fixed (uint256[10]) and dynamic (uint256[]) flavours. Structs let you bundle related fields into a custom type. Mappings are the workhorse of contract state. They look like hash maps but behave more like sparse arrays: every possible key already has a default value, you just write at the keys that matter. A typical balance tracker looks like this:
mapping(address => uint256) public balances;
struct Account {
uint256 balance;
bool isActive;
uint64 lastSeen;
}
mapping(address => Account) public accounts;The first line is enough to track every wallet's balance. The second pattern, mapping to a struct, is how almost every non trivial contract organises its state. You will see this shape in token contracts, vaults, lending pools, governance contracts, and almost everywhere money or identity is involved.
Section 05 · Functions
Functions, visibility, and mutability
Functions are how state changes. Every function has a visibility level and a mutability promise. Picking the right combination is the difference between safe code and exploitable code.
A function in Solidity has a name, optional parameters, an optional return type, a visibility modifier, and an optional state mutability keyword. Here is the shape:
function transfer(address to, uint256 amount)
public
returns (bool success)
{
require(balances[msg.sender] >= amount, "insufficient");
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}The visibility level determines who can call the function. There are four levels in Solidity. They are not interchangeable, and choosing wrong is one of the most common security mistakes in beginner code. The full deep-dive lives in functions and visibility in Solidity.
public
A public function is callable from inside the contract, from inherited contracts, and from the outside world. Public state variables automatically get a public getter function. Use this when you genuinely need both internal and external access. Default state variables to public only when an external getter is part of the interface.
external
An external function is only callable from outside the contract. Other contracts and externally owned accounts can call it; the contract itself cannot, except through the address.call pattern. External functions are slightly cheaper than public ones for large array arguments because the data stays in calldata.
internal
An internal function is callable from within the contract and from any contract that inherits from it. It is the natural choice for helper logic that you want to reuse across child contracts but never expose to the outside world. State variables default to internal if you do not specify a visibility.
private
A private function is only callable from inside the exact contract that declares it. Children cannot call it. Important caveat: private only restricts who can call the function, not who can read the data. All contract storage is publicly readable on chain regardless of the keyword. Private is for code organisation, not for hiding secrets.
Then there are the state mutability keywords, which tell the compiler what your function touches. view says the function reads state but never writes. pure goes further: it neither reads nor writes state. Both can be called for free as off chain reads. payable is the opposite end, marking a function that is allowed to receive ether along with the call. If you forget payable on a deposit function, every transaction that sends ether to it will revert.
Section 06 · Control Flow
Conditionals, loops, and modifiers
Branching and iteration in Solidity work the way they do in any C family language. The wrinkle is gas. Every iteration costs money, and unbounded loops are a known footgun.
Conditionals use the familiar if, else if, and else keywords. Solidity also has require, assert, and revert for guard conditions. require is the one you will use most: it checks a condition and reverts the transaction with an optional message if it fails. Failed requires refund all unused gas.
Loops behave like their C and JavaScript equivalents. The for loop is the workhorse. while and do while exist but are rare in production code because they are easier to get wrong. break and continue work as expected. There is one important rule: never write a loop where the iteration count depends on data that an attacker can grow without limit. A loop over a user controlled array can be made arbitrarily long, run out of gas, and brick the function permanently. The convention is to either cap the iteration count, or pull the loop off chain and have users settle results in batches.
Modifiers are a Solidity specific construct that wrap functions in reusable preconditions. They are how access control is expressed idiomatically:
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setFee(uint256 newFee) public onlyOwner {
fee = newFee;
}The underscore inside the modifier is where the body of the wrapped function gets injected. Anything before the underscore runs as a precondition, anything after runs as a postcondition. OpenZeppelin's Ownable and AccessControl contracts ship modifiers like this in production grade form. You almost never need to write your own from scratch.
Section 07 · Putting It Together
A complete example: a simple bank contract
Here is one self contained contract that exercises everything covered so far. State variables, events, mappings, modifiers, payable functions, and access control.
The contract below lets users deposit ether, track their own balance, withdraw their funds, and lets the contract owner pause new deposits in an emergency. It is small enough to read in one sitting and large enough to be representative of real Solidity code.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleBank {
address public owner;
bool public paused;
mapping(address => uint256) public balances;
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event PausedChanged(bool paused);
error NotOwner();
error Paused();
error InsufficientBalance();
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
modifier whenNotPaused() {
if (paused) revert Paused();
_;
}
constructor() {
owner = msg.sender;
}
function deposit() external payable whenNotPaused {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 amount) external {
if (balances[msg.sender] < amount) revert InsufficientBalance();
balances[msg.sender] -= amount;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "transfer failed");
emit Withdraw(msg.sender, amount);
}
function setPaused(bool value) external onlyOwner {
paused = value;
emit PausedChanged(value);
}
function totalDeposited() external view returns (uint256) {
return address(this).balance;
}
}Walk through it once. The contract declares two flags (the owner and a pause switch), one mapping for balances, three events, and three custom errors. The constructor runs once at deploy time and sets the owner to whoever sent the deployment transaction. The two modifiers wrap functions in their preconditions. The four functions cover the user actions and the admin action.
Notice the patterns. Every state mutating function emits an event so that off chain indexers can rebuild the story. The withdraw function updates state before sending ether (the checks effects interactions pattern that prevents reentrancy). The owner check uses a custom error rather than a string, which costs less gas. The deposit function is external payable because external is cheaper for the simple case and payable is required to receive ether. None of this is optional in production code.
Custom errors save real gas
Solidity 0.8.4 introduced custom errors as a cheaper alternative to revert strings. A revert with a string costs the gas to encode and store the string. A revert with a custom error costs only four bytes for the selector. On a high traffic contract, switching from string reverts to custom errors can save users tens of thousands of gas across a typical session.
Section 08 · Pitfalls
The mistakes every new Solidity developer makes
Solidity is small but unforgiving. Most catastrophic bugs in production come from a short list of known patterns. Knowing them by name halves your chance of shipping one.
Quick answer
What goes wrong most often? Reentrancy attacks during external calls, integer overflow from unchecked math, missing access control on admin functions, leaving private data on chain (it is publicly readable), unbounded loops over user controlled arrays, and trusting block.timestamp for randomness. Every one of these has cost real users real money.
Reentrancy
If your contract calls an external address before updating its own state, the called contract can call back into yours and exploit the stale state. The fix is the checks effects interactions pattern: validate inputs first, change state second, make external calls last. Or use OpenZeppelin's ReentrancyGuard modifier on any function that sends ether or tokens.
Trusting block.timestamp
Validators can nudge block timestamps by a few seconds. Using block.timestamp as a source of randomness or as a sub second timing oracle is unsafe. For randomness, use Chainlink VRF or commit reveal schemes. For timing, treat block.timestamp as accurate to about 15 seconds.
Privacy that is not private
Private and internal in Solidity are visibility keywords for code, not encryption. Every byte of contract storage is readable by anyone running an Ethereum node. If you store a password, an API key, or an unhashed secret, assume it is public. Sensitive data either lives off chain or is hashed before storage.
Unbounded loops
If a function loops over a dynamic array that anyone can grow, a determined attacker can fill the array until your function exceeds the block gas limit and reverts every time. Cap the loop, paginate the work, or move iteration off chain entirely.
The pragma ^0.8.0 already protects you from the historical integer overflow class of bugs because the compiler inserts checks by default. Older code on 0.7 and below required SafeMath. If you are reading legacy contracts, that is the first thing to look for.
The single best habit you can develop early is to read OpenZeppelin's contracts source code. Their ERC20, ERC721, Ownable, AccessControl, ReentrancyGuard, and Pausable contracts are short, beautifully written, and audited by the entire industry. Reading them is how you learn what production grade Solidity looks like.
Section 09 · Concept deep dives
Drill into any Solidity concept
Each of the building blocks above has its own focused guide with code, SVG diagrams, and the use cases beginners ask about most.
Data types
uint and uint256 · int and int256 · the address type · the bool type · the string type · the bytes type
Composite state
mapping · arrays · structs · packing fields together for gas savings
Behaviour
functions and visibility · modifiers for access control · events for off-chain consumers
Pillar guide
The whole picture is this page. Bookmark it and move outward to the deep-dives as needed.
Suggested reading order for a complete picture: uint → int → bool → address → string → bytes → mapping → arrays → structs → functions and visibility → modifiers → events.
Section 10 · FAQ
Frequently asked questions
Is Solidity hard to learn?
The surface syntax is easy if you have written any C family language. A motivated JavaScript or Python developer can write and deploy a working contract within a weekend. The hard part is not the syntax. The hard part is the EVM gas model, the security patterns, the immutability of deployed code, and the fact that mistakes cost real money. Most people are productive in a week and dangerous in three months. Becoming production safe takes six months to a year of focused work.
What is the difference between Solidity and JavaScript?
They share surface syntax but almost nothing else. Solidity is statically typed, compiled, and runs on the EVM. JavaScript is dynamically typed, interpreted, and runs in browsers and Node. Solidity has explicit memory locations (storage, memory, calldata), gas costs on every operation, deterministic execution required by consensus, and a deploy once forever model. JavaScript has none of these. The mental model is closer to embedded systems C than to Node.
Is Solidity still in demand in 2026?
Yes, by a wide margin. Solidity remains the language with the largest amount of deployed total value across all blockchains because Ethereum and the major EVM L2s collectively dominate decentralised finance, NFTs, and DAO infrastructure. New Solidity audits, integrations, and protocol launches happen weekly. Job listings for senior Solidity engineers consistently rank among the highest paying in software in 2026.
How long does it take to learn Solidity well enough to ship?
Plan for one to two weeks to write your first working contract on a testnet. Plan for two to three months to be comfortable writing typical token, vault, or governance contracts using OpenZeppelin patterns. Plan for six to twelve months to be trusted with production code that holds real funds. The last stage requires deeply internalising security patterns and doing many code reviews on real audits.
What tools should I install to start?
Start with Remix in the browser; you do not need to install anything. Once you outgrow it, install Foundry (the modern choice for testing and deployment) or Hardhat (more JavaScript ecosystem integration). Add MetaMask for transaction signing. Add OpenZeppelin Contracts as a dependency for your contracts. Add Slither for static security analysis. That short list covers the daily workflow of most production Solidity teams.
Can I use Solidity outside Ethereum?
Yes, on any EVM compatible chain. Polygon, Arbitrum, Optimism, Base, BNB Chain, Avalanche C Chain, Fantom, Gnosis Chain, zkSync, Linea, Scroll, and most other major networks all run the EVM. The same Solidity contract compiles once and deploys to any of them. Solana and the other non EVM chains use different languages such as Rust; for those you have to switch toolchains.