Back to blogs
Written by
Mayowa Olatunji
Published on
February 24, 2025

Missing or Improper Input Validation in Smart Contracts

Learn how improper input validation in Solidity can lead to exploits. Explore best practices for securing your smart contracts and preventing security risks.

Table of Contents

In Solidity, users or external contracts interact with smart contracts by invoking functions, which may require passing input parameters. Inputs can range from simple values like addresses and transaction amounts to complex data structures that shape the contract’s behavior and execution.

Given the immutable nature of smart contracts, ensuring inputs are properly validated is crucial. Input validation is the first line of defense against vulnerabilities. Without proper validation, malicious actors can manipulate contract logic, cause unintended behaviors, or even gain unauthorized access to sensitive functions. 

In traditional applications, flawed input handling can usually be patched with a quick update. Conversely, errors in smart contracts can lead to irreversible financial losses and security breaches.

Therefore, this article will explore the importance of input validation in Solidity and the risks associated with missing or improper validation. It will also share best practices for securing smart contracts against such vulnerabilities.

First, let's look at how input validation happens at compile-time and runtime in Solidity.

How compile-time and runtime validation works in Solidity

Input validation in Solidity occurs at two stages—compile-time and runtime—each playing an important role in ensuring contract correctness and security.

Compile-time validation in Solidity

Compile-time validation occurs when the Solidity compiler checks for errors before deploying the contract. The compiler enforces type safety, function visibility rules, and structural correctness, but it does not validate logical constraints on inputs.

For example, the following contract will fail to compile due to a type mismatch in the input parameter _input and the storage variable value:

// This contract will not compile due to a type mismatch
pragma solidity ^0.8.20;

contract Example {
    uint256 public value;

    function setValue(string memory _input) public {
        value = _input; // Error: Type mismatch (string assigned to uint256)
    }
}

Here, assigning a string to a uint256 triggers a compile-time error. The Solidity compiler prevents such issues before deployment, ensuring basic type safety. However, it does not ensure a value is within a valid range or follows a specific format.

Runtime validation in Solidity

Runtime input validation in Solidity enforces constraints on user-provided inputs during contract execution. It typically uses conditional statements or functions like require(), revert(), and assert(), which terminate execution and revert state changes when inputs do not meet expected conditions.

At runtime, the Ethereum Virtual Machine (EVM) enforces constraints like gas limits, type safety, memory access restrictions, and arithmetic operations to maintain execution integrity. However, it does not inherently validate business logic—such as verifying whether an input is within a valid range or if a caller has the necessary permissions. Developers must explicitly implement these checks. Otherwise, the EVM will execute transactions without restriction.

In the SafeContract example below, input validation occurs at runtime.

pragma solidity ^0.8.0;

contract SafeContract {
    uint256 public balance;

    function deposit(uint256 _amount) public {
        require(_amount > 100, "Amount must be greater than 100");
        balance += _amount;
    }
}

The require() statement ensures that _amount is always greater than 100. If a user attempts to send _amount <100, the transaction will fail at runtime, preventing invalid input from affecting the contract's state.

Now that we have explored these input validation types, let us define what qualifies as improper or missing input validation.

What constitutes an improper or missing input validation?

Improper or missing input validation occurs when a smart contract does not adequately verify the correctness, type, or range of the inputs it processes. Below are some forms of improper or missing input validation in Solidity.

1. No range checks

Failing to ensure that inputs fall within acceptable boundaries can lead to unintended behavior. For example, in setPrice, a user could set an extremely high or negative price (if stored in an int), disrupting contract logic.

function setPrice(uint256 _price) public {
    price = _price; // No upper or lower limit enforced
}


2. No type enforcement

Without strict type enforcement, unwanted values will slip through and may break the contract.In setAddress, a malicious user can pass address(0), causing logic failures if the contract assumes a valid address.

function setAddress(address _user) public {
    user = _user; // No validation for zero address
}

3. Failure to restrict unauthorized inputs 

Failing to restrict input values in functions can result in an inconsistent contract state. For example, a user could call transfer, with more than their available balance, leading to incorrect state updates in the balances mapping.

function transfer(address _to, uint256 _amount) public {
    balances[_to] += _amount; // No validation of sender balance
}

4. Improper length or format checks

An incorrectly formatted input is any value that does not adhere to the expected structure, length, or character constraints defined by the contract's logic. For instance, an attacker could call setUsername with excessively large strings, increasing gas costs or triggering unexpected contract behavior.

function setUsername(string memory _name) public {
    username = _name; // No check on length
}

5. Ignoring edge cases in boolean inputs

Boolean variables (true/false) seem safe, but they can still be misused if improperly validated. For example, if the _flag controls access to a sensitive function and is set incorrectly, unintended behavior can occur.

function setFlag(bool _flag) public {
flag = _flag;
}

// Should have used:
// require(_flag == true, "Invalid flag value"); before setting flag to //inputs


Now that we've defined improper and missing input validation, let's explore deeper through the following case studies.

Case study of missing or improper input validation: vulnerabilities and mitigation

Improper input validation is a leading cause of many confirmed vulnerabilities in smart contracts, often resulting in severe security breaches. Below are some examples of reported vulnerabilities due to improper or missing input validation.


1. Missing input validation in the Sovryn smart contracts borrow function

Consider this vulnerable borrow function in the Sovryn-smart-contracts. The function takes in several input arguments and implements several input validations, as seen below. However, the function is still vulnerable to malicious attacks due to missing input validation. 

function borrow(
    bytes32 loanId, // 0 if new loan
    uint256 withdrawAmount,
    uint256 initialLoanDuration, // duration in seconds
    uint256 collateralTokenSent,
    address collateralTokenAddress,
    address borrower,
    address receiver,
    bytes memory /*loanDataBytes*/ // arbitrary order data (for future use)
) public payable nonReentrant hasEarlyAccessToken returns (uint256, uint256) {
    require(withdrawAmount != 0, "6");

    _checkPause();

    // Temporary: limit transaction size
    if (transactionLimit[collateralTokenAddress] > 0) {
        require(collateralTokenSent <= transactionLimit[collateralTokenAddress]);
    }

    require(msg.value == 0 || msg.value == collateralTokenSent, "7");
    require(collateralTokenSent != 0 || loanId != 0, "8");
    require(collateralTokenAddress != address(0) || msg.value != 0 || loanId != 0, "9");

    if (collateralTokenAddress == address(0)) {
        collateralTokenAddress = wrbtcTokenAddress;
    }

    require(collateralTokenAddress != loanTokenAddress, "10");

//**************SOME OTHER LOGIC***********//
}

The borrow function allows malicious users to pass in the loanId parameter of unused collaterals and any receiver address as input values. This validation gap allows a malicious user to take out a loan using another user's collateral—without obligation to repay.

To fix this vulnerability, the function must validate that new users cannot enter with an existing loanId or the caller must equal the borrower to ensure authorized use of the existing loan.

require(loanId == 0 || msg.sender == borrower, "13");

2. Insufficient input validation for the vesting schedule in the Ocean Vesting Wallet

The input arguments in the constructor of the VestingWalletHalving contract, shown below, lack sufficient input validations. The only validation present is a check to ensure that beneficiaryAddress is not a zero address.

constructor(
        address beneficiaryAddress,
        uint64 startTimestamp,
        uint256 halfLife,
        uint256 duration
    ) payable {
        require(
            beneficiaryAddress != address(0),
            "VestingWallet: beneficiary is zero address"
        );
        _beneficiary = beneficiaryAddress;
        _start = startTimestamp;
        _halfLife = halfLife;
        _duration = duration;
    }

The first vulnerability in this example is that the _halfLife argument in the constructor can be set to zero. While this doesn’t cause an immediate revert, it will trigger issues in functions that depend on _halfLife for calculations.

For instance, If _halfLife == 0, the getAmount function in the contract will always revert.

  • t / h causes a division by zero (this will revert).
  • t % h causes a modulo by zero (this will revert).
  • (p * t) / h / 2 causes another division by zero (this will revert).

function getAmount(
        uint256 value,
        uint256 t,
        uint256 h
    ) public pure returns (uint256) {
        uint256 p = value >> (t / h);
        t %= h;
        return value - p + (p * t) / h / 2;
    }

Note: In Solidity, modulo by zero (% 0) causes a revert because modulo is defined as a division operation that returns the remainder. Since division by zero is undefined in mathematics, Solidity follows this rule and automatically reverts when attempting a modulo by zero.

Additionally, if duration is set to zero, users instantaneously release tokens and Ether.

To mitigate these vulnerabilities, add the following input validation to the constructor.

uint64 currentTime = uint64(block.timestamp);

require(
    startTimestamp >= currentTime && startTimestamp <= currentTime + 5000 days,
    "VestingWallet: startTimestamp out of range"
);

require(
    halfLife > 0,
    "VestingWallet: halfLife must be greater than zero"
);

require(
    duration >= 30 days, 
    "VestingWallet: duration must be at least 30 days"
);

Now, let's examine the potential impact of access control, a vulnerability closely related to improper input validation.

Improper input validation vs. improper access control

Improper or missing input validation allows unexpected or malicious data to enter a contract.

Improper access control occurs when a contract fails to properly restrict who can call certain functions. This allows unauthorized users to execute sensitive actions, leading to security breaches such as unauthorized fund transfers, privilege escalation, or contract manipulation.

In the example below, unsafeWithdrawFunds() allows anyone to withdraw all contract funds due to missing access control, while safeWithdrawFunds() restricts withdrawals to the contract owner using the onlyOwner modifier.

pragma solidity ^0.8.0;

contract SecureContract {
    address public owner;
    
    constructor() {
        owner = msg.sender; // Set contract deployer as the owner
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }

    function unsafeWithdrawFunds() public {
        payable(msg.sender).transfer(address(this).balance);
    }

    function safeWithdrawFunds() public onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }

    receive() external payable {} // Function to receive Ether
}

We have explored various forms of improper or missing input validation and the risks associated with them. Now, let’s look at how to prevent these vulnerabilities in smart contracts.

How to prevent improper or missing input validation in smart contracts

1. Identify input sources and attack surfaces

Examine all input sources and determine where they can be manipulated.

This includes user-supplied inputs (function arguments, constructor parameters, and external calls), state variables that are updated via input parameters, external dependencies like oracles and third-party contracts, and transaction metadata such as msg.sender, msg.value, tx.origin, and block.timestamp.

2. Define constraints based on business logic and security requirements

Confirm each input has well-defined constraints to avoid unintended behavior. Constraints should be based on:

  • Logical bounds: Values are within an expected range.
  • Security considerations: Anticipate how an attacker could manipulate values.
  • Business logic enforcement: Values align with expected contract behavior.

3. Validate inputs at compile-time where possible

Some validation can be enforced before deployment to reduce runtime errors. Use appropriate data types such as uint8 instead of uint256 for small numbers to prevent invalid values at the type level, removing the need for runtime checks.

4. Implement fallback mechanisms for unexpected inputs

Implement fallback mechanisms for unexpected inputs by assigning default values to missing or incorrect data and handling unexpected cases instead of reverting.

function safeSetAmount(uint256 _amount) public {
    if (_amount == 0) {
        _amount = 100; // Assign default value
    }
    amount = _amount;
}

5. Use fuzz testing to uncover edge cases

Fuzz testing is a powerful technique for detecting missing or improper input validation in smart contracts. By generating random and unexpected inputs, fuzz tests help uncover edge cases where validation checks may fail, leading to potential vulnerabilities. Regular fuzz testing can proactively identify weak input handling before deployment.

Conclusion

Input validation is the first line of defense against a wide range of vulnerabilities. To implement it properly, developers must carefully assess all attack surfaces related to input parameters and enforce well-defined constraints that align with both security best practices and business logic.

Input validation vulnerabilities are just one of many entry points for malicious actors. Preventing these attacks requires a deep understanding of potential attack vectors and sound auditing techniques. To master smart contract auditing and learn best practices for writing secure, efficient protocols, check out Updraft’s Security & Auditing course.

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.