Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 115 additions & 42 deletions contract-dev/techniques/contract-sharding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,153 @@ title: "Contract sharding"
import { Aside } from '/snippets/aside.jsx';
import { Image } from '/snippets/image.jsx';

Some protocols need to store a lot of information in contracts, for example, tokens that have many users. In TON, there is a limit on how much can be stored in a single contract. The solution in TON is to split the data across many different contracts, where you can quickly find the right contract by a key and retrieve the required information from it.
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.

In such protocols, there is a child contract that initially contains the information identified by a key. In some protocols, it is important to know the Parent contract, which acts as the information manager.
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.
Comment thread
aigerimu marked this conversation as resolved.
Outdated

To avoid having to know the key upfront, we do not populate that field in `StateInit`; we only populate the key field. This makes it easy to locate the required contract later.
## Child contract address by key

The contract address depends on the initial data provided in [`StateInit`](/foundations/messages/deploy). 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.
Comment thread
aigerimu marked this conversation as resolved.
Outdated

<Aside
type="caution"
>
Child contracts should store information about the Parent so that only it can authorize important state changes.
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.
</Aside>

## 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.
Comment thread
aigerimu marked this conversation as resolved.
Outdated

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.
Comment thread
aigerimu marked this conversation as resolved.
Outdated
Comment thread
aigerimu marked this conversation as resolved.
Outdated

<Image
src="/resources/images/child_light.png"
darkSrc="/resources/images/child_dark.png"
alt="Shard pattern"
/>

Consider NFTs: the collection serves as the Parent contract, and the NFT items are the child contracts. The key in this case is the index, and only a message from the collection can set the initial owner.
## Unbounded data structures

For Jettons, the Parent is the minter and the Children are user wallets. The key is the user's smart contract address, and the value is the user's token balance.
Contract sharding supports an unbounded number of potential child contracts.

In general, jettons and NFTs share this principle, but broadly speaking, jetton protocols have a unique contract per user, while NFTs have a single contract per item (by index) that is shared across all users.
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.

## Unbounded data structures

Comment on lines +42 to 43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Issue #500 scope is still incomplete (missing Poll/Voter + bounced-message logic).

The page now has a solid parent/child sample, but it still does not include (or link to) the requested Poll/Voter Tolk example demonstrating bounced-message handling from the linked issue scope.

Also applies to: 85-110, 114-167

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contract-dev/techniques/contract-sharding.mdx` around lines 42 - 43, The
sample is missing the Poll/Voter example and bounced-message handling from Issue
`#500`: add two new contract examples named Poll and Voter (mirroring the
parent/child pattern) where Poll deploys Voter instances and assigns sequence
numbers, extend the shared storage definitions to include the Poll/Voter message
types and a bounced-message envelope, and implement explicit bounce handling in
the Voter (e.g., an onBounce or handle_bounced_message function and examples of
sending messages that may bounce and how Poll reacts); update the narrative to
show deployment, message flow, and bounced-message test cases and link to Issue
`#500` for scope reference.

An interesting property of this pattern is that the number of potential children is unbounded! We can have an infinite number of children.
```tolk title="storage.tolk"
// Shared storage layouts and helper used by both contracts.

In general, infinite data structures that can actually scale to billions are very difficult to implement on blockchain efficiently. This pattern showcases the power of TON.
struct TodoChildStorage {
seqno: uint64
}

```tact Tact
import "@stdlib/deploy";
fun TodoChildStorage.load() {
return TodoChildStorage.fromCell(contract.getData())
}

// we have multiple instances of the children
contract TodoChild {
fun TodoChildStorage.save(self) {
Comment thread
aigerimu marked this conversation as resolved.
contract.setData(self.toCell())
}

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

// when deploying an instance, we must specify its index (sequence number)
init(seqno: Int) {
self.seqno = seqno;
}
fun TodoParentStorage.load() {
return TodoParentStorage.fromCell(contract.getData())
}

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

// this message handler will just debug print the seqno so we can see when it's called
receive("identify") {
dump(self.seqno);
// 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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should move it to parent.tolk (I am not sure)

Copy link
Copy Markdown
Contributor Author

@aigerimu aigerimu Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by moving calcDeployedTodoChild to parent.tolk. But this still needs to be validated

cc @Shvandre @Kaladin13

seqno: uint64,
todoChildCode: cell,
): AutoDeployAddress {
val childStorage: TodoChildStorage = {
Comment thread
aigerimu marked this conversation as resolved.
Outdated
seqno,
};

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

// we have one instance of the parent
contract TodoParent with Deployable {
```tolk title="child.tolk"
import "storage"

numChildren: Int as uint64;
type TodoChildMessage = Identify

init() {
self.numChildren = 0;
}
fun onInternalMessage(in: InMessage) {
val msg = lazy TodoChildMessage.fromSlice(in.body);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but I think the else branch in the match below may not be reachable as written. Since TodoChildMessage = Identify is a single-variant union, lazy TodoChildMessage.fromSlice(in.body) should throw on any body that doesn't parse as Identify - including an empty body, since the opcode parse fails first

If the intent is to ignore empty top-up messages, the empty-body check probably needs to happen before the lazy fromSlice call for instance

 fun onInternalMessage(in: InMessage) {
+    if (in.body.isEmpty()) {
+        return;
+    }
     val msg = lazy TodoChildMessage.fromSlice(in.body);

it definitely worths double-checking against actual Tolk semantics


// this message handler will cause the contract to deploy another child
receive("deploy another") {
self.numChildren = self.numChildren + 1;
let init: StateInit = initOf TodoChild(self.numChildren);
send(SendParameters{
to: contractAddress(init),
value: ton("0.1"), // pay for message, the deployment, and give some TON for storage
mode: SendIgnoreErrors,
code: init.code, // attaching the `StateInit` will cause the message to deploy
data: init.data,
body: "identify".asComment() // we must piggyback the deployment on another message
});
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;
}
```
Comment thread
aigerimu marked this conversation as resolved.


get fun numChildren(): Int {
return self.numChildren;
```tolk title="parent.tolk"
import "storage"

type TodoParentMessage = DeployAnother

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

match (msg) {
DeployAnother => {
Comment thread
aigerimu marked this conversation as resolved.
var storage = lazy TodoParentStorage.load();
storage.numChildren += 1;
Comment thread
aigerimu marked this conversation as resolved.

// Send a message to the auto-calculated address and attach
// the child code and initial data so the child is deployed.
val deployMsg = createMessage({
Comment thread
aigerimu marked this conversation as resolved.
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;
}
```
Comment thread
aigerimu marked this conversation as resolved.
Loading