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();
}
}
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(())
}
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());
}
}
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);
}
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
);
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);
}
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);
}
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");
}
For tokens that don't return a bool (USDT, BNB), use OpenZeppelin's SafeERC20:
using SafeERC20 for IERC20;
token.safeTransfer(msg.sender, amount);
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);
}
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);
}
}
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)?;
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;
}
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:
- Development: Slither + Aderyn in CI (EVM) or Sec3 X-ray (Solana)
- Pre-deployment: Professional audit + formal verification for critical paths
- Post-deployment: Runtime monitoring + circuit breakers + bug bounty
- 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)