Smart-contract security: auditing what money runs on
5 June 2026 · 8 min read · Nintech Engineering
Most software ships with an implicit safety net: if it breaks, you patch it. Smart contracts remove the net. The code is public, the attacker can read it as carefully as you can, the funds it controls are the prize, and once deployed you often cannot change it. That combination — immutable, transparent, adversarial, and directly monetised — is a threat model almost nothing else in software shares. Auditing it properly means treating deployment less like a release and more like launching a satellite: everything you did not catch beforehand travels with it.
The classic failure classes have not gone away
Reentrancy is still the canonical example for a reason. When a contract sends value to an external address before updating its own state, the recipient — which can be a contract with a fallback function — can call back in and drain funds against stale balances. The DAO lost around 3.6 million ETH to exactly this in 2016, and variants (cross-function, cross-contract, read-only reentrancy against price views) keep appearing in post-mortems a decade later. The fix is boring and mechanical: checks-effects-interactions ordering, reentrancy guards, and treating every external call as a point where your invariants must already hold.
The other perennials are just as unglamorous. Access-control gaps — an initialiser left callable, an owner check missing on a privileged function — account for a large share of real losses; the Parity multisig freeze came down to an unprotected initialisation. Arithmetic assumptions bite differently now that Solidity 0.8 reverts on overflow, but precision loss, rounding direction, and unchecked blocks still create exploitable drift in fee and share calculations. And oracle manipulation is arguably the dominant modern class: if your contract prices assets from a spot pool that an attacker can move within a single transaction using a flash loan, they will. Time-weighted prices, multiple sources, and sanity bounds are table stakes, not sophistication.
Upgradeability trades one risk for another
Proxy patterns exist because immutability is terrifying: they let you point a stable address at new implementation code, so a bug becomes a fix rather than a funeral. But the upgrade mechanism is itself an attack surface, and a large one. Storage-slot collisions between proxy and implementation, uninitialised implementation contracts, and delegatecall into attacker-influenced addresses have all produced real losses. More fundamentally, an upgradeable contract is only as trustworthy as whoever holds the upgrade key — a single EOA with upgrade rights means users are trusting one private key with everything, whatever the code says.
The honest position is that upgradeability is a governance decision wearing a technical costume. If you keep it, constrain it: a multisig with a meaningful threshold, a timelock long enough that users can exit before a malicious or compromised upgrade executes, and monitoring on the admin functions. If the protocol is mature and the logic is stable, consider burning the upgrade path entirely. Announcing that trade-off explicitly is worth more to sophisticated users than any amount of security theatre.
Formal verification, fuzzing, and audits do different jobs
These three get lumped together as 'security work' but they answer different questions. Formal verification proves that specific stated properties hold for all inputs — powerful, but only as good as the specification, and writing a complete spec for a DeFi protocol is genuinely hard. It shines on narrow, high-value invariants: total supply conservation, access predicates, no path to locked funds. Fuzzing and property-based testing (Echidna, Foundry's invariant testing) attack from the other direction: you state invariants and the tool hammers the contract with generated call sequences trying to break them. It finds the weird multi-step interactions humans do not enumerate, cheaply and repeatably in CI.
Independent audits add the thing tools cannot: an adversarial human reading your economic design, not just your code. A good auditor asks whether the incentive structure itself is exploitable — can someone profit by behaving in ways your model did not anticipate, even with bug-free code? The caveats are real, though. Audits are time-boxed samples, not guarantees; plenty of exploited protocols were audited, sometimes by multiple firms. Treat an audit as one layer, scope it after your own fuzzing has cleared the shallow bugs so the auditors spend their hours on design-level issues, and be suspicious of any audit report used primarily as a marketing asset.
Compiling and passing tests proves almost nothing
Unit tests verify the behaviour you thought of. Mainnet attackers specialise in the behaviour you did not. A test suite full of happy-path assertions can pass at 100% coverage while the contract is trivially drainable through a call sequence nobody wrote a test for — flash loan in, manipulate a price read, reenter through a callback, exit in one atomic transaction. Coverage measures which lines ran, not which adversarial compositions were considered. The mainnet is the only test environment where the test author is paid by your losses.
The practical posture: assume the first serious review of your contract by a motivated adversary happens within hours of deployment, because bots scan verified source and mempool activity continuously. Layer defences accordingly — invariant fuzzing in CI, at least one independent audit, a staged rollout with deposit caps, monitoring on privileged calls and anomalous flows, and a rehearsed incident plan including pause mechanisms where they are acceptable. None of this makes you invulnerable. It changes the economics: raising attacker cost and shrinking the blast radius is the whole game when you cannot patch.
Working on something like this? Talk to an engineer.