Lesson 15 of 23
In Progress

Proper Handling of User Input and External Calls

Smart contracts often rely on user input and external calls to other contracts for their functionality. However, it is important to handle these inputs and calls correctly to avoid security vulnerabilities. In this article, we will discuss some best practices for handling user input and external calls in smart contracts.

Validating User Input

User input can be a potential security risk if it is not properly validated. Malicious users may try to enter malicious or unexpected data into your contract to exploit vulnerabilities. To prevent this, it is important to validate all user input before processing it.

There are several ways to validate user input in smart contracts. One common method is to use require statements to ensure that the input meets certain conditions. For example, you can use a require statement to check that a user-provided address is not the zero address:

function setOwner(address _newOwner) public {
    require(_newOwner != address(0), "New owner cannot be the zero address");
    owner = _newOwner;
}

You can also use the isValid() function provided by the ERC-165 interface to check that an address is a valid Ethereum address:

function setOwner(address _newOwner) public {
    require(_newOwner.isValid(), "New owner must be a valid Ethereum address");
    owner = _newOwner;
}

It is also a good practice to check the length and format of user-provided strings to ensure that they are not too long or contain unexpected characters. For example, you can use the bytes type and the bytes.length property to check the length of a string:

function setName(bytes memory _name) public {
    require(_name.length <= 32, "Name must be 32 characters or less");
    name = _name;
}

You can also use regular expressions to check the format of a string. For example, you can use the keccak256 function and the bytes32 type to check that a string only contains alphanumeric characters:

function setId(string memory _id) public {
    bytes32 idHash = keccak256(abi.encodePacked(_id));
    require(idHash == bytes32(hex"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"), "ID must only contain alphanumeric characters");
    id = _id;
}

By properly validating user input, you can help prevent vulnerabilities and ensure that your contracts operate as intended.

Handling External Calls

External calls to other contracts can also introduce security risks if they are not handled properly. One common vulnerability is the reentrancy attack, where an attacker can repeatedly call a contract’s functions to exploit a vulnerability. To prevent this, it is important to use the call.value() function to make external calls, rather than using the transfer or send functions. The call.value() function allows you to specify a maximum amount of gas to be used for the external call, which can help prevent reentrancy attacks.

It is also a good practice to use the require function to check the return value of external calls. This can help ensure that the external call was successful, and prevent vulnerabilities if the external contract behaves unexpectedly.

Here is an example of a contract that uses the call.value() function to make an external call and the require function to check the return value:

function sendEther(address _recipient, uint _amount) public {
    require(address(this).call.value(_amount).gas(500000)(), "External call failed");
    emit EtherSent(_recipient, _amount);
}

In addition to using the call.value() function and the require function, it is also important to properly handle exceptions and failures when making external calls. One way to do this is to use the try-catch pattern, which allows you to catch and handle exceptions that may be thrown during an external call.

Here is an example of a contract that uses the try-catch pattern to handle exceptions when making an external call:

function sendEther(address _recipient, uint _amount) public {
    try {
        address(this).call.value(_amount).gas(500000)();
        emit EtherSent(_recipient, _amount);
    } catch (bytes memory err) {
        emit Error(err);
    }
}

By properly handling exceptions and failures, you can help prevent vulnerabilities and ensure that your contracts continue to operate even if an external call fails.

Conclusion

Properly handling user input and external calls is important for the security of your smart contracts. By validating user input, using the call.value() function and the require function when making external calls, and properly handling exceptions and failures, you can help prevent vulnerabilities and ensure that your contracts operate as intended.

Exercises

To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.

  1. Exercise: Validate User Input

In this exercise, you will modify a smart contract to validate user input.

Here is the original contract:

pragma solidity ^0.6.0;

contract UserInput {
    uint public balance;

    function setBalance(uint _balance) public {
        balance = _balance;
    }
}

Your task is to modify the setBalance function to validate the user-provided _balance value. Specifically, you should add a require statement to check that the _balance value is greater than 0.

Here is the modified contract with the solution:

pragma solidity ^0.6.0;

contract UserInput {
    uint public balance;

    function setBalance(uint _balance) public {
        require(_balance > 0, "Balance must be greater than 0");
        balance = _balance;
    }
}
  1. Exercise: Handle External Calls

In this exercise, you will modify a smart contract to properly handle external calls.

Here is the original contract:

pragma solidity ^0.6.0;

Your task is to modify the sendEther function to use the call.value() function instead of the transfer function, and to add a require statement to check the return value of the external call.

Here is the modified contract with the solution: "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract ExternalCalls {
    using SafeMath for uint;

    uint public balance;

    function sendEther(address _recipient, uint _amount) public {
        balance = balance.sub(_amount);
        _recipient.transfer(_amount);
    }
}

Your task is to modify the sendEther function to use the call.value() function instead of the transfer function, and to add a require statement to check the return value of the external call.

Here is the modified contract with the solution:

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract ExternalCalls {
    using SafeMath for uint;

    uint public balance;

    function sendEther(address _recipient, uint _amount) public {
        balance = balance.sub(_amount);
        require(address(this).call.value(_amount).gas(500000)(), "External call failed");
    }
}
  1. Exercise: Handle Exceptions and Failures

In this exercise, you will modify a smart contract to properly handle exceptions and failures when making external calls.

Here is the original contract:

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract ExceptionHandling {
    using SafeMath for uint;

    uint public balance;

    function sendEther(address _recipient, uint _amount) public {
        balance = balance.sub(_amount);
        _recipient.transfer(_amount);
    }
}

Your task is to modify the sendEther function to use the try-catch pattern to handle exceptions when making an external call. You should also add an event that is emitted when an exception is caught.

Here is the modified contract with the solution:

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract ExceptionHandling {
    using SafeMath for uint;

    uint public balance;

    event Error(bytes memory err);

    function sendEther(address _recipient, uint _amount) public {
        balance = balance.sub(_amount);
        try {
            _recipient.transfer(_amount);
        } catch (bytes memory err) {
            emit Error(err);
        }
    }
}
  1. Exercise: Use Safe Math Library

In this exercise, you will modify a smart contract to use a safe math library to prevent integer overflow and underflow.

Here is the original contract:

pragma solidity ^0.6.0;

contract SafeMath {
    uint public balance;

    function add(uint _x, uint _y) public pure returns (uint) {
        return _x + _y;
    }

    function sub(uint _x, uint _y) public pure returns (uint) {
        return _x - _y;
    }
}

Your task is to modify the add and sub functions to use the SafeAdd and SafeSub functions from the OpenZeppelin SafeMath library, respectively. You should also import the SafeMath library at the top of the contract.

Here is the modified contract with the solution:

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract SafeMath {
    using SafeMath for uint;

    uint public balance;

    function add(uint _x, uint _y) public pure returns (uint) {
        return _x.add(_y);
    }

    function sub(uint _x, uint _y) public pure returns (uint) {
        return _x.sub(_y);
    }
}
  1. Exercise: Properly Handle User Input

In this exercise, you will modify a smart contract to properly handle user input.

Here is the original contract:

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract UserInput {
    using SafeMath for uint;

    uint public balance;

    function setBalance(uint _balance) public {
        balance = _balance;
    }
}

Your task is to modify the setBalance function to properly handle user input. Specifically, you should add a require statement to check that the _balanceargument is not zero. If the _balance argument is zero, you should throw an exception with the message “Balance cannot be zero”.

Here is the modified contract with the solution:

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract UserInput {
    using SafeMath for uint;

    uint public balance;

    function setBalance(uint _balance) public {
        require(_balance != 0, "Balance cannot be zero");
        balance = _balance;
    }
}