Skip to content
Draft
Changes from all commits
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.
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.

"a key that allows direct access to the required contract and its data" repeats "contract" a bit awkwardly. it makes sense to read cleaner as something like: a key that uniquely determines its address


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 on lines +12 to +14
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.

[HIGH] Missing code font for StateInit

The identifier StateInit is referenced inside link text on this page without being consistently treated as a code-styled identifier across revisions. The style guide requires code identifiers to appear in code font with exact casing so they are visually distinct from surrounding prose and easy to scan. Using plain text for an identifier like StateInit reduces clarity, especially for readers skimming for specific technical terms. Keeping identifier formatting consistent across the docs also improves searchability and reduces the chance of misreading the term.

Suggested change
## 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.
## 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.

Please leave a reaction 👍/👎 to this suggestion to improve future reviews for everyone!


<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.

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 on lines +22 to +28
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.

[HIGH] Casing of “jetton” at sentence start

Suggested change
## 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.

Please leave a reaction 👍/👎 to this suggestion to improve future reviews for everyone!


<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

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) {
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(
seqno: uint64,
todoChildCode: cell,
): AutoDeployAddress {
val childStorage: TodoChildStorage = {
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 guess the child StateInit only includes seqno, so two parents using the same child code and sequence number can derive the same child address. it also contradicts the caution above, because the child has no stored parent address to verify

to fix it is to include the parent address in the child's initial storage, pass it into address derivation, and verify it in the child handler

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;
}
```


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 => {
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.

DeployAnother is accepted from any sender and spends ton("0.1") from the parent. If copied as-is, anyone can repeatedly trigger child deployments and drain the parent balance

either mark this handler as demo-only, or add authorization. For example:

 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
 }
         DeployAnother => {
             var storage = lazy TodoParentStorage.load();
+            assert (in.senderAddress == storage.adminAddress) throw 0xFFFF;
             storage.numChildren += 1;

var storage = lazy TodoParentStorage.load();
storage.numChildren += 1;
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.

minor point: numChildren increments before the send, and the send uses SEND_MODE_IGNORE_ERRORS. If the action fails to queue, the counter still bumps, so the next deploy uses seqno = N+2 and leaves a gap

absolutely not a bug if contiguous numbering doesn't matter, but might be worth a sentence in the prose or a short comment in the sample so readers don't assume sequence numbers are guaranteed dense :)


// Send a message to the auto-calculated address and attach
// the child code and initial data so the child is deployed.
val deployMsg = createMessage({
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.

the createMessage call omits explicit bounce behavior. The Tolk message docs say createMessage should specify bounce behavior, and the full examples in this repo do so

https://docs.ton.org/languages/tolk/features/message-handling#bouncemode-in-createmessage

I'd add an explicit mode, for example:

             val deployMsg = createMessage({
+                bounce: BounceMode.Only256BitsOfBody,
                 dest: calcDeployedTodoChild(
                     storage.numChildren,
                     storage.todoChildCode,
                 ),

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;
}
```
Loading