Skip to main content
Sigvex

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.

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’s Address Utilities

Use the Address library from OpenZeppelin’s library to verify that a target is a contract before making external calls:

import "@audited/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;
}

References