Skip to main content
Some protocols need to store a lot of information in contracts, for example, token contracts with many users. In TON, there is a limit on how much can be stored in a single contract. The solution is to split the data across multiple contracts. Each such contract, referred to as a child contract, is associated with a key that allows direct access to the required contract and its data. Some protocols also introduce a parent contract that coordinates child contracts.

Child contract address by key

The contract address depends on the initial data provided in StateInit. To ensure that a child contract can be accessed using only the key, the initial data includes the key but does not include the associated value. As a result, the address of the child contract can be determined from the key alone.

NFT and jetton examples

Consider NFTs: the collection acts as the parent contract, and each NFT item is a child contract. The key in this case is the item index, and only the collection can set the initial owner. For jettons, the parent contract is the minter, and the child contracts are user wallets. The key is the user’s smart contract address, and the value is the user’s token balance. Both patterns follow the same principle: each key maps to a separate contract. In jetton protocols, there is a unique contract per user, while in NFT collections, there is one contract per item (by index) that is shared across all users.

Unbounded data structures

Contract sharding supports an unbounded number of potential child contracts. In general, data structures that can scale to very large sizes are difficult to implement efficiently on blockchains. This pattern allows such scaling by distributing data across multiple contracts.
storage.tolk
// Shared storage layouts and helper used by both contracts.

struct TodoChildStorage {
    seqno: uint64
}

fun TodoChildStorage.load() {
    return TodoChildStorage.fromCell(contract.getData())
}

fun TodoChildStorage.save(self) {
    contract.setData(self.toCell())
}

struct TodoParentStorage {
    numChildren: uint64 = 0
    // Parent must know the child contract code to deploy new instances.
    todoChildCode: cell
}

fun TodoParentStorage.load() {
    return TodoParentStorage.fromCell(contract.getData())
}

fun TodoParentStorage.save(self) {
    contract.setData(self.toCell())
}

// Child prints its sequence number when it receives this message.
struct (0x49f29a21) Identify {}

// Parent deploys another child when it receives this message.
struct (0x5b6f1392) DeployAnother {}

// Build StateInit for a TodoChild instance with the given seqno.
fun calcDeployedTodoChild(
    seqno: uint64,
    todoChildCode: cell,
): AutoDeployAddress {
    val childStorage: TodoChildStorage = {
        seqno,
    };

    return {
        stateInit: {
            code: todoChildCode,
            data: childStorage.toCell(),
        }
    }
}
child.tolk
import "storage"

type TodoChildMessage = Identify

fun onInternalMessage(in: InMessage) {
    val msg = lazy TodoChildMessage.fromSlice(in.body);

    match (msg) {
        Identify => {
            val storage = lazy TodoChildStorage.load();
            debug.print(storage.seqno);
        }
        else => {
            // Ignore empty top-up messages, reject everything else.
            assert (in.body.isEmpty()) throw 0xFFFF;
        }
    }
}

get fun seqno(): uint64 {
    val storage = lazy TodoChildStorage.load();
    return storage.seqno;
}
parent.tolk
import "storage"

type TodoParentMessage = DeployAnother

fun onInternalMessage(in: InMessage) {
    val msg = lazy TodoParentMessage.fromSlice(in.body);

    match (msg) {
        DeployAnother => {
            var storage = lazy TodoParentStorage.load();
            storage.numChildren += 1;

            // Send a message to the auto-calculated address and attach
            // the child code and initial data so the child is deployed.
            val deployMsg = createMessage({
                dest: calcDeployedTodoChild(
                    storage.numChildren,
                    storage.todoChildCode,
                ),
                value: ton("0.1"),
                body: Identify {},
            });

            storage.save();
            deployMsg.send(SEND_MODE_IGNORE_ERRORS);
        }
    }
}

get fun numChildren(): uint64 {
    val storage = lazy TodoParentStorage.load();
    return storage.numChildren;
}