Smart contracts: immutable code with real money attached
January 4, 2026
A pragmatic view of building on-chain — when blockchains are actually the right tool, what makes smart contracts uniquely dangerous, and how to audit effectively.
Start with the trust question
Blockchains solve a specific problem: shared state between parties that don't trust each other, verified without a trusted intermediary. If you have a trusted intermediary — or if you can create one — a traditional database with proper access controls is simpler, cheaper, and easier to fix when something goes wrong.
The projects that use blockchain effectively are the ones where "trusted intermediary" is genuinely absent or unacceptable: multi-party settlement between competitors, transparent financial protocols, proof of existence and provenance for third-party verification.
The projects that use blockchain unnecessarily are the ones where a Postgres database with row-level security and an audit log would have been fine.
The fundamental engineering difference
Smart contracts are different from normal software in one important way: deployment is publishing, and bugs are public.
In a normal application, you find a bug, fix it, and deploy. Users experience the fix. In a smart contract, you find a bug, and depending on your upgrade mechanism, you may have no path to fixing it while funds are still at risk. The money doesn't wait for your deployment pipeline.
This changes how you approach the entire development lifecycle:
- Write invariants explicitly and test them with fuzzing, not just unit tests
- Keep contracts small — a contract that does one thing is auditable; a contract that does many things is not
- Use proven libraries (OpenZeppelin) for standard patterns instead of reimplementing them
- Treat every external call as untrusted code that can call back into your contract
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Vault is Ownable, ReentrancyGuard {
function withdraw(uint256 amount) external nonReentrant {
// state update before external call — checks-effects-interactions
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "transfer failed");
}
}
The ReentrancyGuard and the state-update-before-external-call pattern aren't defensive programming — they're the difference between a contract that survives and one that doesn't.
Vulnerabilities worth memorizing
The vulnerabilities that have drained the most funds aren't exotic:
Reentrancy — an external call re-enters your function before the first call finishes, exploiting state that hasn't been updated yet. The DAO hack, still the most famous smart contract exploit.
Integer overflow/underflow — Solidity 0.8+ has overflow protection by default. Older contracts and assembly code don't. Know what version you're on.
Oracle manipulation — if your contract uses an on-chain price feed as ground truth, an attacker with enough capital can manipulate that price within a single transaction. Flash loan attacks exploit this. Use time-weighted average prices, not spot prices.
Access control gaps — functions that should be owner-only or role-restricted aren't. Audit every function's visibility and modifier usage explicitly.
DApps: the UX problem is the security problem
Most user losses in Web3 don't come from smart contract bugs. They come from bad UX: unclear signing prompts that approve broad permissions, drained wallets from malicious sites that users authorized, lost funds from mistyped addresses with no reversal mechanism.
Transaction simulation before signing — showing the user what will actually happen — is the highest-leverage UX improvement in this space. If your dapp's signing flow doesn't show a clear, human-readable summary of what the user is approving, that's a security issue.
Audits are not the end
A security audit is a point-in-time review by a finite number of humans. It catches known vulnerability classes and design issues visible at audit time. It doesn't catch all bugs. It doesn't account for deployment configuration. It doesn't cover integrations added after the audit.
Treat audits as one layer: add automated analysis (Slither, Mythril) in CI, implement on-chain circuit breakers for large value flows, deploy with timelocks on sensitive admin functions, and run a bug bounty for production systems holding significant value.
The best audited contracts have still been exploited. Security is continuous work.
References
Hi, I'm Martin Duchev. You can find more about my projects on my GitHub.