Writing secure Solidity is fundamentally different from writing secure code in any other language. Immutability means you can't patch bugs after deployment. Financial exposure means every vulnerability has a direct dollar cost. And composability means your contract will be called by code you've never seen.
This handbook covers the security patterns that separate production-ready Solidity from audit nightmares.
The Foundations
Before diving into patterns, internalize three principles:
Solidity Security Principles
1. Assume adversarial callers
Every external call to your contract could be from an attacker. Never trust msg.sender's intentions — only verify their permissions.
2. Minimize state between calls
The more state that carries between transactions, the more opportunities for manipulation. Keep functions as atomic as possible.
3. Fail safely
When something unexpected happens, the contract should revert to a safe state — not continue with corrupted data.
Pattern 1: Checks-Effects-Interactions
The most fundamental Solidity security pattern. We covered why this matters so deeply in our reentrancy deep-dive — here's the practical implementation.
Checks
Validate all conditions — require statements, access control, parameter validation
Effects
Update all state variables — balances, flags, counters, mappings
Interactions
Make external calls last — transfers, delegatecalls, calls to other contracts
🛑The One Rule That Prevents Most Exploits
If every Solidity developer followed checks-effects-interactions religiously, reentrancy vulnerabilities would nearly disappear. It sounds simple, but under pressure — with complex multi-step operations and callback patterns — developers consistently violate it.
Combine with OpenZeppelin's ReentrancyGuard for defense in depth. As noted in our smart contract checklist, use it on every function that makes external calls.
Pattern 2: Access Control
The most common vulnerability class in our audit experience:
Weak Access Control
- •Single owner address (one key to rule them all)
- •Using tx.origin for authentication
- •Hardcoded admin address in constructor only
- •Missing access checks on critical functions
Strong Access Control
- •Role-based access with OpenZeppelin AccessControl
- •Always use msg.sender, never tx.origin
- •Two-step ownership transfer (Ownable2Step)
- •Timelock on admin operations for transparency
Role-Based Access
Use OpenZeppelin's AccessControl instead of simple Ownable:
- Define granular roles:
MINTER_ROLE,PAUSER_ROLE,UPGRADE_ROLE - Each role can be managed independently
- Roles can be revoked without affecting other permissions
- Multi-sig can hold different roles for different operations
Pattern 3: Safe External Calls
External calls are the most dangerous operations in Solidity:
External Call Safety
Use SafeERC20 for token transfers
Some ERC-20 tokens don't return a boolean on transfer. SafeERC20 handles both cases, preventing silent failures where tokens aren't actually transferred but your contract thinks they were.
Check return values on low-level calls
address.call() returns (bool success, bytes memory data). If you don't check success, a failed call is silently ignored. Always require(success).
Handle fee-on-transfer tokens
If a user transfers 100 tokens but a 1% fee is deducted, your contract receives 99. Calculate actual received = balanceAfter - balanceBefore, not the input amount.
Set gas limits on callbacks
When sending ETH to unknown addresses, use a gas limit to prevent expensive callback execution: call{value: amount, gas: 2300}("").
Pattern 4: Safe Math and Precision
Since Solidity 0.8+, arithmetic overflow/underflow reverts by default. But precision issues remain. We see these in 40%+ of DeFi audits:
Multiply before dividing
a * c / b preserves more precision than a / b * c. With integers, division truncates — doing it first loses information permanently.
Use sufficient decimals
18 decimals (1e18) is the standard for DeFi calculations. Using fewer decimals in intermediate calculations causes compounding rounding errors.
Protect first depositor
In share-based systems (vaults, staking), the first depositor can manipulate the share price. Use virtual offsets or minimum deposits.
Round in the protocol's favor
When dividing, round down for user withdrawals and round up for user deposits. This prevents dust-based extraction attacks.
💡The Precision Rule
Always multiply before dividing. Use 1e18 scaling for all intermediate calculations. Round against the user (down for withdrawals, up for deposits). These three rules prevent the majority of precision-related exploits.
Pattern 5: Secure Upgradeability
Upgradeable contracts introduce a whole category of risks. We cover the security trade-offs in detail in our upgradability guide.
Key patterns:
- Always initialize the implementation — Call
_disableInitializers()in the constructor - Use UUPS over Transparent Proxy — The upgrade logic lives in the implementation, which can be removed when no longer needed
- Storage layout compatibility — Never remove or reorder storage variables in upgrades. Only append.
- Governance-controlled upgrades — Never let a single EOA upgrade contracts. Use timelock + multisig.
Pattern 6: Secure Oracle Integration
Price oracle manipulation is the #1 DeFi vulnerability we encounter, and a primary vector for flash loan attacks:
Dangerous Oracle Patterns
- •Reading spot price from a DEX pool
- •Single oracle source with no fallback
- •No staleness checks on oracle data
- •No circuit breaker for extreme price movements
Safe Oracle Patterns
- •Chainlink or decentralized oracle network
- •Multiple oracle sources with fallback logic
- •Revert if oracle data is older than threshold
- •Pause protocol if price deviates more than X% from reference
Pattern 7: Emergency Controls
Every production protocol needs a safety net:
Pause
OpenZeppelin Pausable — halt all critical operations in an emergency
Rate Limit
Maximum withdrawal/transfer per time period to limit damage from exploits
Timelock
Delay between admin action proposal and execution for community oversight
Guardian
Emergency multisig that can pause but not upgrade — separate from admin keys
Pre-Deployment Checklist
Before deploying, verify against our comprehensive Smart Contract Audit Checklist:
- All functions follow checks-effects-interactions
- ReentrancyGuard on all external-call functions
- Access control on all admin/privileged functions
- SafeERC20 for all token operations
- Oracles validated for freshness and manipulation resistance
- Implementation contracts properly initialized
- Storage layout compatible with any previous version
- Emergency pause and rate limiting implemented
- All test pass with 90%+ coverage on critical paths
- External audit completed
Writing Solidity for production? Our Smart Contract Auditor checks all these patterns automatically, and our expert review catches the protocol-specific vulnerabilities that patterns alone can't prevent. Get your code reviewed before deployment — not after.