Input Validation Remediation
How to properly validate user-supplied inputs in smart contracts to prevent arbitrary fund routing, unbounded operations, and zero-address vulnerabilities.
Input Validation Remediation
Overview
Related Detector: Missing Input Validation
Missing input validation is a broad vulnerability class where functions accept user-supplied parameters and use them in sensitive operations (fund transfers, storage writes, external calls) without verifying that the values conform to expected constraints. The recommended fix is to validate all external inputs at function entry before any state changes occur, following the checks-effects-interactions pattern.
Input validation failures have contributed to some of the largest smart contract exploits in history, including the Poly Network exploit ($611M, 2021) where an attacker-controlled address parameter was passed directly to a cross-chain call without allowlist validation.
Recommended Fix
Before (Vulnerable)
contract VulnerableVault {
mapping(address => uint256) public balances;
// VULNERABLE: No validation on recipient or amount
function withdraw(address recipient, uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, ) = recipient.call{value: amount}("");
require(success);
}
}
After (Fixed)
contract SecureVault {
mapping(address => uint256) public balances;
// FIXED: Validates all inputs before state changes
function withdraw(address recipient, uint256 amount) external {
// Input validation — all checks at function entry
require(recipient != address(0), "Zero address recipient");
require(amount > 0, "Zero withdrawal amount");
require(amount <= balances[msg.sender], "Insufficient balance");
// Effects — state changes before external calls
balances[msg.sender] -= amount;
// Interactions — external call after state updates
(bool success, ) = recipient.call{value: amount}("");
require(success, "Transfer failed");
}
}
The fix validates the recipient address against the zero address, ensures the amount is positive and within the sender’s balance, and follows the checks-effects-interactions pattern to prevent reentrancy.
Alternative Mitigations
Address Allowlisting
For functions that route calls or funds to external contracts, maintain an allowlist of approved addresses rather than accepting arbitrary user-supplied addresses:
mapping(address => bool) public approvedRecipients;
modifier onlyApprovedRecipient(address recipient) {
require(approvedRecipients[recipient], "Recipient not approved");
_;
}
function transferTo(address recipient, uint256 amount)
external
onlyApprovedRecipient(recipient)
{
// Safe — recipient is pre-approved
_transfer(recipient, amount);
}
OpenZeppelin Address Utilities
Use the Address library from OpenZeppelin to verify that a target is a contract before making external calls:
import "@openzeppelin/contracts/utils/Address.sol";
function callTarget(address target, bytes calldata data) external {
require(Address.isContract(target), "Target is not a contract");
// Proceed with validated target
}
Bounded Numeric Ranges
For numeric inputs that should fall within specific ranges, define constants and enforce them:
uint256 public constant MAX_SLIPPAGE_BPS = 500; // 5%
uint256 public constant MIN_DEPOSIT = 0.01 ether;
function swap(uint256 amount, uint256 slippageBps) external {
require(amount >= MIN_DEPOSIT, "Below minimum deposit");
require(slippageBps <= MAX_SLIPPAGE_BPS, "Slippage too high");
// Proceed with bounded values
}
Common Mistakes
Validating Only Some Parameters
A frequent mistake is validating some inputs but not others. Every parameter that flows into a sensitive operation must be validated:
// WRONG: Validates amount but not recipient
function transfer(address to, uint256 amount) external {
require(amount > 0, "Zero amount"); // amount checked
// to is NOT checked — could be address(0)
_transfer(to, amount);
}
Checking After State Changes
Validation must happen before any state modifications, not after:
// WRONG: State change before validation
function withdraw(uint256 amount) external {
balances[msg.sender] -= amount; // State change first!
require(amount <= maxWithdrawal); // Check after — too late
payable(msg.sender).transfer(amount);
}
Trusting msg.sender as Validation
The fact that msg.sender is an authenticated caller does not validate the parameters they supply. A legitimate user can still pass malicious input values:
// WRONG: Assumes msg.sender won't pass bad inputs
function setPrice(uint256 newPrice) external onlyOwner {
// Even the owner could accidentally set price to 0 or type(uint256).max
price = newPrice;
}
// RIGHT: Validate even privileged inputs
function setPrice(uint256 newPrice) external onlyOwner {
require(newPrice > 0, "Zero price");
require(newPrice <= MAX_PRICE, "Price exceeds maximum");
price = newPrice;
}