Back

SC04: Lack of Input Validation - Overview

What is Lack of Input Validation?

Lack of Input Validation occurs when a smart contract trusts caller-supplied parameters — addresses, amounts, arrays, indexes, IDs, fees, deadlines, or raw calldata — and acts on them without checking that they are within the values the logic actually expects. On a public blockchain anyone can call your functions with any arguments the ABI permits, so every unvalidated parameter is an open control that an attacker can turn to a value you never intended.

Unlike a traditional web app, a smart contract cannot “sanitise and retry.” State changes are executed exactly as written and, once mined, are permanent. A single missing require can send tokens to address(0) (burning them forever), let a caller drain more than they own, set a fee above 100 %, brick the contract by assigning an unusable owner, or redirect an external call to a malicious target. The flaw is not exotic cryptography — it is the everyday gap between what a parameter could be and what the code assumes it is.

Core Concept

Validated input (secure):
  address recipient  -> require(recipient != address(0))
  uint256 amount     -> require(amount > 0 && amount <= balance[msg.sender])
  uint256 feeBps     -> require(feeBps <= 10_000)            // <= 100.00%
  address[] users /  -> require(users.length == amounts.length)
  uint256[] amounts     require(users.length <= MAX_BATCH)
  uint256 tokenId    -> require(tokenId < totalSupply)
  address token      -> require(allowedTokens[token])       // allow-list
  uint256 deadline   -> require(block.timestamp <= deadline)

Unvalidated input (vulnerable):
  address recipient  -> transfer straight to whatever was passed (incl. 0x0)
  uint256 amount     -> subtract without checking the balance/allowance
  uint256 feeBps     -> store 50_000 (500%) with no upper bound
  address[] users /  -> loop over mismatched arrays, corrupt state
  uint256[] amounts
  uint256 tokenId    -> index an array out of range or reuse an ID
  address token      -> call an arbitrary, attacker-chosen token/target
  uint256 deadline   -> honour a signature/order that should have expired

Why It's Critical for Smart Contracts

Several properties of on-chain code make missing validation uniquely dangerous:

Why Does This Matter?

Business Impact

Technical Impact

Technical Context

Common Missing-Validation Scenarios

1. Missing Zero-Address Checks

function setOwner(address newOwner) external onlyOwner {
    owner = newOwner;          // newOwner == address(0) bricks every onlyOwner call
}

function withdraw(address to, uint256 amount) external {
    token.transfer(to, amount); // to == address(0) burns the tokens forever
}

Risk: Burned funds, or a contract with no reachable owner — permanent and unrecoverable.

2. Unchecked Amounts

function transfer(address to, uint256 amount) external {
    balances[msg.sender] -= amount;   // no check amount <= balance
    balances[to] += amount;           // underflow guard exists post-0.8, but
}                                     // logic still allows amount == 0 spam / bad invariants

Risk: Spending more than owned (pre-0.8 underflow), zero-value operations that corrupt invariants, or amounts exceeding an allowance.

3. Mismatched Parallel Arrays (Airdrop Loops)

function airdrop(address[] calldata users, uint256[] calldata amounts) external {
    for (uint256 i = 0; i < users.length; i++) {
        token.transfer(users[i], amounts[i]);  // amounts.length may be < users.length
    }                                           // -> revert, or reuse of a stale slot
}

Risk: Reverts, unintended payouts, or an unbounded loop that exceeds the block gas limit.

4. Unvalidated Indexes / IDs

function claim(uint256 index) external {
    Reward memory r = rewards[index];  // index >= rewards.length or already-claimed
    payable(msg.sender).transfer(r.value);
}

Risk: Out-of-range access, double-claims, or operating on an ID that does not belong to the caller.

5. Missing Bounds on Fees / Percentages

function setFee(uint256 feeBps) external onlyOwner {
    fee = feeBps;              // no require(feeBps <= 10_000) -> fee can exceed 100%
}

Risk: A fee above the intended maximum can capture the entire transaction value.

6. Unchecked Token / Target Addresses

function sweep(address token, address to) external {
    IERC20(token).transfer(to, IERC20(token).balanceOf(address(this)));
}                              // token/to are attacker-chosen -> arbitrary external call

Risk: Interaction with a malicious token/target, arbitrary external calls, and re-entrancy or approval abuse.

Where Missing Validation Hides

Input TypeTypical Missing CheckConsequence
Address (owner/recipient)!= address(0)Burned funds, bricked contract
Amount / value> 0, within balance/allowanceOver-spend, corrupted accounting
Parallel arraysEqual length, capped lengthState corruption, gas-limit DoS
Index / token IDWithin range, ownershipOut-of-range read, double-claim
Fee / percentageUpper bound (e.g. <= 10_000)Value seizure, uneconomic system
Token / call targetAllow-list, code-sizeArbitrary external call, theft
Deadline / signatureNot expired, valid signerReplay, expiry bypass

Real-World Impact

Case Study 1: Zero-Address Burns (recurring class)

Missing validation:

Impact:

Root Cause: A single missing require(to != address(0)). This class is common enough that the OpenZeppelin ERC-20/ERC-721 base contracts add the zero-address check for exactly this reason.

Case Study 2: Arbitrary Token / Target Abuse (recurring class)

Missing validation:

Impact:

Root Cause: Trusting a caller-supplied address as if it were a known-good contract. The defensive pattern is an allow-list of vetted tokens/targets plus SafeERC20 for token calls.

Case Study 3: Unbounded / Mismatched Array Loops (recurring class)

Missing validation:

Impact:

Root Cause: No require(a.length == b.length) and no maximum batch size. Fixed by validating lengths and bounding iteration.

Note: these are described as classes of incident. Specific losses vary by protocol and year; the durable lesson is that each traces back to a caller-supplied value that the contract acted on without checking.

Prevalence and Statistics

Lack of Input Validation sits in the OWASP Smart Contract Top 10 (2025) as SC04 because it is both extremely common and easy to introduce: any function that takes a parameter is a candidate. It frequently underlies findings that get labelled as other issues (access-control bypass, re-entrancy setup, accounting bugs) because a missing check is what opened the door.

Rather than cite a single figure, the defensible picture is:

Common Misunderstandings

Myth 1: “Solidity 0.8 checked arithmetic covers me”

Reality: Built-in overflow/underflow checks stop a specific class of bug. They do not validate that an address is non-zero, that a fee is under 100 %, that arrays match, or that a target is trusted. Those remain your job.

Myth 2: “The front-end already validates the inputs”

Reality: Anyone can call the contract directly with crafted calldata, bypassing your UI entirely. Client-side checks are convenience, never security. Validate at the contract boundary.

Myth 3: “Only privileged functions need checks”

Reality: Public functions that move value or write state need validation regardless of who can call them. Access control answers who; input validation answers with what values — both are required.

Myth 4: “A zero address is obviously wrong, no one would pass it”

Reality: Uninitialised variables, buggy front-ends, and integrating contracts pass address(0) routinely. Because the result is a permanent burn or brick, it is one of the highest-severity omissions.

Myth 5: “Validating costs too much gas”

Reality: A require or custom-error revert is a handful of gas — negligible against the cost of permanently lost funds. Custom errors (revert ZeroAddress()) are cheaper than string reverts and keep validation affordable.

Myth 6: “If it reverts on bad input, that's good enough”

Reality: A late revert deep in a function can still leave partial state changes in some patterns, waste gas, or enable griefing. Validate early, at the top of the function, before any state is touched.

How Lack of Input Validation Differs from Related Issues

AspectLack of Input Validation (SC04)Access ControlRe-entrancy
Root causeUntrusted parameter valuesMissing caller authorisationUnsafe external call ordering
Question it answersWith what values?Who may call?When is state final?
Typical fixrequire/custom errors at entryRole/owner modifiersChecks-effects-interactions
DetectionParameter review, fuzzingModifier auditCall-graph analysis

Key Takeaways

  1. Every external parameter is attacker-controlled — anyone can call your functions with any ABI-valid value.
  2. Zero-address checks are non-negotiable for owners, recipients, and token addresses — the failure mode is permanent.
  3. Bound everything — amounts, fees, percentages, and array lengths all need explicit upper (and lower) limits.
  4. Allow-list critical addresses and targets rather than trusting whatever a caller supplies.
  5. Validate early and fail clearly — check at the top of the function with require or custom errors, before touching state.

How to Identify if You're Vulnerable

Ask these questions about your contract:

If you answered “no” or “not sure” to several of these, you likely have exploitable missing validation today.

Next Steps