DEV Community

ohmygod
ohmygod

Posted on

The OWASP Smart Contract Top 10 (2026): A Practical Defense Playbook With Solidity and Anchor Code for Every Risk

The OWASP Smart Contract Top 10 — 2026 edition — landed in February, and it's the most data-driven version yet: rankings derived from 2025 incident data, practitioner surveys, and projected risk trajectories. If you're building on EVM or Solana, this is the closest thing to a "what will actually get you hacked" scorecard.

But awareness documents don't stop exploits. Code does.

This article walks through each of the 10 categories with:

  • What it actually means in production
  • A real 2025–2026 exploit that demonstrates it
  • Defense code you can copy into your next audit checklist

Let's go.


SC01: Access Control Vulnerabilities — Still #1, Still Devastating

The risk: Unauthorized callers invoking admin/governance/upgrade functions.

Access control has topped the list for three consecutive years. In 2025 alone, access control failures accounted for more stolen funds than any other category — from unprotected initialize() on proxy contracts to governance proposal hijacks.

Defense Pattern: Role-Based Access With Explicit Enumeration

Solidity (OpenZeppelin AccessControl):

import "@openzeppelin/contracts/access/AccessControl.sol";

contract SecureVault is AccessControl {
    bytes32 public constant WITHDRAWER = keccak256("WITHDRAWER");
    bytes32 public constant PAUSER = keccak256("PAUSER");

    constructor(address admin) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        // No roles granted by default — explicit only
    }

    function withdraw(uint256 amount) external onlyRole(WITHDRAWER) {
        // Critical: never rely on tx.origin
        _safeTransfer(msg.sender, amount);
    }

    function pause() external onlyRole(PAUSER) {
        _pause();
    }
}
Enter fullscreen mode Exit fullscreen mode

Solana/Anchor:

#[account]
pub struct VaultConfig {
    pub authority: Pubkey,
    pub pauser: Pubkey,
    pub is_initialized: bool,
}

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let config = &mut ctx.accounts.config;
    // Guard: prevent re-initialization
    require!(!config.is_initialized, ErrorCode::AlreadyInitialized);
    config.authority = ctx.accounts.authority.key();
    config.is_initialized = true;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Key lesson: Never ship a contract where initialize() can be called twice. On Solana, always store an is_initialized flag — the Anchor init constraint handles this, but custom programs often miss it.


SC02: Business Logic Vulnerabilities — The "Code Works But the Math Doesn't" Problem

The risk: Design-level flaws in lending, AMM, reward, or governance logic that break intended economic rules.

The Venus Protocol donation attack (early 2026) is a textbook example: an audit flagged the issue, the team dismissed it, and attackers exploited the same first-depositor share inflation bug — twice — for $2.15M in bad debt.

Defense Pattern: First-Depositor Protection + Invariant Assertions

contract SecureLendingPool {
    uint256 public constant MINIMUM_SHARES = 1000;

    function deposit(uint256 assets) external returns (uint256 shares) {
        if (totalSupply() == 0) {
            // Mint dead shares to prevent donation attack
            shares = assets - MINIMUM_SHARES;
            _mint(address(0xdead), MINIMUM_SHARES);
        } else {
            shares = (assets * totalSupply()) / totalAssets();
        }

        require(shares > 0, "Zero shares");
        _mint(msg.sender, shares);

        // Invariant: total assets must back total shares
        assert(totalAssets() >= totalSupply());
    }
}
Enter fullscreen mode Exit fullscreen mode

Takeaway: If your audit firm flags a business logic concern, don't dismiss it because "the probability is low." Attackers specifically hunt for dismissed findings in public audit reports.


SC03: Price Oracle Manipulation — The Enabler of 60%+ of DeFi Exploits

The risk: Weak oracles let attackers skew reference prices, enabling under-collateralized borrowing, unfair liquidations, and mispriced swaps.

The Aave CAPO oracle misfire showed how even a well-designed oracle with rate caps can fail when timestamp-to-ratio synchronization drifts — liquidating $26M in wstETH positions.

Defense Pattern: Multi-Oracle Aggregation With Staleness + Deviation Checks

function getSecurePrice(address asset) public view returns (uint256) {
    // Primary: Chainlink
    (, int256 clPrice, , uint256 clUpdatedAt, ) =
        chainlinkFeed.latestRoundData();
    require(block.timestamp - clUpdatedAt < MAX_STALENESS, "Stale CL feed");
    require(clPrice > 0, "Invalid CL price");

    // Secondary: TWAP from Uniswap V3 (30-min window)
    uint256 twapPrice = getTwapPrice(asset, 1800);

    // Cross-check: deviation > 5% = circuit breaker
    uint256 deviation = _percentDiff(uint256(clPrice), twapPrice);
    require(deviation < 500, "Oracle deviation too high");

    return uint256(clPrice);
}
Enter fullscreen mode Exit fullscreen mode

For Solana: Use Pyth with confidence interval checks — reject prices where the confidence band exceeds your protocol's tolerance:

let price_feed = load_price_feed_from_account_info(&ctx.accounts.pyth_price)?;
let current_price = price_feed.get_price_no_older_than(clock.unix_timestamp, 60)
    .ok_or(ErrorCode::StalePriceFeed)?;

// Reject if confidence interval > 2% of price
require!(
    current_price.conf as u64 * 50 < current_price.price.unsigned_abs(),
    ErrorCode::PriceTooUncertain
);
Enter fullscreen mode Exit fullscreen mode

SC04: Flash Loan–Facilitated Attacks — The Amplifier

The risk: Uncollateralized flash loans magnify small logic/pricing bugs into catastrophic drains.

Flash loans don't create vulnerabilities — they amplify them. Every bug in SC01–SC03 becomes 100x worse when an attacker can borrow $50M for zero collateral within a single transaction.

Defense Pattern: Per-Block Limits + Same-Transaction Detection

mapping(address => uint256) private _lastActionBlock;

modifier noFlashLoan() {
    require(
        _lastActionBlock[msg.sender] < block.number,
        "Same-block reentrancy"
    );
    _lastActionBlock[msg.sender] = block.number;
    _;
}

function borrow(uint256 amount) external noFlashLoan {
    // Attacker can't deposit + borrow in same block
    _executeBorrow(msg.sender, amount);
}
Enter fullscreen mode Exit fullscreen mode

SC05: Lack of Input Validation — The Quiet Killer

The risk: Missing validation on user inputs corrupts state or breaks protocol invariants.

The Resolv USR exploit (March 2026, $25M) was literally a missing max-mint check. The fix would have been one line of code.

Defense Pattern: Validate Everything, Trust Nothing

function mint(uint256 amount, address recipient) external {
    require(amount > 0, "Zero amount");
    require(amount <= MAX_MINT_PER_TX, "Exceeds max mint");
    require(recipient != address(0), "Zero address");
    require(totalSupply() + amount <= SUPPLY_CAP, "Supply cap exceeded");

    _mint(recipient, amount);
}
Enter fullscreen mode Exit fullscreen mode

One require statement. $25M saved. That's the cost-benefit ratio of input validation.


SC06: Unchecked External Calls — Silent Failures, Loud Exploits

The risk: External calls that fail silently, leaving state inconsistent.

Defense Pattern: Check-Effects-Interactions + Explicit Return Handling

function withdrawToken(IERC20 token, uint256 amount) external {
    // Effects FIRST
    balances[msg.sender] -= amount;

    // Interaction LAST with explicit check
    bool success = token.transfer(msg.sender, amount);
    require(success, "Transfer failed");
}
Enter fullscreen mode Exit fullscreen mode

For tokens that don't return a bool (USDT, BNB), use OpenZeppelin's SafeERC20:

using SafeERC20 for IERC20;
token.safeTransfer(msg.sender, amount);
Enter fullscreen mode Exit fullscreen mode

SC07: Arithmetic Errors — Death by a Thousand Rounding Errors

The risk: Subtle precision loss in share/interest/AMM calculations that can be repeatedly exploited.

Defense Pattern: Always Round Against the User

// Deposit: round DOWN (user gets fewer shares)
function convertToShares(uint256 assets) public view returns (uint256) {
    return totalSupply() == 0
        ? assets
        : assets.mulDiv(totalSupply(), totalAssets(), Math.Rounding.Down);
}

// Withdrawal: round UP (user needs more shares to redeem)
function convertToAssets(uint256 shares) public view returns (uint256) {
    return totalSupply() == 0
        ? shares
        : shares.mulDiv(totalAssets(), totalSupply(), Math.Rounding.Up);
}
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: In any division, ask "who benefits from rounding?" — it should always be the protocol, never the user.


SC08: Reentrancy Attacks — Old But Not Dead

The risk: External calls re-entering vulnerable functions before state updates complete.

Despite being well-known, reentrancy still makes the Top 10 because cross-function and cross-contract reentrancy patterns keep catching teams off guard.

Defense Pattern: ReentrancyGuard + CEI (Mandatory Combo)

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureVault is ReentrancyGuard {
    function withdraw(uint256 amount) external nonReentrant {
        require(balances[msg.sender] >= amount);

        // Effects before interactions
        balances[msg.sender] -= amount;

        // Interaction last
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);
    }
}
Enter fullscreen mode Exit fullscreen mode

On Solana, reentrancy is architecturally prevented by the runtime (a program can't CPI back into itself in the same instruction). But cross-program reentrancy via CPI chains is still possible — always validate account state after CPI calls.


SC09: Integer Overflow/Underflow — Solved in Theory, Broken in Practice

The risk: Unchecked arithmetic on platforms without native overflow protection.

Solidity 0.8+ has built-in overflow checks, but unchecked {} blocks (used for gas optimization) reintroduce the risk. On Solana, Rust's release builds use wrapping arithmetic by default.

Defense Pattern: Explicit Overflow Guards Where It Matters

// Solana/Anchor: use checked arithmetic in financial calculations
let new_balance = user_balance
    .checked_add(deposit_amount)
    .ok_or(ErrorCode::Overflow)?;

let interest = principal
    .checked_mul(rate)?
    .checked_div(RATE_DENOMINATOR)?;
Enter fullscreen mode Exit fullscreen mode

For Solidity: Never use unchecked around user-supplied values or financial math. Reserve it exclusively for loop counters and known-safe increments.


SC10: Proxy & Upgradeability Vulnerabilities — The Keys to the Kingdom

The risk: Misconfigured proxies allow attackers to take over implementations or reinitialize contracts.

Defense Pattern: UUPS With Initializer Guard + Storage Gap

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract SecureProtocol is UUPSUpgradeable, OwnableUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers(); // Prevent impl initialization
    }

    function initialize(address owner) external initializer {
        __Ownable_init(owner);
        __UUPSUpgradeable_init();
    }

    function _authorizeUpgrade(address) internal override onlyOwner {}

    // Reserve storage slots for future upgrades
    uint256[50] private __gap;
}
Enter fullscreen mode Exit fullscreen mode

The Meta-Lesson: Defense in Depth

The OWASP Top 10 isn't a checklist — it's a dependency graph. Flash loans amplify oracle manipulation. Oracle manipulation enables business logic exploits. Business logic exploits succeed because of missing input validation.

Your security stack should be:

  1. Development: Slither + Aderyn in CI (EVM) or Sec3 X-ray (Solana)
  2. Pre-deployment: Professional audit + formal verification for critical paths
  3. Post-deployment: Runtime monitoring + circuit breakers + bug bounty
  4. Ongoing: OWASP Top 10 as quarterly team review material

The teams that get hacked in 2026 won't be the ones who never heard of these vulnerabilities. They'll be the ones who knew about them and shipped anyway.


The full OWASP Smart Contract Top 10: 2026 is at scs.owasp.org/sctop10. The ranking methodology and 2025 incident data are also published — worth reading if you're doing threat modeling.

Building something and want eyes on your security architecture? Drop a comment — happy to dig in.

Top comments (0)