Securite·12 min de lecture·Par Solingo

Top 10 Solidity Vulnerabilities Every Developer Must Know

From reentrancy to front-running, these are the vulnerabilities that have cost billions. Learn to identify and prevent them.

# 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 unchecked when 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

  • Price Manipulation: Borrow → Manipulate price → Exploit → Repay
  • Governance Attacks: Borrow governance tokens → Vote → Execute → Repay
  • Arbitrage Exploitation: Exploit price discrepancies amplified by large capital
  • 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:

  • Always follow Checks-Effects-Interactions pattern
  • Use Solidity 0.8.0+ for automatic overflow protection
  • Implement proper access control with OpenZeppelin
  • Check return values of external calls
  • Use time-weighted prices, never spot prices
  • Implement replay protection with nonces and chain ID
  • Match storage layouts in proxy patterns
  • Test extensively with fuzzing and formal verification
  • Get professional audits before mainnet deployment
  • Stay updated on new attack vectors
  • 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:

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement