TON DocsTON Docs
Techniques

Contract sharding

Some protocols need to store a lot of information in contracts, for example, token contracts with many users. In TON, there is a limit max_acc_state_bits 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 determines its address within a parent contract. 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 within a specific parent, the initial data includes the key and the parent address, but does not include the associated value. As a result, the address of the child contract can be determined from the parent address and key alone.

Child contracts should store the parent contract address and verify that incoming messages originate from it. This ensures that only the parent contract can perform authorized state changes.

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 jetton wallets. The key is the address of the jetton wallet's owner, and the value is the owner's jetton balance.

Both patterns follow the same principle: each key maps to a separate contract. In jetton protocols, there is a unique wallet contract per user, while in NFT collections, there is a unique item contract per NFT index, regardless of who owns the item.

Shard pattern

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.

The following example shows a parent contract that deploys child contracts and assigns each child a sequence number. The shared storage file defines the data layouts and message types used by both contracts.

storage.tolk
// Shared storage layouts, messages, and errors used by both contracts.

enum Errors {
    NotFromParent  = 100
    NotFromAdmin   = 101
    InvalidMessage = 0xFFFF
}

struct TodoChildStorage {
    parentAddress: address
    // Sequence number of a child.
    seqno: uint64
}

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

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

struct TodoParentStorage {
    // Initialize this field in the parent's StateInit during deployment.
    adminAddress: address
    numChildren: uint64 = 0
    // Parent must know the child contract code to deploy new instances.
    todoChildCode: cell
}

fun TodoParentStorage.load(): TodoParentStorage {
    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 {}

The child contract stores the parent address in its initial data and accepts the Identify message only from that parent.

child.tolk
import "storage"

type TodoChildMessage = Identify

contract TodoChild {
    storage: TodoChildStorage
    incomingMessages: TodoChildMessage
}

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

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

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

The parent contract owns the deployment logic. It derives each child address from the parent address, the child sequence number, and the child code.

parent.tolk
import "storage"

type TodoParentMessage = DeployAnother

contract TodoParent {
    storage: TodoParentStorage
    incomingMessages: TodoParentMessage
}

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

    return {
        stateInit: {
            code: todoChildCode,
            data: childStorage.toCell(),
        }
    }
}

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

    match (msg) {
        DeployAnother => {
            var storage = lazy TodoParentStorage.load();
            assert (in.senderAddress == storage.adminAddress) throw Errors.NotFromAdmin;
            // `numChildren` is used as the next child id. Because deployment
            // is sent with SEND_MODE_IGNORE_ERRORS, failed sends can leave gaps.
            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({
                bounce: BounceMode.Only256BitsOfBody,
                dest: calcDeployedTodoChild(
                    contract.getAddress(),
                    storage.numChildren,
                    storage.todoChildCode,
                ),
                value: grams("0.1"),
                body: Identify {},
            });

            storage.save();
            deployMsg.send(SEND_MODE_IGNORE_ERRORS);
        }
        else => {
            // Ignore empty top-up messages, reject everything else.
            assert (in.body.isEmpty()) throw Errors.InvalidMessage;
        }
    }
}

// The deploy message sent with BounceMode.Only256BitsOfBody can bounce back if the child fails to deploy.
fun onBouncedMessage(_in: InMessageBounced) {}

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

get fun childAddress(seqno: uint64): address {
    val storage = lazy TodoParentStorage.load();
    return calcDeployedTodoChild(
        contract.getAddress(),
        seqno,
        storage.todoChildCode,
    ).calculateAddress();
}

On this page