Smart Contract Security
Smart contract security is unusual among software security disciplines in that the consequences are immediate, quantified, and public. There is no gradual data exfiltration to detect and no window to patch: an exploit executes in one transaction, the funds are gone, the code cannot be changed, and the transaction that did it is permanently readable by everyone.
The threat model follows from the environment. Your contract is a public API with no rate limits and no authentication beyond what you implement. Callers can be other contracts, so they can re-enter your functions mid-execution. Attackers can borrow tens of millions of dollars with no collateral for the duration of one transaction. They can see your pending transactions before they execute and reorder around them. And they can compose your contract with any other contract on the chain in ways you never considered.
Most large losses aren't exotic. They're missing access control, an oracle that trusted a manipulable price, or an external call in the wrong place.
TL;DR
- Access control failures and oracle manipulation cause the largest losses — not clever cryptography breaks.
- Reentrancy: an external call lets the callee re-enter before your state settles. Checks-Effects-Interactions prevents it.
- Flash loans give any attacker unlimited temporary capital. Assume every economic assumption can be stressed with $100M.
- Never price assets from a spot AMM reserve; use a TWAP or a robust oracle like Chainlink.
- The mempool is public — anything profitable and order-dependent will be front-run.
- Invariant and fuzz testing find real bugs; happy-path unit tests do not.
- Audits reduce risk, they don't eliminate it. Pair them with monitoring, timelocks, pause switches, and a bounty.
- Complexity is the root risk. Every additional integration and branch is attack surface.
Quick Example
The reentrancy bug that drained the DAO, and its fix:
The fix is a two-line reordering. That's characteristic of the whole field: the bugs are rarely sophisticated, and the discipline that prevents them is applied uniformly or not at all.
The Major Vulnerability Classes
Access control
The largest single cause of losses, and the most mundane.
Audit every state-changing function for "who may call this?" A checklist that catches this: list every external/public function, write down the intended caller, and verify a guard exists. Uninitialized proxies, missing onlyOwner, and privileged functions left public during development are recurring real-world causes.
Reentrancy
Read-only reentrancy is the modern variant that catches experienced teams: your contract is safe, but an external protocol calls your getPrice() while your state is temporarily inconsistent and acts on the wrong value.
Defenses, in order: Checks-Effects-Interactions as the primary discipline, nonReentrant guards as defense in depth, and the pull payment pattern (users withdraw rather than being pushed funds) which removes the external call entirely.
Oracle manipulation
An attacker with a flash loan swaps enough to move the pool, calls your contract while the price is wrong, and unwinds — all atomically, so they take no market risk.
Note that simply calling latestRoundData() isn't enough — an unchecked stale price is its own well-documented exploit class. Where an on-chain source is required, use a TWAP over a meaningful window so manipulation must be sustained across blocks and becomes expensive.
Flash loans
A flash loan lends any amount with no collateral, provided it's repaid in the same transaction. It isn't a vulnerability itself; it removes capital as a barrier to exploiting one.
The design rule: never assume an attacker's capital is limited. Governance weighted by a token balance snapshot at the current block, collateral valued by spot price, and rewards proportional to instantaneous share are all flash-loanable. Use time-weighted balances, checkpointed voting power, and prices that can't move within a block.
MEV, front-running, and sandwiching
Your transaction sits in a public mempool before execution. Anything profitable to reorder around will be.
Beyond slippage limits: use commit-reveal for anything auction-like, private relays (Flashbots Protect) for sensitive transactions, and avoid mechanisms whose fairness depends on transaction ordering.
Integer and arithmetic issues
Solidity 0.8+ reverts on overflow, which removed the classic bug — but arithmetic problems persist:
Denial of service
Unbounded loops over user-controlled arrays also fail permanently once the array exceeds the block gas limit — the contract is bricked with no recovery.
Signature issues
Also guard against signature malleability (use OpenZeppelin's ECDSA, which rejects the malleable high-s form) and always check that ecrecover didn't return the zero address.
Testing and Verification
Invariant testing is the highest-value technique
Unit tests confirm your code does what you expected. Invariant tests confirm that no sequence of calls can violate a property that must always hold — which is much closer to what an attacker is searching for.
Foundry generates random call sequences against a handler and checks the invariants after each. This is how "deposit, transfer, withdraw in this exact order with these amounts" bugs get found.
The layered approach
Slither is essentially free and belongs in CI from day one. Formal verification (Certora, halmos, Kontrol) is worth it for the small set of properties that define your protocol's solvency.
Operational Security
Code is one half. The other half is how you deploy and run it.
Keys and privileges
Single-key admin control is a critical vulnerability regardless of contract quality. Use a multisig (Safe) for privileged roles, with a timelock in front of upgrades and parameter changes so users can observe and exit before a change takes effect. Split roles — the address that pauses should not be the address that upgrades.
Monitoring
Deploy monitoring at the same time as the contract, not after an incident. Watch for large or anomalous transfers, privileged function calls, oracle deviations, and invariant violations checked from off-chain. Several exploits have run for hours because nobody was watching. See Alerting and Monitoring.
Pause switches and incident response
A circuit breaker that halts sensitive operations buys time, and it's most valuable when you've rehearsed using it. Write the runbook in advance: who can pause, how they're reached, what gets communicated, and how funds are recovered. See Runbooks.
Staged rollout
Deposit caps at launch, raised gradually as value at risk grows, limit the size of an early exploit to something survivable. Launching with uncapped deposits is a bet that your first version is correct.
Best Practices
Reduce complexity before adding defenses
Every integration, branch, and configurable parameter is attack surface. The most reliably secure contracts are the ones that do less. If a feature can be moved off-chain or omitted, that eliminates a whole class of bugs rather than mitigating it.
Apply Checks-Effects-Interactions universally
Not "where reentrancy seems possible" — everywhere. It costs nothing, and the cases where reentrancy is possible are exactly the ones you didn't anticipate.
Use audited libraries for everything standard
Token standards, access control, proxies, signature recovery, math utilities. OpenZeppelin and Solady are audited and battle-tested; your reimplementation is not.
Validate every external input and return value
Oracle prices need staleness and sanity checks. Token transfers need SafeERC20. External call return values need checking. Assume anything outside your contract can return garbage or revert.
Write the invariants down before writing the code
"Total shares always corresponds to total assets." "No user can withdraw more than they deposited plus yield." "The contract is always solvent." Written invariants become both your test suite and the specification an auditor works against.
Test against forked mainnet
Most bugs live at integration boundaries. Fork tests exercise your contract against the real Uniswap, the real oracle, the real token — including the non-standard behaviors that mocks paper over.
Assume the deployment key will eventually be compromised
Design so that a compromised key can't immediately drain the protocol: multisig, timelock, role separation, and an emergency pause held by a different set of signers.
Common Mistakes
Trusting a spot price
Using an unvalidated oracle response
Leaving a proxy uninitialized
Governance by current token balance
Rounding in the user's favor
Treating an audit as completion
FAQ
What causes the most losses in practice?
Access control failures and oracle/price manipulation, consistently, across every year of published incident data. Reentrancy still appears but is better understood. Bridges are disproportionately represented among the largest incidents, usually through signature verification or validator key compromise rather than contract logic. The pattern is that operational and design mistakes dominate over exotic technical exploits.
How much does an audit cost, and is it enough?
Reputable firms charge in the tens of thousands of dollars upward, scaling with code size and complexity, with lead times measured in weeks. It is not sufficient on its own: audits are time-boxed reviews by humans who did not design your system, and audited protocols are exploited regularly. Treat an audit as one layer among invariant testing, monitoring, timelocks, caps, and a bounty.
Can formal verification prove my contract is safe?
It can prove specific properties about specific code — that solvency holds, that an accounting identity is preserved, that a function can't revert unexpectedly. It cannot prove your design is economically sound, that your oracle is trustworthy, or that a composition with another protocol is safe. It's a strong tool for core invariants and not a substitute for judgment.
What should I do if my contract is exploited?
Have decided this in advance. Pause if you can. Alert users publicly and immediately — silence is worse than bad news. Preserve the transaction data. Contact a security firm and the relevant exchanges to flag addresses. Consider a whitehat negotiation, which has recovered funds in several notable cases. The response quality is largely determined by whether a runbook existed before the incident.
Are there tools that catch bugs automatically?
Slither (static analysis) is fast, free, and catches real issues — run it in CI. Echidna and Foundry's fuzzing find property violations. Mythril and halmos do symbolic execution. Semgrep rules encode team-specific patterns. All of them catch known shapes of bugs; none catch the design and economic flaws that cause the largest losses.
Is Solidity itself the problem?
Partly, and mostly not. Solidity has sharp edges — silent truncation in casts, delegatecall semantics, the historical transfer gas stipend — but the dominant risks come from the environment: immutability, public state, adversarial callers, composability, and unlimited attacker capital. A different language would eliminate some footguns and leave the hard problems untouched.
Related Topics
- Solidity — The language and the safe patterns
- Ethereum & the EVM — Gas, the mempool, and MEV mechanics
- Blockchain Fundamentals — Why immutability and public state drive the threat model
- Layer 2 Scaling — Bridge and sequencer trust assumptions
- Security — General application security practice
- OWASP Top 10 — The web equivalent of a vulnerability taxonomy
- Testing — Fuzzing and property-based testing in general
- Monitoring · Alerting — Detecting an exploit while it's happening
- Secrets Management — Protecting deployment and admin keys