Integers are whole numbers that include zero, with signed integers (int
) allowing negatives and unsigned integers (uint
) only allowing positives. Integers can be declared in a range of sizes from 8-bit to 256-bit, with 256-bit being the default size.
pragma solidity ^0.8.23;
contract IntegersExample {
// 256-bit integers
int public signedInteger = -50; // Allows negative and positive values
uint public unsignedInteger = 50; // Only allows positive values
// Small-size integers
// Only multiples of 8 allowed for integer sizes
uint8 public smallUint = 255;
int16 public smallMediumInt = -32768;
uint24 public mediumUint = 16777216;
int32 mediumLargeInt = -2147478648;
// and so on until uint256 and int256
// Declaring integers inside a function
function someFunction() public pure {
int localSignedInteger = 50;
uint localUnsignedInteger = 100;
}
// Accepting integers as function inputs
function addValues(uint x, uint y) public pure returns(uint result) {
result = x + y;
}
// Returning integers from a function
function getValue() public view returns(uint, int) {
return (unsignedInteger, signedInteger);
}
}
Integers are a simple data type used extensively in Solidity to represent whole numbers. They are declared with the uint
and int
keywords.
Integers in Solidity come in both signed (int
) and unsigned (uint
) variants. Signed integers include negative numbers, but have half the maximum value as unsigned integers.
All integers have a default size of 256 bits, but smaller sizes such as uint8
or uint64
can also be defined by the developer.
Syntax
Let’s first see how to define (default-size) integers:
contract Integers {
uint public someUint;
int public someInt;
}
Deploying and calling someUint
and someInt
will return the value 0
for both variables, which is the default value for all integers.
Assigning a value is done with the assignment operator (=
):
contract Integers {
uint public someUint = 50;
int public someInt = 50;
}
If you try to assign negative values to these variables, then you’ll find that someUint
will fail to compile, but someInt
will compile just fine.
Because integers are a simple data type, using them inside functions is very straightforward:
contract Integers {
uint private someUint = 50;
int private someInt = 50;
// Sets the values for someUint and someInt
function setUint(uint newValue) public {
someUint = newValue;
}
function setInt(int newValue) public {
someInt = newValue;
}
// Returns the values of someUint and someInt
function getUint() public view returns(uint){
return someUint;
}
function getInt() public view returns(int){
return someInt;
}
}
Signed vs Unsigned Integers
Integers come in two varieties: Signed (int
) and unsigned (uint
). Signed integers have a positive/negative sign in their bits, and can represent negative numbers. Unsigned integers don’t have a sign bit, are always positive, and can store twice as many positive values as signed integers.
Unlike many other languages, unsigned 256-bit integers are the “default” integer type in Solidity. For example, arrays return uint256
integers for their .length
property:
contract SignedUnsignedIntegers {
uint8[] public someArray = [0, 1, 2, 3];
// This will fail to compile
// Returns an int, but someArray.length is a uint
function getLength1() public view returns(int){
return someArray.length;
}
// This is fine
function getLength2() public view returns(uint){
return someArray.length;
}
// This is also fine
function getLength3() public view returns(int){
return int(someArray.length);
}
}
Because someArray.length
is returned as a uint256
, we cannot return it as an int
without explicitly type-converting it, which we do in getLength3()
.
In general, it is best to avoid using signed integers unless your specific use case requires them.
Integer Sizes
Across all binary computer systems, integers are a sequence of binary digits, or “bits”, which are used for counting in binary. Integer sizes in Solidity are defined by how many bits they contain, counted in multiples of 8.
We can define a custom-sized integer by including the number of bits it has after the keyword:
contract IntegerSizes {
// Unsigned integers and their maximum values
uint8 tinyUint = 255;
uint16 smallUint = 65535;
uint24 smallMediumUint = 16777215;
uint32 mediumUint = 4294967295;
uint40 mediumLargeUint = 1099511627775;
// And so on...
// Signed integers and their maximum values
int8 tinyUint = 127;
int16 smallUint = 32767;
uint24 smallMediumInt = 8388607;
int32 mediumUint = 2147483647;
int40 mediumLargeUint = 549755813887;
// And so on...
}
As long as the number of bits is a multiple of 8, we can use it. This means uint13
will fail to compile, but uint8
and uint16
are fine.
Integer size only becomes relevant for reference data type storage costs, as structs and arrays allow smaller integer sizes to be “packed” together in the same storage slot to reduce storage fees.
Default Sizes
Often, we don’t use specific sizes for integers, and just use the default size provided by uint
or int
. These keywords are aliases for uint256
and int256
.
Smaller integers incur higher gas costs during runtime and have fewer values. Therefore, smaller integer sizes are only useful when storing integers in complex data types, where “data packing” can be applied to massively reduce storage costs. In all other contexts, using the default 256-bit integer size is preferable.
Integer Min/Max Values
Unsigned and Signed integers have different ranges of values they can hold. Because signed integers reserve one of their bits for the negative sign, they can only reach half the positive values that unsigned integers can, but they can also reach an equal amount of negative values.
Solidity offers a built-in method for obtaining min/max values for integers that doesn’t rely on exponents or integer underflow tricks:
contract Integers {
uint public maxUint = type(uint).max;
uint8 public maxUint8 = type(uint8).max;
uint public minUint = type(uint).min;
uint8 public minUint8 = type(uint8).min;
int public maxInt = type(int).max;
int8 public maxInt8 = type(int8).max;
int public minInt = type(int).min;
int8 public minInt8 = type(int8).min;
}
Using this contract, we can see the full range of values for uint
, int
, uint8
, and int8
. For example, uint8
has a range of 0
to 255
, while int8
has a range of -128
to 127
. In either case, both data types can store 256 different numbers, including 0
.
While the syntax is difficult to remember, this is a very simple shortcut that works for any integer type and size.
Use Cases
Integers have a vast number of use cases, many of which are related to their use with arithmetic operations. Some common use cases include token balances, loop counters, and sequential ID numbers.
Arithmetic Operations
Integers are frequently used in arithmetic operations, and can be manipulated by all arithmetic operators in Solidity.
These operators include:
- Addition (
+
) and Subtraction (-
) - Multiplication (
*
) - Exponentiation (
**
) - Integer Division (
/
) - Modular Division (
%
) - Increment (
++
) and Decrement (--
) - Compound Assignment (
+=
,-=
,*=
, etc.)
Let’s see some examples:
contract IntegerArithmetic {
uint someUint;
// These increment someUint by 1:
function increment1() public {
someUint = someUint + 1;
}
function increment2() public {
someUint += 1;
}
function increment3() public {
someUint++;
}
// These multiply someUint by a value
function multiply1(uint value) public {
someUint = someUint * value;
}
function multiply2(uint value) public {
someUint *= value;
}
function add(uint value1, uint value2) public pure returns(uint result){
result = value1 + value2;
}
}
Tracking Token Balances (ERC20)
The ERC20 token standard combines mapping
and uint
to assign and track token balances across accounts.
The full code is available inside OpenZeppelin’s repository.
In the ERC20 contract, mapping(address => uint256) private _balances;
maps account addresses to their individual token balances, which are stored as a uint256
value.
This combines with the _transfer()
and _update()
functions to reduce the sender’s balance and increase the recipient’s balance, after a lot of safety checks have been performed.
Counter Variables and Token IDs
We often use integers as counters for a sequential process. This spans many use cases, from counting the number of repetitions in a looping structure to counting NFT token IDs (if they are sequential).
Let’s see a simple counter variable design pattern:
contract Counter {
uint256 public counter;
function increment() public {
counter++;
}
function decrement() public {
counter--;
}
}
This creates a state variable that can be incremented or decremented in units of 1.
In cases where we are using a counter to assign unique IDs, we wouldn’t want to include a decrementing function. For example, let’s create an NFT collection where NFTs are minted with sequential IDs:
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract TurtleCats is ERC721 {
// Begins at 0, increments with each mint
uint public nextID;
// Mints an initial supply of NFTs
constructor(uint initSupply) ERC721("TurtleCats", "WBC") {
while(initSupply > 0) mintNext();
}
// Mints a new NFT to the caller
function mintNext() public {
_mint(msg.sender, nextID);
incrementID();
}
function incrementID() public {
nextID++;
}
}
Whereas the ERC721 contract requires specifying which token ID to mint, this contract automatically determines the token ID by using a counter variable.
Best Practices And Common Mistakes
While it is usually quite difficult to mess up on integers, there are some exceptional situations that can produce exceptionally undesirable results.
These situations have to do with type mixing, type conversion, and integer overflow/underflow bugs, and can be distilled into two general rules:
1. Convert Small to Large
While Solidity allows us to declare custom-sized integers, it doesn’t allow us to co-mingle these integers in all situations.
Let’s see some examples where the compiler will accept or reject mixing integer sizes:
contract MixingSizes {
uint16 someUint16 = 100; // Max: 65535
uint8 someUint8 = 100; // Max: 255
// Passes
uint16 result1 = someUint16 + someUint8;
uint16 result2 = someUint8 + someUint16;
// Fails
uint8 result3 = someUint16 + someUint8;
}
This occurs because a uint8
will always fit inside a uint16
, but not the other way around.
For instance, if someUint16
has a value greater than 255
, then it will cause an overflow to occur when converted to a uint8
. So, the compiler rejects result3
because it can’t safely convert someUint16
to a uint8
type for all potential values of someUint16
, even though its current value won’t cause an overflow.
For this reason, we should always convert up in size, which the compiler won’t complain about.
If we absolutely must convert to a smaller size, then we must also account for overflow and underflow bugs. For example:
contract MixingSizes {
uint16 someUint16 = 500;
uint8 someUint8 = 100;
function addUints() public view returns(uint8){
require(
someUint16 <= type(uint8).max,
"Overflow detected"
);
return uint8(someUint16) + someUint8;
}
}
Here we manually handle the situation where the uint16
variable will cause an overflow when converted to a uint8
, which happens for any value greater than 255
.
2. Convert Unsigned to Signed
If we are mixing unsigned and signed integers, then the best general practice is to convert the unsigned integers to signed integers.
This is because signed integers include negative values, and converting them to unsigned integers can cause an integer underflow to occur. Converting them to uint
also removes their purpose for existing in a Solidity contract in the first place: Storing negative numbers.
However, we still need to be cautious, as uint
has twice as many positive values as int
does, and it’s possible that converting a large uint
to an int
will cause an overflow to occur. So, we would need to test for this situation.
There is no situation where the Solidity compiler will be okay with mixing signed and unsigned integers. You must convert one to the other, and be prepared to deal with overflow/underflow bugs:
// This will fail to compile
contract BadConversionExamples {
function failAddUint(uint someUint, int someInt) public pure returns(uint){
return someUint + someInt;
}
function failAddInt(uint someUint, int someInt) public pure returns(int){
return someUint + someInt;
}
}
// This will compile
contract GoodConversionExamples {
// Converts from int to uint
function addUint(uint someUint, int someInt) public pure returns(uint){
// Test for integer underflow
require(someInt >= 0, "Underflow detected");
return someUint + uint(someInt);
}
// Converts from uint to int
function addInt(uint someUint, int someInt) public pure returns(int){
// Test for integer overflow
require(someUint <= uint(type(int).max), "Overflow detected");
return int(someUint) + someInt;
}
}
In the “good” examples, we have to test that the input integer isn’t going to cause an underflow or overflow when we convert it.
Generally, you should try to avoid mixing signed and unsigned integers unless you know what you are doing. Advanced design patterns such as Accumulator Modifier and Delta Adjustment patterns require use of negatives, but most non-advanced designs use strictly-positive values.