Back to Cheat Sheets

🛡️ Lack of Input Validation

OWASP Smart Contract Top 10 (2025) - SC04

HIGH RISK

📋 What Is It?

Lack of Input Validation occurs when a contract trusts caller-supplied parameters - addresses, amounts, arrays, indexes, IDs, fees, deadlines, or raw calldata - and acts on them without checking they are within the values the logic expects. On a public chain anyone can call any function with any ABI-valid argument, so every unvalidated parameter is an open control. A single missing require can send tokens to address(0) (burning them), set a fee above 100%, brick the contract with an unusable owner, or redirect an external call to a malicious target - all permanent once mined.

SC04 OWASP Rank
Boundary Validate Here
1 Line To Fix

⚠️ Common Exploits

  • Zero-Address: Recipient/owner unchecked → burned funds or bricked contract
  • Unchecked Amounts: No > 0 or balance/allowance bound
  • Mismatched Arrays: Unequal or uncapped parallel arrays in loops
  • Unbounded Fees: Percentage set above 100% seizes all value
  • Bad Index/ID: Out-of-range read, double-claim, no ownership check
  • Arbitrary Target: Caller-supplied token/target → arbitrary external call
  • Expiry/Signature Bypass: Stale deadline or unbound signer honoured

🔴 Attack Flow

1. Enumerate external functions and their parameters
↓
2. Craft calldata directly, bypassing any front-end
↓
3. Pass a malicious value (0x0, huge fee, fake token)
↓
4. Contract acts on it with no check
↓
5. IMPACT: Funds burned, contract bricked, or state corrupted!

❌ Vulnerable Code

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Vault { address public owner; uint256 public feeBps; // newOwner == address(0) bricks every onlyOwner call forever function setOwner(address newOwner) external { require(msg.sender == owner, "not owner"); owner = newOwner; ← no zero-address check } // Nothing stops bps = 50_000 (500%) - fee can exceed the amount function setFeeBps(uint256 bps) external { feeBps = bps; ← no upper bound } // token and to are attacker-chosen → arbitrary external call function collect(address token, address to) external { IERC20(token).transferFrom(msg.sender, to, 1e18); ← untrusted } // amt may be shorter than to → revert; to may be huge → gas DoS function airdrop(address[] calldata to, uint256[] calldata amt) external { for (uint256 i; i < to.length; ++i) token.transfer(to[i], amt[i]); ← no length/cap check } }

✅ Secure Code

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {SafeERC20, IERC20} from "@openzeppelin/.../SafeERC20.sol"; contract Vault { using SafeERC20 for IERC20; address public owner; uint256 public feeBps; mapping(address => bool) public allowedToken; uint256 constant MAX_FEE_BPS = 1_000; // cap at 10% uint256 constant MAX_BATCH = 200; // Custom errors: cheaper than strings, self-documenting error ZeroAddress(); error FeeTooHigh(uint256 bps, uint256 max); error TokenNotAllowed(address token); error LengthMismatch(); error BatchTooLarge(uint256 len, uint256 max); function setOwner(address newOwner) external { if (newOwner == address(0)) revert ZeroAddress(); // validate first owner = newOwner; } function setFeeBps(uint256 bps) external { if (bps > MAX_FEE_BPS) revert FeeTooHigh(bps, MAX_FEE_BPS); feeBps = bps; } function collect(address token, uint256 amount) external { if (!allowedToken[token]) revert TokenNotAllowed(token); // allow-list IERC20(token).safeTransferFrom(msg.sender, address(this), amount); } function airdrop(address[] calldata to, uint256[] calldata amt) external { if (to.length != amt.length) revert LengthMismatch(); if (to.length > MAX_BATCH) revert BatchTooLarge(to.length, MAX_BATCH); for (uint256 i; i < to.length; ++i) { if (to[i] == address(0)) revert ZeroAddress(); token.safeTransfer(to[i], amt[i]); } } }

✓ Prevention Checklist

  • Reject address(0) for owners/recipients/tokens
  • Require amounts > 0 and within balance/allowance
  • Enforce equal array length and a MAX_BATCH cap
  • Bounds-check indexes and IDs; verify ownership
  • Give every fee/percentage an explicit upper bound
  • Allow-list caller-supplied tokens and call targets
  • Validate deadlines, signers, and nonces before acting
  • Check code.length when a contract is required
  • Wrap token calls with SafeERC20
  • Validate early, before any state change (CEI)
  • Prefer custom errors over string reverts
  • Write negative tests proving bad inputs revert

🔍 Detection & Tools

Analysis Tools:

Slither Mythril Foundry Echidna Manticore

Libraries:

OpenZeppelin SafeERC20 ECDSA / EIP-712 Tenderly

How to Test:

  • Fuzz every external function: forge test --fuzz-runs 10000
  • Negative tests: transfer(address(0), x) must revert
  • Assert setFeeBps(20_000) reverts with the expected error
  • Feed mismatched/oversized airdrop arrays and confirm revert
  • Run slither . to flag missing zero-address/bounds checks

🌍 Real-World

  • Zero-address burns (recurring class): Transfer, mint, or ownership functions accepted a recipient without checking address(0); funds sent there are irretrievable, and setting ownership to zero made privileged functions permanently uncallable. OpenZeppelin's ERC-20/721 base contracts add this check for exactly this reason.
  • Arbitrary token / target abuse: A function accepted a caller-supplied token or call target with no allow-list; attackers supplied a contract they controlled to force arbitrary external calls, approvals, or fake return values.
  • Unbounded / mismatched array loops: Airdrops and multi-sends looped over caller arrays without equal-length or size checks; mismatched lengths paid wrong amounts, and oversized arrays exceeded the block gas limit - a denial of service.
  • Common thread: Each traces back to a caller-supplied value acted on without a check - often mislabeled later as access-control or re-entrancy, but a missing require opened the door.

📌 Quick Tips

  • DO NOT trust the front-end to validate
  • DO NOT store/send to an unchecked address
  • DO NOT act on a caller-supplied token/target blindly
  • DO validate at the top of every function
  • DO bound amounts, fees, and array lengths
  • DO allow-list critical addresses
  • DO fuzz and write negative tests