Smart Contract Security
Why this code gets attacked harder than any other, the bug classes that keep recurring, and how audits actually work.
Adversarial By Default
That inverts the usual economics of software defects. A web bug is a support ticket; a contract bug is a withdrawal you cannot reverse, and there is a standing financial incentive for strangers to look for it. So the discipline is closer to cryptography than to product engineering: conservative patterns, established libraries, and a strong preference for the boring option.
Reentrancy & Common Bugs
Reentrancy is the canonical bug. When your contract sends value to an address, that address may be a contract, and it runs code — which can call back into your function before the first call has finished updating state.
// Vulnerable: the balance is cleared after the external call
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: amount}(""); // attacker re-enters here
require(ok);
balances[msg.sender] = 0; // too late
}
// Safe: checks, then effects, then interactions
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
balances[msg.sender] = 0; // effect first
(bool ok, ) = msg.sender.call{value: amount}(""); // interaction last
require(ok);
}
The rule the fix encodes is checks-effects-interactions: validate, update your own state, and only then call out. A reentrancy guard is a useful belt on top, but ordering is the actual defence.
| Bug class | Shape |
|---|---|
| Missing access control | A privileged function anyone can call |
| Unchecked return values | A failed transfer treated as success |
| Front-running | The mempool is public; your transaction can be jumped |
| Integer assumptions | Decimals, rounding and truncation losing value each call |
| Delegatecall to untrusted code | Someone else's code writing your storage |
| Denial of service | A loop over an array anyone can grow until it runs out of gas |
Oracle & Economic Attacks
The expensive incidents are frequently not code bugs at all. The code does exactly what it says, using a number an attacker was able to move.
A price oracle brings off-chain reality on-chain, and reading a price directly from a single pool is the classic mistake: a flash loan borrows an enormous sum with no collateral, distorts the pool, and the contract prices a trade against a number that existed for one transaction.
| Weak source | Sturdier |
|---|---|
| Spot price from one pool | Time-weighted average across blocks |
| A single reporter | A decentralised oracle network with aggregation |
| No sanity bounds | Reject moves outside a plausible range |
| No staleness check | Reject a price older than a threshold |
Auditing & Formal Verification
An audit is a time-boxed review by people who do this constantly. It raises the bar considerably and guarantees nothing — several audited protocols have been drained — so it belongs in a chain of defences rather than at the end of one.
| Layer | Finds |
|---|---|
| Static analysis (Slither) | Known bad patterns, fast and free |
| Fuzzing and invariants | Property violations you did not anticipate |
| Manual audit | Logic and economic flaws tools cannot see |
| Formal verification | Mathematical proof that a property always holds |
| Bug bounty | Continuous review, priced against the exploit |
Formal verification proves a stated property over all possible inputs — total supply never exceeds the cap, no path lets a user withdraw more than they deposited. It is strong and narrow: it proves what you asked, so an unstated assumption is still a hole.
Interview Questions
What is reentrancy?
Sending value to an address runs that address's code, which can call back into your function before it finished updating state — so balances are read again before they were cleared.
What is the checks-effects-interactions pattern?
Validate inputs, update your own state, then make external calls. Ordering is the real defence; a reentrancy guard is a secondary control.
Why is reading a spot price from one pool dangerous?
A flash loan can move that pool within a single transaction, so the contract prices against a number the attacker created. Time-weighted averages and aggregated oracle networks resist that.
What do flash loans change?
They remove capital as a barrier. An attacker can borrow an enormous sum with no collateral, provided it is repaid in the same transaction, so any attack profitable at scale is available to anyone.
Does an audit make a contract safe?
No. It is a time-boxed review that raises the bar. Audited protocols have been drained — it belongs alongside static analysis, fuzzing, invariants, bounties and operational limits.
What does formal verification actually prove?
That a stated property holds for all inputs — a supply cap, a withdrawal bound. It is exactly as complete as the properties you thought to state.