# Top 10 Solidity Vulnerabilities Every Developer Must Know
Smart contract security is non-negotiable. A single vulnerability can result in millions of dollars lost. In this comprehensive guide, we cover the 10 most critical Solidity vulnerabilities that every developer must understand and know how to prevent.
1. Reentrancy — The Classic Attack
What it is: Reentrancy occurs when an external call is made to an untrusted contract before the state is updated. The external contract can then call back into the original function, exploiting the stale state.
Historic Impact: The DAO hack (2016) resulted in $60M stolen through reentrancy. This attack led to the Ethereum hard fork creating Ethereum Classic.
Vulnerable Code
// ❌ VULNERABLE
contract VulnerableBank {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
// External call BEFORE state update
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount; // Too late!
}
}
The Attack: A malicious contract can implement a receive() function that calls withdraw() again before the balance is updated, draining the contract.
Secure Code
// ✅ SECURE — Checks-Effects-Interactions Pattern
contract SecureBank {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
// Update state FIRST
balances[msg.sender] -= amount;
// External call LAST
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Alternative: Use OpenZeppelin's ReentrancyGuard modifier.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract ProtectedBank is ReentrancyGuard {
function withdraw(uint256 amount) public nonReentrant {
// Function logic here
}
}
2. Integer Overflow/Underflow
What it is: Before Solidity 0.8.0, arithmetic operations could silently overflow or underflow without reverting.
Impact: Still relevant when using unchecked blocks for gas optimization.
Pre-0.8.0 Vulnerability
// ❌ VULNERABLE (Solidity < 0.8.0)
contract Token {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
// No overflow check!
balances[msg.sender] -= amount; // Can underflow
balances[to] += amount; // Can overflow
}
}
Modern Pitfall — Unchecked Blocks
// ⚠️ BE CAREFUL
function batchTransfer(address[] calldata recipients) public {
uint256 totalAmount;
unchecked {
// Optimization, but overflow possible!
for (uint i = 0; i < recipients.length; i++) {
totalAmount += 100;
}
}
require(balances[msg.sender] >= totalAmount);
}
Prevention:
- Use Solidity 0.8.0+ (automatic overflow checks)
- Only use
uncheckedwhen you're absolutely certain overflow is impossible
- For pre-0.8.0: Use SafeMath library
3. Access Control Issues
What it is: Critical functions lack proper access restrictions, allowing unauthorized users to execute privileged operations.
Missing Access Control
// ❌ VULNERABLE — Anyone can mint tokens!
contract Token {
mapping(address => uint256) public balances;
function mint(address to, uint256 amount) public {
balances[to] += amount;
}
}
Proper Access Control
// ✅ SECURE
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureToken is Ownable {
mapping(address => uint256) public balances;
function mint(address to, uint256 amount) public onlyOwner {
balances[to] += amount;
}
}
tx.origin vs msg.sender
// ❌ VULNERABLE — tx.origin can be exploited
function withdraw() public {
require(tx.origin == owner, "Not owner");
// Phishing attack possible!
}
// ✅ SECURE — Always use msg.sender
function withdraw() public {
require(msg.sender == owner, "Not owner");
}
Why tx.origin is dangerous: If the owner calls a malicious contract, that contract can call your function and tx.origin will still be the owner.
4. Unchecked External Calls
What it is: Low-level calls (call, delegatecall, staticcall) don't revert on failure — they return false. Ignoring the return value leads to silent failures.
Vulnerable Code
// ❌ VULNERABLE — Ignores return value
function sendEther(address payable recipient, uint256 amount) public {
recipient.call{value: amount}("");
// Transaction marked as successful even if call failed!
}
Secure Code
// ✅ SECURE — Check return value
function sendEther(address payable recipient, uint256 amount) public {
(bool success,) = recipient.call{value: amount}("");
require(success, "Transfer failed");
}
// ✅ EVEN BETTER — Use transfer or send
function sendEtherSafe(address payable recipient, uint256 amount) public {
recipient.transfer(amount); // Reverts on failure
}
Note: transfer and send have a 2300 gas stipend, which may not be enough for some recipients (e.g., contracts with complex receive() functions).
5. Front-Running / MEV Attacks
What it is: Attackers observe pending transactions in the mempool and submit their own transaction with higher gas to execute first.
Common Scenarios:
- DEX trades (sandwich attacks)
- NFT mints
- Governance votes
- Auction bids
Vulnerable Pattern
// ❌ VULNERABLE to front-running
contract SimpleDEX {
function swap(uint256 amountIn, uint256 minAmountOut) public {
// Front-runner sees this, front-runs with large swap,
// price moves, victim gets less than expected
uint256 amountOut = getAmountOut(amountIn);
require(amountOut >= minAmountOut, "Slippage too high");
// Execute swap
}
}
Mitigation Strategies
// ✅ PARTIAL MITIGATION
contract ProtectedDEX {
function swap(
uint256 amountIn,
uint256 minAmountOut,
uint256 deadline
) public {
require(block.timestamp <= deadline, "Expired");
// Deadline prevents transaction from being executed much later
// minAmountOut provides slippage protection
}
}
Better Solutions:
- Commit-reveal schemes
- Flashbots/MEV protection
- Batch auctions
- Time-locked transactions
6. Denial of Service (DoS)
What it is: Contract functionality becomes permanently unavailable due to gas limits, reverts, or unbounded operations.
Gas Limit DoS
// ❌ VULNERABLE — Unbounded loop
contract Airdrop {
address[] public recipients;
function distribute() public {
// If recipients array is too large, this will run out of gas
for (uint i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(1 ether);
}
}
}
Secure Pattern — Pull over Push
// ✅ SECURE — Pull pattern
contract SecureAirdrop {
mapping(address => uint256) public claimableAmount;
function claim() public {
uint256 amount = claimableAmount[msg.sender];
require(amount > 0, "Nothing to claim");
claimableAmount[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
Revert DoS
// ❌ VULNERABLE — One failing transfer blocks everyone
contract VulnerableAuction {
address public highestBidder;
uint256 public highestBid;
function bid() public payable {
require(msg.value > highestBid);
// Refund previous bidder
payable(highestBidder).transfer(highestBid); // DoS if this fails!
highestBidder = msg.sender;
highestBid = msg.value;
}
}
7. Oracle Manipulation
What it is: Price oracles can be manipulated through flash loans or low-liquidity pools, leading to incorrect pricing.
Historic Impact: Numerous DeFi exploits totaling $100M+ in 2020-2021.
Vulnerable Code
// ❌ VULNERABLE — Relies on spot price
contract VulnerableLending {
IUniswapV2Pair public pair;
function getPrice() public view returns (uint256) {
(uint112 reserve0, uint112 reserve1,) = pair.getReserves();
return (reserve1 * 1e18) / reserve0; // Spot price!
}
function borrow(uint256 collateralAmount) public {
uint256 price = getPrice(); // Can be manipulated!
uint256 borrowAmount = collateralAmount * price / 1e18;
// Issue loan based on manipulated price
}
}
Secure Approach
// ✅ SECURE — Use TWAP or Chainlink
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract SecureLending {
AggregatorV3Interface public priceFeed;
function getPrice() public view returns (uint256) {
(, int256 price,,,) = priceFeed.latestRoundData();
return uint256(price);
}
}
Best Practices:
- Use Chainlink Price Feeds (decentralized oracles)
- Implement TWAP (Time-Weighted Average Price)
- Use multiple oracle sources
- Add price deviation checks
8. Signature Replay Attacks
What it is: A valid signature can be reused in unintended contexts, either on the same chain or across different chains.
Vulnerable Code
// ❌ VULNERABLE — No replay protection
contract VulnerableVault {
function withdrawWithSignature(
uint256 amount,
bytes memory signature
) public {
bytes32 hash = keccak256(abi.encodePacked(amount));
address signer = recoverSigner(hash, signature);
require(signer == owner, "Invalid signature");
payable(msg.sender).transfer(amount);
// Signature can be reused multiple times!
}
}
Secure Code
// ✅ SECURE — With nonce and chain ID
contract SecureVault {
mapping(address => uint256) public nonces;
function withdrawWithSignature(
uint256 amount,
uint256 nonce,
bytes memory signature
) public {
bytes32 hash = keccak256(abi.encodePacked(
amount,
nonce,
block.chainid, // Prevents cross-chain replay
address(this) // Prevents cross-contract replay
));
bytes32 ethSignedHash = hash.toEthSignedMessageHash();
address signer = ethSignedHash.recover(signature);
require(signer == owner, "Invalid signature");
require(nonce == nonces[signer], "Invalid nonce");
nonces[signer]++;
payable(msg.sender).transfer(amount);
}
}
Use EIP-712 for even better security:
// ✅ BEST PRACTICE — EIP-712
bytes32 public DOMAIN_SEPARATOR;
constructor() {
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("SecureVault")),
keccak256(bytes("1")),
block.chainid,
address(this)
));
}
9. Delegatecall Storage Collision
What it is: delegatecall executes code in the context of the calling contract. Storage layout mismatches between contracts lead to data corruption.
Common in: Proxy patterns, upgradeable contracts.
The Problem
// ❌ VULNERABLE — Storage collision
contract Proxy {
address public implementation; // Slot 0
address public owner; // Slot 1
fallback() external payable {
address impl = implementation;
assembly {
delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
}
}
}
contract Implementation {
uint256 public data; // Slot 0 — COLLIDES with implementation!
function setData(uint256 _data) public {
data = _data; // Actually overwrites implementation address!
}
}
Secure Pattern
// ✅ SECURE — Matching storage layout
contract Proxy {
address public implementation; // Slot 0
address public owner; // Slot 1
}
contract Implementation {
address public implementation; // Slot 0 — MATCHES
address public owner; // Slot 1 — MATCHES
uint256 public data; // Slot 2 — Safe!
function setData(uint256 _data) public {
data = _data;
}
}
Best Practice: Use OpenZeppelin's upgradeable contracts pattern.
10. Flash Loan Attacks
What it is: Flash loans allow borrowing large amounts of capital without collateral within a single transaction. Attackers use this to manipulate markets, exploit governance, or drain protocols.
Impact: $1B+ stolen through flash loan attacks in DeFi history.
Attack Vectors
Vulnerable Pattern
// ❌ VULNERABLE — Relies on instant price
contract VulnerableProtocol {
function getValue() public view returns (uint256) {
// Gets instant price from a DEX
return dex.getPrice();
}
function liquidate(address user) public {
uint256 value = getValue(); // Manipulable with flash loan!
if (userDebt[user] > value * collateral[user]) {
// Liquidate
}
}
}
Mitigation
// ✅ SECURE — Multiple protections
contract SecureProtocol {
// 1. Use TWAP, not spot price
function getValue() public view returns (uint256) {
return oracle.getTWAP(30 minutes);
}
// 2. Implement time locks
mapping(address => uint256) public lastAction;
modifier rateLimit() {
require(block.timestamp >= lastAction[msg.sender] + 1 hours);
lastAction[msg.sender] = block.timestamp;
_;
}
// 3. Use commit-reveal for critical actions
function liquidate(address user) public rateLimit {
// Implementation
}
}
Defense Strategies:
- Use time-weighted prices (TWAP)
- Implement delays for critical operations
- Require multiple transaction blocks for state changes
- Use decentralized oracles (Chainlink)
- Add circuit breakers for unusual activity
Conclusion
These 10 vulnerabilities represent the most critical threats to smart contract security. Understanding and preventing them is essential for any Solidity developer.
Key Takeaways:
Practice on Solingo: Master vulnerability detection with 60 audit challenges ranging from beginner to expert level. Each challenge includes real-world exploit scenarios and step-by-step solutions.
Learn to think like an attacker — because that's how you become a great defender.
---
Additional Resources:
- SWC Registry — Smart Contract Weakness Classification
- Rekt News — Analysis of major DeFi hacks
- Solingo Audit Track — Interactive security challenges