Skip to content

Integers

Arithmetic in smart contracts on TON is always performed with integers and never with floating-point numbers, since floats are unpredictable. Therefore, a strong emphasis is placed on integers and their handling.

The only primitive number type in Tact is Int, for 257257-bit signed integers.
It’s capable of storing integers between 2256-2^{256} and 22561.2^{256} - 1.

Notation

Tact supports various ways of writing primitive values of Int as integer literals.

Most of the notations allow adding underscores (_) between digits, except for:

  • Representations in strings, as seen in the nano-tons case.
  • Decimal numbers written with a leading zero 0.0. Their use is generally discouraged; see below.

Additionally, consecutive underscores, as in 4__24\_\_2, or trailing underscores, as in 42_42\_, are not allowed.

Decimal

The most common and widely used way of representing numbers is using the decimal numeral system: 123456789.123456789.
You can use underscores (_) to improve readability: 123_456_789123\_456\_789 is equal to 123456789.123456789.

Hexadecimal

Represent numbers using the hexadecimal numeral system, denoted by the 0x\mathrm{0x} (or 0X\mathrm{0X}) prefix: 0xFFFFFFFFF\mathrm{0xFFFFFFFFF}. Use underscores (_) to improve readability: 0xFFF_FFF_FFF\mathrm{0xFFF\_FFF\_FFF} is equal to 0xFFFFFFFFF\mathrm{0xFFFFFFFFF}.

Octal

Represent numbers using the octal numeral system, denoted by the 0o\mathrm{0o} (or 0O\mathrm{0O}) prefix: 0o777777777\mathrm{0o777777777}. Use underscores (_) to improve readability: 0o777_777_777\mathrm{0o777\_777\_777} is equal to 0o777777777\mathrm{0o777777777}.

Binary

Represent numbers using the binary numeral system, denoted by the 0b\mathrm{0b} (or 0B\mathrm{0B}) prefix: 0b111111111\mathrm{0b111111111}. Use underscores (_) to improve readability: 0b111_111_111\mathrm{0b111\_111\_111} is equal to 0b111111111\mathrm{0b111111111}.

NanoToncoin

Arithmetic with dollars requires two decimal places after the dot — these are used for the cents value. But how would we represent the number $1.251.25 if we are only able to work with integers? The solution is to work with cents directly. In this way, $1.251.25 becomes 125125 cents. We simply remember that the two rightmost digits represent the numbers after the decimal point.

Similarly, working with Toncoin, the main currency of TON Blockchain, requires nine decimal places instead of two. One can say that nanoToncoin is one-billionth (1109\frac{1}{10^{9}}) of a Toncoin.

Therefore, the amount of 1.251.25 Toncoin, which can be represented in Tact as ton("1.25"), is actually the number 12500000001250000000. We refer to such numbers as nanoToncoin(s) (or nano-ton(s)) rather than cents.

Serialization

When encoding Int values to persistent state (fields or parameters of contracts and fields of traits), it is usually better to use smaller representations than 257 bits to reduce storage costs. The use of such representations is also called “serialization” because they represent the native TL-B types on which TON Blockchain operates.

The persistent state size is specified in every declaration of a state variable after the as keyword:

contract SerializationExample {
// persistent state variables
oneByte: Int as int8 = 0; // ranges from -128 to 127 (takes 8 bits = 1 byte)
twoBytes: Int as int16; // ranges from -32,768 to 32,767 (takes 16 bits = 2 bytes)
init() {
// needs to be initialized in the init() because it does not have a default value
self.twoBytes = 55*55;
}
}

Integer serialization is also available for the fields of Structs and Messages, as well as in the key/value types of maps:

struct StSerialization {
martin: Int as int8;
}
message MsgSerialization {
seamus: Int as int8;
mcFly: map<Int as int8, Int as int8>;
}

The motivation is very simple:

  • Storing 1000 257-bit integers in the state costs about 0.184 TON per year.
  • Storing 1000 32-bit integers only costs 0.023 TON per year by comparison.

Common serialization types

NameTL-BInclusive rangeSpace taken
uint8uint800 to 2812^{8} - 188 bits = 11 byte
uint16uint1600 to 21612^{16} - 11616 bits = 22 bytes
uint32uint3200 to 23212^{32} - 13232 bits = 44 bytes
uint64uint6400 to 26412^{64} - 16464 bits = 88 bytes
uint128uint12800 to 212812^{128} - 1128128 bits = 1616 bytes
uint256uint25600 to 225612^{256} - 1256256 bits = 3232 bytes
int8int827-2^{7} to 2712^{7} - 188 bits = 11 byte
int16int16215-2^{15} to 21512^{15} - 11616 bits = 22 bytes
int32int32231-2^{31} to 23112^{31} - 13232 bits = 44 bytes
int64int64263-2^{63} to 26312^{63} - 16464 bits = 88 bytes
int128int1282127-2^{127} to 212712^{127} - 1128128 bits = 1616 bytes
int256int2562255-2^{255} to 225512^{255} - 1256256 bits = 3232 bytes
int257int2572256-2^{256} to 225612^{256} - 1257257 bits = 3232 bytes + 11 bit
coinsVarUInteger 1600 to 212012^{120} - 1Between 44 and 124124 bits, see below

Arbitrary types of fixed bit-width

Available since Tact 1.5

In addition to common serialization types, it is possible to specify arbitrary bit-width integers by using the prefix int or uint, followed by digits. For example, writing int7 refers to a signed 77-bit integer.

The minimum allowed bit-width of an Int type is 11, while the maximum is 257257 for the int prefix (signed integers) and 256256 for the uint prefix (unsigned integers).

NameTL-BInclusive rangeSpace taken
uintXuintX00 to 2X12^{X} - 1XX bits, where XX is between 11 and 256256
intXintX2X1-2^{X - 1} to 2X112^{X - 1} - 1XX bits, where XX is between 11 and 257257

Types of variable bit-width

NameTL-BInclusive rangeSpace taken
coinsVarUInteger 1600 to 212012^{120} - 1between 44 and 124124 bits

In Tact, the variable coins format is an alias to VarUInteger 16 in TL-B representation, i.e. it takes a variable bit length depending on the optimal number of bytes needed to store the given integer and is commonly used for storing nanoToncoin amounts.

This serialization format consists of two TL-B fields:

  • len, a 44-bit unsigned big-endian integer storing the byte length of the provided value
  • value, an 8len8 * len-bit unsigned big-endian representation of the provided value

That is, integers serialized as coins occupy between 44 and 124124 bits (44 bits for len and 00 to 1515 bytes for value) and have values in the inclusive range from 00 to 212012^{120} - 1.

Examples:

struct Scrooge {
// len: 0000, 4 bits (always)
// value: none!
// in total: 4 bits
a: Int as coins = 0; // 0000
// len: 0001, 4 bits
// value: 00000001, 8 bits
// in total: 12 bits
b: Int as coins = 1; // 0001 00000001
// len: 0010, 4 bits
// value: 00000001 00000010, 16 bits
// in total: 20 bits
c: Int as coins = 258; // 0010 00000001 00000010
// len: 1111, 4 bits
// value: hundred twenty 1's in binary
// in total: 124 bits
d: Int as coins = pow(2, 120) - 1; // hundred twenty 1's in binary
}
NameTL-BInclusive rangeSpace taken
varuint16VarUInteger 16same as coinssame as coins
varint16VarInteger 162119-2^{119} to 211912^{119} - 1between 44 and 124124 bits
varuint32VarUInteger 3200 to 224812^{248} - 1between 55 and 253253 bits
varint32VarInteger 322247-2^{247} to 224712^{247} - 1between 55 and 253253 bits

Available since Tact 1.6

The varuint16 format is equivalent to coins. Its signed variant, varint16, has the same memory layout except for the signed value field, which allows a different range of values: from 2119-2^{119} to 211912^{119} - 1, including both endpoints.

To store greater values, use the varuint32 and varint32 formats. These are serialized almost identically to coins and other smaller variable integer formats but use a 55-bit len field for storing the byte length. This allows the value to use up to 248248 bits for storing the actual number, meaning that both varuint32 and varint32 can occupy up to 253253 bits in total.

Examples:

struct BradBit {
// len: 00000, 5 bits
// value: none!
// in total: 5 bits
a: Int as varuint32 = 0; // 00000
// len: 00001, 5 bits
// value: 00000001, 8 bits
// in total: 13 bits
b: Int as varuint32 = 1; // 00001 00000001
// len: 00010, 5 bits
// value: 00000001 00000010, 16 bits
// in total: 21 bits
c: Int as varuint32 = 258; // 00010 00000001 00000010
// len: 11111, 5 bits
// value: two hundred and forty-eight 1's in binary
// in total: 253 bits
d: Int as varuint32 = pow(2, 248) - 1; // two hundred and forty-eight 1's in binary
}

Operations

All runtime calculations with numbers are performed at 257 bits, so overflows are quite rare. Nevertheless, if any math operation overflows, an exception will be thrown, and the transaction will fail. You could say that Tact’s math is safe by default.

Note that there is no problem with mixing variables of different state sizes in the same calculation. At runtime, they are all the same type no matter what — 257-bit signed integers, so overflows won’t occur at this stage.

However, this can still lead to errors in the compute phase of the transaction. Consider the following example:

contract ComputeErrorsOhNo {
oneByte: Int as uint8; // persistent state variable, max value is 255
init() {
self.oneByte = 255; // initial value is 255, everything fits
}
// Empty receiver for the deployment
receive() {
// Forward the remaining value in the
// incoming message back to the sender
cashback(sender());
}
receive("lets break it") {
let tmp: Int = self.oneByte * 256; // no runtime overflow
self.oneByte = tmp; // whoops, tmp value is out of expected range of oneByte
}
}

Here, oneByte is serialized as a uint8, which occupies only one byte and ranges from 00 to 2812^{8} - 1, which is 255255. When used in runtime calculations, no overflow occurs since everything is calculated as 257257-bit signed integers. However, the moment we decide to store the value of tmp back into oneByte, we get an error with exit code 5, which states the following: Integer out of expected range.