Solidity Signed Integer

Solidity signed integers enable using negative numbers in a smart contract. This article documents how they are used at the EVM level. Basic familiarity with the EVM and binary numbers is assumed.

Two’s Complement Explained

Solidity and the EVM use Two’s Complement representation for signed integers

Like every datatype, Solidity still uses 32 byte words to represent signed integers. There isn’t any semantic indicator of the type in the EVM, just like there isn’t any indicator that a 32 byte slot is actually a boolean, an address, or a 160 bit number. The value is “treated” as negative one at compilation time.

Because you can get the max value of an integer with “type(int256).max” or with the .min field to get the minimum. The indicator for whether a number is positive or negative requires an extra bit, so it can only store numbers up to one bit less than the unsigned version.

One’s complement means a uint256 becomes a uint255, with the leftmost bit indicating if it is positive or negative. If the EVM used one’s complement. This would mean that type(int256).max == absoluteValue(type(int256.min)) but this is not the case. The maximum magnitude of a Two’s Complement negative number is one higher than the maximum magnitude of the positive number. For example, the maximum positive number for int8 is 127, but the maximum magnitude negative number for int8 is -128.

Patterns and Examples of Two’s complement arithmetic.

Rather than going into a bunch of mathematical proofs, let’s use some real examples (this is not intended to be proof by example for Two’s Complement arithmetic, there is plenty of literature on Two’s Complement for the interested reader).

Let’s use an int8 to make the examples more readable. The following is in binary, not hex.

int8(0) == 0000 0000
type(int8).max == 0111 1111
type(int8).min == 1000 000

It is instructive to see the representations for +1 and -1:

int8(1) == 0000 0001
int8(-1) == 1111 1111

Let’s count downward in two’s complement so that a pattern becomes apparent:

int8(-2) == 1111 1110
int8(-3) == 1111 1101
int8(-4) == 1111 1100
int8(-5) == 1111 1011

You can roughly think of two’s complement negative numbers as “counting down.”

Here’s the interesting feature of two’s complement. -2 + -2 should equal -4, and adding in two’s complement and allowing overflow enables this. Here is adding -2 to itself in Python using the two’s complement representation

>>> (int(b'11111110', 2) + int(b'11111110', 2) ) % 256
252
>>> bin(252)
'0b11111100'

This matches the expected pattern above.

What if we add +4 to -2? We should get +2. Let’s see that in action

>>> # -2 + 4
>>> (int(b'11111110', 2) + int(b'00000100', 2)) % 256
>>> 2

This only works if both numbers are in two’s complement representation. Solidity does not allow you to add unsigned and signed integers together since it is ambiguous what the intent is.

Two’s Complement also works with multiplication. The expected result of -2 and -2 is +4, and the reader is encouraged to copy the code from earlier to verify this.

This does not work for all arithmetic operations.

Two’s Complement does not require changes to addition, subtraction, multiplication, or even a left bitshift (<<). These correspond to the EVM op codes ADD, SUB, MUL, and SHL. We will discuss why shift left still works in two’s complement in a later section in this tutorial.

However, multiplication, modulo, right shift, and casting to a larger signed integer cannot be done using the signed methods and require their own op codes. Similarly, the traditional comparison operators won’t work, as negative numbers “appear” to be larger than positive ones.

Ethereum op codes for signed arithmetic

sdiv

Gas cost: 5

SDIV, or signed division, is for dividing signed numbers. This opcode is used behind the scenes in code like the following.

function divide(int256 a, int256 b) public pure returns (int256 quotient) 
{
    quotient = a / b;
}

smod

Gas cost: 5

Since two’s complement arithmetic needs it’s own opcode for div, it’s no surprise the same applies to take the modulus (remainder).

function divide(int256 a, int256 b) public pure returns (int256 remainder) 
{
    remainder = a % b;
}

slt and sgt

Gas cost: 3

To compare the magnitude of signed numbers, we first need to determine if it is positive or negative, then compare the magnitude. These op codes do taht operation in one step.

Like the unsigned counterparts, it is more gas efficient to avoid the >= and <= where possible and use the strict inequality operators instead.

sar – signed arithmetic shift right

Gas cost: 3

SAR is a very rarely used op code, but it will appear on the compilation result of this solidity code. Note that x is an integer and y is an unsigned integer.

contract SarExample {
    function main(int256 x, uint256 y) public pure returns (int256 res) {
        res = x >> y;
    }
}

How do we make sense of this? In terms of ordinary unsigned numbers, shifting the bits right by one has the effect of dividing by two, shifting by two has the effect of dividing by four, etc.

uint256 x = 8 >> 2; // x = 2
uint256 y = 4 >> 1; // y = 2

If you do this to a signed integer, this phenomena is preserved.

int256 x = -8 >> 2; // x = -2
int256 y = -4 >> 1; // y = -2

Why isn’t there a SAL (signed arithmetic left shift) op code? What should happen in the following example?

int256 x = -8 << 2; // x = -32
int256 y = -4 << 1; // y = -8

We multiplied by 4 and 2, respectively. In Two’s Complement, shifting left preserves the number as expected.

Under the hood, a regular SHL (shift left) opcode was used. There is no need for a special case for arithmetic left shift. This might seem unintuitive, as the number gets larger as rightmost bits are zeroed out. But remember, the maximum negative value in Two’s Complement is when the leftmost bit is one, and all the other bits are zero.

signextend evm

Gas cost: 5

A signed integer smaller than 256 bits will have leading zeros. However, Two’s Complement negative numbers always start with the leftmost bit at one. Therefore, if a Two’s Complement integer is upcasted to a larger type, the value will change from negative to positive since the leftmost bits will be zero. Signextend handles this transition seamlessly.

signextend solidity

You can’t use signextend directly in Solidity, but it’s used behind the scenes when a smaller integer is casted to a larger one. The following code contains the signextend opcode in its compiled bytecode to cast the int8 to a int256.

contract SignExtendExample {
    function main(int8 x) public pure returns (int256 res) {
        res = x;
    }
}

It should be obvious at this point that larger integers cannot be cast to smaller ones.

Learn more

Learn more advanced topics in our expert solidity training course.

Originally Published Apr 11, 2023

20 Common Solidity Beginner Mistakes

20 Common Solidity Beginner Mistakes Our intent is not to be patronizing towards developers early in their journey with this article. Having reviewed code from numerous Solidity developers, we’ve seen some mistakes occur more frequently and we list those here. By no means is this an exhaustive list of mistakes a Solidity developer can make. […]

Smart Contract Foundry Upgrades with the OpenZeppelin Plugin

Smart Contract Foundry Upgrades with the OpenZeppelin Plugin Upgrading a smart contract is a multistep and error-prone process, so to minimize the chances of human error, it is desirable to use a tool that automates the procedure as much as possible. Therefore, the OpenZeppelin Upgrade Plugin streamlines deploying, upgrading and managing smart contracts built with Foundry or […]

UUPS: Universal Upgradeable Proxy Standard (ERC-1822)

UUPS: Universal Upgradeable Proxy Standard (ERC-1822) The UUPS pattern is a proxy pattern where the upgrade function resides in the implementation contract, but changes the implementation address stored in the proxy contract via a delegatecall from the proxy. The high level mechanism is shown in the animation below: Similar to the Transparent Upgradeable Proxy, the […]

Try Catch and all the ways Solidity can revert

Try Catch and all the ways Solidity can revert This article describes all the kinds of errors that can happen when a smart contract is called, and how the Solidity Try / Catch block responds (or fails to respond) to each of them. To understand how Try / Catch works in Solidity, we must understand […]