Solidity Signed & Unsigned Integers (Syntax + Code Examples)

whiteboard crypto logo
Published by:
Whiteboard Crypto
on

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 uintintuint8, 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.

solidity integers
whiteboard crypto logo

WhiteboardCrypto is the #1 online resource for crypto education that explains topics of the cryptocurrency world using analogies, stories, and examples so that anyone can easily understand them. Growing to over 870,000 Youtube subscribers, the content has been shared around the world, played in public conferences and universities, and even in Congress.