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.
- 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;
}
}
- 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");
}
}
- 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);
}
}
}
- 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);
}
}
- 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;
}
}