DevelopmentAll levels

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 assumeInside a contractWhat to do instead
1Only your users can call your APIAnyone can call every public and external function — including another contractAn access check on every function that changes state; assume the caller is hostile
2A bug is fixed by deploying a patchDeployed code never changesDecide before launch: immutable, or an upgrade path behind a timelock
3A secret can live in config or in the databaseAll storage is readable by anyone; private only hides it from other contractsKeep secrets off-chain; commit–reveal for hidden choices
4Calling another service just returns an answerAn external call hands control to code you do not own, which can call you backChecks, effects, interactions — in that order — and a reentrancy guard
5Requests run concurrently and in arrival orderTransactions run one at a time, in an order the block builder choosesAssume someone sees your transaction first: slippage limits, deadlines
6A failed request leaves partial state to clean upA failed transaction reverts every change — and the caller still pays for the gas usedValidate early and revert; rely on atomicity, not on clean-up code
7A loop costs CPU timeEvery instruction costs gas, paid by the caller, within a per-block limitNo loops over lists users can grow; let each user withdraw instead
8Money can be a decimal or a floatNo fractions: 256-bit integers, and division rounds downMultiply before dividing; round in the protocol’s favour
9The server knows the time and can make random numbersblock.timestamp is the block’s time, not the call’s, and nothing on-chain is unpredictable to the block’s proposerA verifiable randomness oracle (VRF), or commit–reveal
10You can fetch a price from an APIA contract sees only the chain; a price is whatever some contract reportsA manipulation-resistant oracle, with staleness and bounds checks
11An admin can fix data by handAn admin power is a function that whoever holds the key can callA multisig, a timelock, and published limits on what admins can do
12Code size and stack depth are someone else’s problemA contract is capped at 24 KB of code, and the EVM reaches only 16 stack slotsSplit 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.

Only an account controlled by a private key can start a transaction. The transaction may call a contract, which may call others, and the whole chain either commits together or reverts together. An external call hands control to the other contract, which can call straight back in before the caller has finished — the root of reentrancy. Every instruction costs gas, paid by the sender; storage writes are by far the most expensive part, and if the gas runs out the transaction reverts but the gas is still spent. Every node executes the same transaction and must reach the same result, which is why a contract cannot read a clock, a file or the Internet.

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.

OperationGasWhat it means
Any transaction, before it does anything21,000The fixed price of a plain transfer
Add two numbers3Computation is almost free
Read a storage slot, first time in the transaction2,100About 700 additions
Read the same slot again100Still worth caching in a local variable
Write a storage slot that was zero20,000+Creating state is the most expensive thing you do
Change a non-zero slot2,900+Updating is far cheaper than creating
Call another contract, first time2,600+Plus whatever the callee spends
Calldata, per non-zero byte16Inputs 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 . 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 livesFor how longCostUse it for
storageForever, on every nodeThousands of gas per slotState variables — balances, owners, settings
transientOne transaction (since Cancun, March 2024)About 100 gasReentrancy locks; values passed between calls in one transaction
memoryOne function callA few gas per word, rising with sizeWorking copies of arrays and structs
calldataOne external call, read-onlyCheapest — already paid forParameters of external functions
Events (logs)Forever, but contracts cannot read themCheap compared with storageWhat 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 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.

When a contract sends ether, the receiving contract's code runs — and it can call the sender again before the sender has finished. If the vault updates the balance after sending, every re-entry still sees the old balance and is paid again, until the vault is empty. That is how The DAO lost about 3.6 million ether in 2016. If the vault sets the balance to zero before sending — checks, effects, interactions — the re-entry finds nothing owed and reverts. A reentrancy guard on top blocks re-entry into any protected function.

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.

A proxy keeps all the state and runs the implementation's code against it with delegatecall. The implementation's variables are mapped to the proxy's storage by position — slot 0, slot 1, slot 2 — not by name. So a new version that inserts a variable above existing ones makes every later variable read its neighbour's slot: here fee picks up the old total, and total picks up the old paused flag. Appending new variables after the old ones leaves every existing slot where it was. OpenZeppelin's upgrades plugin compares the two layouts and refuses an incompatible upgrade. The values are illustrative.

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.

A flash loan lends any amount with no collateral, provided it is repaid within the same transaction — otherwise the whole transaction reverts. For the length of that one transaction, anyone can move a pool's price with borrowed millions, let a protocol that reads the pool act on the distorted price, move the price back and repay. In October 2020 Harvest Finance lost about $24 million this way. A time-weighted average over many blocks, or an oracle network that aggregates many sources, cannot be moved by one transaction; staleness and bounds checks catch the rest. The amounts are illustrative.

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 . A fork test runs against a copy of mainnet state, with the real tokens and oracles the contract will meet.

LayerToolsWhat it catches
Static analysisSlither; the compiler’s own warningsKnown bug patterns — reentrancy, unchecked calls, shadowed variables
Fuzzing and invariantsFoundry, Echidna, MedusaInputs and call sequences nobody thought to write down
Formal verificationCertora, Halmos, the compiler’s SMTCheckerProof that a property holds for every input — for the properties you wrote
AuditAn external firm, or a public contestDesign and logic errors a team cannot see in its own code
Bug bountyImmunefi and similar platformsWhatever is left, found by people paid to look after launch
Monitoring and pauseOn-chain alerts; a guardian role that can pauseAn 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 , 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.

CaseWhat happenedWhat was missing
The DAO, Jun 2016Reentrancy drained about 3.6 million ether; Ethereum hard-forked to reverse itEffects before interactions
Parity, Jul and Nov 2017Anyone could call the initialiser: 153,000 ether stolen, then 513,774 frozenAn initialiser callable once — and no kill
Nomad, Aug 2022An upgrade made the zero hash a trusted root; $190 million taken by copycatsOne negative test of the upgraded contract
Euler, Mar 2023A new function skipped the solvency check; about $197 million, later returnedAn invariant: no action leaves an account undercollateralised
Bybit, Feb 2025A compromised signing interface made signers approve a new implementation; about $1.5 billionSigners 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.origin used for authorisation, or msg.sender == tx.origin taken 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

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.