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.

1
Reentrancy Attacks
Critical

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.

2
Access Control Failures
Critical

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.

3
Oracle & Price Manipulation
Critical

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.

4
Front-Running & MEV Exploitation
High

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).

5
Arithmetic Overflows & Precision Loss
High

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.

6
Flash Loan Attack Surfaces
Critical

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:

  1. Borrow massive amount via flash loan
  2. Manipulate a price oracle or pool balance
  3. Exploit the protocol at the manipulated price
  4. 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.

7
Signature Replay & Malleability
High

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.

8
Denial of Service (DoS)
Medium

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.

9
Proxy & Upgrade Vulnerabilities
High

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.

10
Unsafe Token Handling
High

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.