Smart contract security has never been more critical. As DeFi total value locked continues to grow past $200 billion, attackers are getting more sophisticated. Automated tools, flash loan infrastructure, and MEV bots create an attack surface that didn't exist even two years ago.
This guide covers the 10 most exploited vulnerability classes we see in smart contracts today. For each one, we'll show you what it looks like, why it's dangerous, and how to detect it — both manually and with automated scanning.
Reentrancy remains the most iconic smart contract vulnerability. It occurs when a contract makes an external call before updating its own state, allowing the called contract to re-enter the original function and exploit the stale state.
The Classic Pattern
The vulnerability exists when ETH or tokens are sent to an external address before the sender's balance is updated:
Solidity // VULNERABLE: state update after external call function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount, "Insufficient"); // External call BEFORE state update (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] -= amount; // Too late! }
An attacker contract's receive() function can call withdraw() again before the balance is decremented, draining the contract.
The Fix: Checks-Effects-Interactions
Solidity // SAFE: state update before external call function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount, "Insufficient"); balances[msg.sender] -= amount; // State update FIRST (bool success, ) = msg.sender.call{value: amount}(""); require(success); }
Modern reentrancy also includes cross-function reentrancy (re-entering a different function that reads stale state) and read-only reentrancy (exploiting view functions during reentrancy to manipulate price oracles). Always use the Checks-Effects-Interactions pattern and consider OpenZeppelin's ReentrancyGuard for additional protection.
Access control vulnerabilities are the single largest source of financial losses in smart contract exploits. They occur when critical functions — such as minting, pausing, upgrading, or withdrawing funds — can be called by unauthorized addresses.
Common Patterns
Solidity // VULNERABLE: No access control on critical function function mint(address to, uint256 amount) external { _mint(to, amount); // Anyone can mint! } // VULNERABLE: tx.origin instead of msg.sender function transferOwnership(address newOwner) external { require(tx.origin == owner); // Phishing attack vector owner = newOwner; } // SAFE: Proper access control function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { _mint(to, amount); }
Always use role-based access control (like OpenZeppelin's AccessControl) for sensitive functions. Never use tx.origin for authorization. Audit every external and public function to verify the correct access modifier is applied.
DeFi protocols depend on accurate price data. When a protocol uses on-chain spot prices from a DEX as its price oracle, an attacker can manipulate that price within a single transaction using a flash loan, then exploit the stale or manipulated price to extract value.
Vulnerable Pattern
Solidity // VULNERABLE: Spot price from AMM pool function getPrice() public view returns (uint256) { uint256 reserve0 = pair.reserve0(); uint256 reserve1 = pair.reserve1(); return reserve1 * 1e18 / reserve0; // Manipulable! } // SAFE: Use time-weighted average price (TWAP) function getPrice() public view returns (uint256) { (int24 arithmeticMeanTick, ) = OracleLibrary .consult(pool, 1800); // 30-min TWAP return OracleLibrary.getQuoteAtTick( arithmeticMeanTick, 1e18, token0, token1 ); }
Always use TWAPs with sufficient time windows, Chainlink oracles, or multiple independent price sources. Never rely on single-block spot prices for financial calculations.
Front-running occurs when an attacker observes a pending transaction in the mempool and submits their own transaction with a higher gas price to execute before the victim's transaction. MEV (Maximal Extractable Value) bots automate this at scale.
Common Targets
- DEX swaps without slippage protection: Bots sandwich the trade, buying before and selling after for guaranteed profit
- Token approvals followed by transfers: Attackers can front-run an allowance change to spend the old allowance first
- Commit-reveal schemes with weak randomness: If the reveal value is predictable, the commit can be front-run
- Liquidation calls: Bots compete to execute liquidations first for the liquidation bonus
Solidity // VULNERABLE: No slippage protection function swap(uint256 amountIn) external { router.swapExactTokensForTokens( amountIn, 0, // minAmountOut = 0 means accept ANY output path, msg.sender, block.timestamp ); } // SAFE: Require minimum output amount function swap(uint256 amountIn, uint256 minOut) external { router.swapExactTokensForTokens( amountIn, minOut, // Revert if sandwiched too hard path, msg.sender, block.timestamp + 300 ); }
Protect against front-running with slippage parameters, commit-reveal patterns, batch auctions, or integration with private transaction pools (Flashbots Protect, MEV Blocker).
Solidity 0.8+ includes built-in overflow/underflow checks, but precision loss and rounding errors remain a significant vulnerability class. Protocols that perform division before multiplication, or that don't account for token decimal differences, can leak value.
Solidity // VULNERABLE: Division before multiplication loses precision uint256 share = userDeposit / totalDeposits * rewardPool; // If userDeposit < totalDeposits, share = 0! // SAFE: Multiply first, divide last uint256 share = userDeposit * rewardPool / totalDeposits; // VULNERABLE: unchecked block bypasses 0.8 safety unchecked { uint256 result = a - b; // Can underflow! } // WATCH: Different token decimals // USDC = 6 decimals, DAI = 18 decimals // Direct comparison without normalization = wrong
Even with Solidity 0.8+ safety, review all unchecked blocks, ensure multiplication before division, and always normalize token amounts when working with different decimal precisions.
Flash loans allow anyone to borrow unlimited capital with zero collateral, as long as it's returned within one transaction. This eliminates the capital requirement for exploits and amplifies every other vulnerability on this list.
Flash loans don't create vulnerabilities on their own — they amplify existing ones. The typical attack pattern is:
- Borrow massive amount via flash loan
- Manipulate a price oracle or pool balance
- Exploit the protocol at the manipulated price
- Repay the flash loan with profit
Defense Strategies
- Use TWAPs instead of spot prices (see #3)
- Add cross-block delays for large operations
- Implement per-block limits on sensitive operations
- Track whether the caller is a known flash loan provider
Key insight: If your protocol's security model assumes attackers need capital, flash loans break that assumption entirely. Design your protocol assuming any attacker has unlimited capital for a single transaction.
Protocols that use off-chain signatures for gasless transactions, meta-transactions, or permit approvals must protect against signature replay attacks. A valid signature used on one chain or nonce can potentially be replayed on another.
Solidity // VULNERABLE: No nonce or chain ID function executeWithSig(address to, uint256 amount, bytes calldata sig ) external { bytes32 hash = keccak256(abi.encodePacked(to, amount)); address signer = ECDSA.recover(hash, sig); require(signer == owner); // Same signature can be replayed forever! } // SAFE: Include nonce, chain ID, contract address function executeWithSig(address to, uint256 amount, uint256 nonce, bytes calldata sig ) external { require(nonce == nonces[owner]++); bytes32 hash = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(TYPEHASH, to, amount, nonce)) )); address signer = ECDSA.recover(hash, sig); require(signer == owner); }
Always use EIP-712 typed structured data for signatures. Include a nonce, chain ID, contract address, and deadline in every signed message. Mark used nonces to prevent replay.
DoS vulnerabilities make contract functions permanently unusable or excessively expensive to call. Common patterns include unbounded loops over growing arrays, reliance on external calls succeeding, and block gas limit exhaustion.
Solidity // VULNERABLE: Unbounded loop grows with users function distributeRewards() external { for (uint i = 0; i < stakers.length; i++) { // If stakers array grows large enough, // this exceeds block gas limit and ALWAYS reverts payable(stakers[i]).transfer(rewards[stakers[i]]); } } // SAFE: Pull pattern - users claim their own rewards function claimReward() external { uint256 reward = pendingRewards[msg.sender]; pendingRewards[msg.sender] = 0; payable(msg.sender).transfer(reward); }
Use the pull-over-push pattern for distributions. Avoid unbounded loops over arrays that can grow. Don't assume external calls will succeed — handle failures gracefully. Set reasonable limits on array sizes and iteration counts.
Upgradeable contracts using the proxy pattern introduce unique risks. Storage collisions between proxy and implementation, uninitialized implementations, and unsafe upgrade paths can all lead to critical vulnerabilities.
Solidity // VULNERABLE: Storage collision // Proxy stores admin at slot 0 // Implementation stores balance at slot 0 // Writing to balance overwrites admin! // VULNERABLE: Uninitialized implementation // If initialize() is not called on the implementation // contract itself, anyone can call it and become owner // SAFE: Use ERC-1967 storage slots bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; // This is keccak256("eip1967.proxy.admin") - 1 // Guaranteed not to collide with normal storage
Use battle-tested proxy patterns (OpenZeppelin TransparentProxy or UUPS). Always initialize implementation contracts with _disableInitializers(). Use ERC-1967 standard storage slots. Test upgrades thoroughly before deploying.
Not all ERC-20 tokens behave the same. Fee-on-transfer tokens, rebasing tokens, tokens that return false instead of reverting, and tokens with callbacks (ERC-777) each require special handling.
Solidity // VULNERABLE: Assumes transfer amount == received amount function deposit(uint256 amount) external { token.transferFrom(msg.sender, address(this), amount); balances[msg.sender] += amount; // Wrong for fee-on-transfer! } // SAFE: Check actual balance change function deposit(uint256 amount) external { uint256 before = token.balanceOf(address(this)); token.transferFrom(msg.sender, address(this), amount); uint256 received = token.balanceOf(address(this)) - before; balances[msg.sender] += received; // Actual amount received } // SAFE: Use SafeERC20 for non-standard returns using SafeERC20 for IERC20; token.safeTransfer(to, amount); // Handles bool/void returns
Always use OpenZeppelin's SafeERC20 wrapper. When handling arbitrary tokens, check actual balance changes rather than assuming the input amount was fully transferred. Document which token types your protocol supports.
Detection: Manual vs. Automated
Each of these vulnerability classes has distinct code patterns that can be detected. The key is combining multiple approaches:
- Automated pattern scanning catches the obvious instances — external calls before state updates, missing access modifiers, spot price reads. This is fast, free, and should be part of every development workflow.
- AI-powered deep analysis can reason about more complex interactions: cross-function reentrancy, economic attack paths, and business logic flaws that simple pattern matching misses.
- Manual expert review is essential for protocols handling significant value. Human auditors can reason about protocol-specific assumptions, game theory, and novel attack vectors.
Our recommendation: Use automated scanning continuously during development (it's free). Add AI deep analysis before testnet deployment. Commission a full manual audit before mainnet launch. This layered approach catches the widest range of issues at the lowest cost.
How to Scan Your Contracts Now
The Solari free scanner checks for all 10 vulnerability classes covered in this article. Paste your Solidity code and get instant results with specific line-level findings, severity ratings, and remediation guidance. No signup required.
For deeper analysis that includes cross-contract interactions, economic attack modeling, and business logic review, our paid tiers provide AI-powered scanning starting at $99.