Back to Blog
SoliditySmart ContractsGuide

Solidity Security Patterns: A Developer's Handbook

January 20, 20266 min readRedVolt Team

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.

01

Checks

Validate all conditions — require statements, access control, parameter validation

02

Effects

Update all state variables — balances, flags, counters, mappings

03

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:

1

Multiply before dividing

a * c / b preserves more precision than a / b * c. With integers, division truncates — doing it first loses information permanently.

2

Use sufficient decimals

18 decimals (1e18) is the standard for DeFi calculations. Using fewer decimals in intermediate calculations causes compounding rounding errors.

3

Protect first depositor

In share-based systems (vaults, staking), the first depositor can manipulate the share price. Use virtual offsets or minimum deposits.

4

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:

01

Pause

OpenZeppelin Pausable — halt all critical operations in an emergency

02

Rate Limit

Maximum withdrawal/transfer per time period to limit damage from exploits

03

Timelock

Delay between admin action proposal and execution for community oversight

04

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:

  1. All functions follow checks-effects-interactions
  2. ReentrancyGuard on all external-call functions
  3. Access control on all admin/privileged functions
  4. SafeERC20 for all token operations
  5. Oracles validated for freshness and manipulation resistance
  6. Implementation contracts properly initialized
  7. Storage layout compatible with any previous version
  8. Emergency pause and rate limiting implemented
  9. All test pass with 90%+ coverage on critical paths
  10. 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.

Want to secure your application or smart contract?

Request an Expert Review