SC02:2026 - Business Logic Vulnerabilities

Description

Business logic vulnerabilities describe any situation where a smart contract’s intended economic or functional behavior can be subverted even though individual low-level checks (e.g., type safety, reentrancy guards, access control) are correct. These are design flaws in how the system’s rules, incentives, state transitions, and invariants are modeled on-chain. Unlike low-level bugs (overflow, reentrancy), business logic flaws arise when the rules themselves are unsafe—the code “does what it says”, but what it says permits exploitable outcomes.

This applies across all smart contract domains: DeFi (lending, AMMs, vaults, yield strategies), NFTs (minting logic, royalties, marketplace mechanics), DAOs (voting, delegation, proposal execution), bridges (burn/mint asymmetry, liquidity rules), gaming (reward distribution, fairness), and cross-chain/L2 systems where multi-hop state transitions create emergent vulnerabilities.

Few areas to focus on:

Attackers exploit:

Example (Vulnerable Lending Logic)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerableLending {
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    uint256 public collateralFactorBps = 7500; // 75%

    function depositCollateral() external payable {
        collateral[msg.sender] += msg.value;
    }

    // Vulnerable: calculates borrow capacity using the *new* amount, not total
    function borrow(uint256 amount) external {
        uint256 allowed = (amount * collateralFactorBps) / 10_000;
        require(allowed >= amount, "not enough collateral"); // meaningless check

        debt[msg.sender] += amount;
        // send tokens from pool (omitted)
    }
}

Issues:

Example (Fixed: Invariant-Based Borrow Logic)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IPriceOracle {
    function getCollateralPrice() external view returns (uint256); // 1e18
}

contract SaferLending {
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    uint256 public collateralFactorBps = 7500; // 75%
    IPriceOracle public oracle;

    constructor(IPriceOracle _oracle) {
        oracle = _oracle;
    }

    function depositCollateral() external payable {
        require(msg.value > 0, "no collateral");
        collateral[msg.sender] += msg.value;
    }

    function _maxBorrow(address user) internal view returns (uint256) {
        uint256 price = oracle.getCollateralPrice(); // e.g. ETH price in USD 1e18
        uint256 collateralUsd = (collateral[user] * price) / 1e18;
        return (collateralUsd * collateralFactorBps) / 10_000;
    }

    function borrow(uint256 amountUsd) external {
        uint256 maxBorrowUsd = _maxBorrow(msg.sender);
        require(debt[msg.sender] + amountUsd <= maxBorrowUsd, "exceeds limit");

        debt[msg.sender] += amountUsd;
        // transfer stablecoin from pool (omitted)
    }
}

Security Improvements:

2025 Case Studies

Best Practices & Mitigations