Solidity Boolean and Integer Types – A Helpful Guide with Video

Types are among the most important building blocks of a statically-typed programming language, and since Solidity falls into that category, we finally have the opportunity to entertain ourselves with this crucial topic.

By learning about types, we’ll get more closely familiar with the Solidity features that are based on idiosyncrasies of a particular data type.

โ–ถ๏ธ You can find the slide deck of this video at the end of this article.

Traditionally, this article follows official documentation for Solidity 0.8.15 (the latest version at the time the first article in the series was published).

Types

๐Ÿ’ก Note: A general statically-typed programming language asserts that the type of each class, object, function or method return type, etc. has to be explicitly defined. Checking of the data types in statically-typed languages is performed by the compiler during compile time, as well as error reporting in case of a type mismatch. This approach is in contrast with dynamically-typed programming languages, which infer the types of their objects during runtime, i.e. during interpretation of the source code.

Solidity being the statically-typed programming language, the type of each variable, regardless of the memory area it resides in, must be specified.

This rule applies equally to both simple types, as well as more complex, custom types.

Operator Precedence

Different types can be combined in expressions that contain operators, but an order of operator precedence is always obeyed.

โ„น๏ธ Info: Order of operator precedence is a must in almost every programming language, and Solidity is no exception. A quick reference guide is available at the link, and it’s always useful to check it out when we find ourselves uncertain about the order of operation priority.

Uninitialized Variables

One of the Solidity’s features is not using undefined or null values for uninitialized variables or data structures.

Instead, every newly declared uninitialized variable is automatically assigned with the type-determined default value.

๐Ÿ’ก Best practices for handling unexpected values include using the revert function to restore the transaction to its initial state when an unwanted value occurs.

Value Types

Value types imply that the variables of these (simple) types will always be passed by their value e.g. as an argument to a function or a member in an assignment expression, instead of their reference (memory address).

In other words, a copy of the variable will be exchanged, and the original value of the variable will remain intact.

Booleans

The bool type is the simplest value type, with only two possible values: true and false.

Boolean Operators

Available operators for the bool type are:

  • ! (logical negation, changes true into false and vice versa)
  • && (logical conjunction, โ€œandโ€)
  • || (logical disjunction, โ€œorโ€)
  • == (equality)
  • != (inequality)

Short Circuiting

๐Ÿ’ก Note: We should have in mind that the operators for logical conjunction && and logical disjunction || apply the rules of short-circuit evaluation.

Short-circuit evaluation means that if in a boolean expression A || B || ..., operand A  = true, evaluation will terminate because true || <anything> = true, regardless of what comes after A.

The equivalent applies to a boolean expression A && B && ..., where if A = false, B won’t be evaluated since false && ... = false, regardless of what comes after A.

This is an especially important property because if B is a function with side effects, the side effects will be left out since B won’t execute.

Integers

As we remember from our primary school math, integers are whole numbers.

Signed and Unsigned Integers

There are two simple kinds, signed integers of type int (>= 0), and unsigned integers of type uint.

Signed and unsigned integers come in various sizes, starting with 8-bit int8/uint8, increasing in steps of 8 bits, up to 256-bit int256/uint256. It is worth mentioning that the aliases int and uint (without bit width) are synonyms for int256/uint256.

Integer Operators

There are four operation categories available for integer variables, and their corresponding operators are:

  • Comparisons: <=, <, ==, !=, >=, > (evaluate to bool)
  • Bit operators: &, |, ^ (bitwise exclusive or), ~ (bitwise negation)
  • Shift operators: << (left shift), >> (right shift)
  • Arithmetic operators: +, -, unary - (only for signed integers), *, /, % (modulo), ** (exponentiation)

As we as readers come from various professional backgrounds, I’ll give us a bit of explanation for some not commonly known operators.

Bit Operators

Bit operations operate over a sequence of bits, commonly expressed in multiples of bytes (1 byte = 8 bits). The operator takes into account each bit’s position and applies the operation involving only the bits at the same position, e.g., if

  • A = 01001010 (74 in decimal form) and B = 11011011 (219 in decimal form),
  • A & B = 01001010 (74 in decimal form),
  • A | B = 11011011 (219 in decimal form),
  • A ^ B = 10010001 (91 in decimal form),
  • ~A = 10110101 (-75 in signed variant or 265 in unsigned variant of a decimal form),
  • ~B = 00100100 (36 in decimal form).

The first bit of a byte set to 1 leads to a two-fold interpretation of a byte, both as a positive or a negative decimal number, hence two interpretations in parentheses.

Bit operations work on the two’s complement representation of a number.

In effect, this results in operation results like ~int256(0) == int256(-1). There’s an excellent, short article for those of us who’d like to dive deeper into the subject.

Shift Operators

Shift operators for our two examples, A = 01001010 (74 in decimal form) and B = 11011011 (219 in decimal form) work like this (note the truncation of the leftmost digit(s) with << and the rightmost digit(s) with >>):

A << 1 = 10010100 (-108 in signed variant or 148 in unsigned variant of a decimal form), B >> 1 = 01101101 (109 in decimal form).

If we observe the results more closely, we’ll notice that the left shift by 1 multiplies the number with 21 = 2 and that the right shift by 1 divides (integer division) the number with 21 = 2.

In general, shifting a number left by X multiplies the number with 2X, and shifting the number right by X divides the number with 2X, rounded down to negative infinity.

Type of the result of the shift operation corresponds to the left operand, as in <left_operand> << <right_operand>.

๐Ÿ’ก Note: Overflow checks are not performed for left and right shift operations. The result is truncated instead.

Arithmetic Operators

Arithmetic operators which may be unfamiliar to less experienced readers among us are % (modulo, the remainder operator) and ** (exponentiation).

Modulo Operator

The modulo operator % is applied by the number n on the number a and produces the remainder r: a % n = r.

Two formulas that may look intimidating, but are very simple, are: q = int(a / n) and r = a - (n * q).

  • q stands for the integer division quotient, as in a = 7, n = 3, q = int(7 / 2) = 3, meaning that 3 fits into 7 whole 2 times.
  • r stands for the remainder and is calculated as: a (the original number) - (divisor n * quotient q), so 7 – 3 * 2 = 1. In simple words, 3 fits into 7 whole 2 times, and what remains is 1.

The modulo operator yields the result of the same sign as its left operand (or zero); for a negative original number a, we have a % n == -(-a % n):

  • int256(5) % int256(2) == int256(1)
  • int256(5) % int256(-2) == int256(1)
  • int256(-5) % int256(2) == int256(-1)
  • int256(-5) % int256(-2) == int256(-1)

๐Ÿ’ก Note: Modulo with zero causes a Panic error. This check can not be disabled through unchecked { ... }.

A**2 = 49, A**3 = 343 etc.

โ„น๏ธ Info: There’s a neat feature for an integer type X that enables us to find the minimum and maximum values (limits) representable by the specific integer type. Remember, the integer types are available from int8/uint8 to int256/uint256 in increments of 8. We can find their ranges (min and max) by calling function type(...), as in type(X).min and type(X).max.

โšก Warning: Because integers in Solidity have a range of acceptable values (from min to max value), there are two modes in which arithmetic operations are performed on these types: the โ€œwrappingโ€ or โ€œuncheckedโ€ mode and the โ€œcheckedโ€ mode.

By default (since Solidity v.0.8.0), arithmetic is always โ€œcheckedโ€, i.e. if the result of an operation falls outside the value range of the type, the call is reverted through a failing assertion.

If we want to use the “unchecked”/”wrapping” mode, which implies the automatic use of additional libraries required for handling overflows and underflows, the unchecked block is available:

// 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;
    }
}

The call to f(2, 3) will result in 2**256-1, while g(2, 3) will trigger a failing assertion.

Here’s a paragraph from the official Solidity documentation that I find requires additional explanation:

“If you have int x = type(int).min;, then -x does not fit the positive range. This means that unchecked { assert(-x == x); } works, and the expression -x when used in checked mode will result in a failing assertion.”.

Let’s take an imaginary, signed number type that covers the range of -3 to 2.

You’ll notice that the right range is “shorter” for one value because it also includes 0. In other words, the negative side consists of -3, -2, -1 and the positive side consists of 0, 1, 2.

Following int x = type(int).min; our x would hold -3. However, -x would imply -(-3) = 3, which our imaginary type cannot hold because it’s too big.

๐Ÿง  To find out what our value 3 becomes, let’s consider the following mental exercise: our imaginary type can hold 2 (the maximum). Generally, 3 = 2 + 1, so this excess of 1 overflows to the negative side and 2 + 1 = -3. That’s why, in the end, unchecked { assert(-x == x); }.

In other words, we should be careful when using the unchecked block. It all boils down to overflow and underflow conditions.

โ„น๏ธ Note: With the previous explanation in mind, “the expression type(int).min / (-1) is the only case where division causes an overflow. In checked arithmetic mode, this will cause a failing assertion, while in wrapping mode, the value will be type(int).min.

“Underflow is a condition which occurs in a computer or similar device when a mathematical operation results in a number which is smaller than what the device is capable of storing. It is the opposite of overflow, which relates to a mathematical operation resulting in a number that is bigger than what the machine can store. Similar to overflow, underflow can cause significant errors.” (source).

Exponentiation Operator

Exponentiation is available only to unsigned types in the exponent, meaning the smallest number in the exponent is 0.

As an exponent consists of a base and an exponent, the resulting type of exponentiation will always be equal to the type of the base. Therefore, we should always be careful that the base type is large enough to hold the result of our exponentiation, otherwise, we should be prepared for potential assertion failures (checked mode) or wrapping behavior (unchecked mode).

๐Ÿ’ก Note: Exponentiation executed in a checked mode uses only cheap exp operation code for small bases. To be sure of the final gas cost, it is necessary and recommended to use the optimizer and check the gas cost.

๐Ÿ’ก Note: 0**0 is defined in EVM as 1.

A Personal Note

After looking at the size of this article, and knowing what is behind us and what still lies ahead and awaits to be learned, I felt a need to write a word or two of encouragement.

We shouldn’t ever let ourselves be demotivated by the size of the task we’ve set out to accomplish; after all, this is all just knowing that we’ll put what we’ve learned to good use, support our creativity in solving problems and innovate great solutions.

Just practice patience and consistency, take on any task step by step and you’ll make it!

Conclusion

In this article, we just started studying the Solidity types and there was already so much to say!

First, we learned the difference between statically-typed and dynamically-typed programming languages. We also found out that Solidity is a statically-typed language.

Second, we discovered the value and reference types of data.

Third, we discussed the boolean data type and the corresponding operations.

Fourth, we met with the integer data type and discovered what it does in its spare time.

๐Ÿ‘‰ Recommended Tutorial: Solidity Fixed Point Numbers and Address Types (Howto)

Summary Slide Deck Data Types


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):