Skip to content

SC02:2026 Business Logic Vulnerabilities

Vulnerability: 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:

  • Invariant violations across modules (e.g., vault ↔ strategy ↔ gauge, collateral ↔ debt, supply ↔ backing)
  • Reward and fee logic (double-counting, under/over-accrual, wrong beneficiary)
  • Eligibility and limit checks that are bypassable or inconsistently enforced (borrowing caps, mint limits, liquidation thresholds)
  • Path-dependent state machines that allow manipulative action sequences to reach impossible or inconsistent states
  • Cross-module and cross-chain assumptions (e.g., bridge liquidity, L2 finality, message ordering)

Attackers exploit:

  • Arbitrage between inconsistent accounting (vault vs. strategy, internal vs. external balance)
  • Order-of-operations edge cases (deposit/withdraw/claim sequences that break invariants)
  • Eligibility bypasses (e.g., claiming rewards without stake, liquidating healthy positions)
  • Parameter manipulation (interest curves, fees, collateral factors) that create economically irrational states

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:

  • allowed is computed from the requested borrow amount, not the user’s collateral balance.
  • The check allowed >= amount always holds for collateralFactorBps >= 10_000, and is otherwise simply a tautology when misused like this, failing to enforce any economic invariant.

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:

  • Borrow limits derive from total collateral, not just requested amounts.
  • Economic invariant: debt[user] <= maxBorrow(user) is enforced on every borrow.
  • Price oracle is explicitly integrated (and can be hardened per SC03).

2025 Case Studies

Best Practices & Mitigations

  • Model protocol economics explicitly (e.g., with adversarial simulations / agent-based models) rather than relying on intuition.
  • Express core invariants in code and tests:
  • “Total value withdrawn cannot exceed total deposits + realized yield”
  • “Rewards distribution is proportional to time-weighted stake”
  • “Liquidations never result in protocol loss under honest oracle data”
  • Use formal verification and property-based fuzzing for key accounting paths (vaults, strategies, reward distribution).
  • Version and gate new strategies / spells:
  • Roll out behind caps.
  • Monitor metrics and on-chain invariants before raising limits.
  • Ensure governance and operations teams understand invariants, not just auditors.