Skip to main content
The TVM (virtual machine) has only SLICE (binary data). There are no usual “strings” (likewise, no floating-point numbers). However, several techniques exist to store and return string-like data on-chain.

Embedding addresses as strings

Use address("...") to insert a constant standard address:
const REFUND_ADDR = address("EQCRDM9h4k3UJdOePPuyX40mCgA4vxge5Dc5vjBR8djbEKC5")
Its type is address, it has workchain and hash, but internally it’s a TVM SLICE, with 267 bits and 0 refs. To return a human‑readable address, just address. Client‑side libraries reconstruct and display addresses according to their own formatting rules.

Raw string literals

The syntax val s = "abcd" is valid, but it does not produce a string; it produces a binary slice: each character is encoded with its char no.
// `slice` with 4 bytes: 97,98,99,100 (0x61626364)
const SLICE1 = "abcd"
Literals cannot exceed 127 characters, because a cell can contain up to 1023 bits.

Hex string literals

Use stringHexToSlice("...") to embed hexadecimal binary data:
// `slice` with 2 bytes: 16,32 (0x1020)
const SLICE2 = stringHexToSlice("1020")

Concatenating string literals

An example for "ab" and "cd". Because they are slices, concatenation is performed by via a builder:
val concat = beginCell()
            .storeSlice("ab")
            .storeSlice("cd")
            .asSlice();         // builder-to-slice

How to return a string from a contract

A plain slice cannot be deserialized directly. Like int — use int32 / coins / etc. to specify encoding. Similarly, string‑like data must use an explicit encoding to be stored on‑chain.

Way #1: fixed-size strings via bitsN

If a response always contains 4 bytes of text, fixed‑size encodings may be used: bits32 or bytes4.
struct Response1 {
    someData: uint8
    str: bits32
}

fun generateResponse(): Response1 {
    return {
        someData: 0xFF,
        str: "abcd" as bits32,
    }
}

Way #2: “snake string”, or “tail string”

“Snake strings” (also called “tail strings”) are a standard encoding for arbitrary‑length strings, including those exceeding 127 characters. Snake encoding is: store portion of data → store rest in a ref cell. Here is a string xxxxyyyyzzzz split into 3 parts:
"xxxx".ref("yyyy".ref("zzzz"))
The name originates from the fact that data occupies all remaining bits in the current cell:
struct Response2 {
    someData: int8
    str: MyTailString
}

fun generateResponse(): Response2 {
    return {
        someData: 0xFF,
        str: /* "xxxx".ref("yyyy".ref("zzzz")) */,
    }
}
Then a cell will be "FFxxxx".ref(...). So “a tail” is because it goes after existing data up to the end. MyTailString can be described in the simplest form as the remainder of a slice — RemainingBitsAndRefs (mentioned in an article about cells).
type MyTailString = RemainingBitsAndRefs
When decoding on‑chain, no reference iteration is required. Client‑side tooling can reconstruct and display the full string.

Way #3: variable-length encoding

Variable‑length encodings may also be implemented manually. For example, short strings like “abcd” can be stored like
  • 8 bits for N = string length
  • N bits of data
This can be implemented using custom serializers:
type ShortString = slice

fun ShortString.packToBuilder(self, mutate b: builder) {
    val nBits = self.remainingBitsCount();
    b.storeUint(nBits, 8);
    b.storeSlice(self);
}

fun ShortString.unpackFromSlice(mutate s: slice) {
    val nBits = s.loadUint(8);
    return s.loadBits(nBits);
}
Then use it like a regular type:
struct Response3 {
    someData: int8
    str: ShortString
}

fun generateResponse(): Response3 {
    return {
        someData: 0xFF,
        str: "abcd",
    }
}

Calculate hex / crc32 / etc. at compile-time

There are several functions operating on strings:
  • stringCrc32("some_str")
  • stringCrc16("some_str")
  • stringSha256("some_crypto_key")
  • stringSha256_32("some_crypto_key")
  • stringToBase256("AB")
For examples, see standard library, they are at the top.