The difference between a smart contract developer and a secure smart contract developer isn't knowledge of more patterns — it's a mindset shift. In Web2, bugs cause errors. In Web3, bugs cause permanent fund loss. This guide covers the defensive programming practices that separate secure contracts from vulnerable ones.
The Web3 Security Mindset
- Assume all external inputs are malicious — Every parameter, every callback, every external contract
- Minimize trust surface — The less your contract trusts, the less can go wrong
- Fail closed, not open — When in doubt, revert. Silent failures are exploitable
- Make it correct first, then optimize — Gas optimization that introduces bugs is a net loss
Pattern: Checks-Effects-Interactions (CEI)
The single most important pattern in Solidity security. Check preconditions, update state, then interact with external contracts.
function withdraw(uint256 amount) external nonReentrant {
// CHECKS
require(amount > 0, "Zero amount");
require(balances[msg.sender] >= amount, "Insufficient balance");
// EFFECTS
balances[msg.sender] -= amount;
totalDeposits -= amount;
// INTERACTIONS
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawal(msg.sender, amount);
}
Pattern: Input Validation
function setParameters(
address oracle, uint256 fee, uint256 maxBorrow
) external onlyAdmin {
require(oracle != address(0), "Zero oracle");
require(fee <= MAX_FEE, "Fee too high"); // Cap at 10%
require(maxBorrow > MIN_BORROW, "Too low"); // Prevent zero-config
require(maxBorrow <= MAX_BORROW, "Too high"); // Prevent overflow scenarios
_oracle = oracle;
_fee = fee;
_maxBorrow = maxBorrow;
emit ParametersUpdated(oracle, fee, maxBorrow);
}
Testing for Security
Invariant Testing (Foundry)
Define properties that should always be true and let the fuzzer try to break them:
function invariant_totalSupplyMatchesBalances() public {
uint256 sum;
for (uint i = 0; i < actors.length; i++) {
sum += vault.balanceOf(actors[i]);
}
assertEq(vault.totalSupply(), sum);
}
function invariant_contractSolvency() public {
assertGe(
address(vault).balance,
vault.totalDeposits()
);
}
Security-First Development Workflow
- Write specification first (what should the contract do?)
- Implement with CEI pattern throughout
- Write unit tests including edge cases (0, 1, MAX)
- Write invariant tests (what should ALWAYS be true?)
- Run Slither locally before every commit
- Get an audit before deployment
- Set up monitoring and incident response for post-deployment
Secure code starts with secure habits. Validate your approach with a professional audit — we catch what testing misses.