Solidity
Solidity is the language most smart contracts are written in: programs on Ethereum-compatible chains that hold money, run on every node and cannot be patched. Every function is open to anyone, including other contracts that can call back in the middle of a function; every step costs the caller gas; and a bug is fixable only if an upgrade path was built in advance. The page opens with twelve assumptions a backend developer brings and what is true on the EVM instead, then covers the machine and the language up to Solidity 0.8.37, external calls and reentrancy, upgrades and admin keys, money and prices, testing and audits, five documented losses, and where it degrades — including when a contract is the wrong tool.
- Current version
- Solidity 0.8.37 · 10 Sep 2026
- Runs on
- The EVM — every node runs every call
- After deployment
- Code cannot change; upgrades only by design
Twelve things a backend developer assumes, and what is true inside a contract
A smart contract looks like a backend class: state in fields, methods that change it, require where a controller would validate input. Almost every habit a backend developer brings is wrong somewhere in it, because the machine underneath is not a server. It is the Ethereum Virtual Machine — the EVM — run by every node on the network. It executes transactions one after another, charges for every instruction, keeps every byte public, and never lets the code change. Here are twelve assumptions that hold on a server, and what holds instead. Later sections cite the rows by number.
| # | On a server you assume | Inside a contract | What to do instead |
|---|---|---|---|
| 1 | Only your users can call your API | Anyone can call every public and external function — including another contract | An access check on every function that changes state; assume the caller is hostile |
| 2 | A bug is fixed by deploying a patch | Deployed code never changes | Decide before launch: immutable, or an upgrade path behind a timelock |
| 3 | A secret can live in config or in the database | All storage is readable by anyone; private only hides it from other contracts | Keep secrets off-chain; commit–reveal for hidden choices |
| 4 | Calling another service just returns an answer | An external call hands control to code you do not own, which can call you back | Checks, effects, interactions — in that order — and a reentrancy guard |
| 5 | Requests run concurrently and in arrival order | Transactions run one at a time, in an order the block builder chooses | Assume someone sees your transaction first: slippage limits, deadlines |
| 6 | A failed request leaves partial state to clean up | A failed transaction reverts every change — and the caller still pays for the gas used | Validate early and revert; rely on atomicity, not on clean-up code |
| 7 | A loop costs CPU time | Every instruction costs gas, paid by the caller, within a per-block limit | No loops over lists users can grow; let each user withdraw instead |
| 8 | Money can be a decimal or a float | No fractions: 256-bit integers, and division rounds down | Multiply before dividing; round in the protocol’s favour |
| 9 | The server knows the time and can make random numbers | block.timestamp is the block’s time, not the call’s, and nothing on-chain is unpredictable to the block’s proposer | A verifiable randomness oracle (VRF), or commit–reveal |
| 10 | You can fetch a price from an API | A contract sees only the chain; a price is whatever some contract reports | A manipulation-resistant oracle, with staleness and bounds checks |
| 11 | An admin can fix data by hand | An admin power is a function that whoever holds the key can call | A multisig, a timelock, and published limits on what admins can do |
| 12 | Code size and stack depth are someone else’s problem | A contract is capped at 24 KB of code, and the EVM reaches only 16 stack slots | Split into libraries and modules; keep functions small |
The machine and the language: accounts, gas, and Solidity 0.8
Ethereum has two kinds of account. An externally owned account — an EOA — is controlled by a private key: a person, a wallet, a server holding a key. A contract account is controlled by its code. Only an EOA starts a transaction. The transaction may call a contract, which may call other contracts, and the whole chain of calls either succeeds together or reverts together. Every node runs the same transaction and must reach the same result, which is why a contract cannot read a clock, a file or the Internet: anything that could differ between two nodes is simply not available.
Gas is how the network prices that work. Every EVM instruction has a fixed gas cost; the sender sets a maximum and pays for what is used, at a price per unit of gas set by the market — a base fee, which is burned, plus a tip to the block’s proposer, the model in place since August 2021. If a transaction runs out of gas it reverts, and the gas is still spent. Since December 2025 a block holds 60 million gas by default and a single transaction at most about 16.8 million. In practice the numbers say one thing: storage is expensive and computation is cheap.
| Operation | Gas | What it means |
|---|---|---|
| Any transaction, before it does anything | 21,000 | The fixed price of a plain transfer |
| Add two numbers | 3 | Computation is almost free |
| Read a storage slot, first time in the transaction | 2,100 | About 700 additions |
| Read the same slot again | 100 | Still worth caching in a local variable |
| Write a storage slot that was zero | 20,000+ | Creating state is the most expensive thing you do |
| Change a non-zero slot | 2,900+ | Updating is far cheaper than creating |
| Call another contract, first time | 2,600+ | Plus whatever the callee spends |
| Calldata, per non-zero byte | 16 | Inputs are paid for byte by byte |
These are today’s prices, and they are not permanent. The next network upgrade, Glamsterdam — on the Sepolia testnet from 6 October 2026, with no mainnet date yet — reprices state creation and state access, and its announcement asks developers to re-test gas estimates and anything that hardcodes a gas amount. That is the general lesson: a contract that depends on an exact gas cost depends on something the protocol has changed several times and will change again. It is also why Solidity 0.8.31 deprecated transfer and send, which forward a fixed 2,300 gas.
The language: what 0.8 changed, and what to write today
Solidity looks like JavaScript or Java with types, and the resemblance is skin-deep. A contract is a class whose fields are stored permanently on the chain — its state variables — and whose functions are its APIApplication Programming Interface — contract between programsThe rules one program follows to call another: which requests it may send, in what shape, and what comes back. To the caller it is a contract — everything behind it stays a black box, which is exactly what lets the other side be rewritten without breaking you.Full article →All terms. Version 0.8.0, in December 2020, made the most important change in the language’s history: arithmetic is checked by default, so an overflow reverts the transaction instead of wrapping around. Before it, every contract needed the SafeMath library, and a missing one was a recurring way to mint tokens out of nothing. Code written for 0.7 and earlier is common in old repositories and audits, and its arithmetic has to be read with that in mind.
| Where data lives | For how long | Cost | Use it for |
|---|---|---|---|
storage | Forever, on every node | Thousands of gas per slot | State variables — balances, owners, settings |
transient | One transaction (since Cancun, March 2024) | About 100 gas | Reentrancy locks; values passed between calls in one transaction |
memory | One function call | A few gas per word, rising with size | Working copies of arrays and structs |
calldata | One external call, read-only | Cheapest — already paid for | Parameters of external functions |
| Events (logs) | Forever, but contracts cannot read them | Cheap compared with storage | What off-chain indexers and interfaces need |
Four visibility keywords decide who can call a function: external and public — anyone, as row 1 says; internal — this contract and contracts that inherit from it; private — this contract only. None of them hides data: they control calls, not reads. Two globals say who is calling. msg.sender is the immediate caller — the EOA or the contract one hop away — and it is the one to check. tx.origin is the EOA that started the whole transaction, and checking it is a classic mistake: a malicious contract that the owner is tricked into calling passes a tx.origin == owner check. Since May 2025 an EOA can also carry code of its own (EIP-7702), so the old require(msg.sender == tx.origin) test for "called by a person, not a contract" no longer means what it used to.
What a contract written in 2026 looks like: a fixed compiler version — pragma solidity 0.8.37; rather than ^0.8.0 — so the audited bytecodeinstructions for a virtual machineCompact instructions for a virtual machine rather than for a real processor — what the JVM, CPython, PHP’s OPcache, React Native’s Hermes and the Ethereum Virtual Machine run. A compiler produces it from source code ahead of time, so the runtime skips parsing; the virtual machine then interprets it or compiles the busy parts further into machine code.Full article →All terms is the deployed bytecode; custom errors (error Unauthorized(); with revert Unauthorized();) rather than strings, which are cheaper and decodable; OpenZeppelin Contracts for tokens, access control and guards instead of hand-written versions; an event for every state change an off-chain system needs; and no transfer, send or selfdestruct. The first two are deprecated, and selfdestruct has not deleted a contract since March 2024, except within the transaction that created it.
External calls and reentrancy
Row 4 is the bug that defined the field. When a contract sends ether or calls another contract, execution passes to the other side, and the other side can run any code it likes — including a call straight back into the contract that has not finished yet. If the first contract sent the money before updating its records, the second call finds the old balance and is paid again. And again, until the gas or the money runs out. This is reentrancy, and the order of three lines decides it.
The rule is called checks–effects–interactions. First check every condition. Then apply every effect to your own state — set the balance to zero. Only then interact with the outside world: send ether, call a contract, transfer tokens. In that order a reentrant call finds the state already updated and gets nothing. On top of the order, add a guard: OpenZeppelin’s ReentrancyGuard locks every function marked nonReentrant, and since the Cancun upgrade the lock can live in transient storage for a fraction of the old cost. And prefer letting users withdraw what they are owed — pull — over sending to a list of recipients in a loop — push — which also removes row 7’s unbounded loop.
Reentrancy has outgrown its textbook form. A cross-function attack re-enters through a different function that shares the same state. A read-only one calls a view function of a contract in the middle of an update, reads a price or balance that is momentarily wrong, and uses it in a third contract that trusted it. A lock on one function protects against none of these, which is why the order of effects and interactions matters more than any single guard.
Upgrades, proxies and admin keys
Row 2 has one escape hatch, and it is a pattern rather than a feature. A proxy is a small contract that holds all the state and forwards every call, with delegatecall, to an implementation contract that holds the code. delegatecall runs the other contract’s code against the caller’s own storage. Upgrading means pointing the proxy at a new implementation; the address users know, and the data, stay the same. OpenZeppelin ships the common variants — transparent, UUPS and beacon proxies — and EIP-1967 fixes the storage slot where the implementation’s address lives, so it cannot collide with ordinary variables.
The pattern trades one risk for three. The first is storage layout. The implementation’s variables are laid out by position, slot by slot, in the proxy’s storage, so a new version that inserts a variable above an old one makes every later variable read the wrong slot. Append only, and let a tool compare the layouts — OpenZeppelin’s upgrades plugin refuses an incompatible one. The second is initialisation. A proxied contract cannot use a constructor, because the constructor runs on the implementation, not on the proxy; it uses an initialize function instead, which must be callable exactly once, and the implementation itself must be locked with _disableInitializers() so that nobody else can initialise it. The third is the admin: whoever can upgrade can replace the code with anything, which makes the upgrade key the most valuable thing the project owns.
Money, prices and flash loans
There are no fractions on the EVM. Amounts are integers in a token’s smallest unit — ether has 18 decimals, so one ether is 10^18 wei, while USDC has 6 — and every division rounds down. Two rules follow from row 8. Multiply before you divide: a * b / c keeps precision that a / c * b throws away. And decide the direction of every rounding on purpose, in the protocol’s favour — round down what users receive, round up what they owe — because a rounding error repeated a thousand times inside one transaction is a withdrawal. Mixing tokens with different decimals without scaling them is the other recurring bug: one unit of USDC is a millionth of a dollar, one unit of DAI a quintillionth.
ERC-20, the token standard, is less uniform than its interface suggests. USDT’s transfer returns nothing instead of true, so a contract that checks the return value reverts on it; some tokens take a fee on every transfer, so the amount that arrives is less than the amount sent; some can pause or blacklist addresses. Use OpenZeppelin’s SafeERC20 wrappers, measure the balance before and after a transfer when it matters, and keep a list of the tokens you actually support.
Row 10 is where most protocol logic meets the outside world. A lending protocol needs to know what collateral is worth, and the tempting answer is the price in a decentralised exchange pool, readable in one call. A flash loan makes that answer worthless. A flash loan lends any amount with no collateral, on one condition: it is repaid in the same transaction, or the whole transaction reverts, exactly as row 6 promises. So for the length of one transaction anyone has tens of millions to move a pool’s price, act on the distorted price, move it back and repay.
In October 2020 Harvest Finance lost about $24 million this way: its vaults priced deposits and withdrawals from a Curve pool whose price a flash-loaned trade had just moved. The defences are a price that cannot be moved in one transaction — a time-weighted average over many blocks, or an oracle network such as Chainlink that aggregates many sources — plus checks that the answer is recent and within sane bounds, and limits on how far one transaction can move the protocol.
Testing, audits and shipping
A contract’s test suite has to do the job a production incident would otherwise do, because there is no hotfix. Unit tests are the floor. The tools that pay for themselves are fuzzing — random inputs, thousands per test — and invariant testing, which runs random sequences of calls and checks after each one that a property still holds: "the sum of balances equals the total supply", "no account can withdraw more than it deposited". Foundry runs both from Solidity test files; Hardhat does the same from TypeScriptJavaScript with type checkingJavaScript plus a description of what each function takes and returns, checked in the editor before anything runs. It never reaches the browser — it is compiled back into plain JavaScript first, so the types are a development-time tool and nothing more.Full article →All terms. A fork test runs against a copy of mainnet state, with the real tokens and oracles the contract will meet.
| Layer | Tools | What it catches |
|---|---|---|
| Static analysis | Slither; the compiler’s own warnings | Known bug patterns — reentrancy, unchecked calls, shadowed variables |
| Fuzzing and invariants | Foundry, Echidna, Medusa | Inputs and call sequences nobody thought to write down |
| Formal verification | Certora, Halmos, the compiler’s SMTChecker | Proof that a property holds for every input — for the properties you wrote |
| Audit | An external firm, or a public contest | Design and logic errors a team cannot see in its own code |
| Bug bounty | Immunefi and similar platforms | Whatever is left, found by people paid to look after launch |
| Monitoring and pause | On-chain alerts; a guardian role that can pause | An attack in progress — where minutes matter |
Deployment has its own checklist. Verify the source on a block explorer, so anyone can read what they are trusting. Move every admin role from the deploying key to the multisig and the timelock, and renounce the roles nobody needs. Test the upgrade path and the pause on a testnet with the same configuration. And publish what admins can and cannot do: users price that risk, and an honest answer protects the team as much as it protects them.
Five losses, and what each one was missing
Each case is documented — by the team’s own post-mortema written review after a failureA written review held after something went wrong, covering what happened, why, and what changes so it does not happen the same way. The word "blameless" in front of it is not politeness: an engineer who expects to be blamed reports less, and a review that produces a lesson rather than a change to a system has produced nothing.All terms, an independent analysis or the authorities. They run from the first great failure to the largest, and the largest was the only one with no bug in contract code at all. The DAO and Parity are told above, where their mechanism lives.
| Case | What happened | What was missing |
|---|---|---|
| The DAO, Jun 2016 | Reentrancy drained about 3.6 million ether; Ethereum hard-forked to reverse it | Effects before interactions |
| Parity, Jul and Nov 2017 | Anyone could call the initialiser: 153,000 ether stolen, then 513,774 frozen | An initialiser callable once — and no kill |
| Nomad, Aug 2022 | An upgrade made the zero hash a trusted root; $190 million taken by copycats | One negative test of the upgraded contract |
| Euler, Mar 2023 | A new function skipped the solvency check; about $197 million, later returned | An invariant: no action leaves an account undercollateralised |
| Bybit, Feb 2025 | A compromised signing interface made signers approve a new implementation; about $1.5 billion | Signers checking the transaction itself, independently |
The third case is a single wrong value at initialisation. Nomad was a bridge: it released tokens on one chain when a message proved they had been locked on another. A message counted as proven if its Merkle root was in a mapping of trusted roots. During a routine upgrade the contract was initialised with the zero hash as a trusted root. In a Solidity mapping every missing key reads as zero, so any message that had never been proven looked up the zero root — and passed. On 1 August 2022 the first attacker withdrew funds with a forged message; within hours hundreds of addresses copied the transaction, changing only the recipient, and about $190 million left the bridge. The missing control was a test that the upgraded contract rejects an unproven message — one negative test.
The fourth is an invariant nobody wrote down. Euler Finance, a lending protocol, let users "donate" part of their collateral to the protocol’s reserves through donateToReserves, a function added by an upgrade in 2022. It did not check that the donor’s loan was still covered afterwards. On 13 March 2023 an attacker used a flash loan to build a large leveraged position, donated enough collateral to make it insolvent on purpose, and liquidated it from a second account at the discount the protocol’s rules allowed — about $197 million in one sequence. The attacker returned almost all of it within weeks. The invariant that would have caught it fits in one sentence: no action may leave an account undercollateralised.
The fifth is the largest theft so far, and no line of contract code was wrong. On 21 February 2025 Bybit, an exchange, moved ether from a cold wallet — a Safe multisig contract — with several signers approving in the Safe{Wallet} web interface. The interface had been compromised: an attacker had got into a Safe{Wallet} developer’s machine and planted JavaScript in the site’s storage that targeted Bybit’s wallet. The signers saw an ordinary transfer. What they signed was a delegatecall to the attacker’s contract, which overwrote the slot where the Safe proxy keeps its implementation address. With the code replaced, about 401,000 ether — some $1.5 billion — left in minutes. Five days later the FBI attributed the theft to North Korea. The missing control was independent verification: each signer checking the raw transaction — target, operation type and data — on a separate device before signing.
Where it degrades
Solidity degrades in two ways: as code that assumes it runs on a server, and as process that claims more safety than it has. The first is the twelve rows; the second is theatre:
- Functions that change state with no access check, because "nobody knows the function is there" (row 1).
- External calls before state updates, or a reentrancy guard on one function and not on its siblings (row 4).
- A price read from one pool in one block, with no bounds and no staleness check (row 10).
- Loops over arrays that users can grow, which one day cost more gas than a transaction may use (row 7).
tx.originused for authorisation, ormsg.sender == tx.origintaken as proof of a person.- Upgradeable contracts controlled by one key, or by a 2-of-3 multisig whose signers sit in one office.
- An "audited" badge for a commit that is no longer the one deployed.
- Signers approving what a web page shows instead of what the transaction contains.
- A floating
pragma, and a compiler upgrade shipped without re-running the tests. - Code copied from a tutorial written for Solidity 0.6, unchecked arithmetic and all.
The honest limit is a question the page has not asked yet: does this need a contract at all? A smart contract earns its costs — public code, gas, no patches, hostile callers — only when users must be able to trust the system without trusting whoever runs it: custody nobody can seize, rules nobody can change quietly, settlement between parties who do not trust each other. If the operator is trusted anyway — it runs the front end, holds an admin key that can upgrade everything, or can pause it at will — a database does the same job faster, cheaper and with the ability to fix a bug on a Tuesday. Many products put on a chain in the last decade were in the second group. Ask the question before the first line of Solidity.
The practical check, for a contract you are about to trust or ship: read the pragma and the known-bugs list for that version. Find every function without an access check and ask why. Find every external call and check that state was updated before it. Find where prices come from. List who can upgrade or pause the contract, and how long the timelock is. And compare the deployed bytecode with the audited commit.
When to use it
Use it when
- Custody and settlement between parties who do not trust each other or an operator.
- Tokens and assets that must move between independent applications under shared rules.
- Rules that must be public and unchangeable — or changeable only after a published delay.
- Products on Ethereum or an EVM-compatible chain, where Solidity has the largest tooling and audit base.
Avoid it when
- Any system whose operator is trusted anyway — a database is cheaper, faster and fixable.
- Logic that needs secrets, private data, real randomness or outside data without an oracle.
- Teams without a budget for audits, monitoring and a key-management process.
- Heavy computation or large data — gas prices every byte stored and every step of a loop.
Sources and method
What this page stands on
- 23 sources
- 6 documented failures
- Checked October 13, 2026
The failures this page learns from
the section “Five losses, and what each one was missing”, and the Vyper case after it- ArticleHard Fork Completed (opens in a new tab)Ethereum Foundation Blog · Jul 20, 2016
- PostmortemThe Multi-sig Hack: A PostmortemParity Technologies · Jul 19, 2017
- PostmortemA Postmortem on the Parity Multi-Sig Library Self-DestructParity Technologies · Nov 6, 2017
- PostmortemHack Analysis: Nomad Bridge, August 2022 (opens in a new tab)Immunefi · Aug 2022
- PostmortemVyper Nonreentrancy Lock Vulnerability Technical Post-Mortem Report (opens in a new tab)Vyper · Aug 2023
- ArticleEuler Finance Exploiter Returns All ‘Recoverable Funds’ From $200M Hack (opens in a new tab)Decrypt · Apr 2023
- Regulator reportNorth Korea Responsible for $1.5 Billion Bybit Hack (opens in a new tab)FBI · Internet Crime Complaint Center · Feb 26, 2025
Research
what the losses were, counted across a yearPractices and where the terms come from
the twelve assumptions, storage, gas and the compiler- DocumentationSolidity — Security Considerations (opens in a new tab)docs.soliditylang.org
- DocumentationSolidity — Layout of State Variables in Storage and Transient Storage (opens in a new tab)docs.soliditylang.org
- DocumentationSolidity v0.8.0 Breaking Changes (opens in a new tab)docs.soliditylang.org
- DocumentationSolidity — List of Known Bugs (opens in a new tab)docs.soliditylang.org
- Documentationargotorg/solidity — Releases (opens in a new tab)GitHub
- ArticleBuilding on Ethereum in 2026: what has changed (opens in a new tab)ethereum.org · 2026
- ArticleGlamsterdam Testnet AnnouncementEthereum Foundation · 2026
The protocol changes behind the gas table
code size, storage pricing, transient storage, fees and limits- StandardEIP-170: Contract code size limit (opens in a new tab)Ethereum Improvement Proposals
- StandardEIP-1559: Fee market change for ETH 1.0 chain (opens in a new tab)Ethereum Improvement Proposals
- StandardEIP-2929: Gas cost increases for state access opcodes (opens in a new tab)Ethereum Improvement Proposals
- StandardEIP-1153: Transient storage opcodes (opens in a new tab)Ethereum Improvement Proposals
- StandardEIP-6780: SELFDESTRUCT only in same transaction (opens in a new tab)Ethereum Improvement Proposals
- StandardEIP-7702: Set Code for EOAs (opens in a new tab)Ethereum Improvement Proposals
- StandardEIP-7825: Transaction Gas Limit Cap (opens in a new tab)Ethereum Improvement Proposals
- StandardEIP-7935: Set default gas limit to 60M (opens in a new tab)Ethereum Improvement Proposals
Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.
Found this useful?
Share it with someone who is working on the same problem.