Solidity Checked and Unchecked Expressions

In this article, we’ll learn about checked and unchecked expressions and when to use each of them.

It’s part of our long-standing tradition to make this (and other) articles a faithful companion or a supplement to the official Solidity documentation for this article’s topic.

We should consider whether an expression should be checked or unchecked in the Solidity programming language. Checked expressions will throw an exception and halt the program if a certain condition is not met, while unchecked expressions do not have this behavior. We should be aware of this distinction because unchecked expressions can lead to performance increases at the cost of possible vulnerabilities in our smart contracts, while checked expressions assist us in ensuring the correctness and security of our code.

Checked Expressions

Checked expressions will throw an exception and halt the program if conditions of interest are unmet, or continue execution if they are satisfied.

For example, if we want to ensure that a certain variable contains a valid result before performing an operation, we can use a checked expression to throw an exception if the variable has an unwanted value.

require Function

There are several ways we can use checked expressions in Solidity. The most common way is to use the require(...) function, which will throw an exception if the condition passed to it is not met.

The following example shows how we can do it:

function divide(uint numerator, uint denominator) public {
    // Throws an exception if the denominator is zero.
    require(denominator != 0, "Division by zero");

    // Performs the division operation.
    uint result = numerator / denominator;
}

The require(...) function throws an exception if the denominator is zero. This helps us in ensuring that the division operation will not be performed if the denominator is zero, which would result in an error.

assert Function

Another way to use checked expressions is to use the assert(...) function, which is similar to require(...). The main difference between assert(...) and require(...) is that assert(...) is recommended for internal consistency checks, while require(...) is better suited for input validation.

In addition to the require(...) and assert(...) functions, there are several other ways we can take to check expressions in Solidity. For example, we can use the revert(...) function to throw an exception and revert the current transaction, or we can use the abort(...) function to throw an exception and halt the program completely.

We should remember that checked expressions are an important tool for ensuring the correctness and security of our Solidity source code. By using checked expressions, we can ensure that we’re meeting specific conditions before operations are performed, which aids in preventing errors and vulnerabilities in our smart contract.

Unchecked Expressions

On the other hand, unchecked expressions do not throw an exception or terminate the program if a specific condition is not met. This behavior can be useful in situations where we want to allow for a certain level of flexibility in our source code, but we should be aware it can also lead to vulnerabilities if not used carefully.

For example, if we have an unchecked expression that does not check the length of an array before performing an operation, it could potentially lead to an out-of-bounds exception.

Another way to use unchecked expressions is to use the call(...) function that allows us to call another contract function without checking for errors.

πŸ’‘ Reminder: The call(...) function is a low-level call function used for interaction with other contracts. It represents the recommended way for sending Ether via a call to the fallback function.

⚑ Warning: Although available, the call(...) function is not recommended for calling functions, because reverts are not forwarded up the call stack, type checks are bypassed, and function existence checks are omitted.

Let’s take a look at the following example of using the call(...) function:

function sendTransaction(address destContractAddress, uint amount) public {
    // Calls the 'receiveTransaction' function of the 
   // 'destContractAddress' contract without checking for errors
    (bool success, bytes memory data) = destContractAddress.call{value: amount}("receiveTransaction");
}

The .call(...) function is used for calling the receiveTransaction(...) function of the destContractAddress contract without checking for errors. This can be useful in situations where we want to allow some flexibility in our code, but it can also potentially lead to vulnerabilities if not used carefully.

Checked and Unchecked Arithmetic

Checked and unchecked expressions are a natural way of dealing with two distinct situations during arithmetic operations, overflow, and underflow. These two operations occur when an arithmetic operation is expected on an unrestricted integer, that falls outside the range of the resulting type.

πŸ’‘ Info: Before Solidity v0.8.0, arithmetic operations weren’t checked, so external libraries were needed to introduce additional checks. With Solidity v0.8.0, all arithmetic operations revert by default when overflow and underflow situations occur, introducing the needed behavior and eliminating the requirement for external libraries.

However, regardless of the checked behavior being the default one, we can still invoke the unchecked behavior by using an unchecked block, e.g.:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
    function f(uint a, uint b) pure public returns (uint) {
        // This subtraction will wrap on underflow.
        unchecked { return a - b; }
    }
    function g(uint a, uint b) pure public returns (uint) {
        // This subtraction will revert on underflow.
        return a - b;
    }
}

In this example, the f(...) function takes two uint arguments (only whole positive, unsigned numbers). By calling it as f(2, 3), the result will be 2**256-1, or in other words, the result will wrap around, starting on the left and coming on the far right side.

The same arithmetic operation, but with a checked variant, won’t make a wrap but instead cause a failing assertion.

We can use the unchecked block inside a block, but not as a standalone block or a replacement for a block.

⚑ Warning: An unchecked block cannot be nested. Also, the unchecked block effect applies only to the statements that are syntactically in the same block. Any function called from inside the block is not affected by the unchecked effect.

πŸ’‘ Recommended: Top 8 Scary Smart Contract Hacks They Use to Exploit Your DApp [+Video]

Checked and Unchecked Expressions in Solidity

To avoid ambiguity, we cannot use the _; symbol inside an unchecked block. _; is a special symbol that marks the insertion point in a modifier’s body. A modified function’s code will be inserted and executed at this specific point (docs).

These operators will cause a failing assertion on overflow or underflow situations; they will also wrap without an error if they’re used inside an unchecked block: ++, --, +, binary -, unary -, *, /, %, **, +=, -=, *=, /=, and %=.

⚑ Warning: We cannot disable the check for division by zero or modulo by zero using the unchecked block.

Integer division and multiplication operations by a power of 2 can be substituted by equivalent bitwise shift operators. However, bitwise shift operators do not check for overflow or underflow situations. Specifically, type(uint256).max << 3 does not revert even though type(uint256).max * 8 does.

πŸ’‘ Note: If we do an edge-of-the-interval operation, such as in int x = type(int).min; -x; it will result in an overflow because the negative range always holds one more value than the positive range, e.g. as in an imaginary range [-5, ..., 0, 4], where -5 cannot be converted to 5.

If we do an explicit type conversion, it will always truncate without a failing assertion, except when an integer is converted to an enum type.

Unchecked expressions can be useful in certain situations, but it’s important to use them carefully and consider the potential risks and vulnerabilities they may introduce. By using unchecked expressions wisely, we can achieve a balance between flexibility, performance, and security that is right for our particular use case.

Conclusion

In this article, we learned that checked and unchecked expressions are important concepts in Solidity programming. Checked expressions can enforce the correctness and security of our code, while unchecked expressions can improve the performance, but potentially lead to vulnerabilities if they’re used carelessly.

First, we got introduced to checked expressions in general. Then we learned about require(...) and assert(...) functions, and also mentioned the revert(...) and abort(...) functions.

Second, we explained the difference between the checked and unchecked expressions and showed how the call(...) function represents an unchecked expression.

Third, we went into detail on checked and unchecked arithmetic, explained when checked arithmetic became a default, and showed how to override the default behavior and perform the unchecked arithmetic.

What’s Next?

This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):