Simple Values
This section covers the fundamental types used in smart contracts:
- Fixed-width numbers
- Arbitrary width (big) numbers
- Boolean values
Fixed-width numbers
Small numbers can be stored in variables of up to 64 bits. We utilize big-endian encoding for all numbers in our projects.
Rust types: u8
, u16
, u32
, usize
, u64
, i8
, i16
, i32
, isize
, i64
.
Top-level encoding: The same as for all numerical types, using the minimum number of bytes that can fit their 2's complement, big-endian representation.
Nested encoding: Fixed-width big-endian encoding of the type, using 2's complement.
🚨 Regarding the types usize and isize : These Rust-specific types match the width of the underlying architecture, i.e., 32 bits on 32-bit systems and 64 bits on 64-bit systems. However, smart contracts always run on a wasm32 architecture, so these types will always be identical to u32 and i32 , respectively. Even when simulating smart contract execution on 64-bit systems, they must still be serialized on 32 bits. |
Examples
Type | Number | Top-level encoding | Nested encoding |
---|---|---|---|
u8 | 0 | 0x | 0x00 |
u8 | 1 | 0x01 | 0x01 |
u8 | 0x11 | 0x11 | 0x11 |
u8 | 255 | 0xFF | 0xFF |
u16 | 0 | 0x | 0x0000 |
u16 | 0x11 | 0x11 | 0x0011 |
u16 | 0x1122 | 0x1122 | 0x1122 |
u32 | 0 | 0x | 0x00000000 |
u32 | 0x11 | 0x11 | 0x00000011 |
u32 | 0x1122 | 0x1122 | 0x00001122 |
u32 | 0x112233 | 0x112233 | 0x00112233 |
u32 | 0x11223344 | 0x11223344 | 0x11223344 |
u64 | 0 | 0x | 0x0000000000000000 |
u64 | 0x11 | 0x11 | 0x0000000000000011 |
u64 | 0x1122 | 0x1122 | 0x0000000000001122 |
u64 | 0x112233 | 0x112233 | 0x0000000000112233 |
u64 | 0x11223344 | 0x11223344 | 0x0000000011223344 |
u64 | 0x1122334455 | 0x1122334455 | 0x0000112233445566 |
u64 | 0x112233445566 | 0x112233445566 | 0x0000112233445566 |
u64 | 0x11223344556677 | 0x11223344556677 | 0x0011223344556677 |
u64 | 0x1122334455667788 | 0x1122334455667788 | 0x1122334455667788 |
usize | 0 | 0x | 0x00000000 |
usize | 0x11 | 0x11 | 0x00000011 |
usize | 0x1122 | 0x1122 | 0x00001122 |
usize | 0x112233 | 0x112233 | 0x00112233 |
usize | 0x11223344 | 0x11223344 | 0x11223344 |
i8 | 0 | 0x | 0x00 |
i8 | 1 | 0x01 | 0x01 |
i8 | -1 | 0xFF | 0xFF |
i8 | 127 | 0x7F | 0x7F |
i8 | -0x11 | 0xEF | 0xEF |
i8 | -128 | 0x80 | 0x80 |
i16 | -1 | 0xFF | 0xFFFF |
i16 | -0x11 | 0xEF | 0xFFEF |
i16 | -0x1122 | 0xEEDE | 0xEEDE |
i32 | -1 | 0xFF | 0xFFFFFFFF |
i32 | -0x11 | 0xEF | 0xFFFFFFEF |
i32 | -0x1122 | 0xEEDE | 0xFFFFEEDE |
i32 | -0x112233 | 0xEEDDCD | 0xFFEEDDCD |
i32 | -0x11223344 | 0xEEDDCCBC | 0xEEDDCCBC |
i64 | -1 | 0xFF | 0xFFFFFFFFFFFFFFFF |
i64 | -0x11 | 0xEF |
0xFFFFFFFFFFFFFFEF
|
| i64
| -0x1122
| 0xEEDE
| 0xFFFFFFFFFFFFEEDE
|
| i64
| -0x112233
| 0xEEDDCD
| 0xFFFFFFFFFFEEDDCD
|
| i64
| -0x11223344
| 0xEEDDCCBC
| 0xFFFFFFFFEEDDCCBC
|
| i64
| -0x1122334455
| 0xEEDDCCBBAB
| 0xFFFFFFEEDDCCBBAB
|
| i64
| -0x112233445566
| 0xEEDDCCBBAA9A
| 0xFFFFEEDDCCBBAA9A
|
| i64
| -0x11223344556677
| 0xEEDDCCBBAA9989
| 0xFFEEDDCCBBAA9989
|
| i64
| -0x1122334455667788
| 0xEEDDCCBBAA998878
| 0xEEDDCCBBAA998878
|
| isize
| 0
| 0x
| 0x00000000
|
| isize
| -1
| 0xFF
| 0xFFFFFFFF
|
| isize
| -0x11
| 0xEF
| 0xFFFFFFEF
|
| isize
| -0x1122
| 0xEEDE
| 0xFFFFEEDE
|
| isize
| -0x112233
| 0xEEDDCD
| 0xFFEEDDCD
|
| isize
| -0x11223344
| 0xEEDDCCBC
| 0xEEDDCCBC
|
Arbitrary width (big) numbers
In many smart contract applications, numbers larger than the maximum uint64
value are required. For example, KLV balances are represented as fixed-point decimal numbers with 18 decimals. This means that representing just 100 KLV requires the number 100 * 10^18, which exceeds the capacity of a regular 64-bit integer.
Rust types: BigUint
, BigInt
.
🚨 These types are managed by the Klever VM, and in many cases, the contract never sees the data directly, only a handle. This is done to reduce the burden on the smart contract. |
Top-level encoding: The same as for all numerical types, using the minimum number of bytes that can fit their 2's complement, big-endian representation.
Nested encoding: Since these types are variable length, we need to encode their length to indicate when decoding should stop. The length of the encoded number is always encoded first, using 4 bytes (usize
/u32
). Next, we encode:
- For
BigUint
, the big-endian bytes. - For
BigInt
, the shortest 2's complement number that can unambiguously represent the number. Positive numbers must always have the most significant bit as0
, while negative ones have1
. See examples below.
Examples
Type | Number | Top-level encoding | Nested encoding | Explanation |
---|---|---|---|---|
BigUint | 0 | 0x | 0x00000000 | The length of 0 is considered 0 . |
BigUint | 1 | 0x01 | 0x0000000101 | 1 can be represented on 1 byte, so the length is 1. |
BigUint | 256 | 0x0100 | 0x000000020100 | 256 is the smallest number that takes 2 bytes. |
BigInt | 0 | 0x | 0x00000000 | Signed 0 is also represented as zero-length bytes. |
BigInt | 1 | 0x01 | 0x0000000101 | Signed 1 is also represented as 1 byte. |
BigInt | -1 | 0xFF | 0x00000001FF | The shortest 2's complement representation of -1 is FF . The most significant bit is 1. |
BigUint | 127 | 0x7F | 0x000000017F | |
BigInt | 127 | 0x7F | 0x000000017F | |
BigUint | 128 | 0x80 | 0x0000000180 | |
BigInt | 128 | 0x0080 | 0x000000020080 | The most significant bit of this number is 1, so to avoid ambiguity, an extra 0 byte needs to be prepended. |
BigInt | 255 | 0x00FF | 0x0000000200FF | Same as above. |
BigInt | 256 | 0x0100 | 0x000000020100 | 256 requires 2 bytes to represent, with the MSB as 0, so no more 0 byte needs to be prepended. |
Boolean values
Booleans are serialized the same as a byte (u8
) that can take values 1
or 0
.
Rust type: bool
Values
Type | Value | Top-level encoding | Nested encoding |
---|---|---|---|
bool | true | 0x01 | 0x01 |
bool | false | 0x | 0x00 |
Byte Slices and ASCII Strings
Byte slices are a special case of the list types and are commonly considered basic types. Their encoding adheres to the rules for lists of "byte items."
🚨 From a serialization perspective, strings are treated as sequences of bytes. While Unicode strings are often preferred in programming, they can add unnecessary overhead to smart contracts. Unicode strings undergo input validation and concatenation checks. As a best practice, we recommend using Unicode on the frontend but keeping all messages and error messages in ASCII format at the smart contract level. |
Rust types: ManagedBuffer
, BoxedBytes
, &[u8]
, Vec<u8>
, String
, &str
.
Top-level encoding: The byte slice remains as-is.
Nested encoding: The byte slice's length is encoded using 4 bytes, followed by the byte slice as-is.
Examples
Type | Value | Top-level encoding | Nested encoding | Explanation |
---|---|---|---|---|
&'static [u8] | b"abc" | 0x616263 | 0x00000003616263 | ASCII strings are byte slices of buffers. |
ManagedBuffer | ManagedBuffer::from("abc") | 0x616263 | 0x00000003616263 | Use Vec for a buffer that can grow. |
BoxedBytes | BoxedBytes::from( b"abc") | 0x616263 | 0x00000003616263 | BoxedBytes are optimized owned byte slices that cannot grow. |
Vec<u8> | b"abc".to_vec() | 0x616263 | 0x00000003616263 | Use Vec for a buffer that can grow. |
&'static str | "abc" | 0x616263 | 0x00000003616263 | Unicode string (slice). |
String | "abc".to_string() | 0x616263 | 0x00000003616263 | Unicode string (owned). |
ℹ️ Inside contracts, ManagedBuffer is the recommended type for generic bytes. |
Address
Klever addresses consist of 32-byte arrays, and they are serialized accordingly, both in top-level and nested encodings.
Token Identifiers
Klever KDA token identifiers are of the form XXXXXX-123456
, where the first part is the token ticker, which can range from 3 to 20 characters in length, and the last part is a randomly generated number.
At the top level, they are encoded as-is, preserving the exact bytes.
Due to their variable length, they are serialized like variable-length byte slices when nested. The length is explicitly encoded at the beginning.
Type | Value | Top-level encoding | Nested encoding |
---|---|---|---|
TokenIdentifier | ABC-123456 | 0x4142432d313233343536 | 0x0000000A4142432d313233343536 |