- name
- input-validation
- description
- Auto-loaded by logic-auditor agent during Phase 2. Provides detection patterns for: missing zero checks, address validation, array bounds, parameter constraints, calldata validation, encoding issues. Core artifact: Input Validation Matrix.
- user-invocable
- false
# Input Validation Vulnerability Analysis
**OWASP SC04:2025** - Lack of Input Validation allows attackers to pass unexpected values that break contract invariants or enable exploits.
**2025-2026 Statistics**: Input validation failures account for **34.6% of all smart contract vulnerabilities**, contributing to $420M+ in losses.
---
## Why Input Validation Fails (Root Causes)
### Root Cause 1: Assumption of Good Faith
Developers assume callers will provide sensible inputs.
```solidity
// VULNERABLE: Assumes recipient is valid
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount;
balances[to] += amount; // @audit to = address(0) burns tokens
}
```
**Attacker's view**: "They didn't check my input. Let me see what happens with edge cases."
### Root Cause 2: Missing Bounds Checking
Parameters have implicit bounds that aren't enforced.
```solidity
// VULNERABLE: No bounds on fee
function setFee(uint256 newFee) external onlyOwner {
fee = newFee; // @audit newFee = 100% drains users
}
```
### Root Cause 3: Type Confusion
Solidity's loose typing allows unexpected conversions.
```solidity
// VULNERABLE: Assumes bytes4 is valid selector
function executeCall(address target, bytes4 selector, bytes calldata data) external {
(bool success,) = target.call(abi.encodePacked(selector, data));
// @audit selector could be anything, including dangerous functions
}
```
### Root Cause 4: Array/Calldata Trust
Trusting array lengths and calldata structure from external sources.
```solidity
// VULNERABLE: Trusts array lengths match
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint256 i = 0; i < recipients.length; i++) {
_transfer(recipients[i], amounts[i]); // @audit Arrays may have different lengths
}
}
```
---
## The Input Validation Matrix (Core Artifact)
For each external function, document:
| Function | Parameter | Expected Range | Actual Check | Gap |
|----------|-----------|----------------|--------------|-----|
| transfer | to | != address(0) | None | **YES** |
| transfer | amount | <= balance | require(balance >= amount) | No |
| setFee | newFee | 0-1000 (0-10%) | None | **YES** |
| batchTransfer | recipients | len > 0 | None | **YES** |
| batchTransfer | amounts | len == recipients.len | None | **YES** |
---
## Detection Patterns
### Pattern 1: Missing Zero Address Check
**Root Cause**: Assumption of Good Faith
```solidity
// VULNERABLE: Zero address burns tokens/ETH
function withdraw(address recipient) external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(recipient).transfer(amount); // @audit recipient = 0x0 burns ETH
}
// VULNERABLE: Zero address as critical role
function setAdmin(address newAdmin) external onlyOwner {
admin = newAdmin; // @audit newAdmin = 0x0 bricks admin functions
}
```
**Attack Flow**:
1. Attacker (or accident) calls with address(0)
2. Tokens/ETH sent to zero address are burned
3. Or critical role set to zero, bricking functionality
4. Irreversible loss
**Search Queries**:
```
Grep("address.*=|= address", glob="**/*.sol")
Grep("require.*!= address\\(0\\)", glob="**/*.sol")
Grep("setAdmin|setOwner|set.*Address", glob="**/*.sol")
```
**Mitigation**:
```solidity
function setAdmin(address newAdmin) external onlyOwner {
require(newAdmin != address(0), "Zero address");
admin = newAdmin;
}
```
### Pattern 2: Missing Amount/Value Bounds
**Root Cause**: Missing Bounds Checking
```solidity
// VULNERABLE: Fee can be set to 100%
function setFee(uint256 newFee) external onlyOwner {
fee = newFee; // @audit No maximum check
}
function withdraw(uint256 amount) external {
uint256 feeAmount = amount * fee / 10000;
uint256 netAmount = amount - feeAmount; // @audit Can underflow if fee > 10000
token.transfer(msg.sender, netAmount);
}
```
**Attack Flow**:
1. Malicious/compromised admin sets fee = 10000 (100%)
2. All user withdrawals get 0 tokens
3. Or fee = 20000 causes underflow (reverts all withdrawals)
**Search Queries**:
```
Grep("setFee|setRate|setMultiplier|setPercent", glob="**/*.sol")
Grep("external.*uint.*\\{[^}]*=[^}]*\\}", glob="**/*.sol")
```
**Mitigation**:
```solidity
uint256 public constant MAX_FEE = 1000; // 10%
function setFee(uint256 newFee) external onlyOwner {
require(newFee <= MAX_FEE, "Fee too high");
fee = newFee;
}
```
### Pattern 3: Array Length Mismatch
**Root Cause**: Array/Calldata Trust
```solidity
// VULNERABLE: No length check
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external {
for (uint256 i = 0; i < recipients.length; i++) {
_transfer(recipients[i], amounts[i]); // @audit Out of bounds if amounts shorter
}
}
```
**Attack Flow**:
1. Attacker calls with recipients.length = 10, amounts.length = 5
2. Loop accesses amounts[5], amounts[6], etc.
3. Reverts or reads garbage data
4. Unexpected behavior
**Search Queries**:
```
Grep("\\[\\].*calldata.*,.*\\[\\].*calldata", glob="**/*.sol")
Grep("for.*recipients\\.length|for.*addresses\\.length", glob="**/*.sol")
```
**Mitigation**:
```solidity
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external {
require(recipients.length == amounts.length, "Length mismatch");
require(recipients.length > 0, "Empty array");
require(recipients.length <= MAX_BATCH_SIZE, "Batch too large");
// ...
}
```
### Pattern 4: Missing Zero Amount Check
**Root Cause**: Assumption of Good Faith
```solidity
// VULNERABLE: Zero amount wastes gas and may break logic
function stake(uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
stakes[msg.sender] += amount;
emit Staked(msg.sender, amount);
// @audit amount = 0 creates empty stake event, may break off-chain tracking
}
// VULNERABLE: Zero amount mints shares
function deposit(uint256 assets) external returns (uint256 shares) {
shares = convertToShares(assets);
// @audit If totalAssets = 0, assets = 0 gives shares = 0, wasting gas
_mint(msg.sender, shares);
}
```
**Search Queries**:
```
Grep("function.*deposit.*uint|function.*stake.*uint", glob="**/*.sol")
Grep("require.*> 0|require.*!= 0", glob="**/*.sol")
```
### Pattern 5: Dangerous Selector/Calldata
**Root Cause**: Type Confusion
```solidity
// VULNERABLE: Arbitrary function call
function execute(address target, bytes calldata data) external onlyOwner {
(bool success,) = target.call(data); // @audit Can call ANY function
require(success);
}
// VULNERABLE: Callback with untrusted selector
function processCallback(bytes4 selector, bytes calldata params) external {
(bool success,) = msg.sender.call(abi.encodePacked(selector, params));
// @audit Attacker controls selector
}
```
**Search Queries**:
```
Grep("\\.call\\(data\\)|\\.call\\(.*calldata", glob="**/*.sol")
Grep("bytes4.*selector|abi\\.encodePacked\\(.*selector", glob="**/*.sol")
```
**Mitigation**:
```solidity
// Whitelist allowed selectors
mapping(bytes4 => bool) public allowedSelectors;
function execute(address target, bytes calldata data) external onlyOwner {
bytes4 selector = bytes4(data[:4]);
require(allowedSelectors[selector], "Selector not allowed");
(bool success,) = target.call(data);
require(success);
}
```
### Pattern 6: Contract Address vs EOA
**Root Cause**: Missing Address Type Check
```solidity
// VULNERABLE: Assumes EOA, but could be contract
function sendReward(address winner) external {
payable(winner).transfer(reward); // @audit Contract might reject ETH
}
// VULNERABLE: Assumes contract, but could be EOA
function callHook(address hook) external {
IHook(hook).onAction(); // @audit EOA call succeeds silently (no code)
}
```
**Search Queries**:
```
Grep("\\.transfer\\(|\\.send\\(", glob="**/*.sol")
Grep("address\\.code\\.length|isContract", glob="**/*.sol")
```
**Mitigation**:
```solidity
function sendReward(address winner) external {
// Use call instead of transfer
(bool success,) = winner.call{value: reward}("");
require(success, "Transfer failed");
}
function callHook(address hook) external {
require(hook.code.length > 0, "Not a contract");
IHook(hook).onAction();
}
```
### Pattern 7: Deadline/Timestamp Validation
**Root Cause**: Missing Temporal Bounds
```solidity
// VULNERABLE: No deadline check
function swap(uint256 amountIn, uint256 minAmountOut) external {
// Swap can execute at any time, even if price changed
uint256 amountOut = _calculateSwap(amountIn);
require(amountOut >= minAmountOut);
// @audit Transaction pending for hours still executes
}
```
**Mitigation**:
```solidity
function swap(
uint256 amountIn,
uint256 minAmountOut,
uint256 deadline
) external {
require(block.timestamp <= deadline, "Expired");
// ...
}
```
---
## Input Validation Checklist
### Address Validation
- [ ] Zero address rejected where appropriate
- [ ] Self-address rejected if dangerous
- [ ] Contract vs EOA distinguished when needed
- [ ] Address format validated (if applicable)
### Amount Validation
- [ ] Zero amounts handled appropriately
- [ ] Maximum bounds enforced
- [ ] Minimum bounds enforced
- [ ] Overflow impossible (Solidity 0.8+)
### Array Validation
- [ ] Empty arrays rejected or handled
- [ ] Maximum length enforced
- [ ] Multiple array lengths match
- [ ] No out-of-bounds access
### Temporal Validation
- [ ] Deadlines enforced for time-sensitive ops
- [ ] Future timestamps validated
- [ ] Past timestamps handled
### Calldata Validation
- [ ] Selectors whitelisted if arbitrary calls
- [ ] Encoding validated
- [ ] Return data validated
---
## Search Query Reference
```
# Find missing zero checks
Grep("address.*=|setAdmin|setOwner|set.*Address", glob="**/*.sol")
Grep("payable\\(.*\\)\\.transfer", glob="**/*.sol")
# Find parameter setters
Grep("function set.*external|function update.*external", glob="**/*.sol")
Grep("onlyOwner|onlyAdmin", glob="**/*.sol")
# Find array operations
Grep("\\[\\].*calldata|\\[\\].*memory", glob="**/*.sol")
Grep("\\.length", glob="**/*.sol")
# Find arbitrary calls
GitHub에서 보기